Last active
August 3, 2023 04:55
-
-
Save JohnnyonFlame/5ccd797d5681bf691eb18e581ddb3239 to your computer and use it in GitHub Desktop.
Compile-time parsed AOB patterns example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# 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)); | |
} |
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
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
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