Skip to content

Commit ac4aaa9

Browse files
Support bitmasks in signatures (#35)
* Initial support for signature bitmasks * Support new syntax in signature parsing * Fix warning * Fix ANOTHER warning * Ensure zeros for non-masked bits * Add benchmark compare against STL * Update README with new syntax * Fix wording * Update README.md Co-authored-by: CrackedMatter <81803926+CrackedMatter@users.noreply.github.com> --------- Co-authored-by: CrackedMatter <81803926+CrackedMatter@users.noreply.github.com>
1 parent 4471878 commit ac4aaa9

File tree

9 files changed

+277
-113
lines changed

9 files changed

+277
-113
lines changed

README.md

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,39 @@ may break at any time without the MAJOR version number being incremented.
1818
The table below compares the single threaded throughput in bytes/s (real time) between
1919
libhat and [two other](test/benchmark/vendor) commonly used implementations for pattern
2020
scanning. The input buffers were randomly generated using a fixed seed, and the pattern
21-
scanned does not contain any match in the buffer. The benchmark was run on a system with
22-
an i7-9700K (which supports libhat's [AVX2](src/arch/x86/AVX2.cpp) scanner implementation).
21+
scanned does not contain any match in the buffer. The benchmark was compiled on Windows
22+
with `clang-cl` 21.1.1, using the MSVC 14.44.35207 toolchain and the default release mode
23+
flags (`/GR /EHsc /MD /O2 /Ob2`). The benchmark was run on a system with an i7-14700K
24+
(supporting [AVX2](src/arch/x86/AVX2.cpp)) and 64GB (4x16GB) DDR5 6000 MT/s (30-38-38-96).
2325
The full source code is available [here](test/benchmark/Compare.cpp).
2426
```
25-
---------------------------------------------------------------------------------------
26-
Benchmark Time CPU Iterations bytes_per_second
27-
---------------------------------------------------------------------------------------
28-
BM_Throughput_Libhat/4MiB 131578 ns 48967 ns 21379 29.6876Gi/s
29-
BM_Throughput_Libhat/16MiB 813977 ns 413524 ns 3514 19.1959Gi/s
30-
BM_Throughput_Libhat/128MiB 6910936 ns 3993486 ns 403 18.0873Gi/s
31-
BM_Throughput_Libhat/256MiB 13959379 ns 8121906 ns 202 17.9091Gi/s
32-
33-
BM_Throughput_UC1/4MiB 4739731 ns 2776015 ns 591 843.93Mi/s
34-
BM_Throughput_UC1/16MiB 19011485 ns 10841837 ns 147 841.597Mi/s
35-
BM_Throughput_UC1/128MiB 152277511 ns 82465278 ns 18 840.571Mi/s
36-
BM_Throughput_UC1/256MiB 304964544 ns 180555556 ns 9 839.442Mi/s
37-
38-
BM_Throughput_UC2/4MiB 9633499 ns 4617698 ns 291 415.218Mi/s
39-
BM_Throughput_UC2/16MiB 38507193 ns 22474315 ns 73 415.507Mi/s
40-
BM_Throughput_UC2/128MiB 307989100 ns 164930556 ns 9 415.599Mi/s
41-
BM_Throughput_UC2/256MiB 616449240 ns 331250000 ns 5 415.282Mi/s
27+
---------------------------------------------------------------------------------------------------
28+
Benchmark Time CPU Iterations bytes_per_second
29+
---------------------------------------------------------------------------------------------------
30+
BM_Throughput_libhat/4MiB 67686 ns 67816 ns 82254 57.7110Gi/s
31+
BM_Throughput_libhat/16MiB 319801 ns 319558 ns 18287 48.8585Gi/s
32+
BM_Throughput_libhat/128MiB 5325733 ns 5282315 ns 1056 23.4709Gi/s
33+
BM_Throughput_libhat/256MiB 10921878 ns 10814951 ns 510 22.8898Gi/s
34+
35+
BM_Throughput_std_search/4MiB 1364050 ns 1361672 ns 4108 2.86372Gi/s
36+
BM_Throughput_std_search/16MiB 5470025 ns 5458783 ns 1019 2.85648Gi/s
37+
BM_Throughput_std_search/128MiB 43622456 ns 43483527 ns 129 2.86550Gi/s
38+
BM_Throughput_std_search/256MiB 88093320 ns 87158203 ns 64 2.83790Gi/s
39+
40+
BM_Throughput_std_find_std_equal/4MiB 178567 ns 178586 ns 31410 21.8755Gi/s
41+
BM_Throughput_std_find_std_equal/16MiB 806394 ns 805228 ns 7005 19.3764Gi/s
42+
BM_Throughput_std_find_std_equal/128MiB 8944718 ns 8953652 ns 623 13.9747Gi/s
43+
BM_Throughput_std_find_std_equal/256MiB 18092713 ns 18102751 ns 309 13.8177Gi/s
44+
45+
BM_Throughput_UC1/4MiB 1727027 ns 1721236 ns 3268 2.26183Gi/s
46+
BM_Throughput_UC1/16MiB 6878188 ns 6849054 ns 819 2.27167Gi/s
47+
BM_Throughput_UC1/128MiB 55181849 ns 55300245 ns 102 2.26524Gi/s
48+
BM_Throughput_UC1/256MiB 110209374 ns 110000000 ns 50 2.26841Gi/s
49+
50+
BM_Throughput_UC2/4MiB 4011942 ns 4001524 ns 1394 997.023Mi/s
51+
BM_Throughput_UC2/16MiB 16136510 ns 16166908 ns 346 991.540Mi/s
52+
BM_Throughput_UC2/128MiB 130954740 ns 130087209 ns 43 977.437Mi/s
53+
BM_Throughput_UC2/256MiB 261157833 ns 261160714 ns 21 980.250Mi/s
4254
```
4355

4456
## Platforms
@@ -60,16 +72,53 @@ Below is a summary of the support of libhat OS APIs on various platforms:
6072
| `hp::module::for_each_segment` ||| |
6173

6274
## Quick start
63-
### Pattern scanning
75+
### Defining patterns
76+
libhat's signature syntax consists of space-delimited tokens and is backwards compatible with IDA syntax:
77+
78+
- 8 character sequences are interpreted as binary
79+
- 2 character sequences are interpreted as hex
80+
- 1 character must be a wildcard (`?`)
81+
82+
Any digit can be substituted for a wildcard, for example:
83+
- `????1111` is a binary sequence, and matches any byte with all ones in the lower nibble
84+
- `A?` is a hex sequence, and matches any byte of the form `1010????`
85+
- Both `????????` and `??` are equivalent to `?`, and will match any byte
86+
87+
A complete pattern might look like `AB ? 12 ?3`. This matches any 4-byte
88+
subrange `s` for which all the following conditions are met:
89+
- `s[0] == 0xAB`
90+
- `s[2] == 0x12`
91+
- `s[3] & 0x0F == 0x03`
92+
93+
Due to how various scanning algorithms are implemented, there are some restrictions when defining a pattern:
94+
95+
1) A pattern must contain at least one fully masked byte (i.e. `AB` or `10011001`)
96+
2) The first byte with a non-zero mask must have a full mask
97+
- `?1 02` is disallowed
98+
- `01 02` is allowed
99+
- `?? 01` is allowed
100+
101+
In code, there are a few ways to initialize a signature from its string representation:
102+
64103
```cpp
65104
#include <libhat/scanner.hpp>
66105

67106
// Parse a pattern's string representation to an array of bytes at compile time
68107
constexpr hat::fixed_signature pattern = hat::compile_signature<"48 8D 05 ? ? ? ? E8">();
69108

70-
// ...or parse it at runtime
109+
// Parse using the UDLs at compile time
110+
using namespace hat::literals;
111+
constexpr hat::fixed_signature pattern = "48 8D 05 ? ? ? ? E8"_sig; // stack owned
112+
constexpr hat::signature_view pattern = "48 8D 05 ? ? ? ? E8"_sigv; // static lifetime (requires C++23)
113+
114+
// Parse it at runtime
71115
using parsed_t = hat::result<hat::signature, hat::signature_parse_error>;
72116
parsed_t runtime_pattern = hat::parse_signature("48 8D 05 ? ? ? ? E8");
117+
```
118+
119+
### Scanning patterns
120+
```cpp
121+
#include <libhat/scanner.hpp>
73122
74123
// Scan for this pattern using your CPU's vectorization features
75124
auto begin = /* a contiguous iterator over std::byte */;
@@ -97,6 +146,21 @@ const std::byte* address = result.get();
97146
const std::byte* relative_address = result.rel(3);
98147
```
99148

149+
libhat has a few optimizations for searching for patterns in `x86_64` machine code:
150+
```cpp
151+
#include <libhat/scanner.hpp>
152+
153+
// If a byte pattern matches at the start of a function, the result will be aligned on 16-bytes.
154+
// This can be indicated via the defaulted `alignment` parameter (all overloads have this parameter):
155+
std::span<std::byte> range = /* ... */;
156+
hat::signature_view pattern = /* ... */;
157+
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X16);
158+
159+
// Additionally, x86_64 contains a non-uniform distribution of byte pairs. By passing the `x86_64`
160+
// scan hint, the search can be based on the least common byte pair that is found in the pattern.
161+
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64);
162+
```
163+
100164
### Accessing offsets
101165
```cpp
102166
#include <libhat/access.hpp>

include/libhat/scanner.hpp

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,7 @@ namespace hat::detail {
269269
break;
270270
}
271271
// Compare everything after the first byte
272-
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1, [](auto opt, auto byte) {
273-
return !opt.has_value() || *opt == byte;
274-
});
272+
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1);
275273
if (match) LIBHAT_UNLIKELY {
276274
return i;
277275
}
@@ -293,9 +291,7 @@ namespace hat::detail {
293291

294292
for (auto i = scanBegin; i != scanEnd; i += 16) {
295293
if (*i == firstByte) {
296-
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1, [](auto opt, auto byte) {
297-
return !opt.has_value() || *opt == byte;
298-
});
294+
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1);
299295
if (match) LIBHAT_UNLIKELY {
300296
return i;
301297
}
@@ -318,7 +314,7 @@ namespace hat::detail {
318314
// Truncate the leading wildcards from the signature
319315
size_t offset = 0;
320316
for (const auto& elem : signature) {
321-
if (elem.has_value()) {
317+
if (elem.any()) {
322318
break;
323319
}
324320
offset++;

include/libhat/signature.hpp

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ LIBHAT_EXPORT namespace hat {
2020
/// Effectively std::optional<std::byte>, but with the added flexibility of being able to use std::bit_cast on
2121
/// instances of the class in constant expressions.
2222
struct signature_element {
23-
constexpr signature_element() noexcept {}
23+
constexpr signature_element() noexcept = default;
2424
constexpr signature_element(std::nullopt_t) noexcept {}
25-
constexpr signature_element(const std::byte valueIn) noexcept : val(valueIn), present(true) {}
25+
constexpr signature_element(const std::byte value) noexcept : value_{value}, mask_{0xFF} {}
26+
constexpr signature_element(const std::byte value, const std::byte mask) noexcept : value_{value & mask}, mask_{mask} {}
2627

2728
constexpr signature_element& operator=(std::nullopt_t) noexcept {
2829
return *this = signature_element{};
@@ -36,24 +37,49 @@ LIBHAT_EXPORT namespace hat {
3637
*this = std::nullopt;
3738
}
3839

39-
[[nodiscard]] constexpr bool has_value() const noexcept {
40-
return this->present;
41-
}
42-
4340
[[nodiscard]] constexpr std::byte value() const noexcept {
44-
return this->val;
41+
return this->value_;
4542
}
4643

47-
[[nodiscard]] constexpr operator bool() const noexcept {
48-
return this->has_value();
44+
[[nodiscard]] constexpr std::byte mask() const noexcept {
45+
return this->mask_;
4946
}
5047

5148
[[nodiscard]] constexpr std::byte operator*() const noexcept {
5249
return this->value();
5350
}
51+
52+
[[nodiscard]] constexpr bool all() const noexcept {
53+
return this->mask_ == std::byte{0xFF};
54+
}
55+
56+
[[nodiscard]] constexpr bool any() const noexcept {
57+
return this->mask_ != std::byte{0x00};
58+
}
59+
60+
[[nodiscard]] constexpr bool none() const noexcept {
61+
return this->mask_ == std::byte{0x00};
62+
}
63+
64+
[[nodiscard]] constexpr bool has(const uint8_t digit) const noexcept {
65+
const auto m = std::to_integer<uint8_t>(this->mask_);
66+
return (m & (1u << digit)) != 0;
67+
}
68+
69+
[[nodiscard]] constexpr bool at(const uint8_t digit) const noexcept {
70+
const auto v = std::to_integer<uint8_t>(this->value_);
71+
return (v & (1u << digit)) != 0;
72+
}
73+
74+
[[nodiscard]] constexpr std::strong_ordering operator<=>(const signature_element& other) const noexcept = default;
75+
76+
[[nodiscard]] constexpr bool operator==(const std::byte byte) const noexcept {
77+
return (byte & this->mask_) == this->value_;
78+
}
79+
5480
private:
55-
std::byte val{};
56-
bool present = false;
81+
std::byte value_{};
82+
std::byte mask_{};
5783
};
5884

5985
using signature = std::vector<signature_element>;
@@ -107,26 +133,68 @@ LIBHAT_EXPORT namespace hat {
107133
empty_signature,
108134
};
109135

110-
[[nodiscard]] LIBHAT_CONSTEXPR_RESULT result<size_t, signature_parse_error> parse_signature_to(std::output_iterator<signature_element> auto out, std::string_view str) {
136+
namespace detail {
137+
138+
LIBHAT_CONSTEXPR_RESULT std::optional<signature_element> parse_signature_element(const std::string_view str, const uint8_t base) {
139+
uint8_t value{};
140+
uint8_t mask{};
141+
for (auto& ch : str) {
142+
value *= base;
143+
mask *= base;
144+
if (ch != '?') {
145+
auto digit = hat::parse_int<uint8_t>(&ch, &ch + 1, base);
146+
if (!digit.has_value()) [[unlikely]] {
147+
return std::nullopt;
148+
}
149+
value += digit.value();
150+
mask += static_cast<uint8_t>(base - 1);
151+
}
152+
}
153+
154+
return signature_element{std::byte{value}, std::byte{mask}};
155+
}
156+
}
157+
158+
[[nodiscard]] LIBHAT_CONSTEXPR_RESULT result<size_t, signature_parse_error> parse_signature_to(std::output_iterator<signature_element> auto out, const std::string_view str) {
111159
size_t written = 0;
112160
bool containsByte = false;
113-
for (const auto& word : str | std::views::split(' ')) {
114-
if (word.empty()) {
115-
continue;
116-
}
117-
if (word[0] == '?') {
118-
*out++ = signature_element{std::nullopt};
119-
written++;
120-
} else {
121-
const auto sv = std::string_view{word.begin(), word.end()};
122-
const auto parsed = parse_int<uint8_t>(sv, 16);
123-
if (parsed.has_value()) {
124-
*out++ = signature_element{static_cast<std::byte>(parsed.value())};
161+
162+
for (auto&& sub : str | std::views::split(' ')) {
163+
const std::string_view word{sub.begin(), sub.end()};
164+
switch (word.size()) {
165+
case 0: {
166+
continue;
167+
}
168+
case 1: {
169+
if (word.front() != '?') {
170+
return result_error{signature_parse_error::parse_error};
171+
}
172+
*out++ = signature_element{std::nullopt};
125173
written++;
126-
} else {
174+
break;
175+
}
176+
case 2:
177+
case 8: {
178+
const uint8_t base = word.size() == 2 ? 16 : 2;
179+
auto element = detail::parse_signature_element(word, base);
180+
if (element) {
181+
*out++ = *element;
182+
written++;
183+
184+
if (!containsByte && element->any()) {
185+
if (!element->all()) {
186+
return result_error{signature_parse_error::missing_byte};
187+
}
188+
containsByte = true;
189+
}
190+
} else {
191+
return result_error{signature_parse_error::parse_error};
192+
}
193+
break;
194+
}
195+
default: {
127196
return result_error{signature_parse_error::parse_error};
128197
}
129-
containsByte = true;
130198
}
131199
}
132200
if (written == 0) {
@@ -179,14 +247,28 @@ LIBHAT_EXPORT namespace hat {
179247
std::string ret;
180248
ret.reserve(signature.size() * 3);
181249
for (auto& element : signature) {
182-
if (element.has_value()) {
250+
const bool a = (element.mask() & std::byte{0xF0}) == std::byte{0xF0};
251+
const bool b = (element.mask() & std::byte{0x0F}) == std::byte{0x0F};
252+
if (a || b) {
183253
ret += {
184-
hex[static_cast<size_t>(element.value() >> 4) & 0xFu],
185-
hex[static_cast<size_t>(element.value() >> 0) & 0xFu],
254+
a ? hex[static_cast<size_t>(element.value() >> 4) & 0xFu] : '?',
255+
b ? hex[static_cast<size_t>(element.value() >> 0) & 0xFu] : '?',
186256
' '
187257
};
188-
} else {
258+
} else if (element.none()) {
189259
ret += "? ";
260+
} else {
261+
ret += {
262+
element.has(7) ? (element.at(7) ? '1' : '0') : '?',
263+
element.has(6) ? (element.at(6) ? '1' : '0') : '?',
264+
element.has(5) ? (element.at(5) ? '1' : '0') : '?',
265+
element.has(4) ? (element.at(4) ? '1' : '0') : '?',
266+
element.has(3) ? (element.at(3) ? '1' : '0') : '?',
267+
element.has(2) ? (element.at(2) ? '1' : '0') : '?',
268+
element.has(1) ? (element.at(1) ? '1' : '0') : '?',
269+
element.has(0) ? (element.at(0) ? '1' : '0') : '?',
270+
' '
271+
};
190272
}
191273
}
192274
ret.pop_back();

include/libhat/strconv.hpp

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@ LIBHAT_EXPORT namespace hat {
2727
const int digits = base < 10 ? base : 10;
2828
const int letters = base > 10 ? base - 10 : 0;
2929

30-
for (auto iter = begin; iter != end; iter++) {
31-
const char ch = *iter;
32-
33-
if constexpr (std::is_signed_v<Integer>) {
34-
if (iter == begin) {
35-
if (ch == '+') {
36-
continue;
37-
} else if (ch == '-') {
38-
sign = -1;
39-
continue;
40-
}
30+
auto iter = begin;
31+
if constexpr (std::is_signed_v<Integer>) {
32+
if (iter != end) {
33+
if (*iter == '+') {
34+
iter++;
35+
} else if (*iter == '-') {
36+
sign = -1;
37+
iter++;
4138
}
4239
}
40+
}
4341

42+
for (; iter != end; iter++) {
43+
const char ch = *iter;
4444
value *= base;
4545
if (ch >= '0' && ch < '0' + digits) {
4646
value += static_cast<Integer>(ch - '0');
@@ -49,7 +49,6 @@ LIBHAT_EXPORT namespace hat {
4949
} else if (ch >= 'a' && ch < 'a' + letters) {
5050
value += static_cast<Integer>(ch - 'a' + 10);
5151
} else {
52-
// Throws an exception at runtime AND prevents constexpr evaluation
5352
return result_error{parse_int_error::illegal_char};
5453
}
5554
}

0 commit comments

Comments
 (0)