Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 85 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,39 @@ may break at any time without the MAJOR version number being incremented.
The table below compares the single threaded throughput in bytes/s (real time) between
libhat and [two other](test/benchmark/vendor) commonly used implementations for pattern
scanning. The input buffers were randomly generated using a fixed seed, and the pattern
scanned does not contain any match in the buffer. The benchmark was run on a system with
an i7-9700K (which supports libhat's [AVX2](src/arch/x86/AVX2.cpp) scanner implementation).
scanned does not contain any match in the buffer. The benchmark was compiled on Windows
with `clang-cl` 21.1.1, using the MSVC 14.44.35207 toolchain and the default release mode
flags (`/GR /EHsc /MD /O2 /Ob2`). The benchmark was run on a system with an i7-14700K
(supporting [AVX2](src/arch/x86/AVX2.cpp)) and 64GB (4x16GB) DDR5 6000 MT/s (30-38-38-96).
The full source code is available [here](test/benchmark/Compare.cpp).
```
---------------------------------------------------------------------------------------
Benchmark Time CPU Iterations bytes_per_second
---------------------------------------------------------------------------------------
BM_Throughput_Libhat/4MiB 131578 ns 48967 ns 21379 29.6876Gi/s
BM_Throughput_Libhat/16MiB 813977 ns 413524 ns 3514 19.1959Gi/s
BM_Throughput_Libhat/128MiB 6910936 ns 3993486 ns 403 18.0873Gi/s
BM_Throughput_Libhat/256MiB 13959379 ns 8121906 ns 202 17.9091Gi/s

BM_Throughput_UC1/4MiB 4739731 ns 2776015 ns 591 843.93Mi/s
BM_Throughput_UC1/16MiB 19011485 ns 10841837 ns 147 841.597Mi/s
BM_Throughput_UC1/128MiB 152277511 ns 82465278 ns 18 840.571Mi/s
BM_Throughput_UC1/256MiB 304964544 ns 180555556 ns 9 839.442Mi/s

BM_Throughput_UC2/4MiB 9633499 ns 4617698 ns 291 415.218Mi/s
BM_Throughput_UC2/16MiB 38507193 ns 22474315 ns 73 415.507Mi/s
BM_Throughput_UC2/128MiB 307989100 ns 164930556 ns 9 415.599Mi/s
BM_Throughput_UC2/256MiB 616449240 ns 331250000 ns 5 415.282Mi/s
---------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations bytes_per_second
---------------------------------------------------------------------------------------------------
BM_Throughput_libhat/4MiB 67686 ns 67816 ns 82254 57.7110Gi/s
BM_Throughput_libhat/16MiB 319801 ns 319558 ns 18287 48.8585Gi/s
BM_Throughput_libhat/128MiB 5325733 ns 5282315 ns 1056 23.4709Gi/s
BM_Throughput_libhat/256MiB 10921878 ns 10814951 ns 510 22.8898Gi/s

BM_Throughput_std_search/4MiB 1364050 ns 1361672 ns 4108 2.86372Gi/s
BM_Throughput_std_search/16MiB 5470025 ns 5458783 ns 1019 2.85648Gi/s
BM_Throughput_std_search/128MiB 43622456 ns 43483527 ns 129 2.86550Gi/s
BM_Throughput_std_search/256MiB 88093320 ns 87158203 ns 64 2.83790Gi/s

BM_Throughput_std_find_std_equal/4MiB 178567 ns 178586 ns 31410 21.8755Gi/s
BM_Throughput_std_find_std_equal/16MiB 806394 ns 805228 ns 7005 19.3764Gi/s
BM_Throughput_std_find_std_equal/128MiB 8944718 ns 8953652 ns 623 13.9747Gi/s
BM_Throughput_std_find_std_equal/256MiB 18092713 ns 18102751 ns 309 13.8177Gi/s

BM_Throughput_UC1/4MiB 1727027 ns 1721236 ns 3268 2.26183Gi/s
BM_Throughput_UC1/16MiB 6878188 ns 6849054 ns 819 2.27167Gi/s
BM_Throughput_UC1/128MiB 55181849 ns 55300245 ns 102 2.26524Gi/s
BM_Throughput_UC1/256MiB 110209374 ns 110000000 ns 50 2.26841Gi/s

BM_Throughput_UC2/4MiB 4011942 ns 4001524 ns 1394 997.023Mi/s
BM_Throughput_UC2/16MiB 16136510 ns 16166908 ns 346 991.540Mi/s
BM_Throughput_UC2/128MiB 130954740 ns 130087209 ns 43 977.437Mi/s
BM_Throughput_UC2/256MiB 261157833 ns 261160714 ns 21 980.250Mi/s
```

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

## Quick start
### Pattern scanning
### Defining patterns
libhat's signature syntax consists of space-delimited tokens and is backwards compatible with IDA syntax:

- 8 character sequences are interpreted as binary
- 2 character sequences are interpreted as hex
- 1 character must be a wildcard (`?`)

Any digit can be substituted for a wildcard, for example:
- `????1111` is a binary sequence, and matches any byte with all ones in the lower nibble
- `A?` is a hex sequence, and matches any byte of the form `1010????`
- Both `????????` and `??` are equivalent to `?`, and will match any byte

A complete pattern might look like `AB ? 12 ?3`. This matches any 4-byte
subrange `s` for which all the following conditions are met:
- `s[0] == 0xAB`
- `s[2] == 0x12`
- `s[3] & 0x0F == 0x03`

Due to how various scanning algorithms are implemented, there are some restrictions when defining a pattern:

1) A pattern must contain at least one fully masked byte (i.e. `AB` or `10011001`)
2) The first byte with a non-zero mask must have a full mask
- `?1 02` is disallowed
- `01 02` is allowed
- `?? 01` is allowed

In code, there are a few ways to initialize a signature from its string representation:

```cpp
#include <libhat/scanner.hpp>

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

// ...or parse it at runtime
// Parse using the UDLs at compile time
using namespace hat::literals;
constexpr hat::fixed_signature pattern = "48 8D 05 ? ? ? ? E8"_sig; // stack owned
constexpr hat::signature_view pattern = "48 8D 05 ? ? ? ? E8"_sigv; // static lifetime (requires C++23)

// Parse it at runtime
using parsed_t = hat::result<hat::signature, hat::signature_parse_error>;
parsed_t runtime_pattern = hat::parse_signature("48 8D 05 ? ? ? ? E8");
```

### Scanning patterns
```cpp
#include <libhat/scanner.hpp>

// Scan for this pattern using your CPU's vectorization features
auto begin = /* a contiguous iterator over std::byte */;
Expand Down Expand Up @@ -97,6 +146,21 @@ const std::byte* address = result.get();
const std::byte* relative_address = result.rel(3);
```

libhat has a few optimizations for searching for patterns in `x86_64` machine code:
```cpp
#include <libhat/scanner.hpp>

// If a byte pattern matches at the start of a function, the result will be aligned on 16-bytes.
// This can be indicated via the defaulted `alignment` parameter (all overloads have this parameter):
std::span<std::byte> range = /* ... */;
hat::signature_view pattern = /* ... */;
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X16);

// Additionally, x86_64 contains a non-uniform distribution of byte pairs. By passing the `x86_64`
// scan hint, the search can be based on the least common byte pair that is found in the pattern.
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64);
```

### Accessing offsets
```cpp
#include <libhat/access.hpp>
Expand Down
10 changes: 3 additions & 7 deletions include/libhat/scanner.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,7 @@ namespace hat::detail {
break;
}
// Compare everything after the first byte
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1, [](auto opt, auto byte) {
return !opt.has_value() || *opt == byte;
});
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1);
if (match) LIBHAT_UNLIKELY {
return i;
}
Expand All @@ -293,9 +291,7 @@ namespace hat::detail {

for (auto i = scanBegin; i != scanEnd; i += 16) {
if (*i == firstByte) {
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1, [](auto opt, auto byte) {
return !opt.has_value() || *opt == byte;
});
auto match = std::equal(signature.begin() + 1, signature.end(), i + 1);
if (match) LIBHAT_UNLIKELY {
return i;
}
Expand All @@ -318,7 +314,7 @@ namespace hat::detail {
// Truncate the leading wildcards from the signature
size_t offset = 0;
for (const auto& elem : signature) {
if (elem.has_value()) {
if (elem.any()) {
break;
}
offset++;
Expand Down
142 changes: 112 additions & 30 deletions include/libhat/signature.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ LIBHAT_EXPORT namespace hat {
/// Effectively std::optional<std::byte>, but with the added flexibility of being able to use std::bit_cast on
/// instances of the class in constant expressions.
struct signature_element {
constexpr signature_element() noexcept {}
constexpr signature_element() noexcept = default;
constexpr signature_element(std::nullopt_t) noexcept {}
constexpr signature_element(const std::byte valueIn) noexcept : val(valueIn), present(true) {}
constexpr signature_element(const std::byte value) noexcept : value_{value}, mask_{0xFF} {}
constexpr signature_element(const std::byte value, const std::byte mask) noexcept : value_{value & mask}, mask_{mask} {}

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

[[nodiscard]] constexpr bool has_value() const noexcept {
return this->present;
}

[[nodiscard]] constexpr std::byte value() const noexcept {
return this->val;
return this->value_;
}

[[nodiscard]] constexpr operator bool() const noexcept {
return this->has_value();
[[nodiscard]] constexpr std::byte mask() const noexcept {
return this->mask_;
}

[[nodiscard]] constexpr std::byte operator*() const noexcept {
return this->value();
}

[[nodiscard]] constexpr bool all() const noexcept {
return this->mask_ == std::byte{0xFF};
}

[[nodiscard]] constexpr bool any() const noexcept {
return this->mask_ != std::byte{0x00};
}

[[nodiscard]] constexpr bool none() const noexcept {
return this->mask_ == std::byte{0x00};
}

[[nodiscard]] constexpr bool has(const uint8_t digit) const noexcept {
const auto m = std::to_integer<uint8_t>(this->mask_);
return (m & (1u << digit)) != 0;
}

[[nodiscard]] constexpr bool at(const uint8_t digit) const noexcept {
const auto v = std::to_integer<uint8_t>(this->value_);
return (v & (1u << digit)) != 0;
}

[[nodiscard]] constexpr std::strong_ordering operator<=>(const signature_element& other) const noexcept = default;

[[nodiscard]] constexpr bool operator==(const std::byte byte) const noexcept {
return (byte & this->mask_) == this->value_;
}

private:
std::byte val{};
bool present = false;
std::byte value_{};
std::byte mask_{};
};

using signature = std::vector<signature_element>;
Expand Down Expand Up @@ -107,26 +133,68 @@ LIBHAT_EXPORT namespace hat {
empty_signature,
};

[[nodiscard]] LIBHAT_CONSTEXPR_RESULT result<size_t, signature_parse_error> parse_signature_to(std::output_iterator<signature_element> auto out, std::string_view str) {
namespace detail {

LIBHAT_CONSTEXPR_RESULT std::optional<signature_element> parse_signature_element(const std::string_view str, const uint8_t base) {
uint8_t value{};
uint8_t mask{};
for (auto& ch : str) {
value *= base;
mask *= base;
if (ch != '?') {
auto digit = hat::parse_int<uint8_t>(&ch, &ch + 1, base);
if (!digit.has_value()) [[unlikely]] {
return std::nullopt;
}
value += digit.value();
mask += static_cast<uint8_t>(base - 1);
}
}

return signature_element{std::byte{value}, std::byte{mask}};
}
}

[[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) {
size_t written = 0;
bool containsByte = false;
for (const auto& word : str | std::views::split(' ')) {
if (word.empty()) {
continue;
}
if (word[0] == '?') {
*out++ = signature_element{std::nullopt};
written++;
} else {
const auto sv = std::string_view{word.begin(), word.end()};
const auto parsed = parse_int<uint8_t>(sv, 16);
if (parsed.has_value()) {
*out++ = signature_element{static_cast<std::byte>(parsed.value())};

for (auto&& sub : str | std::views::split(' ')) {
const std::string_view word{sub.begin(), sub.end()};
switch (word.size()) {
case 0: {
continue;
}
case 1: {
if (word.front() != '?') {
return result_error{signature_parse_error::parse_error};
}
*out++ = signature_element{std::nullopt};
written++;
} else {
break;
}
case 2:
case 8: {
const uint8_t base = word.size() == 2 ? 16 : 2;
auto element = detail::parse_signature_element(word, base);
if (element) {
*out++ = *element;
written++;

if (!containsByte && element->any()) {
if (!element->all()) {
return result_error{signature_parse_error::missing_byte};
}
containsByte = true;
}
} else {
return result_error{signature_parse_error::parse_error};
}
break;
}
default: {
return result_error{signature_parse_error::parse_error};
}
containsByte = true;
}
}
if (written == 0) {
Expand Down Expand Up @@ -179,14 +247,28 @@ LIBHAT_EXPORT namespace hat {
std::string ret;
ret.reserve(signature.size() * 3);
for (auto& element : signature) {
if (element.has_value()) {
const bool a = (element.mask() & std::byte{0xF0}) == std::byte{0xF0};
const bool b = (element.mask() & std::byte{0x0F}) == std::byte{0x0F};
if (a || b) {
ret += {
hex[static_cast<size_t>(element.value() >> 4) & 0xFu],
hex[static_cast<size_t>(element.value() >> 0) & 0xFu],
a ? hex[static_cast<size_t>(element.value() >> 4) & 0xFu] : '?',
b ? hex[static_cast<size_t>(element.value() >> 0) & 0xFu] : '?',
' '
};
} else {
} else if (element.none()) {
ret += "? ";
} else {
ret += {
element.has(7) ? (element.at(7) ? '1' : '0') : '?',
element.has(6) ? (element.at(6) ? '1' : '0') : '?',
element.has(5) ? (element.at(5) ? '1' : '0') : '?',
element.has(4) ? (element.at(4) ? '1' : '0') : '?',
element.has(3) ? (element.at(3) ? '1' : '0') : '?',
element.has(2) ? (element.at(2) ? '1' : '0') : '?',
element.has(1) ? (element.at(1) ? '1' : '0') : '?',
element.has(0) ? (element.at(0) ? '1' : '0') : '?',
' '
};
}
}
ret.pop_back();
Expand Down
23 changes: 11 additions & 12 deletions include/libhat/strconv.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@ LIBHAT_EXPORT namespace hat {
const int digits = base < 10 ? base : 10;
const int letters = base > 10 ? base - 10 : 0;

for (auto iter = begin; iter != end; iter++) {
const char ch = *iter;

if constexpr (std::is_signed_v<Integer>) {
if (iter == begin) {
if (ch == '+') {
continue;
} else if (ch == '-') {
sign = -1;
continue;
}
auto iter = begin;
if constexpr (std::is_signed_v<Integer>) {
if (iter != end) {
if (*iter == '+') {
iter++;
} else if (*iter == '-') {
sign = -1;
iter++;
}
}
}

for (; iter != end; iter++) {
const char ch = *iter;
value *= base;
if (ch >= '0' && ch < '0' + digits) {
value += static_cast<Integer>(ch - '0');
Expand All @@ -49,7 +49,6 @@ LIBHAT_EXPORT namespace hat {
} else if (ch >= 'a' && ch < 'a' + letters) {
value += static_cast<Integer>(ch - 'a' + 10);
} else {
// Throws an exception at runtime AND prevents constexpr evaluation
return result_error{parse_int_error::illegal_char};
}
}
Expand Down
Loading