From c1605f9faab99ee12d10277422f5186f4b36ac66 Mon Sep 17 00:00:00 2001 From: Brady Date: Sun, 14 Jun 2026 03:23:01 -0500 Subject: [PATCH 01/66] Initial Neon implementation using sse2neon --- CMakeLists.txt | 16 ++++- include/libhat/scanner.hpp | 1 + include/libhat/system.hpp | 5 +- src/Scanner.cpp | 7 +- src/arch/arm/Neon.cpp | 136 +++++++++++++++++++++++++++++++++++++ src/arch/arm/System.cpp | 8 ++- test/tests/Scanner.cpp | 8 +++ 7 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/arch/arm/Neon.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b667fe5..ca229c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,11 +68,25 @@ set(LIBHAT_SRC src/arch/x86/AVX512.cpp src/arch/x86/System.cpp - src/arch/arm/System.cpp) + src/arch/arm/Neon.cpp + src/arch/arm/System.cpp +) add_library(libhat STATIC ${LIBHAT_SRC}) add_library(libhat::libhat ALIAS libhat) +include(FetchContent) +FetchContent_Declare( + sse2neon + GIT_REPOSITORY https://github.com/DLTcollab/sse2neon + GIT_TAG 92f6de174717aef09033ad21568d5bb9e5470404 # v1.9.1 +) +FetchContent_MakeAvailable(sse2neon) +if (MSVC) + set_source_files_properties(src/arch/arm/Neon.cpp PROPERTIES COMPILE_FLAGS "/Zc:preprocessor") +endif() +target_include_directories(libhat PRIVATE ${sse2neon_SOURCE_DIR}) + if(UNIX) set_target_properties(libhat PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() diff --git a/include/libhat/scanner.hpp b/include/libhat/scanner.hpp index 1a53668..3d62343 100644 --- a/include/libhat/scanner.hpp +++ b/include/libhat/scanner.hpp @@ -149,6 +149,7 @@ namespace hat::detail { SSE, // x86/x64 SSE 4.1 AVX2, // x86/x64 AVX2 AVX512, // x64 AVX512 + Neon, // ARMv7+ Neon }; class scan_context { diff --git a/include/libhat/system.hpp b/include/libhat/system.hpp index 80c14e8..f13a6cc 100644 --- a/include/libhat/system.hpp +++ b/include/libhat/system.hpp @@ -52,8 +52,11 @@ LIBHAT_EXPORT namespace hat { LIBHAT_EXPORT namespace hat { struct system_info_arm : hat::system_info { + struct { + bool neon; + } extensions{}; private: - system_info_arm() = default; + system_info_arm(); friend const system_info_arm& get_system(); static const system_info_arm instance; }; diff --git a/src/Scanner.cpp b/src/Scanner.cpp index fb3e112..04917ba 100644 --- a/src/Scanner.cpp +++ b/src/Scanner.cpp @@ -66,8 +66,8 @@ namespace hat::detail { template<> scan_function_t resolve_scanner(scan_context& context) { -#if defined(LIBHAT_X86) || defined(LIBHAT_X86_64) const auto& ext = get_system().extensions; +#if defined(LIBHAT_X86) || defined(LIBHAT_X86_64) if (ext.bmi) { #if defined(LIBHAT_X86_64) && !defined(LIBHAT_DISABLE_AVX512) if (ext.avx512f && ext.avx512bw) { @@ -83,6 +83,11 @@ namespace hat::detail { return resolve_scanner(context); } #endif +#endif +#if defined(LIBHAT_ARM) || defined(LIBHAT_AARCH64) + if (ext.neon) { + return resolve_scanner(context); + } #endif // If none of the vectorized implementations are available/supported, then fallback to scanning per-byte return resolve_scanner(context); diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp new file mode 100644 index 0000000..0968c69 --- /dev/null +++ b/src/arch/arm/Neon.cpp @@ -0,0 +1,136 @@ +#include + +#if defined(LIBHAT_ARM) || defined(LIBHAT_AARCH64) + +#include + +#include + +#ifdef _MSC_VER + namespace hat::detail { + inline unsigned long bsf(unsigned long num) noexcept { + unsigned long offset; + _BitScanForward(&offset, num); + return offset; + } + } + +#define LIBHAT_BSF32(num) hat::detail::bsf(num) +#else +#define LIBHAT_BSF32(num) __builtin_ctz(num) +#endif + +namespace hat::detail { + + inline void load_signature_128(const signature_view signature, __m128i& bytes, __m128i& mask) { + std::byte byteBuffer[16]{}; // The remaining signature bytes + std::byte maskBuffer[16]{}; // A bitmask for the signature bytes we care about + for (size_t i = 0; i < signature.size(); i++) { + byteBuffer[i] = signature[i].value(); + maskBuffer[i] = signature[i].mask(); + } + bytes = _mm_loadu_si128(reinterpret_cast<__m128i*>(&byteBuffer)); + mask = _mm_loadu_si128(reinterpret_cast<__m128i*>(&maskBuffer)); + } + + template + const_scan_result find_pattern_neon(const std::byte* begin, const std::byte* end, const scan_context& context) { + const auto signature = context.signature; + const auto cmpIndex = cmpeq2 ? *context.pairIndex : context.cmpIndex; + + // 128 bit vector containing first signature byte repeated + const auto firstByte = _mm_set1_epi8(static_cast(*signature[cmpIndex])); + + __m128i secondByte; + if constexpr (cmpeq2) { + secondByte = _mm_set1_epi8(static_cast(*signature[cmpIndex + 1])); + } + + __m128i signatureBytes, signatureMask; + if constexpr (veccmp) { + load_signature_128(signature, signatureBytes, signatureMask); + } + + auto [pre, vec, post] = segment_scan<__m128i, veccmp>(begin, end, signature.size(), cmpIndex); + + if (!pre.empty()) { + const auto result = find_pattern_single(pre.data(), pre.data() + pre.size(), context); + if (result.has_result()) { + return result; + } + } + + for (auto& it : vec) { + const auto cmp = _mm_cmpeq_epi8(firstByte, _mm_load_si128(&it)); + auto mask = static_cast(_mm_movemask_epi8(cmp)); + + if constexpr (cmpeq2) { + const auto cmp2 = _mm_cmpeq_epi8(secondByte, _mm_load_si128(&it)); + auto mask2 = static_cast(_mm_movemask_epi8(cmp2)); + mask &= (mask2 >> 1) | (0b1u << 15); + } + + if constexpr (alignment != scan_alignment::X1) { + mask &= std::rotl(create_alignment_mask(), static_cast(cmpIndex)); + } + + while (mask) { + const auto offset = LIBHAT_BSF32(mask); + const auto i = reinterpret_cast(&it) + offset - cmpIndex; + if constexpr (veccmp) { + const auto data = _mm_loadu_si128(reinterpret_cast(i)); + const auto neqBits = _mm_xor_si128(data, signatureBytes); + const auto match = _mm_testz_si128(neqBits, signatureMask); + if (match) LIBHAT_UNLIKELY { + return i; + } + } else { + const auto match = std::equal(signature.begin(), signature.end(), i); + if (match) LIBHAT_UNLIKELY { + return i; + } + } + mask &= (mask - 1); + } + } + + if (!post.empty()) { + return find_pattern_single(post.data(), post.data() + post.size(), context); + } + return {}; + } + + template<> + scan_function_t resolve_scanner(scan_context& context) { + context.apply_hints({.vectorSize = 16}); + + const auto alignment = context.alignment; + const auto signature = context.signature; + const bool cmpeq2 = context.pairIndex.has_value(); + const bool veccmp = signature.size() <= 16; + + if (alignment == scan_alignment::X1) { + if (cmpeq2 && veccmp) { + return &find_pattern_neon; + } else if (cmpeq2) { + return &find_pattern_neon; + } else if (veccmp) { + return &find_pattern_neon; + } else { + return &find_pattern_neon; + } + } else if (alignment == scan_alignment::X16) { + if (cmpeq2 && veccmp) { + return &find_pattern_neon; + } else if (cmpeq2) { + return &find_pattern_neon; + } else if (veccmp) { + return &find_pattern_neon; + } else { + return &find_pattern_neon; + } + } + LIBHAT_UNREACHABLE(); + } +} +#endif diff --git a/src/arch/arm/System.cpp b/src/arch/arm/System.cpp index ad76b8f..6c263dd 100644 --- a/src/arch/arm/System.cpp +++ b/src/arch/arm/System.cpp @@ -1,9 +1,15 @@ #include -#ifdef LIBHAT_ARM +#if defined(LIBHAT_ARM) || defined(LIBHAT_AARCH64) #include namespace hat { +#ifdef LIBHAT_WINDOWS + system_info_arm::system_info_arm() { + this->extensions.neon = true; + } +#endif + } #endif diff --git a/test/tests/Scanner.cpp b/test/tests/Scanner.cpp index 80f26e1..95c71b9 100644 --- a/test/tests/Scanner.cpp +++ b/test/tests/Scanner.cpp @@ -83,6 +83,14 @@ using FindPatternTestTypes = ::testing::Types< FindPatternParameters, FindPatternParameters, FindPatternParameters, +#endif +#if defined(LIBHAT_ARM) || defined(LIBHAT_AARCH64) + FindPatternParameters, + FindPatternParameters, + FindPatternParameters, + FindPatternParameters, + FindPatternParameters, + FindPatternParameters, #endif FindPatternParameters, FindPatternParameters, From 39f9201235c41f328a40cb17a5911c05314704d6 Mon Sep 17 00:00:00 2001 From: Brady Date: Sun, 14 Jun 2026 03:27:39 -0500 Subject: [PATCH 02/66] Ignore sse2neon warnings --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ca229c4..77fef70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,7 +85,7 @@ FetchContent_MakeAvailable(sse2neon) if (MSVC) set_source_files_properties(src/arch/arm/Neon.cpp PROPERTIES COMPILE_FLAGS "/Zc:preprocessor") endif() -target_include_directories(libhat PRIVATE ${sse2neon_SOURCE_DIR}) +target_include_directories(libhat SYSTEM PRIVATE ${sse2neon_SOURCE_DIR}) if(UNIX) set_target_properties(libhat PROPERTIES POSITION_INDEPENDENT_CODE ON) From 8df8dfcecb3058818fdbf6424866ed2ebd7ac1e0 Mon Sep 17 00:00:00 2001 From: Brady Date: Sun, 14 Jun 2026 03:30:24 -0500 Subject: [PATCH 03/66] Missing Mode name --- test/tests/Scanner.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tests/Scanner.cpp b/test/tests/Scanner.cpp index 95c71b9..483b2d6 100644 --- a/test/tests/Scanner.cpp +++ b/test/tests/Scanner.cpp @@ -114,6 +114,7 @@ class FindPatternTestNameGenerator { else if constexpr (Mode == hat::detail::scan_mode::SSE) return "SSE"; else if constexpr (Mode == hat::detail::scan_mode::AVX2) return "AVX2"; else if constexpr (Mode == hat::detail::scan_mode::AVX512) return "AVX512"; + else if constexpr (Mode == hat::detail::scan_mode::Neon) return "Neon"; else static_assert(sizeof(Mode) == 0); } }; From 17ae035ca3635f7b5e37940c3840856c57501d78 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 00:15:11 -0500 Subject: [PATCH 04/66] Implementation throughput comparison benchmark --- .github/workflows/benchmark.yml | 31 ++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/benchmark/CompareImpl.cpp | 47 +++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 test/benchmark/CompareImpl.cpp diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..287a889 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,31 @@ +name: Benchmark + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + windows: + runs-on: windows-11-vs2026-arm + steps: + - uses: actions/checkout@v6 + + - name: Locate Visual Studio + run: | + $vsInstall = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath + echo "VS_INSTALL_DIR=$vsInstall" >> $env:GITHUB_ENV + + - name: Configure + run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SAMPLE_BIN=OFF -A ARM64 -T v145 + + - name: Build + run: cmake --build ${{github.workspace}}/build -j 4 --config Release + + - name: Test + working-directory: ${{github.workspace}}/build + shell: cmd + run: | + call "${{env.VS_INSTALL_DIR}}\VC\Auxiliary\Build\vcvarsarm64.bat" + ctest --verbose -C Release -R libhat_benchmark_compare_impl diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fda2f3a..9ef35d7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -59,6 +59,7 @@ function(register_test NAME SOURCE) endfunction() register_test(libhat_benchmark_compare benchmark/Compare.cpp) +register_test(libhat_benchmark_compare_impl benchmark/CompareImpl.cpp) register_test(libhat_test_scanner tests/Scanner.cpp) register_test(libhat_test_process tests/Process.cpp) diff --git a/test/benchmark/CompareImpl.cpp b/test/benchmark/CompareImpl.cpp new file mode 100644 index 0000000..b49a863 --- /dev/null +++ b/test/benchmark/CompareImpl.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include + +static constexpr std::string_view test_pattern = "01 02 03 04 05 06 07 08 09"; + +static auto gen_random_buffer(const size_t size) { + std::vector buffer(size); + std::default_random_engine generator(123); + std::uniform_int_distribution distribution(0, 0xFFFFFFFFFFFFFFFF); + for (size_t i = 0; i < buffer.size(); i += 8) { + uint64_t value = distribution(generator); + std::memcpy(&buffer[i], &value, sizeof(value)); + } + return buffer; +} + +template +static void BM_Throughput(benchmark::State& state) { + const size_t size = state.range(0); + const auto buf = gen_random_buffer(size); + const auto begin = std::to_address(buf.begin()); + const auto end = std::to_address(buf.end()); + + const auto sig = hat::parse_signature(test_pattern).value(); + const auto ctx = hat::detail::scan_context::create(sig, hat::scan_alignment::X1, hat::scan_hint::none); + for (auto _ : state) { + benchmark::DoNotOptimize(ctx.scan(begin, end)); + } + state.SetBytesProcessed(static_cast(state.iterations() * size)); +} + +static constexpr int64_t rangeStart = 1 << 22; // 4 MiB +static constexpr int64_t rangeLimit = 1 << 28; // 256 MiB + +#define LIBHAT_BENCHMARK(...) BENCHMARK(__VA_ARGS__) \ + ->Threads(1) \ + ->MinWarmUpTime(2) \ + ->MinTime(4) \ + ->Range(rangeStart, rangeLimit) \ + ->UseRealTime(); + +LIBHAT_BENCHMARK(BM_Throughput); +LIBHAT_BENCHMARK(BM_Throughput); + +BENCHMARK_MAIN(); From ee66f4349d1836c94f1a855f14de3f6911bccbb9 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 00:26:00 -0500 Subject: [PATCH 05/66] Log processor name --- .github/workflows/benchmark.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 287a889..fd7915a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,6 +12,9 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Check Hardware + run: (Get-CimInstance Win32_Processor).Name + - name: Locate Visual Studio run: | $vsInstall = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath From 77fbf3ebd00525bbc2f0ced61201ab47b7f3a559 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 00:28:31 -0500 Subject: [PATCH 06/66] Only build `libhat_benchmark_compare_impl` --- .github/workflows/benchmark.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index fd7915a..dc9971d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -24,7 +24,7 @@ jobs: run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SAMPLE_BIN=OFF -A ARM64 -T v145 - name: Build - run: cmake --build ${{github.workspace}}/build -j 4 --config Release + run: cmake --build ${{github.workspace}}/build -j 4 --config Release --target libhat_benchmark_compare_impl - name: Test working-directory: ${{github.workspace}}/build From 71fd67a7a9f32170b36f5f37c5c8d7239a541298 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 00:52:01 -0500 Subject: [PATCH 07/66] Neon substitutions --- src/arch/arm/Neon.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 0968c69..ac237ff 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -22,15 +22,15 @@ namespace hat::detail { - inline void load_signature_128(const signature_view signature, __m128i& bytes, __m128i& mask) { + inline void load_signature_128(const signature_view signature, int8x16_t& bytes, int8x16_t& mask) { std::byte byteBuffer[16]{}; // The remaining signature bytes std::byte maskBuffer[16]{}; // A bitmask for the signature bytes we care about for (size_t i = 0; i < signature.size(); i++) { byteBuffer[i] = signature[i].value(); maskBuffer[i] = signature[i].mask(); } - bytes = _mm_loadu_si128(reinterpret_cast<__m128i*>(&byteBuffer)); - mask = _mm_loadu_si128(reinterpret_cast<__m128i*>(&maskBuffer)); + bytes = vld1q_s8(&byteBuffer); + mask = vld1q_s8(&maskBuffer); } template @@ -39,19 +39,19 @@ namespace hat::detail { const auto cmpIndex = cmpeq2 ? *context.pairIndex : context.cmpIndex; // 128 bit vector containing first signature byte repeated - const auto firstByte = _mm_set1_epi8(static_cast(*signature[cmpIndex])); + const auto firstByte = vdupq_n_s8(static_cast(*signature[cmpIndex])); - __m128i secondByte; + int8x16_t secondByte; if constexpr (cmpeq2) { - secondByte = _mm_set1_epi8(static_cast(*signature[cmpIndex + 1])); + secondByte = vdupq_n_s8(static_cast(*signature[cmpIndex + 1])); } - __m128i signatureBytes, signatureMask; + int8x16_t signatureBytes, signatureMask; if constexpr (veccmp) { load_signature_128(signature, signatureBytes, signatureMask); } - auto [pre, vec, post] = segment_scan<__m128i, veccmp>(begin, end, signature.size(), cmpIndex); + auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); if (!pre.empty()) { const auto result = find_pattern_single(pre.data(), pre.data() + pre.size(), context); @@ -61,11 +61,11 @@ namespace hat::detail { } for (auto& it : vec) { - const auto cmp = _mm_cmpeq_epi8(firstByte, _mm_load_si128(&it)); + const auto cmp = vceqq_s8(firstByte, vld1q_s8(&it)); auto mask = static_cast(_mm_movemask_epi8(cmp)); if constexpr (cmpeq2) { - const auto cmp2 = _mm_cmpeq_epi8(secondByte, _mm_load_si128(&it)); + const auto cmp2 = vceqq_s8(secondByte, vld1q_s8(&it)); auto mask2 = static_cast(_mm_movemask_epi8(cmp2)); mask &= (mask2 >> 1) | (0b1u << 15); } @@ -78,8 +78,8 @@ namespace hat::detail { const auto offset = LIBHAT_BSF32(mask); const auto i = reinterpret_cast(&it) + offset - cmpIndex; if constexpr (veccmp) { - const auto data = _mm_loadu_si128(reinterpret_cast(i)); - const auto neqBits = _mm_xor_si128(data, signatureBytes); + const auto data = vld1q_s8(i); + const auto neqBits = veorq_s32(data, signatureBytes); const auto match = _mm_testz_si128(neqBits, signatureMask); if (match) LIBHAT_UNLIKELY { return i; From d23f6da26bde6c62ea9bfd2bd9ee0a0f5f24bd9a Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 01:05:30 -0500 Subject: [PATCH 08/66] Architecture guard + use time defaults --- test/benchmark/CompareImpl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/benchmark/CompareImpl.cpp b/test/benchmark/CompareImpl.cpp index b49a863..7990a3b 100644 --- a/test/benchmark/CompareImpl.cpp +++ b/test/benchmark/CompareImpl.cpp @@ -36,12 +36,12 @@ static constexpr int64_t rangeLimit = 1 << 28; // 256 MiB #define LIBHAT_BENCHMARK(...) BENCHMARK(__VA_ARGS__) \ ->Threads(1) \ - ->MinWarmUpTime(2) \ - ->MinTime(4) \ ->Range(rangeStart, rangeLimit) \ ->UseRealTime(); LIBHAT_BENCHMARK(BM_Throughput); +#if defined(LIBHAT_AARCH64) || defined(LIBHAT_ARM) LIBHAT_BENCHMARK(BM_Throughput); +#endif BENCHMARK_MAIN(); From c002976932029971a93cea804881d288b1a89ca6 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 01:15:45 -0500 Subject: [PATCH 09/66] Avoid double movemask --- src/arch/arm/Neon.cpp | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index ac237ff..36186b7 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -22,15 +22,15 @@ namespace hat::detail { - inline void load_signature_128(const signature_view signature, int8x16_t& bytes, int8x16_t& mask) { + inline void load_signature_128(const signature_view signature, uint8x16_t& bytes, uint8x16_t& mask) { std::byte byteBuffer[16]{}; // The remaining signature bytes std::byte maskBuffer[16]{}; // A bitmask for the signature bytes we care about for (size_t i = 0; i < signature.size(); i++) { byteBuffer[i] = signature[i].value(); maskBuffer[i] = signature[i].mask(); } - bytes = vld1q_s8(&byteBuffer); - mask = vld1q_s8(&maskBuffer); + bytes = vld1q_u8(&byteBuffer); + mask = vld1q_u8(&maskBuffer); } template @@ -39,19 +39,19 @@ namespace hat::detail { const auto cmpIndex = cmpeq2 ? *context.pairIndex : context.cmpIndex; // 128 bit vector containing first signature byte repeated - const auto firstByte = vdupq_n_s8(static_cast(*signature[cmpIndex])); + const auto firstByte = vdupq_n_u8(static_cast(*signature[cmpIndex])); - int8x16_t secondByte; + uint8x16_t secondByte; if constexpr (cmpeq2) { - secondByte = vdupq_n_s8(static_cast(*signature[cmpIndex + 1])); + secondByte = vdupq_n_u8(static_cast(*signature[cmpIndex + 1])); } - int8x16_t signatureBytes, signatureMask; + uint8x16_t signatureBytes, signatureMask; if constexpr (veccmp) { load_signature_128(signature, signatureBytes, signatureMask); } - auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); + auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); if (!pre.empty()) { const auto result = find_pattern_single(pre.data(), pre.data() + pre.size(), context); @@ -61,15 +61,16 @@ namespace hat::detail { } for (auto& it : vec) { - const auto cmp = vceqq_s8(firstByte, vld1q_s8(&it)); - auto mask = static_cast(_mm_movemask_epi8(cmp)); + auto cmp = vceqq_u8(firstByte, vld1q_s8(&it)); if constexpr (cmpeq2) { - const auto cmp2 = vceqq_s8(secondByte, vld1q_s8(&it)); - auto mask2 = static_cast(_mm_movemask_epi8(cmp2)); - mask &= (mask2 >> 1) | (0b1u << 15); + const auto cmp2 = vceqq_u8(secondByte, vld1q_s8(reinterpret_cast(&it) + 1)); + cmp = vandq_u8(cmp, cmp2); } + if (!std::bit_cast(vshrn_n_u16(cmp, 4))) continue; + + auto mask = static_cast(_mm_movemask_epi8(cmp)); if constexpr (alignment != scan_alignment::X1) { mask &= std::rotl(create_alignment_mask(), static_cast(cmpIndex)); } @@ -78,8 +79,8 @@ namespace hat::detail { const auto offset = LIBHAT_BSF32(mask); const auto i = reinterpret_cast(&it) + offset - cmpIndex; if constexpr (veccmp) { - const auto data = vld1q_s8(i); - const auto neqBits = veorq_s32(data, signatureBytes); + const auto data = vld1q_u8(i); + const auto neqBits = veorq_u8(data, signatureBytes); const auto match = _mm_testz_si128(neqBits, signatureMask); if (match) LIBHAT_UNLIKELY { return i; From aa6d5ccd9609957523695c25f642e5c5bbaac090 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 13:49:10 -0500 Subject: [PATCH 10/66] Use 4-bit mask --- src/arch/arm/Neon.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 36186b7..23f6143 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -8,16 +8,16 @@ #ifdef _MSC_VER namespace hat::detail { - inline unsigned long bsf(unsigned long num) noexcept { + inline unsigned long bsf(unsigned __int64 num) noexcept { unsigned long offset; - _BitScanForward(&offset, num); + _BitScanForward64(&offset, num); return offset; } } -#define LIBHAT_BSF32(num) hat::detail::bsf(num) +#define LIBHAT_BSF64(num) hat::detail::bsf(num) #else -#define LIBHAT_BSF32(num) __builtin_ctz(num) +#define LIBHAT_BSF64(num) __builtin_ctzll(num) #endif namespace hat::detail { @@ -33,6 +33,15 @@ namespace hat::detail { mask = vld1q_u8(&maskBuffer); } + template + LIBHAT_FORCEINLINE consteval uint64_t create_alignment_mask_neon() { + uint64_t mask{}; + for (size_t i = 0; i < 16; i += alignment_stride) { + mask |= (static_cast(0xF) << (i * 4)); + } + return mask; + } + template const_scan_result find_pattern_neon(const std::byte* begin, const std::byte* end, const scan_context& context) { const auto signature = context.signature; @@ -68,15 +77,13 @@ namespace hat::detail { cmp = vandq_u8(cmp, cmp2); } - if (!std::bit_cast(vshrn_n_u16(cmp, 4))) continue; - - auto mask = static_cast(_mm_movemask_epi8(cmp)); + auto mask = std::bit_cast(vshrn_n_u16(cmp, 4)); if constexpr (alignment != scan_alignment::X1) { - mask &= std::rotl(create_alignment_mask(), static_cast(cmpIndex)); + mask &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); } while (mask) { - const auto offset = LIBHAT_BSF32(mask); + const auto offset = LIBHAT_BSF64(mask) / 4; const auto i = reinterpret_cast(&it) + offset - cmpIndex; if constexpr (veccmp) { const auto data = vld1q_u8(i); @@ -91,7 +98,7 @@ namespace hat::detail { return i; } } - mask &= (mask - 1); + mask &= ~(0xF * (mask & -mask)); } } From c7a5b5e997a2a51e856c406e2b8400f4cba6a8e2 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 13:55:21 -0500 Subject: [PATCH 11/66] Fix warning --- src/arch/arm/Neon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 23f6143..efd9ac8 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -98,7 +98,7 @@ namespace hat::detail { return i; } } - mask &= ~(0xF * (mask & -mask)); + mask &= ~(0xF * (mask & -static_cast(mask))); } } From 4ff162a87bda07a6c47a3118d0782eef2237f12c Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 14:08:39 -0500 Subject: [PATCH 12/66] Increase benchmark time --- test/benchmark/CompareImpl.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/benchmark/CompareImpl.cpp b/test/benchmark/CompareImpl.cpp index 7990a3b..a7d1250 100644 --- a/test/benchmark/CompareImpl.cpp +++ b/test/benchmark/CompareImpl.cpp @@ -36,6 +36,8 @@ static constexpr int64_t rangeLimit = 1 << 28; // 256 MiB #define LIBHAT_BENCHMARK(...) BENCHMARK(__VA_ARGS__) \ ->Threads(1) \ + ->MinWarmUpTime(1) \ + ->MinTime(2) \ ->Range(rangeStart, rangeLimit) \ ->UseRealTime(); From 2013e929c5626eb1c04734e0cb6e01964962d1bb Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 15:53:11 -0500 Subject: [PATCH 13/66] Remove sse2neon --- CMakeLists.txt | 12 ------------ src/arch/arm/Neon.cpp | 6 ++---- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 77fef70..ad66703 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,18 +75,6 @@ set(LIBHAT_SRC add_library(libhat STATIC ${LIBHAT_SRC}) add_library(libhat::libhat ALIAS libhat) -include(FetchContent) -FetchContent_Declare( - sse2neon - GIT_REPOSITORY https://github.com/DLTcollab/sse2neon - GIT_TAG 92f6de174717aef09033ad21568d5bb9e5470404 # v1.9.1 -) -FetchContent_MakeAvailable(sse2neon) -if (MSVC) - set_source_files_properties(src/arch/arm/Neon.cpp PROPERTIES COMPILE_FLAGS "/Zc:preprocessor") -endif() -target_include_directories(libhat SYSTEM PRIVATE ${sse2neon_SOURCE_DIR}) - if(UNIX) set_target_properties(libhat PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index efd9ac8..87d5f3d 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -4,8 +4,6 @@ #include -#include - #ifdef _MSC_VER namespace hat::detail { inline unsigned long bsf(unsigned __int64 num) noexcept { @@ -88,8 +86,8 @@ namespace hat::detail { if constexpr (veccmp) { const auto data = vld1q_u8(i); const auto neqBits = veorq_u8(data, signatureBytes); - const auto match = _mm_testz_si128(neqBits, signatureMask); - if (match) LIBHAT_UNLIKELY { + const auto match = vandq_s64(neqBits, signatureMask); + if (!(vgetq_lane_s64(match, 0) | vgetq_lane_s64(match, 1))) LIBHAT_UNLIKELY { return i; } } else { From 2fa461d7421acff8ac5e9d372227489c12e1ba20 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 15:57:46 -0500 Subject: [PATCH 14/66] Missing neon header include --- src/arch/arm/Neon.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 87d5f3d..fe7549f 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -4,6 +4,8 @@ #include +#include + #ifdef _MSC_VER namespace hat::detail { inline unsigned long bsf(unsigned __int64 num) noexcept { From 1e42ef4d2e0d55d68e61a21e271a1749ffeded57 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 16:02:52 -0500 Subject: [PATCH 15/66] Improve neon intrinsic type conformance --- src/arch/arm/Neon.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index fe7549f..f772a39 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -23,11 +23,11 @@ namespace hat::detail { inline void load_signature_128(const signature_view signature, uint8x16_t& bytes, uint8x16_t& mask) { - std::byte byteBuffer[16]{}; // The remaining signature bytes - std::byte maskBuffer[16]{}; // A bitmask for the signature bytes we care about + uint8_t byteBuffer[16]{}; // The remaining signature bytes + uint8_t maskBuffer[16]{}; // A bitmask for the signature bytes we care about for (size_t i = 0; i < signature.size(); i++) { - byteBuffer[i] = signature[i].value(); - maskBuffer[i] = signature[i].mask(); + byteBuffer[i] = std::to_integer(signature[i].value()); + maskBuffer[i] = std::to_integer(signature[i].mask()); } bytes = vld1q_u8(&byteBuffer); mask = vld1q_u8(&maskBuffer); @@ -48,11 +48,11 @@ namespace hat::detail { const auto cmpIndex = cmpeq2 ? *context.pairIndex : context.cmpIndex; // 128 bit vector containing first signature byte repeated - const auto firstByte = vdupq_n_u8(static_cast(*signature[cmpIndex])); + const auto firstByte = vdupq_n_u8(static_cast(*signature[cmpIndex])); uint8x16_t secondByte; if constexpr (cmpeq2) { - secondByte = vdupq_n_u8(static_cast(*signature[cmpIndex + 1])); + secondByte = vdupq_n_u8(static_cast(*signature[cmpIndex + 1])); } uint8x16_t signatureBytes, signatureMask; @@ -70,10 +70,10 @@ namespace hat::detail { } for (auto& it : vec) { - auto cmp = vceqq_u8(firstByte, vld1q_s8(&it)); + auto cmp = vceqq_u8(firstByte, vld1q_u8(reinterpret_cast(&it))); if constexpr (cmpeq2) { - const auto cmp2 = vceqq_u8(secondByte, vld1q_s8(reinterpret_cast(&it) + 1)); + const auto cmp2 = vceqq_u8(secondByte, vld1q_u8(reinterpret_cast(&it) + 1)); cmp = vandq_u8(cmp, cmp2); } @@ -86,10 +86,10 @@ namespace hat::detail { const auto offset = LIBHAT_BSF64(mask) / 4; const auto i = reinterpret_cast(&it) + offset - cmpIndex; if constexpr (veccmp) { - const auto data = vld1q_u8(i); + const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); - const auto match = vandq_s64(neqBits, signatureMask); - if (!(vgetq_lane_s64(match, 0) | vgetq_lane_s64(match, 1))) LIBHAT_UNLIKELY { + const auto match = vandq_u8(neqBits, signatureMask); + if (!(vgetq_lane_u64(match, 0) | vgetq_lane_u64(match, 1))) LIBHAT_UNLIKELY { return i; } } else { From 826b640edb083c9cde686b814bba0aaeda41df6f Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 16:10:10 -0500 Subject: [PATCH 16/66] Hopefully resolve remaining macOS compile errors --- src/arch/arm/Neon.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index f772a39..5924108 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -29,8 +29,8 @@ namespace hat::detail { byteBuffer[i] = std::to_integer(signature[i].value()); maskBuffer[i] = std::to_integer(signature[i].mask()); } - bytes = vld1q_u8(&byteBuffer); - mask = vld1q_u8(&maskBuffer); + bytes = vld1q_u8(static_cast(byteBuffer)); + mask = vld1q_u8(static_cast(maskBuffer)); } template @@ -98,7 +98,7 @@ namespace hat::detail { return i; } } - mask &= ~(0xF * (mask & -static_cast(mask))); + mask &= ~(0xF * (mask & (~mask + 1))); } } From 82a1517420acf733da06afb4fbf55af3cce78a64 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 16:25:56 -0500 Subject: [PATCH 17/66] Compile Windows on ARM benchmark using Clang --- .github/workflows/benchmark.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index dc9971d..771d0ea 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -8,6 +8,9 @@ on: jobs: windows: + strategy: + matrix: + toolset: [ v145, ClangCL ] runs-on: windows-11-vs2026-arm steps: - uses: actions/checkout@v6 @@ -21,7 +24,7 @@ jobs: echo "VS_INSTALL_DIR=$vsInstall" >> $env:GITHUB_ENV - name: Configure - run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SAMPLE_BIN=OFF -A ARM64 -T v145 + run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SAMPLE_BIN=OFF -A ARM64 -T ${{matrix.toolset}} - name: Build run: cmake --build ${{github.workspace}}/build -j 4 --config Release --target libhat_benchmark_compare_impl From ce4768ebb61ea5f9a11d402dcbcd0e7f14cd3c07 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 16:42:56 -0500 Subject: [PATCH 18/66] Test manual bitwise optimization --- src/arch/arm/Neon.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 5924108..668385a 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -98,7 +98,10 @@ namespace hat::detail { return i; } } - mask &= ~(0xF * (mask & (~mask + 1))); + // thanks msvc? + // mask &= ~(0xF * (mask & (~mask + 1))); + const auto lsb = (mask & static_cast(-static_cast(mask))); + mask &= ~((lsb << 4) - lsb); } } From 16f2eed35f00684db6fc7eacea4908f27c0c7917 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 17:44:10 -0500 Subject: [PATCH 19/66] Benchmark ARM Neon against Chromium --- .github/workflows/benchmark.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 771d0ea..c7599d4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -24,14 +24,14 @@ jobs: echo "VS_INSTALL_DIR=$vsInstall" >> $env:GITHUB_ENV - name: Configure - run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SAMPLE_BIN=OFF -A ARM64 -T ${{matrix.toolset}} + run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -A ARM64 -T ${{matrix.toolset}} - name: Build - run: cmake --build ${{github.workspace}}/build -j 4 --config Release --target libhat_benchmark_compare_impl + run: cmake --build ${{github.workspace}}/build -j 4 --config Release --target libhat_benchmark_compare_impl libhat_benchmark_chromium - name: Test working-directory: ${{github.workspace}}/build shell: cmd run: | call "${{env.VS_INSTALL_DIR}}\VC\Auxiliary\Build\vcvarsarm64.bat" - ctest --verbose -C Release -R libhat_benchmark_compare_impl + ctest --verbose -C Release -R "(libhat_benchmark_compare_impl|libhat_benchmark_chromium)" From af7e14ff7871ff057bd9fbd12d66bb1910cc8363 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 17:45:58 -0500 Subject: [PATCH 20/66] Use cache for benchmark workflow --- .github/workflows/benchmark.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index c7599d4..ed4624b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -15,6 +15,14 @@ jobs: steps: - uses: actions/checkout@v6 + - name: CPM Cache + uses: actions/cache@v5 + with: + path: ${{github.workspace}}/.cpmcache + key: cpm-${{runner.os}}-ARM64-${{hashFiles('test/CMakeLists.txt')}} + restore-keys: | + cpm-${{runner.os}}-ARM64- + - name: Check Hardware run: (Get-CimInstance Win32_Processor).Name From 45a85a2a03d408c626a77895f3b73585375c984b Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 15 Jun 2026 18:20:15 -0500 Subject: [PATCH 21/66] Perhaps --- src/arch/arm/Neon.cpp | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 668385a..b4ed8fa 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -60,7 +60,7 @@ namespace hat::detail { load_signature_128(signature, signatureBytes, signatureMask); } - auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); + auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); if (!pre.empty()) { const auto result = find_pattern_single(pre.data(), pre.data() + pre.size(), context); @@ -70,16 +70,21 @@ namespace hat::detail { } for (auto& it : vec) { - auto cmp = vceqq_u8(firstByte, vld1q_u8(reinterpret_cast(&it))); + auto data = vld1q_u8_x2(reinterpret_cast(&it)); + auto cmp = vceqq_u8(firstByte, data.val[0]); + auto cmp2 = vceqq_u8(firstByte, data.val[1]); if constexpr (cmpeq2) { - const auto cmp2 = vceqq_u8(secondByte, vld1q_u8(reinterpret_cast(&it) + 1)); - cmp = vandq_u8(cmp, cmp2); + auto data2 = vld1q_u8_x2(reinterpret_cast(&it) + 1); + cmp = vandq_u8(cmp, vceqq_u8(secondByte, data2.val[0])); + cmp2 = vandq_u8(cmp2, vceqq_u8(secondByte, data2.val[1])); } auto mask = std::bit_cast(vshrn_n_u16(cmp, 4)); + auto mask2 = std::bit_cast(vshrn_n_u16(cmp2, 4)); if constexpr (alignment != scan_alignment::X1) { mask &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); + mask2 &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); } while (mask) { @@ -103,6 +108,28 @@ namespace hat::detail { const auto lsb = (mask & static_cast(-static_cast(mask))); mask &= ~((lsb << 4) - lsb); } + + while (mask2) { + const auto offset = LIBHAT_BSF64(mask2) / 4 + 16; + const auto i = reinterpret_cast(&it) + offset - cmpIndex; + if constexpr (veccmp) { + const auto data = vld1q_u8(reinterpret_cast(i)); + const auto neqBits = veorq_u8(data, signatureBytes); + const auto match = vandq_u8(neqBits, signatureMask); + if (!(vgetq_lane_u64(match, 0) | vgetq_lane_u64(match, 1))) LIBHAT_UNLIKELY { + return i; + } + } else { + const auto match = std::equal(signature.begin(), signature.end(), i); + if (match) LIBHAT_UNLIKELY { + return i; + } + } + // thanks msvc? + // mask &= ~(0xF * (mask & (~mask + 1))); + const auto lsb = (mask2 & static_cast(-static_cast(mask2))); + mask2 &= ~((lsb << 4) - lsb); + } } if (!post.empty()) { From af24489bf1bcbd7d1d892dc9b87fc219a8841146 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 17 Jun 2026 01:46:17 -0500 Subject: [PATCH 22/66] Extract masks using `vget_lane_u64` --- src/arch/arm/Neon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index b4ed8fa..6c58f7f 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -80,8 +80,8 @@ namespace hat::detail { cmp2 = vandq_u8(cmp2, vceqq_u8(secondByte, data2.val[1])); } - auto mask = std::bit_cast(vshrn_n_u16(cmp, 4)); - auto mask2 = std::bit_cast(vshrn_n_u16(cmp2, 4)); + auto mask = vget_lane_u64(vshrn_n_u16(cmp, 4), 0); + auto mask2 = vget_lane_u64(vshrn_n_u16(cmp2, 4), 0); if constexpr (alignment != scan_alignment::X1) { mask &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); mask2 &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); From 36991044e9143def4027f2ade72e87127d60c5cb Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 17 Jun 2026 04:08:53 -0500 Subject: [PATCH 23/66] Debug mode optimizations for NEON --- src/arch/arm/Neon.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 6c58f7f..bda7c6d 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -69,13 +69,15 @@ namespace hat::detail { } } - for (auto& it : vec) { - auto data = vld1q_u8_x2(reinterpret_cast(&it)); + const auto vec_begin = std::to_address(vec.begin()); + const auto vec_end = std::to_address(vec.end()); + for (auto it = vec_begin; it != vec_end; it++) { + auto data = vld1q_u8_x2(reinterpret_cast(it)); auto cmp = vceqq_u8(firstByte, data.val[0]); auto cmp2 = vceqq_u8(firstByte, data.val[1]); if constexpr (cmpeq2) { - auto data2 = vld1q_u8_x2(reinterpret_cast(&it) + 1); + auto data2 = vld1q_u8_x2(reinterpret_cast(it) + 1); cmp = vandq_u8(cmp, vceqq_u8(secondByte, data2.val[0])); cmp2 = vandq_u8(cmp2, vceqq_u8(secondByte, data2.val[1])); } @@ -89,7 +91,7 @@ namespace hat::detail { while (mask) { const auto offset = LIBHAT_BSF64(mask) / 4; - const auto i = reinterpret_cast(&it) + offset - cmpIndex; + const auto i = reinterpret_cast(it) + offset - cmpIndex; if constexpr (veccmp) { const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); @@ -111,7 +113,7 @@ namespace hat::detail { while (mask2) { const auto offset = LIBHAT_BSF64(mask2) / 4 + 16; - const auto i = reinterpret_cast(&it) + offset - cmpIndex; + const auto i = reinterpret_cast(it) + offset - cmpIndex; if constexpr (veccmp) { const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); From 21696888b9649a4cac365b3ba7f88d7e5c301c3b Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 17 Jun 2026 16:05:00 -0500 Subject: [PATCH 24/66] Revert unrolling --- src/arch/arm/Neon.cpp | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index bda7c6d..9d4c555 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -60,7 +60,7 @@ namespace hat::detail { load_signature_128(signature, signatureBytes, signatureMask); } - auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); + auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); if (!pre.empty()) { const auto result = find_pattern_single(pre.data(), pre.data() + pre.size(), context); @@ -72,21 +72,16 @@ namespace hat::detail { const auto vec_begin = std::to_address(vec.begin()); const auto vec_end = std::to_address(vec.end()); for (auto it = vec_begin; it != vec_end; it++) { - auto data = vld1q_u8_x2(reinterpret_cast(it)); - auto cmp = vceqq_u8(firstByte, data.val[0]); - auto cmp2 = vceqq_u8(firstByte, data.val[1]); + auto cmp = vceqq_u8(firstByte, vld1q_u8(reinterpret_cast(it))); if constexpr (cmpeq2) { - auto data2 = vld1q_u8_x2(reinterpret_cast(it) + 1); - cmp = vandq_u8(cmp, vceqq_u8(secondByte, data2.val[0])); - cmp2 = vandq_u8(cmp2, vceqq_u8(secondByte, data2.val[1])); + const auto cmp2 = vceqq_u8(secondByte, vld1q_u8(reinterpret_cast(it) + 1)); + cmp = vandq_u8(cmp, cmp2); } auto mask = vget_lane_u64(vshrn_n_u16(cmp, 4), 0); - auto mask2 = vget_lane_u64(vshrn_n_u16(cmp2, 4), 0); if constexpr (alignment != scan_alignment::X1) { mask &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); - mask2 &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); } while (mask) { @@ -110,28 +105,6 @@ namespace hat::detail { const auto lsb = (mask & static_cast(-static_cast(mask))); mask &= ~((lsb << 4) - lsb); } - - while (mask2) { - const auto offset = LIBHAT_BSF64(mask2) / 4 + 16; - const auto i = reinterpret_cast(it) + offset - cmpIndex; - if constexpr (veccmp) { - const auto data = vld1q_u8(reinterpret_cast(i)); - const auto neqBits = veorq_u8(data, signatureBytes); - const auto match = vandq_u8(neqBits, signatureMask); - if (!(vgetq_lane_u64(match, 0) | vgetq_lane_u64(match, 1))) LIBHAT_UNLIKELY { - return i; - } - } else { - const auto match = std::equal(signature.begin(), signature.end(), i); - if (match) LIBHAT_UNLIKELY { - return i; - } - } - // thanks msvc? - // mask &= ~(0xF * (mask & (~mask + 1))); - const auto lsb = (mask2 & static_cast(-static_cast(mask2))); - mask2 &= ~((lsb << 4) - lsb); - } } if (!post.empty()) { From df8bb55de589bae15713e19212ef72b062034f8b Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 17 Jun 2026 21:22:09 -0500 Subject: [PATCH 25/66] Add macOS to ARM64 testing --- .github/workflows/cmake.yml | 16 ++++++++++++++-- src/arch/arm/System.cpp | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 0b74308..20ea718 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -18,12 +18,24 @@ jobs: steps: - uses: actions/checkout@v6 + - name: CPM Cache + uses: actions/cache@v5 + with: + path: ${{github.workspace}}/.cpmcache + key: cpm-${{runner.os}}-${{hashFiles('test/CMakeLists.txt')}} + restore-keys: | + cpm-${{runner.os}}- + - name: Configure - run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING=OFF + run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF -DLIBHAT_TESTING_SDE=OFF - name: Build run: cmake --build ${{github.workspace}}/build -j 4 + - name: Test + working-directory: ${{github.workspace}}/build + run: ctest --verbose -C ${{env.BUILD_TYPE}} -R libhat_test_.* + linux: strategy: matrix: @@ -51,7 +63,7 @@ jobs: - name: Configure env: CXX: ${{matrix.compiler.exe}}-${{matrix.compiler.version}} - run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING=ON -DLIBHAT_TESTING_SAMPLE_BIN=OFF + run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF - name: Build run: cmake --build ${{github.workspace}}/build -j 4 diff --git a/src/arch/arm/System.cpp b/src/arch/arm/System.cpp index 6c263dd..832e6f0 100644 --- a/src/arch/arm/System.cpp +++ b/src/arch/arm/System.cpp @@ -5,7 +5,7 @@ namespace hat { -#ifdef LIBHAT_WINDOWS +#if defined(LIBHAT_WINDOWS) || defined(LIBHAT_MAC) system_info_arm::system_info_arm() { this->extensions.neon = true; } From 36b05c7b6e9c516819b476b56800c221ef3310bd Mon Sep 17 00:00:00 2001 From: Imrglop <69129770+Imrglop@users.noreply.github.com> Date: Sun, 31 May 2026 07:46:09 -0700 Subject: [PATCH 26/66] Partial mach-o Process --- CMakeLists.txt | 2 + src/os/linux/Process.cpp | 8 ---- src/os/mac/Process.cpp | 95 ++++++++++++++++++++++++++++++++++++++++ src/os/unix/Process.cpp | 19 ++++++++ 4 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 src/os/mac/Process.cpp create mode 100644 src/os/unix/Process.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ad66703..c232b2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,8 @@ set(LIBHAT_SRC src/Scanner.cpp src/System.cpp + src/os/mac/Process.cpp + src/os/linux/MemoryProtector.cpp src/os/linux/Process.cpp diff --git a/src/os/linux/Process.cpp b/src/os/linux/Process.cpp index 7fc1eaa..053d7e2 100644 --- a/src/os/linux/Process.cpp +++ b/src/os/linux/Process.cpp @@ -13,14 +13,6 @@ namespace hat::process { - hat::process::module get_process_module() { - const auto module = get_module({}); - if (!module) { - std::abort(); - } - return *module; - } - std::span module::get_module_data() const { size_t max{}; diff --git a/src/os/mac/Process.cpp b/src/os/mac/Process.cpp new file mode 100644 index 0000000..b7718a8 --- /dev/null +++ b/src/os/mac/Process.cpp @@ -0,0 +1,95 @@ +#include +#ifdef LIBHAT_MAC + +#include + +#include +#include +#include + +#include + +#include + +namespace hat::process { + + // no-op on 32 bit binaries + void module::for_each_segment(const std::function, hat::protection)>& callback) const { + const uint32_t imageCount = _dyld_image_count(); + for (uint32_t i = 0; i < imageCount; i++) { + const auto* header = reinterpret_cast(_dyld_get_image_header(i)); + if (header == nullptr) { + continue; + } + if (std::bit_cast(header) != this->address()) { + continue; + } + + const auto slide = static_cast(_dyld_get_image_vmaddr_slide(i)); + const auto* cmd = reinterpret_cast( + reinterpret_cast(header) + sizeof(mach_header_64)); + + for (uint32_t j = 0; j < header->ncmds; j++) { + if (cmd->cmd == LC_SEGMENT_64) { + const auto* seg = reinterpret_cast(cmd); + + // skip __PAGEZERO and any unmapped segment + if (seg->vmsize != 0 && seg->initprot != 0) { + const std::span data{ + reinterpret_cast(seg->vmaddr + slide), + seg->vmsize + }; + + hat::protection prot{}; + if (seg->initprot & VM_PROT_READ) prot |= hat::protection::Read; + if (seg->initprot & VM_PROT_WRITE) prot |= hat::protection::Write; + if (seg->initprot & VM_PROT_EXECUTE) prot |= hat::protection::Execute; + + if (!callback(data, prot)) { + return; + } + } + } + cmd = reinterpret_cast( + reinterpret_cast(cmd) + cmd->cmdsize); + } + return; + } + } + + std::optional get_module(const std::string_view name) { + using Handle = std::unique_ptr; + + std::unique_ptr buffer; + + if (!name.empty()) { + buffer = std::make_unique(name.size() + 1); + std::ranges::copy(name, buffer.get()); + } + + const Handle handle{dlopen(buffer.get(), RTLD_LAZY | RTLD_NOLOAD)}; + if (!handle) { + return {}; + } + + std::optional module{}; + + uint32_t imageCount = _dyld_image_count(); + for (uint32_t i = 0; i < imageCount; i++) { + const auto* header = reinterpret_cast(_dyld_get_image_header(i)); + if (header == nullptr) + continue; + + const Handle h{dlopen(_dyld_get_image_name(i), RTLD_LAZY | RTLD_NOLOAD)}; + if (h == handle) { + module = hat::process::module{std::bit_cast(header)}; + } + } + + return module; + } +} + +#endif diff --git a/src/os/unix/Process.cpp b/src/os/unix/Process.cpp new file mode 100644 index 0000000..e3bd5e5 --- /dev/null +++ b/src/os/unix/Process.cpp @@ -0,0 +1,19 @@ +#include +#ifdef LIBHAT_UNIX + +#include + +#include + +namespace hat::process { + + hat::process::module get_process_module() { + const auto module = get_module({}); + if (!module) { + std::abort(); + } + return *module; + } +} + +#endif From ac4712e943587c99a1e16400c89602b6f8081194 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 17 Jun 2026 22:13:10 -0500 Subject: [PATCH 27/66] Missing source file --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c232b2b..733db1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ set(LIBHAT_SRC src/os/linux/MemoryProtector.cpp src/os/linux/Process.cpp + src/os/unix/Process.cpp src/os/unix/System.cpp src/os/win32/MemoryProtector.cpp From 7e7c29d771cb012a7ccb739ab551ab21b4e5d8ea Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 17 Jun 2026 22:23:43 -0500 Subject: [PATCH 28/66] Missing include for `std::abort` --- src/os/unix/Process.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/os/unix/Process.cpp b/src/os/unix/Process.cpp index e3bd5e5..3307161 100644 --- a/src/os/unix/Process.cpp +++ b/src/os/unix/Process.cpp @@ -5,6 +5,8 @@ #include +#include + namespace hat::process { hat::process::module get_process_module() { From afc7c1d7832f95af11d8efecfeb9760d411d8d71 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 18 Jun 2026 00:50:53 -0500 Subject: [PATCH 29/66] Possibly fix macOS process APIs --- CMakeLists.txt | 1 - src/os/linux/Process.cpp | 8 ++++++++ src/os/mac/Process.cpp | 35 +++++++++++++++++++++-------------- src/os/unix/Process.cpp | 21 --------------------- 4 files changed, 29 insertions(+), 36 deletions(-) delete mode 100644 src/os/unix/Process.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 733db1b..c232b2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,6 @@ set(LIBHAT_SRC src/os/linux/MemoryProtector.cpp src/os/linux/Process.cpp - src/os/unix/Process.cpp src/os/unix/System.cpp src/os/win32/MemoryProtector.cpp diff --git a/src/os/linux/Process.cpp b/src/os/linux/Process.cpp index 053d7e2..7fc1eaa 100644 --- a/src/os/linux/Process.cpp +++ b/src/os/linux/Process.cpp @@ -13,6 +13,14 @@ namespace hat::process { + hat::process::module get_process_module() { + const auto module = get_module({}); + if (!module) { + std::abort(); + } + return *module; + } + std::span module::get_module_data() const { size_t max{}; diff --git a/src/os/mac/Process.cpp b/src/os/mac/Process.cpp index b7718a8..cb931c4 100644 --- a/src/os/mac/Process.cpp +++ b/src/os/mac/Process.cpp @@ -13,6 +13,17 @@ namespace hat::process { + hat::process::module get_process_module() { + const uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i != count; i++) { + const auto* header = reinterpret_cast(_dyld_get_image_header(i)); + if (header && header->filetype == MH_EXECUTE) { + return hat::process::module{std::bit_cast(header)}; + } + } + std::abort(); + } + // no-op on 32 bit binaries void module::for_each_segment(const std::function, hat::protection)>& callback) const { const uint32_t imageCount = _dyld_image_count(); @@ -58,37 +69,33 @@ namespace hat::process { } std::optional get_module(const std::string_view name) { + if (name.empty()) { + return get_process_module(); + } + using Handle = std::unique_ptr; - std::unique_ptr buffer; - - if (!name.empty()) { - buffer = std::make_unique(name.size() + 1); - std::ranges::copy(name, buffer.get()); - } - - const Handle handle{dlopen(buffer.get(), RTLD_LAZY | RTLD_NOLOAD)}; + const std::string buffer{name}; + const Handle handle{dlopen(buffer.c_str(), RTLD_LAZY | RTLD_NOLOAD)}; if (!handle) { return {}; } - - std::optional module{}; - uint32_t imageCount = _dyld_image_count(); - for (uint32_t i = 0; i < imageCount; i++) { + const uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i < count; i++) { const auto* header = reinterpret_cast(_dyld_get_image_header(i)); if (header == nullptr) continue; const Handle h{dlopen(_dyld_get_image_name(i), RTLD_LAZY | RTLD_NOLOAD)}; if (h == handle) { - module = hat::process::module{std::bit_cast(header)}; + return hat::process::module{std::bit_cast(header)}; } } - return module; + return {}; } } diff --git a/src/os/unix/Process.cpp b/src/os/unix/Process.cpp deleted file mode 100644 index 3307161..0000000 --- a/src/os/unix/Process.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include -#ifdef LIBHAT_UNIX - -#include - -#include - -#include - -namespace hat::process { - - hat::process::module get_process_module() { - const auto module = get_module({}); - if (!module) { - std::abort(); - } - return *module; - } -} - -#endif From 3cec4918d2bb8f599ff9dfc5c645ad33af7ca152 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 18 Jun 2026 04:18:50 -0500 Subject: [PATCH 30/66] Add Linux to ARM64 testing --- .github/workflows/cmake.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 20ea718..e3cb6d5 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -39,11 +39,12 @@ jobs: linux: strategy: matrix: + os: [ ubuntu-26.04, ubuntu-26.04-arm ] compiler: - { pkg: g++, exe: g++, version: 14 } - { pkg: clang, exe: clang++, version: 18 } cxx_standard: [ 20, 23 ] - runs-on: ubuntu-24.04 + runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v6 @@ -63,7 +64,7 @@ jobs: - name: Configure env: CXX: ${{matrix.compiler.exe}}-${{matrix.compiler.version}} - run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF + run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SDE=${{contains(matrix.os, 'arm') && 'OFF' || 'ON'}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF - name: Build run: cmake --build ${{github.workspace}}/build -j 4 From 7663ec0c42c2e4b3775c73020ef1a344a3a4b9af Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 18 Jun 2026 04:24:37 -0500 Subject: [PATCH 31/66] Fix Linux compile --- src/arch/arm/Neon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 9d4c555..752aa1c 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -79,7 +79,7 @@ namespace hat::detail { cmp = vandq_u8(cmp, cmp2); } - auto mask = vget_lane_u64(vshrn_n_u16(cmp, 4), 0); + auto mask = vget_lane_u64(vshrn_n_u16(vreinterpretq_u16_u8(cmp), 4), 0); if constexpr (alignment != scan_alignment::X1) { mask &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); } @@ -90,7 +90,7 @@ namespace hat::detail { if constexpr (veccmp) { const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); - const auto match = vandq_u8(neqBits, signatureMask); + const auto match = vreinterpretq_u64_u8(vandq_u8(neqBits, signatureMask)); if (!(vgetq_lane_u64(match, 0) | vgetq_lane_u64(match, 1))) LIBHAT_UNLIKELY { return i; } From bf19f5314bf9f80930e3a77febfa388c3c42054e Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 18 Jun 2026 04:30:30 -0500 Subject: [PATCH 32/66] Actually fix compile + adjust workflow --- .github/workflows/cmake.yml | 11 ++++++++--- src/arch/arm/Neon.cpp | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e3cb6d5..d3e46ce 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -39,11 +39,16 @@ jobs: linux: strategy: matrix: - os: [ ubuntu-26.04, ubuntu-26.04-arm ] + target: [ x64, ARM64 ] + cxx_standard: [ 20, 23 ] compiler: - { pkg: g++, exe: g++, version: 14 } - { pkg: clang, exe: clang++, version: 18 } - cxx_standard: [ 20, 23 ] + include: + - target: x64 + os: ubuntu-26.04 + - target: ARM64 + os: ubuntu-26.04-arm runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v6 @@ -64,7 +69,7 @@ jobs: - name: Configure env: CXX: ${{matrix.compiler.exe}}-${{matrix.compiler.version}} - run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SDE=${{contains(matrix.os, 'arm') && 'OFF' || 'ON'}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF + run: cmake -B ${{github.workspace}}/build -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SDE=${{startsWith(matrix.target, 'ARM') && 'OFF' || 'ON'}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF - name: Build run: cmake --build ${{github.workspace}}/build -j 4 diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 752aa1c..1a34551 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -79,7 +79,7 @@ namespace hat::detail { cmp = vandq_u8(cmp, cmp2); } - auto mask = vget_lane_u64(vshrn_n_u16(vreinterpretq_u16_u8(cmp), 4), 0); + auto mask = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(cmp), 4)), 0); if constexpr (alignment != scan_alignment::X1) { mask &= std::rotl(create_alignment_mask_neon(), static_cast(cmpIndex) * 4); } From 3671c7f1e5ead424482125fab3abc9f4aab5c34d Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 18 Jun 2026 04:42:39 -0500 Subject: [PATCH 33/66] Implement `system_info_arm` on Linux --- src/arch/arm/System.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/arch/arm/System.cpp b/src/arch/arm/System.cpp index 832e6f0..f6c439a 100644 --- a/src/arch/arm/System.cpp +++ b/src/arch/arm/System.cpp @@ -3,13 +3,30 @@ #include -namespace hat { - #if defined(LIBHAT_WINDOWS) || defined(LIBHAT_MAC) +namespace hat { system_info_arm::system_info_arm() { this->extensions.neon = true; } +} #endif +#if defined(LIBHAT_LINUX) + +#include +#include + +namespace hat { + system_info_arm::system_info_arm() { +#if defined(LIBHAT_ARM) + unsigned long hwcap = getauxval(AT_HWCAP); + this->extensions.neon = (hwcap & HWCAP_NEON) != 0; +#else // AARCH64 + unsigned long hwcap = getauxval(AT_HWCAP); + this->extensions.neon = (hwcap & HWCAP_ASIMD) != 0; +#endif + } } #endif + +#endif From b747018cc68eb7c6937477b3bb9810194a165c46 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 19 Jun 2026 18:04:31 -0500 Subject: [PATCH 34/66] Adjust mask iteration --- src/arch/arm/Neon.cpp | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 1a34551..175f11a 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -5,17 +5,10 @@ #include #include +#include #ifdef _MSC_VER - namespace hat::detail { - inline unsigned long bsf(unsigned __int64 num) noexcept { - unsigned long offset; - _BitScanForward64(&offset, num); - return offset; - } - } - -#define LIBHAT_BSF64(num) hat::detail::bsf(num) +#define LIBHAT_BSF64(num) _CountTrailingZeros64(num) #else #define LIBHAT_BSF64(num) __builtin_ctzll(num) #endif @@ -85,8 +78,8 @@ namespace hat::detail { } while (mask) { - const auto offset = LIBHAT_BSF64(mask) / 4; - const auto i = reinterpret_cast(it) + offset - cmpIndex; + const auto offset = LIBHAT_BSF64(mask); + const auto i = reinterpret_cast(it) + (offset >> 2) - cmpIndex; if constexpr (veccmp) { const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); @@ -102,8 +95,7 @@ namespace hat::detail { } // thanks msvc? // mask &= ~(0xF * (mask & (~mask + 1))); - const auto lsb = (mask & static_cast(-static_cast(mask))); - mask &= ~((lsb << 4) - lsb); + mask ^= (uint64_t{0xF} << offset); } } From 9ae6cfc2d55316a8b43a761112b0da82dfecdddd Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 19 Jun 2026 18:10:15 -0500 Subject: [PATCH 35/66] ALLEGEDLY I can't use `_CountTrailingZeros64` --- src/arch/arm/Neon.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 175f11a..8d65bcf 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -5,10 +5,19 @@ #include #include -#include #ifdef _MSC_VER -#define LIBHAT_BSF64(num) _CountTrailingZeros64(num) +#include + + namespace hat::detail { + inline unsigned long bsf(unsigned __int64 num) noexcept { + unsigned long offset; + _BitScanForward64(&offset, num); + return offset; + } + } + +#define LIBHAT_BSF64(num) hat::detail::bsf(num) #else #define LIBHAT_BSF64(num) __builtin_ctzll(num) #endif From 5ddba36ec0ea8770c2e9c6d2dd6e3e5ca6cac536 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 19 Jun 2026 19:34:01 -0500 Subject: [PATCH 36/66] Optimize `frequency.py` using NumPy --- scripts/frequency.py | 11 ++++++++--- scripts/requirements.txt | Bin 40 -> 68 bytes 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/frequency.py b/scripts/frequency.py index 9f75422..0bc9cef 100644 --- a/scripts/frequency.py +++ b/scripts/frequency.py @@ -1,13 +1,14 @@ import itertools import sys +import numpy as np import pefile def main(): files = [pefile.PE(path, fast_load=True) for path in sys.argv[1:]] - pair_counts = [0 for _ in range(2 ** 16)] + pair_counts = np.zeros(1 << 16, dtype=np.int64) total_pairs_count = 0 for pe in files: @@ -16,8 +17,12 @@ def main(): continue data = section.get_data() total_pairs_count += len(data) - 1 - for a, b in zip(data[:], data[1:]): - pair_counts[a * 0x100 + b] += 1 + + a = np.frombuffer(data[:-1], dtype=np.uint8).astype(np.uint16) + b = np.frombuffer(data[1:], dtype=np.uint8) + pairs = (a << 8) | b + count = np.bincount(pairs, minlength=1 << 16) + pair_counts += count top_n_pairs = 512 sorted_pairs = sorted( diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 04a2c9b797c42e060d1f7d6cf6957371ae4f708d..54ea379ae8bcb104806a7089af3ad9055c472606 100644 GIT binary patch literal 68 zcmezWFOQ*=A(x?mp_0KC2#px@7)*fJjDeSd3n-e(kj9Y7kOQP4vIamEAbATQHiD`G E0MdsFiU0rr delta 8 PcmZ?KVEX@WqMRB44paj{ From 3d1e9ea35703ffb0bbaeec44f0a58836c9a3b040 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 19 Jun 2026 19:38:03 -0500 Subject: [PATCH 37/66] Avoid `bytes` copy with `memoryview` --- scripts/frequency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/frequency.py b/scripts/frequency.py index 0bc9cef..258fa7e 100644 --- a/scripts/frequency.py +++ b/scripts/frequency.py @@ -15,7 +15,7 @@ def main(): for section in pe.sections: if not section.IMAGE_SCN_MEM_EXECUTE: continue - data = section.get_data() + data = memoryview(section.get_data()) total_pairs_count += len(data) - 1 a = np.frombuffer(data[:-1], dtype=np.uint8).astype(np.uint16) From 95d24374d817b7cfc9343354d1b224792a48c536 Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 22 Jun 2026 18:29:10 -0500 Subject: [PATCH 38/66] Add new NTTP to `segment_scan` call --- src/arch/arm/Neon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 8d65bcf..ad8cb2f 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -62,7 +62,7 @@ namespace hat::detail { load_signature_128(signature, signatureBytes, signatureMask); } - auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); + auto [pre, vec, post] = segment_scan(begin, end, signature.size(), cmpIndex); if (!pre.empty()) { const auto result = find_pattern_single(pre.data(), pre.data() + pre.size(), context); From d280b25781e90007bdb98d31b9a9a9b377caa44b Mon Sep 17 00:00:00 2001 From: Brady Date: Mon, 22 Jun 2026 18:47:41 -0500 Subject: [PATCH 39/66] Replace `u64` extract with `vmaxvq_u32` --- src/arch/arm/Neon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index ad8cb2f..6eb6ed2 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -92,8 +92,8 @@ namespace hat::detail { if constexpr (veccmp) { const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); - const auto match = vreinterpretq_u64_u8(vandq_u8(neqBits, signatureMask)); - if (!(vgetq_lane_u64(match, 0) | vgetq_lane_u64(match, 1))) LIBHAT_UNLIKELY { + const auto match = vreinterpretq_u32_u8(vandq_u8(neqBits, signatureMask)); + if (vmaxvq_u32(match) == 0) LIBHAT_UNLIKELY { return i; } } else { From c0aa5579034da81d68ff38cdbd2262a22d7e4700 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 25 Jun 2026 20:31:19 -0500 Subject: [PATCH 40/66] Update README with macOS support --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b5964ba..8732308 100644 --- a/README.md +++ b/README.md @@ -104,15 +104,15 @@ Below is a summary of the current support for libhat's platform-dependent APIs: |--------------------------------|:-------:|:-----:|:-----:| | `hat::get_system` | ✅ | ✅ | ✅ | | `hat::memory_protector` | ✅ | ✅ | | -| `hp::get_process_module` | ✅ | ✅ | | -| `hp::get_module` | ✅ | ✅ | | +| `hp::get_process_module` | ✅ | ✅ | ✅ | +| `hp::get_module` | ✅ | ✅ | ✅ | | `hp::module_at` | ✅ | | | | `hp::is_readable` | ✅ | ✅ | | | `hp::is_writable` | ✅ | ✅ | | | `hp::is_executable` | ✅ | ✅ | | | `hp::module::get_module_data` | ✅ | ✅ | | | `hp::module::get_section_data` | ✅ | | | -| `hp::module::for_each_segment` | ✅ | ✅ | | +| `hp::module::for_each_segment` | ✅ | ✅ | ✅ | ## Quick start ### Defining patterns From 3c4cfda797776c96999d19e95192fe8363888f7f Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 25 Jun 2026 21:07:55 -0500 Subject: [PATCH 41/66] Add test that validates presence of default module segments --- test/tests/Process.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/tests/Process.cpp b/test/tests/Process.cpp index 76fd19a..e6f6fec 100644 --- a/test/tests/Process.cpp +++ b/test/tests/Process.cpp @@ -4,3 +4,22 @@ TEST(ProcessTest, ProcessModuleMatchesEmptyStr) { EXPECT_EQ(hat::process::get_process_module(), hat::process::get_module({})); } + +TEST(ProcessTest, ProcessModuleHasDefaultSegments) { + bool rx = false; // .text + bool r = false; // .rdata + bool rw = false; // .data + hat::process::get_process_module().for_each_segment([&](auto, auto prot) { + if (prot == (hat::protection::Read | hat::protection::Execute)) { + rx = true; + } else if (prot == (hat::protection::Read)) { + r = true; + } else if (prot == (hat::protection::Read | hat::protection::Write)) { + rw = true; + } + return !rx || !r || !rw; + }); + EXPECT_TRUE(rx); + EXPECT_TRUE(r); + EXPECT_TRUE(rw); +} From 9c613eb24cb9a215f9f9f9485d3d5f2bccf2f6b8 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 25 Jun 2026 21:18:19 -0500 Subject: [PATCH 42/66] Missing include for `std::abort` --- src/os/mac/Process.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/os/mac/Process.cpp b/src/os/mac/Process.cpp index cb931c4..89649df 100644 --- a/src/os/mac/Process.cpp +++ b/src/os/mac/Process.cpp @@ -10,6 +10,7 @@ #include #include +#include namespace hat::process { From 769bfd128e6aeca74c43076fb99ca76c3b497b2a Mon Sep 17 00:00:00 2001 From: Brady Date: Sat, 27 Jun 2026 00:00:57 -0500 Subject: [PATCH 43/66] Recognize `PT_GNU_RELRO` segment type --- src/os/linux/Process.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/os/linux/Process.cpp b/src/os/linux/Process.cpp index 7fc1eaa..15c8510 100644 --- a/src/os/linux/Process.cpp +++ b/src/os/linux/Process.cpp @@ -32,7 +32,7 @@ namespace hat::process { for (size_t i = 0; i < info.dlpi_phnum; i++) { auto& header = info.dlpi_phdr[i]; - if (header.p_type != PT_LOAD) { + if (header.p_type != PT_LOAD && header.p_type != PT_GNU_RELRO) { continue; } max = std::max(max, detail::fast_align_up(header.p_vaddr + header.p_memsz, @@ -60,7 +60,7 @@ namespace hat::process { for (size_t i = 0; i < info.dlpi_phnum; i++) { auto& header = info.dlpi_phdr[i]; - if (header.p_type != PT_LOAD) { + if (header.p_type != PT_LOAD && header.p_type != PT_GNU_RELRO) { continue; } From 61c767365ffbffff1fa4cac2a9d4ef95c0dfde8c Mon Sep 17 00:00:00 2001 From: Brady Date: Sat, 27 Jun 2026 00:08:35 -0500 Subject: [PATCH 44/66] Fix some includes on macOS --- src/os/mac/Process.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/os/mac/Process.cpp b/src/os/mac/Process.cpp index 89649df..c1197c3 100644 --- a/src/os/mac/Process.cpp +++ b/src/os/mac/Process.cpp @@ -9,8 +9,9 @@ #include +#include #include -#include +#include namespace hat::process { From 32cdfa464996bdb51d23f65648a3d169261417a6 Mon Sep 17 00:00:00 2001 From: Brady Date: Sun, 28 Jun 2026 15:40:20 -0500 Subject: [PATCH 45/66] Add Android build to CMake workflow --- .github/workflows/cmake.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index d38e288..6e9d40f 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -78,6 +78,22 @@ jobs: working-directory: ${{github.workspace}}/build run: ctest --verbose -C ${{env.BUILD_TYPE}} -R libhat_test_.* + android: + strategy: + matrix: + cxx_standard: [ 20 ] + abi: [ arm64-v8a ] + sdk: [ android-26 ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Configure + run: cmake -B ${{github.workspace}}/build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=${{matrix.abi}} -DANDROID_PLATFORM=${{matrix.sdk}} -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING=OFF + + - name: Build + run: cmake --build ${{github.workspace}}/build -j 4 + windows: strategy: matrix: From 998580d3c080bde60eda135f64ae18b5b63bc014 Mon Sep 17 00:00:00 2001 From: Brady Date: Sun, 28 Jun 2026 18:07:58 -0500 Subject: [PATCH 46/66] Adjust the minimum Android SDK to `android-21` --- .github/workflows/cmake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 6e9d40f..94a7c24 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -83,7 +83,7 @@ jobs: matrix: cxx_standard: [ 20 ] abi: [ arm64-v8a ] - sdk: [ android-26 ] + sdk: [ android-21 ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 From 457bba02997d2e49a07dcc43b53a7da4e4eaa05d Mon Sep 17 00:00:00 2001 From: Brady Date: Tue, 30 Jun 2026 14:24:17 -0500 Subject: [PATCH 47/66] Build all currently supported Android ABIs --- .github/workflows/cmake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 94a7c24..963252a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -82,7 +82,7 @@ jobs: strategy: matrix: cxx_standard: [ 20 ] - abi: [ arm64-v8a ] + abi: [ armeabi-v7a, arm64-v8a, x86, x86_64 ] sdk: [ android-21 ] runs-on: ubuntu-latest steps: From b51f2352521001228b93544095c961be29c58015 Mon Sep 17 00:00:00 2001 From: Brady Date: Tue, 30 Jun 2026 14:30:57 -0500 Subject: [PATCH 48/66] `vmaxvq_u32` is not supported on ARMv7 --- src/arch/arm/Neon.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index 6eb6ed2..bba99de 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -22,6 +22,12 @@ #define LIBHAT_BSF64(num) __builtin_ctzll(num) #endif +#ifdef LIBHAT_AARCH64 +#define LIBHAT_TEST_ZERO(x) (vmaxvq_u32(vreinterpretq_u32_u8(x)) == 0) +#else +#define LIBHAT_TEST_ZERO(x) (!(vgetq_lane_u64(vreinterpretq_u64_u8(x), 0) | vgetq_lane_u64(vreinterpretq_u64_u8(x), 1))) +#endif + namespace hat::detail { inline void load_signature_128(const signature_view signature, uint8x16_t& bytes, uint8x16_t& mask) { @@ -92,8 +98,8 @@ namespace hat::detail { if constexpr (veccmp) { const auto data = vld1q_u8(reinterpret_cast(i)); const auto neqBits = veorq_u8(data, signatureBytes); - const auto match = vreinterpretq_u32_u8(vandq_u8(neqBits, signatureMask)); - if (vmaxvq_u32(match) == 0) LIBHAT_UNLIKELY { + const auto match = vandq_u8(neqBits, signatureMask); + if (LIBHAT_TEST_ZERO(match)) LIBHAT_UNLIKELY { return i; } } else { From 8fec0d8484eea91d0e22e5556736f0f81a6dec8b Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 14:51:19 -0500 Subject: [PATCH 49/66] Attempt android emulation for testing --- .github/workflows/cmake.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 963252a..bbd127b 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -83,17 +83,28 @@ jobs: matrix: cxx_standard: [ 20 ] abi: [ armeabi-v7a, arm64-v8a, x86, x86_64 ] - sdk: [ android-21 ] + sdk: [ android-24 ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Configure - run: cmake -B ${{github.workspace}}/build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=${{matrix.abi}} -DANDROID_PLATFORM=${{matrix.sdk}} -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING=OFF + run: cmake -B ${{github.workspace}}/build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=${{matrix.abi}} -DANDROID_PLATFORM=${{matrix.sdk}} -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SANITIZE=OFF - name: Build run: cmake --build ${{github.workspace}}/build -j 4 + - name: Test + if: matrix.abi != 'armeabi-v7a' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 24 + arch: ${{ matrix.abi }} + script: | + adb push ${{github.workspace}}/build/test /data/local/tmp/ + adb shell chmod +x /data/local/tmp/test/libhat_test_process + adb shell /data/local/tmp/test/libhat_test_process + windows: strategy: matrix: From 004966fef0ff983e498d6c5fbe47d61a26ea00b4 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 15:05:09 -0500 Subject: [PATCH 50/66] Cache AVD snapshot --- .github/workflows/cmake.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index bbd127b..c05715d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -83,23 +83,43 @@ jobs: matrix: cxx_standard: [ 20 ] abi: [ armeabi-v7a, arm64-v8a, x86, x86_64 ] - sdk: [ android-24 ] + sdk: [ 24 ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - name: AVD Cache + uses: actions/cache@v5 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{matrix.sdk}} + - name: Configure run: cmake -B ${{github.workspace}}/build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=${{matrix.abi}} -DANDROID_PLATFORM=${{matrix.sdk}} -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SANITIZE=OFF - name: Build run: cmake --build ${{github.workspace}}/build -j 4 + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{matrix.sdk}} + arch: ${{matrix.abi}} + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + - name: Test if: matrix.abi != 'armeabi-v7a' uses: reactivecircus/android-emulator-runner@v2 with: - api-level: 24 - arch: ${{ matrix.abi }} + api-level: ${{matrix.sdk}} + arch: ${{matrix.abi}} script: | adb push ${{github.workspace}}/build/test /data/local/tmp/ adb shell chmod +x /data/local/tmp/test/libhat_test_process From 453c100a9427450abf3fc566c675630c3f2214a7 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 15:07:51 -0500 Subject: [PATCH 51/66] Exclude `armeabi-v7a` from AVD cache steps --- .github/workflows/cmake.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index c05715d..b19dc7d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -89,6 +89,7 @@ jobs: - uses: actions/checkout@v6 - name: AVD Cache + if: matrix.abi != 'armeabi-v7a' uses: actions/cache@v5 id: avd-cache with: @@ -104,7 +105,7 @@ jobs: run: cmake --build ${{github.workspace}}/build -j 4 - name: Create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' + if: matrix.abi != 'armeabi-v7a' && steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{matrix.sdk}} From 1f5c8a29d85655ec87a3dde9d07ae6d0d077b260 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 15:12:01 -0500 Subject: [PATCH 52/66] Include ABI in AVD cache key --- .github/workflows/cmake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index b19dc7d..ec22789 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -96,7 +96,7 @@ jobs: path: | ~/.android/avd/* ~/.android/adb* - key: avd-${{matrix.sdk}} + key: avd-${{matrix.sdk}}-${{matrix.abi}} - name: Configure run: cmake -B ${{github.workspace}}/build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=${{matrix.abi}} -DANDROID_PLATFORM=${{matrix.sdk}} -DCPM_SOURCE_CACHE=${{github.workspace}}/.cpmcache -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING_SAMPLE_BIN=OFF -DLIBHAT_TESTING_SDE=OFF -DLIBHAT_TESTING_SANITIZE=OFF From 3b572dd16a360e0ff7dfc2658b420b8f2e7ff191 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 15:13:39 -0500 Subject: [PATCH 53/66] Fix options for Android test step --- .github/workflows/cmake.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ec22789..96f1dfa 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -121,6 +121,9 @@ jobs: with: api-level: ${{matrix.sdk}} arch: ${{matrix.abi}} + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true script: | adb push ${{github.workspace}}/build/test /data/local/tmp/ adb shell chmod +x /data/local/tmp/test/libhat_test_process From 57d79a5d7e844ca8734d7dd88a2d946e10b38a0b Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 18:04:57 -0500 Subject: [PATCH 54/66] Fix `get_module` on Android --- include/libhat/platform.h | 3 +++ src/os/linux/Process.cpp | 1 + 2 files changed, 4 insertions(+) diff --git a/include/libhat/platform.h b/include/libhat/platform.h index a4434b3..5138403 100644 --- a/include/libhat/platform.h +++ b/include/libhat/platform.h @@ -19,6 +19,9 @@ #elif defined(linux) || defined(__linux__) || defined(__linux) #define LIBHAT_UNIX #define LIBHAT_LINUX + #if defined(__ANDROID__) + #define LIBHAT_ANDROID + #endif #elif defined(__APPLE__) && defined(__MACH__) #define LIBHAT_UNIX #define LIBHAT_MAC diff --git a/src/os/linux/Process.cpp b/src/os/linux/Process.cpp index 15c8510..959ba4e 100644 --- a/src/os/linux/Process.cpp +++ b/src/os/linux/Process.cpp @@ -104,6 +104,7 @@ namespace hat::process { std::optional module{}; auto callback = [&](const dl_phdr_info& info) { + if (!info.dlpi_addr) return 0; const Handle h{dlopen(info.dlpi_name, RTLD_LAZY | RTLD_NOLOAD)}; if (h == handle) { module = hat::process::module{std::bit_cast(info.dlpi_addr)}; From 4f2a7a00b5bf8fae79e5c33653d5e02d40a2cc68 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 1 Jul 2026 18:28:35 -0500 Subject: [PATCH 55/66] Exclude `armeabi-v7a` and `arm64-v8a` from testing --- .github/workflows/cmake.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 96f1dfa..78fc536 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -89,7 +89,7 @@ jobs: - uses: actions/checkout@v6 - name: AVD Cache - if: matrix.abi != 'armeabi-v7a' + if: ${{!startsWith(matrix.abi, 'arm')}} uses: actions/cache@v5 id: avd-cache with: @@ -105,7 +105,7 @@ jobs: run: cmake --build ${{github.workspace}}/build -j 4 - name: Create AVD and generate snapshot for caching - if: matrix.abi != 'armeabi-v7a' && steps.avd-cache.outputs.cache-hit != 'true' + if: ${{!startsWith(matrix.abi, 'arm') && steps.avd-cache.outputs.cache-hit != 'true'}} uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{matrix.sdk}} @@ -116,7 +116,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Test - if: matrix.abi != 'armeabi-v7a' + if: ${{!startsWith(matrix.abi, 'arm')}} uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{matrix.sdk}} From 44beb87abbb354d8f2a553a9deafd93a63753355 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 15:06:15 -0500 Subject: [PATCH 56/66] Add 4-byte alignment --- .../src/main/java/me/zero/libhat/Hat.java | 4 +- .../java/me/zero/libhat/ScanAlignment.java | 22 +++++++- include/libhat/c/libhat.h | 5 +- include/libhat/scanner.hpp | 55 +++++++++---------- src/Utils.hpp | 20 +++++++ src/arch/arm/Neon.cpp | 27 ++------- src/arch/x86/AVX2.cpp | 27 ++------- src/arch/x86/AVX512.cpp | 27 ++------- src/arch/x86/SSE.cpp | 27 ++------- src/c/libhat.cpp | 2 + test/tests/Scanner.cpp | 11 ++++ 11 files changed, 105 insertions(+), 122 deletions(-) diff --git a/bindings/java/src/main/java/me/zero/libhat/Hat.java b/bindings/java/src/main/java/me/zero/libhat/Hat.java index a3a5d5a..bfedafb 100644 --- a/bindings/java/src/main/java/me/zero/libhat/Hat.java +++ b/bindings/java/src/main/java/me/zero/libhat/Hat.java @@ -127,7 +127,7 @@ public static OptionalInt findPattern(@NotNull final Signature signature, @NotNu Objects.requireNonNull(signature.handle), new Pointer(start), count, - alignment.ordinal() + alignment.alignment() ); if (result == Pointer.NULL) { @@ -175,7 +175,7 @@ public static Optional findPattern(@NotNull final Signature signature, Objects.requireNonNull(signature.handle), module.handle, section, - alignment.ordinal() + alignment.alignment() ); return Optional.ofNullable(result); diff --git a/bindings/java/src/main/java/me/zero/libhat/ScanAlignment.java b/bindings/java/src/main/java/me/zero/libhat/ScanAlignment.java index b79547d..a2f7863 100644 --- a/bindings/java/src/main/java/me/zero/libhat/ScanAlignment.java +++ b/bindings/java/src/main/java/me/zero/libhat/ScanAlignment.java @@ -7,10 +7,28 @@ public enum ScanAlignment { /** * No byte alignment */ - X1, + X1(1), + + /** + * 4 byte alignment + */ + X4(4), /** * 16 byte alignment */ - X16 + X16(16); + + private final int alignment; + + ScanAlignment(final int alignment) { + this.alignment = alignment; + } + + /** + * @return The scan result alignment requirement, in bytes + */ + public final int alignment() { + return this.alignment; + } } diff --git a/include/libhat/c/libhat.h b/include/libhat/c/libhat.h index 6503c8b..bcaa44c 100644 --- a/include/libhat/c/libhat.h +++ b/include/libhat/c/libhat.h @@ -37,8 +37,9 @@ typedef enum libhat_status_t { } libhat_status_t; typedef enum scan_alignment { - scan_alignment_x1, - scan_alignment_x16, + scan_alignment_x1 = 1, + scan_alignment_x4 = 4, + scan_alignment_x16 = 16, } scan_alignment_t; typedef struct signature { diff --git a/include/libhat/scanner.hpp b/include/libhat/scanner.hpp index a0f21a3..be75a13 100644 --- a/include/libhat/scanner.hpp +++ b/include/libhat/scanner.hpp @@ -103,6 +103,7 @@ LIBHAT_EXPORT namespace hat { enum class scan_alignment : uint8_t { X1 = 1, + X4 = 4, X16 = 16 }; @@ -250,8 +251,31 @@ namespace hat::detail { template<> scan_function_t resolve_scanner(scan_context&); - template - const_scan_result find_pattern_single(const std::byte* begin, const std::byte* end, const scan_context&); + template + const_scan_result find_pattern_single(const std::byte* begin, const std::byte* end, const scan_context& context) { + static constexpr auto stride = alignment_stride; + const auto signature = context.signature; + const auto cmpByte = *signature[context.cmpIndex]; + + const auto scanBegin = align_up(begin) + context.cmpIndex; + const auto scanEnd = align_up(end - signature.size() + 1) + context.cmpIndex; + + if (scanBegin >= scanEnd) { + return nullptr; + } + + for (auto i = scanBegin; i != scanEnd; i += stride) { + if (*i == cmpByte) { + const auto start = i - context.cmpIndex; + const auto match = std::equal(signature.begin(), signature.end(), start); + if (match) LIBHAT_UNLIKELY { + return start; + } + } + } + + return nullptr; + } template<> constexpr const_scan_result find_pattern_single(const std::byte* begin, const std::byte* end, const scan_context& context) { @@ -286,36 +310,11 @@ namespace hat::detail { return nullptr; } - template<> - inline const_scan_result find_pattern_single(const std::byte* begin, const std::byte* end, const scan_context& context) { - static constexpr auto stride = alignment_stride; - const auto signature = context.signature; - const auto cmpByte = *signature[context.cmpIndex]; - - const auto scanBegin = align_up(begin) + context.cmpIndex; - const auto scanEnd = align_up(end - signature.size() + 1) + context.cmpIndex; - - if (scanBegin >= scanEnd) { - return nullptr; - } - - for (auto i = scanBegin; i != scanEnd; i += stride) { - if (*i == cmpByte) { - const auto start = i - context.cmpIndex; - const auto match = std::equal(signature.begin(), signature.end(), start); - if (match) LIBHAT_UNLIKELY { - return start; - } - } - } - - return nullptr; - } - template<> constexpr scan_function_t resolve_scanner(scan_context& context) { switch (context.alignment) { case scan_alignment::X1: return &find_pattern_single; + case scan_alignment::X4: return &find_pattern_single; case scan_alignment::X16: return &find_pattern_single; } LIBHAT_UNREACHABLE(); diff --git a/src/Utils.hpp b/src/Utils.hpp index 105f279..3aa1456 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -3,6 +3,8 @@ #include #include +#include + namespace hat::detail { constexpr uintptr_t fast_align_down(uintptr_t address, size_t alignment) { @@ -12,4 +14,22 @@ namespace hat::detail { constexpr uintptr_t fast_align_up(uintptr_t address, size_t alignment) { return (address + alignment - 1) & ~static_cast(alignment - 1); } + + template + auto* find_specialization_switch(const scan_alignment alignment, const bool cmpeq2, const bool veccmp) { + const auto with_alignment = [&](std::integral_constant) { + if (cmpeq2 && veccmp) return impl.template operator()(); + if (cmpeq2) return impl.template operator()(); + if (veccmp) return impl.template operator()(); + return impl.template operator()(); + }; + + switch (alignment) { + using enum scan_alignment; + case X1: return with_alignment(std::integral_constant{}); + case X4: return with_alignment(std::integral_constant{}); + case X16: return with_alignment(std::integral_constant{}); + } + LIBHAT_UNREACHABLE(); + } } diff --git a/src/arch/arm/Neon.cpp b/src/arch/arm/Neon.cpp index bba99de..74ecdb1 100644 --- a/src/arch/arm/Neon.cpp +++ b/src/arch/arm/Neon.cpp @@ -4,6 +4,8 @@ #include +#include "../../Utils.hpp" + #include #ifdef _MSC_VER @@ -129,28 +131,9 @@ namespace hat::detail { const bool cmpeq2 = context.pairIndex.has_value(); const bool veccmp = signature.size() <= 16; - if (alignment == scan_alignment::X1) { - if (cmpeq2 && veccmp) { - return &find_pattern_neon; - } else if (cmpeq2) { - return &find_pattern_neon; - } else if (veccmp) { - return &find_pattern_neon; - } else { - return &find_pattern_neon; - } - } else if (alignment == scan_alignment::X16) { - if (cmpeq2 && veccmp) { - return &find_pattern_neon; - } else if (cmpeq2) { - return &find_pattern_neon; - } else if (veccmp) { - return &find_pattern_neon; - } else { - return &find_pattern_neon; - } - } - LIBHAT_UNREACHABLE(); + return find_specialization_switch<[]() consteval { + return &find_pattern_neon; + }>(alignment, cmpeq2, veccmp); } } #endif diff --git a/src/arch/x86/AVX2.cpp b/src/arch/x86/AVX2.cpp index fecd6fb..58f402f 100644 --- a/src/arch/x86/AVX2.cpp +++ b/src/arch/x86/AVX2.cpp @@ -4,6 +4,8 @@ #include +#include "../../Utils.hpp" + #include namespace hat::detail { @@ -102,28 +104,9 @@ namespace hat::detail { const bool cmpeq2 = context.pairIndex.has_value(); const bool veccmp = signature.size() <= 32; - if (alignment == scan_alignment::X1) { - if (cmpeq2 && veccmp) { - return &find_pattern_avx2; - } else if (cmpeq2) { - return &find_pattern_avx2; - } else if (veccmp) { - return &find_pattern_avx2; - } else { - return &find_pattern_avx2; - } - } else if (alignment == scan_alignment::X16) { - if (cmpeq2 && veccmp) { - return &find_pattern_avx2; - } else if (cmpeq2) { - return &find_pattern_avx2; - } else if (veccmp) { - return &find_pattern_avx2; - } else { - return &find_pattern_avx2; - } - } - LIBHAT_UNREACHABLE(); + return find_specialization_switch<[]() consteval { + return &find_pattern_avx2; + }>(alignment, cmpeq2, veccmp); } } #endif diff --git a/src/arch/x86/AVX512.cpp b/src/arch/x86/AVX512.cpp index 7cbdcbf..daf0d24 100644 --- a/src/arch/x86/AVX512.cpp +++ b/src/arch/x86/AVX512.cpp @@ -4,6 +4,8 @@ #include +#include "../../Utils.hpp" + #include namespace hat::detail { @@ -99,28 +101,9 @@ namespace hat::detail { const bool cmpeq2 = context.pairIndex.has_value(); const bool veccmp = signature.size() <= 64; - if (alignment == scan_alignment::X1) { - if (cmpeq2 && veccmp) { - return &find_pattern_avx512; - } else if (cmpeq2) { - return &find_pattern_avx512; - } else if (veccmp) { - return &find_pattern_avx512; - } else { - return &find_pattern_avx512; - } - } else if (alignment == scan_alignment::X16) { - if (cmpeq2 && veccmp) { - return &find_pattern_avx512; - } else if (cmpeq2) { - return &find_pattern_avx512; - } else if (veccmp) { - return &find_pattern_avx512; - } else { - return &find_pattern_avx512; - } - } - LIBHAT_UNREACHABLE(); + return find_specialization_switch<[]() consteval { + return &find_pattern_avx512; + }>(alignment, cmpeq2, veccmp); } } #endif diff --git a/src/arch/x86/SSE.cpp b/src/arch/x86/SSE.cpp index fd2b473..37fd384 100644 --- a/src/arch/x86/SSE.cpp +++ b/src/arch/x86/SSE.cpp @@ -4,6 +4,8 @@ #include +#include "../../Utils.hpp" + #include #ifdef _MSC_VER @@ -114,28 +116,9 @@ namespace hat::detail { const bool cmpeq2 = context.pairIndex.has_value(); const bool veccmp = signature.size() <= 16; - if (alignment == scan_alignment::X1) { - if (cmpeq2 && veccmp) { - return &find_pattern_sse; - } else if (cmpeq2) { - return &find_pattern_sse; - } else if (veccmp) { - return &find_pattern_sse; - } else { - return &find_pattern_sse; - } - } else if (alignment == scan_alignment::X16) { - if (cmpeq2 && veccmp) { - return &find_pattern_sse; - } else if (cmpeq2) { - return &find_pattern_sse; - } else if (veccmp) { - return &find_pattern_sse; - } else { - return &find_pattern_sse; - } - } - LIBHAT_UNREACHABLE(); + return find_specialization_switch<[]() consteval { + return &find_pattern_sse; + }>(alignment, cmpeq2, veccmp); } } #endif diff --git a/src/c/libhat.cpp b/src/c/libhat.cpp index ba52b48..3e20022 100644 --- a/src/c/libhat.cpp +++ b/src/c/libhat.cpp @@ -17,6 +17,8 @@ static hat::scan_alignment to_cpp_align(const scan_alignment align) { switch (align) { case scan_alignment_x1: return hat::scan_alignment::X1; + case scan_alignment_x4: + return hat::scan_alignment::X4; case scan_alignment_x16: return hat::scan_alignment::X16; } diff --git a/test/tests/Scanner.cpp b/test/tests/Scanner.cpp index 483b2d6..e226564 100644 --- a/test/tests/Scanner.cpp +++ b/test/tests/Scanner.cpp @@ -128,6 +128,17 @@ TYPED_TEST(FindPatternTest, ScanX1) { }); } +TYPED_TEST(FindPatternTest, ScanX4) { + this->run_cases(hat::scan_alignment::X4, [](auto result, auto expected) { + if (std::bit_cast(expected) % 4 == 0) { + ASSERT_TRUE(result.has_result()); + ASSERT_EQ(result.get(), expected); + } else { + ASSERT_FALSE(result.has_result()); + } + }); +} + TYPED_TEST(FindPatternTest, ScanX16) { this->run_cases(hat::scan_alignment::X16, [](auto result, auto expected) { if (std::bit_cast(expected) % 16 == 0) { From d74b8c4aa7da1cf0308bd0fc537224147cd09c62 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 16:24:40 -0500 Subject: [PATCH 57/66] Switch benchmark to manual dispatch --- .github/workflows/benchmark.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ed4624b..c9615f0 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,10 +1,6 @@ name: Benchmark -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] +on: workflow_dispatch jobs: windows: From eb3690b1db01e10e8f03a990de4169f35e38afb7 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 16:54:12 -0500 Subject: [PATCH 58/66] Add AArch64 scan hint --- CMakeLists.txt | 2 + src/Scanner.cpp | 40 +++++++++---- src/arch/arm/Frequency.hpp | 117 +++++++++++++++++++++++++++++++++++++ src/arch/x86/Frequency.hpp | 5 ++ 4 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 src/arch/arm/Frequency.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bc94639..9ae9c9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ option(LIBHAT_USE_STD_MODULE "Compile the module target using the std module" OF option(LIBHAT_EXAMPLES "Include example targets" ${PROJECT_IS_TOP_LEVEL}) option(LIBHAT_HINT_X86_64 "Enables support for the x86_64 scan hint, requires a small (2KB) data table" ON) +option(LIBHAT_HINT_AARCH64 "Enables support for the aarch64 scan hint, requires a small (2KB) data table" ON) if(LIBHAT_TESTING AND LIBHAT_TESTING_SANITIZE) if(MSVC) @@ -94,6 +95,7 @@ target_compile_definitions(libhat PUBLIC "$<$:LIBHAT_DISABLE_SSE>" "$<$:LIBHAT_DISABLE_AVX512>" "$<$:LIBHAT_HINT_X86_64>" + "$<$:LIBHAT_HINT_AARCH64>" ) if(LIBHAT_STATIC_C_LIB OR LIBHAT_SHARED_C_LIB) diff --git a/src/Scanner.cpp b/src/Scanner.cpp index 5dbc657..c2eec85 100644 --- a/src/Scanner.cpp +++ b/src/Scanner.cpp @@ -7,25 +7,44 @@ #include "arch/x86/Frequency.hpp" #endif +#ifdef LIBHAT_HINT_AARCH64 +#include "arch/arm/Frequency.hpp" +#endif + namespace hat::detail { - void scan_context::apply_hints(const scanner_context& scanner) { - const bool pair0 = static_cast(this->hints & scan_hint::pair0); + static constexpr uint16_t NUM_PAIRS = 512; + using pair_hint_t = std::tuple< + const std::array, NUM_PAIRS>&, + const std::array& + >; + + static constexpr auto get_pair_hint(const scan_hint hints) -> std::optional { #ifdef LIBHAT_HINT_X86_64 - const bool x86_64 = static_cast(this->hints & scan_hint::x86_64); - if (x86_64 && !pair0 && scanner.vectorSize) { - static constexpr auto getScore = [](const std::byte a, const std::byte b) -> uint16_t { - constexpr auto& pairs = hat::detail::x86_64::pairs_x1; - constexpr auto& scores = hat::detail::x86_64::scores_x1; - static_assert(pairs.size() == scores.size()); + if (static_cast(hints & scan_hint::x86_64)) { + return std::tie(hat::detail::x86_64::pairs_x1, hat::detail::x86_64::scores_x1); + } +#endif +#ifdef LIBHAT_HINT_AARCH64 + if (static_cast(hints & scan_hint::aarch64)) { + return std::tie(hat::detail::aarch64::pairs_x1, hat::detail::aarch64::scores_x1); + } +#endif + return std::nullopt; + } - constexpr auto max = static_cast(scores.size()); + void scan_context::apply_hints(const scanner_context& scanner) { + const bool pair0 = static_cast(this->hints & scan_hint::pair0); + const auto pair_hint = get_pair_hint(this->hints); + if (pair_hint && !pair0 && scanner.vectorSize) { + const auto getScore = [&](const std::byte a, const std::byte b) -> uint16_t { + const auto& [pairs, scores] = *pair_hint; const std::pair pair{a, b}; const auto it = std::ranges::lower_bound(pairs, pair); const auto index = static_cast(it - pairs.begin()); - return it != pairs.end() && *it == pair ? scores[index] : max; + return it != pairs.end() && *it == pair ? scores[index] : NUM_PAIRS; }; std::optional> bestPair{}; @@ -46,7 +65,6 @@ namespace hat::detail { this->pairIndex = bestPair->first; } } -#endif // If no "optimal" pair was found, find the first byte pair in the signature if (!this->pairIndex.has_value()) { diff --git a/src/arch/arm/Frequency.hpp b/src/arch/arm/Frequency.hpp new file mode 100644 index 0000000..c95ebc3 --- /dev/null +++ b/src/arch/arm/Frequency.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include + +namespace hat::detail::aarch64 { + + static constexpr auto p(uint8_t a, uint8_t b) { + return std::pair{std::byte{a}, std::byte{b}}; + } + + // Top 512 byte pair occurrences on 1 byte alignment, sorted. Sourced from all PE (.exe and .dll) files included in + // the "clang+llvm-22.1.8-aarch64-pc-windows-msvc" archive. This list accounts for ~50.9% of all byte pairs. + static constexpr inline auto pairs_x1 = std::to_array>({ + p(0x00, 0x00), p(0x00, 0x01), p(0x00, 0x08), p(0x00, 0x11), p(0x00, 0x12), p(0x00, 0x14), p(0x00, 0x20), p(0x00, 0x2A), + p(0x00, 0x34), p(0x00, 0x35), p(0x00, 0x36), p(0x00, 0x37), p(0x00, 0x39), p(0x00, 0x40), p(0x00, 0x51), p(0x00, 0x54), + p(0x00, 0x71), p(0x00, 0x79), p(0x00, 0x80), p(0x00, 0x90), p(0x00, 0x91), p(0x00, 0x94), p(0x00, 0xA9), p(0x00, 0xAA), + p(0x00, 0xB0), p(0x00, 0xB4), p(0x00, 0xB5), p(0x00, 0xB9), p(0x00, 0xD0), p(0x00, 0xD1), p(0x00, 0xF0), p(0x00, 0xF1), + p(0x00, 0xF9), p(0x00, 0xFD), p(0x01, 0x00), p(0x01, 0x01), p(0x01, 0x05), p(0x01, 0x08), p(0x01, 0x09), p(0x01, 0x0A), + p(0x01, 0x0B), p(0x01, 0x0C), p(0x01, 0x15), p(0x01, 0x3F), p(0x01, 0x40), p(0x01, 0x54), p(0x01, 0x71), p(0x01, 0x80), + p(0x01, 0x90), p(0x01, 0x91), p(0x01, 0xA9), p(0x01, 0xAA), p(0x01, 0xB0), p(0x01, 0xB9), p(0x01, 0xD0), p(0x01, 0xD1), + p(0x01, 0xF0), p(0x01, 0xF9), p(0x02, 0x00), p(0x02, 0x01), p(0x02, 0x05), p(0x02, 0x08), p(0x02, 0x40), p(0x02, 0x80), + p(0x02, 0x91), p(0x02, 0xA9), p(0x02, 0xAA), p(0x02, 0xF9), p(0x03, 0x00), p(0x03, 0x01), p(0x03, 0x02), p(0x03, 0x03), + p(0x03, 0x04), p(0x03, 0x05), p(0x03, 0x08), p(0x03, 0x09), p(0x03, 0x13), p(0x03, 0x14), p(0x03, 0x15), p(0x03, 0x16), + p(0x03, 0x17), p(0x03, 0x18), p(0x03, 0x19), p(0x03, 0x1A), p(0x03, 0x1B), p(0x03, 0x1C), p(0x03, 0x1F), p(0x03, 0x40), + p(0x03, 0x5F), p(0x03, 0x80), p(0x03, 0x91), p(0x03, 0xA9), p(0x03, 0xAA), p(0x04, 0x00), p(0x04, 0x40), p(0x04, 0x80), + p(0x04, 0x91), p(0x04, 0xA9), p(0x05, 0x00), p(0x05, 0x01), p(0x05, 0x40), p(0x05, 0x91), p(0x06, 0x00), p(0x06, 0x40), + p(0x06, 0x80), p(0x06, 0x91), p(0x07, 0x00), p(0x07, 0x40), p(0x07, 0x80), p(0x07, 0x91), p(0x08, 0x00), p(0x08, 0x01), + p(0x08, 0x05), p(0x08, 0x09), p(0x08, 0x10), p(0x08, 0x11), p(0x08, 0x21), p(0x08, 0x40), p(0x08, 0x41), p(0x08, 0x61), + p(0x08, 0x81), p(0x08, 0x8B), p(0x08, 0x91), p(0x08, 0xA1), p(0x08, 0xAA), p(0x08, 0xC1), p(0x08, 0xCB), p(0x08, 0xEB), + p(0x09, 0x00), p(0x09, 0x01), p(0x09, 0x05), p(0x09, 0x40), p(0x09, 0x6B), p(0x09, 0x8B), p(0x09, 0x91), p(0x09, 0xEB), + p(0x0A, 0x00), p(0x0A, 0x01), p(0x0A, 0x40), p(0x0A, 0xEB), p(0x0B, 0x00), p(0x0B, 0x40), p(0x0C, 0x00), p(0x0D, 0x00), + p(0x0D, 0x40), p(0x0E, 0x00), p(0x0E, 0x40), p(0x0F, 0x00), p(0x0F, 0x40), p(0x10, 0x00), p(0x10, 0x40), p(0x11, 0x00), + p(0x11, 0x40), p(0x12, 0x00), p(0x12, 0x40), p(0x13, 0x00), p(0x13, 0x40), p(0x13, 0xAA), p(0x14, 0x00), p(0x14, 0xAA), + p(0x14, 0xE0), p(0x14, 0xE8), p(0x14, 0xEB), p(0x15, 0x00), p(0x15, 0xAA), p(0x15, 0xEB), p(0x16, 0x40), p(0x16, 0xAA), + p(0x17, 0x00), p(0x17, 0x08), p(0x17, 0x40), p(0x17, 0x9F), p(0x17, 0xAA), p(0x17, 0xE0), p(0x17, 0xE8), p(0x18, 0xAA), + p(0x19, 0x00), p(0x19, 0xAA), p(0x1A, 0x00), p(0x1A, 0x40), p(0x1A, 0xAA), p(0x1B, 0x00), p(0x1B, 0x40), p(0x1B, 0xAA), + p(0x1C, 0xAA), p(0x1D, 0x00), p(0x1F, 0x00), p(0x1F, 0x01), p(0x1F, 0x05), p(0x1F, 0x09), p(0x1F, 0x20), p(0x1F, 0x2A), + p(0x1F, 0x40), p(0x1F, 0x41), p(0x1F, 0x7C), p(0x1F, 0xAA), p(0x20, 0x00), p(0x20, 0x01), p(0x20, 0x80), p(0x20, 0xD4), + p(0x21, 0x00), p(0x21, 0x40), p(0x22, 0x00), p(0x22, 0x40), p(0x23, 0x00), p(0x23, 0x01), p(0x23, 0x40), p(0x27, 0x00), + p(0x27, 0x40), p(0x28, 0x00), p(0x28, 0x01), p(0x29, 0x00), p(0x29, 0x01), p(0x29, 0x05), p(0x2A, 0x00), p(0x2A, 0xE0), + p(0x2B, 0x00), p(0x2B, 0x40), p(0x2F, 0x00), p(0x2F, 0x40), p(0x33, 0x00), p(0x33, 0x40), p(0x39, 0x08), p(0x39, 0x1F), + p(0x39, 0xE8), p(0x3D, 0x00), p(0x3D, 0xE0), p(0x3F, 0x00), p(0x3F, 0x01), p(0x3F, 0x05), p(0x3F, 0xD6), p(0x40, 0x00), + p(0x40, 0x01), p(0x40, 0x39), p(0x40, 0x79), p(0x40, 0x91), p(0x40, 0x92), p(0x40, 0xA9), p(0x40, 0xB1), p(0x40, 0xB9), + p(0x40, 0xF2), p(0x40, 0xF8), p(0x40, 0xF9), p(0x40, 0xFD), p(0x41, 0x00), p(0x41, 0x39), p(0x41, 0xA9), p(0x41, 0xB9), + p(0x41, 0xF8), p(0x41, 0xF9), p(0x42, 0x00), p(0x42, 0xA9), p(0x42, 0xB9), p(0x42, 0xF9), p(0x43, 0x00), p(0x43, 0x01), + p(0x43, 0x40), p(0x43, 0xA9), p(0x43, 0xF9), p(0x44, 0xA9), p(0x44, 0xF9), p(0x45, 0xF9), p(0x4A, 0x01), p(0x51, 0x1F), + p(0x52, 0x08), p(0x52, 0x09), p(0x52, 0x1F), p(0x52, 0xE0), p(0x52, 0xE3), p(0x52, 0xE8), p(0x53, 0xC2), p(0x54, 0x08), + p(0x54, 0x09), p(0x54, 0x1F), p(0x54, 0x28), p(0x54, 0x29), p(0x54, 0x3F), p(0x54, 0x68), p(0x54, 0x88), p(0x54, 0xE0), + p(0x54, 0xE8), p(0x54, 0xE9), p(0x54, 0xFE), p(0x5B, 0x01), p(0x5B, 0x41), p(0x5F, 0x01), p(0x5F, 0xD6), p(0x5F, 0xF8), + p(0x60, 0x00), p(0x60, 0x02), p(0x61, 0x00), p(0x62, 0x00), p(0x63, 0x00), p(0x68, 0x00), p(0x68, 0x02), p(0x68, 0x0A), + p(0x69, 0x02), p(0x6B, 0x01), p(0x71, 0x61), p(0x7C, 0x92), p(0x7C, 0xD3), p(0x7D, 0x92), p(0x7D, 0xD3), p(0x7F, 0x01), + p(0x7F, 0x02), p(0x80, 0x00), p(0x80, 0x3D), p(0x80, 0x52), p(0x81, 0x00), p(0x82, 0x00), p(0x83, 0x00), p(0x83, 0x01), + p(0x83, 0x02), p(0x88, 0x00), p(0x88, 0x02), p(0x88, 0x9A), p(0x89, 0x9A), p(0x90, 0x08), p(0x91, 0x00), p(0x91, 0x01), + p(0x91, 0x02), p(0x91, 0x03), p(0x91, 0x08), p(0x91, 0x1F), p(0x91, 0x22), p(0x91, 0xC0), p(0x91, 0xE0), p(0x91, 0xE1), + p(0x91, 0xE2), p(0x91, 0xE3), p(0x91, 0xE4), p(0x91, 0xE8), p(0x91, 0xE9), p(0x91, 0xFF), p(0x94, 0x20), p(0x94, 0x68), + p(0x94, 0xE0), p(0x94, 0xE1), p(0x94, 0xE8), p(0x97, 0x20), p(0x97, 0x68), p(0x97, 0xE0), p(0x97, 0xE1), p(0x97, 0xE8), + p(0x9F, 0x02), p(0x9F, 0x1A), p(0xA0, 0x00), p(0xA1, 0x00), p(0xA3, 0x00), p(0xA8, 0x02), p(0xA8, 0xC0), p(0xA9, 0x08), + p(0xA9, 0x1F), p(0xA9, 0xE0), p(0xA9, 0xE8), p(0xA9, 0xF3), p(0xA9, 0xF5), p(0xA9, 0xF7), p(0xA9, 0xF9), p(0xA9, 0xFE), + p(0xA9, 0xFF), p(0xAA, 0x00), p(0xAA, 0x01), p(0xAA, 0x02), p(0xAA, 0x03), p(0xAA, 0x08), p(0xAA, 0x1F), p(0xAA, 0xE0), + p(0xAA, 0xE1), p(0xAA, 0xE2), p(0xAA, 0xE3), p(0xAA, 0xE4), p(0xAA, 0xE5), p(0xAA, 0xE8), p(0xAA, 0xF3), p(0xAA, 0xF4), + p(0xAA, 0xF5), p(0xAA, 0xF6), p(0xAA, 0xF7), p(0xAA, 0xFE), p(0xAA, 0xFF), p(0xB0, 0x08), p(0xB4, 0x08), p(0xB4, 0xE0), + p(0xB9, 0x08), p(0xB9, 0x09), p(0xB9, 0x1F), p(0xB9, 0x29), p(0xB9, 0x3F), p(0xB9, 0x68), p(0xB9, 0xE0), p(0xB9, 0xE1), + p(0xB9, 0xE8), p(0xB9, 0xE9), p(0xB9, 0xFF), p(0xBF, 0x02), p(0xC0, 0x03), p(0xC0, 0x3D), p(0xC1, 0x00), p(0xC2, 0xA8), + p(0xC3, 0x00), p(0xC3, 0x01), p(0xC8, 0x02), p(0xD0, 0x08), p(0xD1, 0xF3), p(0xD4, 0xFF), p(0xD6, 0xE0), p(0xDF, 0x02), + p(0xE0, 0x03), p(0xE0, 0x07), p(0xE0, 0x0F), p(0xE0, 0x23), p(0xE0, 0x43), p(0xE0, 0x63), p(0xE0, 0x83), p(0xE0, 0xC3), + p(0xE1, 0x03), p(0xE1, 0x23), p(0xE1, 0x43), p(0xE1, 0x63), p(0xE1, 0x83), p(0xE1, 0xC3), p(0xE2, 0x03), p(0xE3, 0x00), + p(0xE3, 0x03), p(0xE4, 0x00), p(0xE4, 0x03), p(0xE5, 0x03), p(0xE6, 0x03), p(0xE7, 0x03), p(0xE8, 0x03), p(0xE8, 0x07), + p(0xE8, 0x0B), p(0xE8, 0x0F), p(0xE8, 0x13), p(0xE8, 0x17), p(0xE8, 0x23), p(0xE8, 0x27), p(0xE8, 0x43), p(0xE9, 0x03), + p(0xEA, 0x03), p(0xEB, 0x01), p(0xEB, 0x40), p(0xEB, 0x61), p(0xEB, 0xA1), p(0xEB, 0xC1), p(0xEB, 0xE1), p(0xED, 0x7C), + p(0xF0, 0x08), p(0xF1, 0x7D), p(0xF3, 0x03), p(0xF3, 0x53), p(0xF4, 0x03), p(0xF5, 0x03), p(0xF5, 0x5B), p(0xF5, 0x7B), + p(0xF6, 0x03), p(0xF7, 0x03), p(0xF7, 0x63), p(0xF8, 0x03), p(0xF9, 0x00), p(0xF9, 0x01), p(0xF9, 0x02), p(0xF9, 0x03), + p(0xF9, 0x08), p(0xF9, 0x09), p(0xF9, 0x0A), p(0xF9, 0x1F), p(0xF9, 0x20), p(0xF9, 0x21), p(0xF9, 0x28), p(0xF9, 0x29), + p(0xF9, 0x2A), p(0xF9, 0x3F), p(0xF9, 0x40), p(0xF9, 0x48), p(0xF9, 0x68), p(0xF9, 0x69), p(0xF9, 0x6B), p(0xF9, 0x7F), + p(0xF9, 0x88), p(0xF9, 0xA8), p(0xF9, 0xC8), p(0xF9, 0xE0), p(0xF9, 0xE1), p(0xF9, 0xE2), p(0xF9, 0xE8), p(0xF9, 0xE9), + p(0xF9, 0xEA), p(0xF9, 0xF3), p(0xF9, 0xF4), p(0xF9, 0xF5), p(0xF9, 0xF7), p(0xF9, 0xFB), p(0xF9, 0xFF), p(0xFA, 0x03), + p(0xFA, 0xFF), p(0xFB, 0x73), p(0xFB, 0xFF), p(0xFC, 0xFF), p(0xFD, 0xFF), p(0xFE, 0x0B), p(0xFE, 0xFF), p(0xFF, 0x03), + p(0xFF, 0x17), p(0xFF, 0x43), p(0xFF, 0x54), p(0xFF, 0x83), p(0xFF, 0x97), p(0xFF, 0xB4), p(0xFF, 0xC3), p(0xFF, 0xFF), + }); + + static constexpr inline auto scores_x1 = std::to_array({ + 0x002, 0x040, 0x0D9, 0x04F, 0x0A3, 0x00E, 0x06F, 0x12A, 0x048, 0x0F0, 0x07E, 0x0AE, 0x06E, 0x032, 0x05F, 0x001, + 0x011, 0x0F4, 0x00D, 0x087, 0x006, 0x043, 0x092, 0x018, 0x086, 0x024, 0x0B8, 0x019, 0x08F, 0x0B5, 0x074, 0x01B, + 0x004, 0x0ED, 0x00A, 0x078, 0x14C, 0x073, 0x021, 0x071, 0x0C9, 0x18B, 0x1C0, 0x08C, 0x01C, 0x1A0, 0x05E, 0x046, + 0x10B, 0x027, 0x0A2, 0x05D, 0x114, 0x156, 0x112, 0x145, 0x110, 0x0A6, 0x016, 0x0EE, 0x12C, 0x137, 0x01E, 0x0CA, + 0x04E, 0x0DF, 0x0C6, 0x1A6, 0x009, 0x028, 0x05C, 0x0CF, 0x149, 0x1E7, 0x057, 0x16E, 0x013, 0x01A, 0x02A, 0x033, + 0x054, 0x076, 0x0A1, 0x0D3, 0x106, 0x171, 0x010, 0x072, 0x036, 0x0DB, 0x081, 0x126, 0x195, 0x050, 0x14E, 0x15D, + 0x0B7, 0x184, 0x00C, 0x155, 0x03E, 0x0E2, 0x04C, 0x066, 0x1D5, 0x101, 0x04A, 0x056, 0x1FE, 0x152, 0x02E, 0x026, + 0x058, 0x15A, 0x17B, 0x16D, 0x0D1, 0x0E6, 0x119, 0x1D7, 0x12D, 0x0CC, 0x13D, 0x150, 0x098, 0x136, 0x10E, 0x089, + 0x038, 0x096, 0x191, 0x08B, 0x0EC, 0x138, 0x1FD, 0x03B, 0x077, 0x1FF, 0x06A, 0x190, 0x068, 0x09E, 0x12B, 0x0D2, + 0x1A2, 0x133, 0x0E3, 0x0C5, 0x097, 0x117, 0x153, 0x0B2, 0x0F6, 0x0FB, 0x0E4, 0x0A9, 0x0D5, 0x014, 0x179, 0x01F, + 0x120, 0x1EB, 0x11D, 0x130, 0x02C, 0x180, 0x1E3, 0x03D, 0x0D4, 0x1E2, 0x0DC, 0x1CF, 0x064, 0x05A, 0x16A, 0x093, + 0x129, 0x0BF, 0x199, 0x13A, 0x0F9, 0x100, 0x108, 0x15F, 0x1CB, 0x11A, 0x03F, 0x015, 0x053, 0x1B9, 0x1EC, 0x02D, + 0x0E1, 0x167, 0x1CA, 0x025, 0x045, 0x10F, 0x146, 0x080, 0x052, 0x11B, 0x065, 0x13C, 0x04D, 0x1CD, 0x0DD, 0x169, + 0x139, 0x0AB, 0x10C, 0x0E0, 0x095, 0x0FA, 0x18F, 0x1DF, 0x148, 0x16B, 0x1F1, 0x19E, 0x18D, 0x1C3, 0x1F9, 0x0A4, + 0x1E1, 0x14F, 0x1E4, 0x161, 0x061, 0x160, 0x085, 0x0A7, 0x1F5, 0x029, 0x0D0, 0x1DD, 0x143, 0x0B3, 0x1A1, 0x007, + 0x1CE, 0x144, 0x000, 0x1B3, 0x079, 0x1FB, 0x07B, 0x0C7, 0x15B, 0x039, 0x0BA, 0x0C0, 0x1AC, 0x0AD, 0x088, 0x111, + 0x1ED, 0x103, 0x1A8, 0x165, 0x1BD, 0x1AD, 0x16C, 0x1AF, 0x0D6, 0x1BE, 0x19D, 0x173, 0x1DC, 0x13B, 0x1C7, 0x09D, + 0x0FF, 0x0B0, 0x172, 0x1A4, 0x187, 0x124, 0x17A, 0x044, 0x082, 0x125, 0x170, 0x1FA, 0x1C1, 0x0D7, 0x037, 0x185, + 0x11F, 0x142, 0x0A5, 0x14A, 0x0AA, 0x192, 0x0AF, 0x183, 0x1DE, 0x198, 0x197, 0x19C, 0x182, 0x132, 0x109, 0x188, + 0x1DA, 0x164, 0x069, 0x003, 0x0F1, 0x11C, 0x06B, 0x131, 0x1CC, 0x1B6, 0x0FD, 0x17D, 0x196, 0x1D3, 0x17E, 0x1EA, + 0x116, 0x1F7, 0x0C1, 0x0CB, 0x1A7, 0x099, 0x020, 0x02F, 0x07F, 0x0EB, 0x1F8, 0x094, 0x1D1, 0x1BA, 0x1AA, 0x151, + 0x060, 0x123, 0x063, 0x09A, 0x1B5, 0x09B, 0x186, 0x075, 0x1A3, 0x0B4, 0x1C6, 0x11E, 0x113, 0x166, 0x14D, 0x1D2, + 0x0AC, 0x1DB, 0x1F0, 0x06D, 0x03A, 0x083, 0x0D8, 0x0B6, 0x070, 0x12E, 0x134, 0x1BB, 0x1B0, 0x059, 0x1BC, 0x07C, + 0x030, 0x049, 0x0C4, 0x121, 0x1B1, 0x09C, 0x14B, 0x12F, 0x13F, 0x1E8, 0x1F3, 0x1E6, 0x15C, 0x1E0, 0x122, 0x105, + 0x062, 0x154, 0x07D, 0x1B8, 0x1C9, 0x1C4, 0x0E8, 0x1F6, 0x0DE, 0x128, 0x174, 0x19B, 0x035, 0x08A, 0x15E, 0x1B7, + 0x0BC, 0x19F, 0x1C5, 0x1D9, 0x0B1, 0x127, 0x0E5, 0x1D8, 0x005, 0x0F2, 0x194, 0x0C8, 0x0F7, 0x1D0, 0x0CD, 0x135, + 0x00F, 0x118, 0x0F8, 0x193, 0x104, 0x163, 0x01D, 0x177, 0x042, 0x1C8, 0x09F, 0x0DA, 0x13E, 0x1E5, 0x034, 0x0FC, + 0x1BF, 0x181, 0x176, 0x1C2, 0x168, 0x10A, 0x189, 0x0C3, 0x18C, 0x1AE, 0x102, 0x1D4, 0x18E, 0x1AB, 0x1FC, 0x140, + 0x1EE, 0x0BB, 0x03C, 0x023, 0x04B, 0x067, 0x041, 0x162, 0x091, 0x0BE, 0x084, 0x0E9, 0x051, 0x0C2, 0x1F4, 0x107, + 0x012, 0x06C, 0x157, 0x022, 0x18A, 0x1EF, 0x0A0, 0x0BD, 0x1E9, 0x10D, 0x1A9, 0x158, 0x0A8, 0x1F2, 0x0B9, 0x17C, + 0x0FE, 0x16F, 0x1B2, 0x02B, 0x047, 0x115, 0x031, 0x090, 0x1B4, 0x08D, 0x175, 0x0EF, 0x17F, 0x19A, 0x07A, 0x1A5, + 0x1D6, 0x147, 0x178, 0x0F3, 0x08E, 0x159, 0x055, 0x0CE, 0x008, 0x0F5, 0x017, 0x0E7, 0x05B, 0x141, 0x0EA, 0x00B, + }); +} diff --git a/src/arch/x86/Frequency.hpp b/src/arch/x86/Frequency.hpp index 8729832..2f61da1 100644 --- a/src/arch/x86/Frequency.hpp +++ b/src/arch/x86/Frequency.hpp @@ -1,5 +1,10 @@ #pragma once +#include +#include +#include +#include + namespace hat::detail::x86_64 { static constexpr auto p(uint8_t a, uint8_t b) { From 4a8e7db5c76041143217626d2aeecfaf1e3a5d0c Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 16:58:23 -0500 Subject: [PATCH 59/66] Actually update the scan_hint enum --- include/libhat/scanner.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/libhat/scanner.hpp b/include/libhat/scanner.hpp index be75a13..21b87da 100644 --- a/include/libhat/scanner.hpp +++ b/include/libhat/scanner.hpp @@ -108,9 +108,10 @@ LIBHAT_EXPORT namespace hat { }; enum class scan_hint : uint64_t { - none = 0, // no hints - x86_64 = 1 << 0, // The data being scanned is x86_64 machine code - pair0 = 1 << 1, // Only utilize byte pair based scanning if the signature starts with a byte pair + none = 0, // no hints + x86_64 = 1 << 0, // The data being scanned is x86_64 machine code + pair0 = 1 << 1, // Only utilize byte pair based scanning if the signature starts with a byte pair + aarch64 = 1 << 2, // The data being scanned is AArch64 machine code }; constexpr scan_hint operator|(scan_hint lhs, scan_hint rhs) { From f7dd7124bccde7292a49b0ad9df257f361738a5e Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 19:30:36 -0500 Subject: [PATCH 60/66] Update README to reflect Android support --- README.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8732308..97edd34 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,15 @@ A modern, high-performance library for C++20 designed around game hacking ## Feature overview -- Windows x86/x64 support -- Partial Linux and macOS support - Vectorized scanning for byte patterns - SSE 4.1 and AVX2 on x86/x64 - AVX-512 on x64 + - Neon on ARM/ARM64 - RAII memory protector - Convenience wrappers over OS APIs - Language bindings (C, C#, Java) +- Full Windows support +- Partial (WIP) Linux, macOS, and Android support ## Versioning This project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html). Any declaration that @@ -100,19 +101,21 @@ BM_Throughput_UC2/256MiB 261157833 ns 261160714 ns Below is a summary of the current support for libhat's platform-dependent APIs: -| | Windows | Linux | macOS | -|--------------------------------|:-------:|:-----:|:-----:| -| `hat::get_system` | ✅ | ✅ | ✅ | -| `hat::memory_protector` | ✅ | ✅ | | -| `hp::get_process_module` | ✅ | ✅ | ✅ | -| `hp::get_module` | ✅ | ✅ | ✅ | -| `hp::module_at` | ✅ | | | -| `hp::is_readable` | ✅ | ✅ | | -| `hp::is_writable` | ✅ | ✅ | | -| `hp::is_executable` | ✅ | ✅ | | -| `hp::module::get_module_data` | ✅ | ✅ | | -| `hp::module::get_section_data` | ✅ | | | -| `hp::module::for_each_segment` | ✅ | ✅ | ✅ | +### APIs + +| | Windows | Linux | macOS | Android | +|--------------------------------|:-------:|:-----:|:-----:|:-------:| +| `hat::get_system` | ✅ | ✅ | ✅ | ✅ | +| `hat::memory_protector` | ✅ | ✅ | | ✅ | +| `hp::get_process_module` | ✅ | ✅ | ✅ | ✅ | +| `hp::get_module` | ✅ | ✅ | ✅ | ✅ | +| `hp::module_at` | ✅ | | | | +| `hp::is_readable` | ✅ | ✅ | | ✅ | +| `hp::is_writable` | ✅ | ✅ | | ✅ | +| `hp::is_executable` | ✅ | ✅ | | ✅ | +| `hp::module::get_module_data` | ✅ | ✅ | | ✅ | +| `hp::module::get_section_data` | ✅ | | | | +| `hp::module::for_each_segment` | ✅ | ✅ | ✅ | ✅ | ## Quick start ### Defining patterns From 9ee9705258ad52c2f5fbc7a13c37382912346530 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 19:58:30 -0500 Subject: [PATCH 61/66] Update README to include optimizations specific to ARM --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 97edd34..fb26bc3 100644 --- a/README.md +++ b/README.md @@ -192,18 +192,23 @@ 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: +libhat has a few optimizations for searching for patterns in `x86_64` and `AArch64` machine code: ```cpp #include -// 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): +// Compilers will often align the start address of a function on 16-bytes. Scanning for patterns that +// match the start of a function can take advantage of this by specifying the defaulted `alignment` +// parameter (all overloads have this parameter): std::span 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. +// Or, if the architecture has byte-aligned instructions (such as ARM and AArch64): +hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X4); + +// Additionally, machine code contains a non-uniform distribution of bytes. By passing the respective +// scan hint (either `x86_64` or `aarch64`), the search anchor can be tuned to the least frequent +// bytes that are present in the pattern. hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64); ``` From f7bede0207f00e91137b9404c09e9ad03576115a Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 20:00:01 -0500 Subject: [PATCH 62/66] Adjust formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb26bc3..a330894 100644 --- a/README.md +++ b/README.md @@ -204,12 +204,12 @@ hat::signature_view pattern = /* ... */; hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X16); // Or, if the architecture has byte-aligned instructions (such as ARM and AArch64): -hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X4); +hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X4); // Additionally, machine code contains a non-uniform distribution of bytes. By passing the respective // scan hint (either `x86_64` or `aarch64`), the search anchor can be tuned to the least frequent // bytes that are present in the pattern. -hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64); +hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64); ``` ### Accessing members From 053791eb0b89603adbbf64b08b6988cf6a43739f Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 2 Jul 2026 23:18:31 -0500 Subject: [PATCH 63/66] Remove `-Wno-unused-command-line-argument` workaround --- CMakeLists.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ae9c9d..ad16d32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,10 +80,7 @@ if (MSVC) target_compile_options(libhat PRIVATE /W3 /WX) endif() elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(libhat PRIVATE -Wall -Wextra -Wconversion -Werror - # temp fix for macOS CI failing due to incorrect LIBHAT_COMPILER_X86_OPTIONS value - -Wno-unused-command-line-argument - ) + target_compile_options(libhat PRIVATE -Wall -Wextra -Wconversion -Werror) endif() target_include_directories(libhat PUBLIC From 57561b554fcb448c098ec651dc3eb93aa8da3608 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 3 Jul 2026 01:54:48 -0500 Subject: [PATCH 64/66] Detect ISA extensions at compile time --- include/libhat/system.hpp | 109 ++++++++++++++++++++++++++++++++------ src/Scanner.cpp | 11 ++-- 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/include/libhat/system.hpp b/include/libhat/system.hpp index f13a6cc..8a8a4e5 100644 --- a/include/libhat/system.hpp +++ b/include/libhat/system.hpp @@ -23,38 +23,105 @@ LIBHAT_EXPORT namespace hat { #if defined(LIBHAT_X86) || defined(LIBHAT_X86_64) LIBHAT_EXPORT namespace hat { + struct cpu_extensions { + bool sse; + bool sse2; + bool sse3; + bool ssse3; + bool sse41; + bool sse42; + bool avx; + bool avx2; + bool avx512f; + bool avx512bw; + bool popcnt; + bool bmi; + }; + struct system_info_x86 : hat::system_info { std::string cpu_vendor{}; std::string cpu_brand{}; - struct { - bool sse; - bool sse2; - bool sse3; - bool ssse3; - bool sse41; - bool sse42; - bool avx; - bool avx2; - bool avx512f; - bool avx512bw; - bool popcnt; - bool bmi; - } extensions{}; + cpu_extensions extensions{}; private: system_info_x86(); friend const system_info_x86& get_system(); static const system_info_x86 instance; }; + inline constexpr cpu_extensions compiled_extensions{ +#if defined(__SSE__) || _M_IX86_FP >= 1 + .sse = true, +#else + .sse = false, +#endif +#if defined(__SSE2__) || _M_IX86_FP >= 2 + .sse2 = true, +#else + .sse2 = false, +#endif +#if defined(__SSE3__) + .sse3 = true, +#else + .sse3 = false, +#endif +#if defined(__SSSE3__) + .ssse3 = true, +#else + .ssse3 = false, +#endif +#if defined(__SSE4_1__) + .sse41 = true, +#else + .sse41 = false, +#endif +#if defined(__SSE4_2__) + .sse42 = true, +#else + .sse42 = false, +#endif +#if defined(__AVX__) + .avx = true, +#else + .avx = false, +#endif +#if defined(__AVX2__) + .avx2 = true, +#else + .avx2 = false, +#endif +#if defined(__AVX512F__) + .avx512f = true, +#else + .avx512f = false, +#endif +#if defined(__AVX512BW__) + .avx512bw = true, +#else + .avx512bw = false, +#endif +#if defined(__POPCNT__) + .popcnt = true, +#else + .popcnt = false, +#endif +#if defined(__BMI__) + .bmi = true, +#else + .bmi = false, +#endif + }; + using system_info_impl = system_info_x86; } #elif defined(LIBHAT_ARM) || defined(LIBHAT_AARCH64) LIBHAT_EXPORT namespace hat { + struct cpu_extensions { + bool neon; + }; + struct system_info_arm : hat::system_info { - struct { - bool neon; - } extensions{}; + cpu_extensions extensions{}; private: system_info_arm(); friend const system_info_arm& get_system(); @@ -62,6 +129,14 @@ LIBHAT_EXPORT namespace hat { }; using system_info_impl = system_info_arm; + + inline constexpr cpu_extensions compiled_extensions{ +#if defined(__ARM_NEON) || defined(LIBHAT_WINDOWS) || defined(LIBHAT_MAC) + .neon = true, +#else + .neon = false, +#endif + }; } #endif diff --git a/src/Scanner.cpp b/src/Scanner.cpp index c2eec85..af55124 100644 --- a/src/Scanner.cpp +++ b/src/Scanner.cpp @@ -88,24 +88,25 @@ namespace hat::detail { scan_function_t resolve_scanner(scan_context& context) { const auto& ext = get_system().extensions; #if defined(LIBHAT_X86) || defined(LIBHAT_X86_64) - if (ext.bmi) { + if (compiled_extensions.bmi || ext.bmi) { #if defined(LIBHAT_X86_64) && !defined(LIBHAT_DISABLE_AVX512) - if (ext.avx512f && ext.avx512bw) { + if ((compiled_extensions.avx512f || ext.avx512f) + && (compiled_extensions.avx512bw || ext.avx512bw)) { return resolve_scanner(context); } #endif - if (ext.avx2) { + if (compiled_extensions.avx2 || ext.avx2) { return resolve_scanner(context); } } #if !defined(LIBHAT_DISABLE_SSE) - if (ext.sse41) { + if (compiled_extensions.sse41 || ext.sse41) { return resolve_scanner(context); } #endif #endif #if defined(LIBHAT_ARM) || defined(LIBHAT_AARCH64) - if (ext.neon) { + if (compiled_extensions.neon || ext.neon) { return resolve_scanner(context); } #endif From 946a81db0040a2d0569b74c9719a483b8d28ffea Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 3 Jul 2026 14:32:59 -0500 Subject: [PATCH 65/66] Theoretical 32-bit Mach-O support --- include/libhat/platform.h | 6 ++++++ src/os/mac/Process.cpp | 42 +++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/include/libhat/platform.h b/include/libhat/platform.h index 5138403..1043703 100644 --- a/include/libhat/platform.h +++ b/include/libhat/platform.h @@ -13,6 +13,12 @@ #error Unsupported Architecture #endif +#if defined(__LP64__) || defined(_M_X64) + #define LIBHAT_LP64 +#else + #define LIBHAT_LP32 +#endif + // Detect Operating System #if defined(_WIN32) #define LIBHAT_WINDOWS diff --git a/src/os/mac/Process.cpp b/src/os/mac/Process.cpp index c1197c3..63ac723 100644 --- a/src/os/mac/Process.cpp +++ b/src/os/mac/Process.cpp @@ -15,23 +15,44 @@ namespace hat::process { +#ifdef LIBHAT_LP64 + using mach_header_t = mach_header_64; + using segment_command_t = segment_command_64; + static constexpr uint32_t mh_magic = MH_MAGIC_64; + static constexpr uint32_t mh_cigam = MH_CIGAM_64; + static constexpr uint32_t lc_segment = LC_SEGMENT_64; +#else + using mach_header_t = mach_header; + using segment_command_t = segment_command; + static constexpr uint32_t mh_magic = MH_MAGIC; + static constexpr uint32_t mh_cigam = MH_CIGAM; + static constexpr uint32_t lc_segment = LC_SEGMENT; +#endif + + static bool is_valid_header(const mach_header_t* header) { + return header && (header->magic == mh_magic || header->magic == mh_cigam); + } + hat::process::module get_process_module() { const uint32_t count = _dyld_image_count(); for (uint32_t i = 0; i != count; i++) { - const auto* header = reinterpret_cast(_dyld_get_image_header(i)); - if (header && header->filetype == MH_EXECUTE) { + const auto* header = reinterpret_cast(_dyld_get_image_header(i)); + if (!is_valid_header(header)) { + continue; + } + + if (header->filetype == MH_EXECUTE) { return hat::process::module{std::bit_cast(header)}; } } std::abort(); } - // no-op on 32 bit binaries void module::for_each_segment(const std::function, hat::protection)>& callback) const { const uint32_t imageCount = _dyld_image_count(); for (uint32_t i = 0; i < imageCount; i++) { - const auto* header = reinterpret_cast(_dyld_get_image_header(i)); - if (header == nullptr) { + const auto* header = reinterpret_cast(_dyld_get_image_header(i)); + if (!is_valid_header(header)) { continue; } if (std::bit_cast(header) != this->address()) { @@ -40,11 +61,11 @@ namespace hat::process { const auto slide = static_cast(_dyld_get_image_vmaddr_slide(i)); const auto* cmd = reinterpret_cast( - reinterpret_cast(header) + sizeof(mach_header_64)); + reinterpret_cast(header) + sizeof(mach_header_t)); for (uint32_t j = 0; j < header->ncmds; j++) { - if (cmd->cmd == LC_SEGMENT_64) { - const auto* seg = reinterpret_cast(cmd); + if (cmd->cmd == lc_segment) { + const auto* seg = reinterpret_cast(cmd); // skip __PAGEZERO and any unmapped segment if (seg->vmsize != 0 && seg->initprot != 0) { @@ -87,9 +108,10 @@ namespace hat::process { const uint32_t count = _dyld_image_count(); for (uint32_t i = 0; i < count; i++) { - const auto* header = reinterpret_cast(_dyld_get_image_header(i)); - if (header == nullptr) + const auto* header = reinterpret_cast(_dyld_get_image_header(i)); + if (!is_valid_header(header)) { continue; + } const Handle h{dlopen(_dyld_get_image_name(i), RTLD_LAZY | RTLD_NOLOAD)}; if (h == handle) { From 4f62dffeacf5d25b95743d744e4a53291f1279d3 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 3 Jul 2026 14:33:50 -0500 Subject: [PATCH 66/66] space --- include/libhat/platform.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/libhat/platform.h b/include/libhat/platform.h index 1043703..6c09272 100644 --- a/include/libhat/platform.h +++ b/include/libhat/platform.h @@ -13,7 +13,7 @@ #error Unsupported Architecture #endif -#if defined(__LP64__) || defined(_M_X64) +#if defined(__LP64__) || defined(_M_X64) #define LIBHAT_LP64 #else #define LIBHAT_LP32