Skip to content

Instantly share code, notes, and snippets.

@JohnnyonFlame
Last active August 3, 2023 04:55
Show Gist options
  • Save JohnnyonFlame/5ccd797d5681bf691eb18e581ddb3239 to your computer and use it in GitHub Desktop.
Save JohnnyonFlame/5ccd797d5681bf691eb18e581ddb3239 to your computer and use it in GitHub Desktop.
Compile-time parsed AOB patterns example
/*
# Do what the fuck you want to public license
Version 2, December 2004
Copyright (C) `2021` `Johnny on Flame`
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
*/
/*
This little example allows you to define Array of Byte scans with masks on C++ with
compile time pattern parsing, allowing errors to be caught and reported during compilation,
and thus no parsing happens during runtime.
It's mostly an excuse to study and understand constexpr a little better.
*/
#include <string_view>
#include <iostream>
#include <vector>
using namespace std;
// Wrapper to transform string literals into something that can be passed as
// a template parameter.
// String literals can't be used as template parameters, but arrays with chars can,
// this wrapper explodes the literals into arrays.
template<int N>
struct _str {
char value[N];
const int size = N;
constexpr _str(const char (&str)[N]) {
copy_n(str, N, value);
}
};
// isxdigit isn't constexpr-able...
constexpr int ishex(char c) {
return
((c) >= 'a' && (c) <= 'f') ||
((c) >= 'A' && (c) <= 'F') ||
((c) >= '0' && (c) <= '9');
}
// meh
constexpr uint8_t hex_to_int(char a, char b)
{
#define doit(c) ( \
((c) >= 'a' && (c) <= 'f') ? (c) - 'a' + 10: \
((c) >= 'A' && (c) <= 'F') ? (c) - 'A' + 10: \
(c) - '0' \
)
return doit(a) << 4 | doit(b);
}
// Wrapper struct to copy back results from the lambda in a constexpr form.
// This is necessary to ensure that the final state of our calculation is all available
// in a neatly constexpr'd form, because otherwise, arguments and variables can't be considered
// constant expressions and won't be usable on template arguments and static_assert arguments.
struct result_t {
int state;
int size;
int col;
using storage_type = uint8_t[2048];
storage_type bytes;
storage_type mask;
constexpr result_t(int state, int size, int col, const decltype(bytes) &bytes, const decltype(mask) &mask)
: state(state), size(size), col(col)
{
copy_n(bytes, sizeof(bytes), this->bytes);
copy_n(mask, sizeof(mask), this->mask);
}
};
// The actual compile-time built/guaranteed AOB pattern and mask.
template <int N>
struct aob_t {
const int size = N;
uint8_t bytes[N];
uint8_t mask[N];
template <typename T>
constexpr aob_t(const T &t) {
copy_n(t.bytes, N, bytes);
copy_n(t.mask, N, mask);
}
uint8_t *scan(uint8_t *start, uint8_t *end) const {
for (uint8_t *ptr = start; ptr < end; ptr++) {
int pos = 0;
for (uint8_t *cont = ptr; cont < end; cont++) {
if (mask[pos] == 1 || bytes[pos] == *cont) {
if (++pos >= size)
return (uint8_t*)ptr;
} else {
break;
}
}
}
return NULL;
};
};
template <int ISERR = 0>
constexpr void LAYER()
{
static_assert(ISERR == 0);
}
template <_str str1, int ISERR = 0>
constexpr void ERROR()
{
LAYER<ISERR>();
}
template <typename T>
int constexpr _strlen(T str)
{
return *str ? 1 + _strlen(str + 1) : 0;
}
// Format and pass error around to display
template <_str pattern, int COLUMN, int LAST_STATE = 0>
constexpr void AOB_PARSE_CHECK_ERROR()
{
constexpr auto err = []() {
// choose error type
constexpr const char *msg = []() {
switch (LAST_STATE) {
case -1: return ": Expected hex or <?>, got <%> instead.";
case -2: return ": Expected hex, got <%> instead.";
case -3: return ": Expected separator or <?>, got <%> instead.";
default: return ": Unknown error message.";
}
}();
// Create number string the naive way...
constexpr auto colno = []() {
char colno[100] = "";
int i = COLUMN;
do {
colno[_strlen(colno)] = (i % 10) + '0';
colno[_strlen(colno)] = '\0';
i /= 10;
} while(i > 1 || i < -1);
return _str(colno);
}();
// final string concat & format
constexpr int len = _strlen(msg);
constexpr int len2 = _strlen(colno.value);
#define PREAMBLE "Column "
char err[len + sizeof(PREAMBLE) + len2] = {};
copy_n(PREAMBLE, sizeof(PREAMBLE), err);
copy_n(colno.value, len2, err + _strlen(err));
copy_n(msg, len, err + _strlen(err));
for (int i = 0; i < _strlen(err); i++) {
if (err[i] == '%') {
err[i] = pattern.value[COLUMN];
}
}
return _str(err);
}();
// This will cause gcc to print the formatted error.
ERROR<err, LAST_STATE != 0>();
}
template <_str pattern>
constexpr auto aob_parse()
{
#define head pattern.value[cur]
// The lambda is used in order to build the "result_t" wrapper, this guarantees
// that all variables inside the struct are "constant expressions" compatible, and thus
// we can peek at the final state and pattern size during compile time.
constexpr auto ret = [&]() -> auto {
int state = 0;
int size = 0;
result_t::storage_type bytes = {};
result_t::storage_type mask = {};
int cur;
for (cur = 0; head != '\0' && state >= 0; cur++) {
switch(state) {
//State 0: ..
case 0:
if (ishex(head)) state = 1; //X.
else if (head == '?') state = 2; //?.
else if (head == ' ') state = 0; //..
else state = -1;
break;
//State 1: X.
case 1:
bytes[size++] = hex_to_int(pattern.value[cur-1], pattern.value[cur]);
if (ishex(head)) state = 0; //XX
else state = -2;
break;
//State 2: ?.
case 2:
mask[size++] = 1;
if (head == '?') state = 0; //??
else if (head == ' ') state = 0; //?s
else state = -3;
break;
}
if (state < 0)
break;
}
// This cannot compile yet due to it not "being a constant expression.":
// static_assert(state == 0, "Invalid AOB Syntax");
// - <source>: error: static_assert expression is not an integral constant expression
// So, from here on out, state, size, bytes and mask will be wrapped in 'result_t' and
// available for usage on "constant expressions" such as static_assert and etc on aob_parse.
return result_t(state, size, cur, bytes, mask);
}();
// Warn users about malformed AOB syntaxes during compile time, now possible due to the 'result_t' wrapper.
AOB_PARSE_CHECK_ERROR<pattern, ret.col, ret.state>();
// Can also use the wrapper members as template parameters.
return aob_t<ret.size>(ret);
#undef head
}
// Different instances of the templated function have their own storage, we're going to
// use that fact to keep the pattern static.
template <_str pattern, typename T>
uint8_t *aob_scan(T start, T end) {
static constexpr auto aob = aob_parse<pattern>();
return aob.scan((uint8_t *)start, (uint8_t*)end);
}
int main()
{
const char str[] = "\x01\x02\x03\xFF\xFF\xAA\xBA\xAF\x00\x00";
uint8_t *ptr1 = aob_scan<"FF AA ?? AF 00">(str, str + sizeof(str));
uint8_t *ptr2 = aob_scan<"02 03 ?? ff AA">(str, str + sizeof(str));
printf("ptr1 %p\n", (uint8_t *)((uintptr_t)ptr1 - (uintptr_t)str));
printf("ptr2 %p\n", (uint8_t *)((uintptr_t)ptr2 - (uintptr_t)str));
}
@JohnnyonFlame
Copy link
Author

JohnnyonFlame commented Nov 9, 2021

Do what the fuck you want to public license

Version 2, December 2004

Copyright (C) 2021 Johnny on Flame

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  1. You just DO WHAT THE FUCK YOU WANT TO.

@JohnnyonFlame
Copy link
Author

JohnnyonFlame commented Nov 9, 2021

Example of formatted error:

<source>: In instantiation of 'constexpr void LAYER() [with int ISERR = 1]':
<source>:127:17:   required from 'uint8_t* aob_scan(T, T) [with _str<...auto...> pattern = _str<16>{"FF AA x?? AF 00", 16}; T = const char*; uint8_t = unsigned char]'
<source>:261:48:   required from here
<source>:254:51:   in 'constexpr' expansion of 'aob_parse<_str<16>{"FF AA x?? AF 00", 16}>()'
<source>:242:55:   in 'constexpr' expansion of 'AOB_PARSE_CHECK_ERROR<_str<16>{"FF AA x?? AF 00", 16}, 6, -1>()'
<source>:184:32:   in 'constexpr' expansion of 'ERROR<_str<48>{"Column 6: Expected hex or <?>, got <x> instead.", 48}, 1>()'
<source>:121:25: error: static assertion failed
  121 |     static_assert(ISERR == 0);
      |                   ~~~~~~^~~~
<source>:121:25: note: '(1 == 0)' evaluates to false

@JohnnyonFlame
Copy link
Author

JohnnyonFlame commented Nov 9, 2021

x86-64 clang 13.0.0, -std=c++20 -O3 -ggdb
Interestingly, at O3 clang will determine these are all constants and precalculate the results, very neat code generation.

main:                                   # @main
        push    rax
        mov     edi, offset .L.str
        mov     esi, 4
        xor     eax, eax
        call    printf
        mov     edi, offset .L.str.1
        mov     esi, 1
        xor     eax, eax
        call    printf
        xor     eax, eax
        pop     rcx
        ret
_GLOBAL__sub_I_example.cpp:             # @_GLOBAL__sub_I_example.cpp
        push    rax
        mov     edi, offset std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edi, offset std::ios_base::Init::~Init() [complete object destructor]
        mov     esi, offset std::__ioinit
        mov     edx, offset __dso_handle
        pop     rax
        jmp     __cxa_atexit                    # TAILCALL
.L.str:
        .asciz  "ptr1 %p\n"

.L.str.1:
        .asciz  "ptr2 %p\n"

x86-64 clang 13.0.0, -std=c++20 -O2 -ggdb
At O2, it will instead create jump tables to scan the range, this is what a real world situation will look like with dynamic things.

main:                                   # @main
        push    r14
        push    rbx
        sub     rsp, 24
        movabs  rax, -5784122754932211199
        mov     qword ptr [rsp + 8], rax
        mov     dword ptr [rsp + 15], 175
        mov     dl, 1
        lea     rax, [rsp + 19]
        xor     esi, esi
        xor     ecx, ecx
        cmp     dl, -1
        jne     .LBB0_3
.LBB0_2:
        lea     rdx, [rsp + rcx]
        add     rdx, 9
        cmp     rdx, rax
        jae     .LBB0_3
        cmp     byte ptr [rsp + rcx + 9], -86
        jne     .LBB0_3
        lea     rdx, [rsp + rcx]
        add     rdx, 10
        cmp     rdx, rax
        jae     .LBB0_3
        lea     rdx, [rsp + rcx]
        add     rdx, 11
        cmp     rdx, rax
        jae     .LBB0_3
        cmp     byte ptr [rdx], -81
        jne     .LBB0_3
        lea     rdx, [rsp + rcx]
        add     rdx, 12
        cmp     rdx, rax
        jae     .LBB0_3
        cmp     byte ptr [rdx], 0
        je      .LBB0_11
.LBB0_3:                                # =>This Inner Loop Header: Depth=1
        cmp     rcx, 10
        je      .LBB0_12
        movzx   edx, byte ptr [rsp + rcx + 9]
        add     rcx, 1
        cmp     dl, -1
        jne     .LBB0_3
        jmp     .LBB0_2
.LBB0_11:
        lea     rsi, [rsp + rcx]
        add     rsi, 8
.LBB0_12:
        mov     dl, 1
        xor     ebx, ebx
        xor     ecx, ecx
        cmp     dl, 2
        jne     .LBB0_15
.LBB0_14:
        lea     rdx, [rsp + rcx]
        add     rdx, 9
        cmp     rdx, rax
        jae     .LBB0_15
        cmp     byte ptr [rsp + rcx + 9], 3
        jne     .LBB0_15
        lea     rdx, [rsp + rcx]
        add     rdx, 10
        cmp     rdx, rax
        jae     .LBB0_15
        lea     rdx, [rsp + rcx]
        add     rdx, 11
        cmp     rdx, rax
        jae     .LBB0_15
        cmp     byte ptr [rdx], -1
        jne     .LBB0_15
        lea     rdx, [rsp + rcx]
        add     rdx, 12
        cmp     rdx, rax
        jae     .LBB0_15
        cmp     byte ptr [rdx], -86
        je      .LBB0_23
.LBB0_15:                               # =>This Inner Loop Header: Depth=1
        cmp     rcx, 10
        je      .LBB0_24
        movzx   edx, byte ptr [rsp + rcx + 9]
        add     rcx, 1
        cmp     dl, 2
        je      .LBB0_14
        jmp     .LBB0_15
.LBB0_23:
        lea     rbx, [rsp + rcx]
        add     rbx, 8
.LBB0_24:
        lea     r14, [rsp + 8]
        sub     rsi, r14
        mov     edi, offset .L.str
        xor     eax, eax
        call    printf
        sub     rbx, r14
        mov     edi, offset .L.str.1
        mov     rsi, rbx
        xor     eax, eax
        call    printf
        xor     eax, eax
        add     rsp, 24
        pop     rbx
        pop     r14
        ret
_GLOBAL__sub_I_example.cpp:             # @_GLOBAL__sub_I_example.cpp
        push    rax
        mov     edi, offset std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edi, offset std::ios_base::Init::~Init() [complete object destructor]
        mov     esi, offset std::__ioinit
        mov     edx, offset __dso_handle
        pop     rax
        jmp     __cxa_atexit                    # TAILCALL

.L.str:
        .asciz  "ptr1 %p\n"

.L.str.1:
        .asciz  "ptr2 %p\n"

x86-64 gcc 11.2, -std=c++20 -O3 -ggdb
At this level it doesn't look much different than clang's O2. A win for clang here.

.LC0:
        .string "ptr1 %p\n"
.LC1:
        .string "ptr2 %p\n"
main:
        movabs  rax, -5784122754932211199
        push    rbp
        push    rbx
        sub     rsp, 24
        mov     BYTE PTR [rsp+15], 0
        lea     rbx, [rsp+5]
        mov     QWORD PTR [rsp+5], rax
        xor     eax, eax
        mov     rdx, rbx
        mov     WORD PTR [rsp+13], ax
        lea     rax, [rsp+6]
.L16:
        movzx   ecx, BYTE PTR [rdx]
        mov     rsi, rdx
        mov     rdx, rax
        cmp     cl, -1
        jne     .L4
        lea     rdi, [rsp+16]
        cmp     rax, rdi
        jnb     .L11
        cmp     BYTE PTR [rax], -86
        jne     .L21
        add     rax, 1
        lea     rdi, [rsp+16]
        cmp     rdi, rax
        jbe     .L16
        lea     rcx, [rdx+2]
        cmp     rdi, rcx
        jbe     .L16
        cmp     BYTE PTR [rdx+2], -81
        jne     .L16
        lea     rcx, [rdx+3]
        cmp     rdi, rcx
        jbe     .L16
        cmp     BYTE PTR [rdx+3], 0
        jne     .L16
.L3:
        mov     rdx, rbx
        lea     rax, [rsp+6]
.L17:
        movzx   ecx, BYTE PTR [rdx]
        mov     rbp, rdx
        mov     rdx, rax
        cmp     cl, 2
        jne     .L8
        lea     rdi, [rsp+16]
        cmp     rax, rdi
        jnb     .L13
        cmp     BYTE PTR [rax], 3
        jne     .L22
        add     rax, 1
        lea     rdi, [rsp+16]
        cmp     rdi, rax
        jbe     .L17
        lea     rcx, [rdx+2]
        cmp     rdi, rcx
        jbe     .L17
        cmp     BYTE PTR [rdx+2], -1
        jne     .L17
        lea     rcx, [rdx+3]
        cmp     rdi, rcx
        jbe     .L17
        cmp     BYTE PTR [rdx+3], -86
        jne     .L17
.L7:
        sub     rsi, rbx
        mov     edi, OFFSET FLAT:.LC0
        xor     eax, eax
        call    printf
        mov     rsi, rbp
        mov     edi, OFFSET FLAT:.LC1
        xor     eax, eax
        sub     rsi, rbx
        call    printf
        add     rsp, 24
        xor     eax, eax
        pop     rbx
        pop     rbp
        ret
.L4:
        lea     rsi, [rsp+16]
        cmp     rax, rsi
        jnb     .L11
.L21:
        add     rax, 1
        jmp     .L16
.L8:
        lea     rcx, [rsp+16]
        cmp     rax, rcx
        jnb     .L13
.L22:
        add     rax, 1
        jmp     .L17
.L11:
        xor     esi, esi
        jmp     .L3
.L13:
        xor     ebp, ebp
        jmp     .L7
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment