Skip to content

Commit 130664b

Browse files
ZeroMemesImrglop
andauthored
Vectorized scanning for ARM Neon (#40)
* Initial Neon implementation using sse2neon * Ignore sse2neon warnings * Missing Mode name * Implementation throughput comparison benchmark * Log processor name * Only build `libhat_benchmark_compare_impl` * Neon substitutions * Architecture guard + use time defaults * Avoid double movemask * Use 4-bit mask * Fix warning * Increase benchmark time * Remove sse2neon * Missing neon header include * Improve neon intrinsic type conformance * Hopefully resolve remaining macOS compile errors * Compile Windows on ARM benchmark using Clang * Test manual bitwise optimization * Benchmark ARM Neon against Chromium * Use cache for benchmark workflow * Perhaps * Extract masks using `vget_lane_u64` * Debug mode optimizations for NEON * Revert unrolling * Add macOS to ARM64 testing * Partial mach-o Process * Missing source file * Missing include for `std::abort` * Possibly fix macOS process APIs * Add Linux to ARM64 testing * Fix Linux compile * Actually fix compile + adjust workflow * Implement `system_info_arm` on Linux * Adjust mask iteration * ALLEGEDLY I can't use `_CountTrailingZeros64` * Optimize `frequency.py` using NumPy * Avoid `bytes` copy with `memoryview` * Add new NTTP to `segment_scan` call * Replace `u64` extract with `vmaxvq_u32` * Update README with macOS support * Add test that validates presence of default module segments * Missing include for `std::abort` * Recognize `PT_GNU_RELRO` segment type * Fix some includes on macOS * Add Android build to CMake workflow * Adjust the minimum Android SDK to `android-21` * Build all currently supported Android ABIs * `vmaxvq_u32` is not supported on ARMv7 * Attempt android emulation for testing * Cache AVD snapshot * Exclude `armeabi-v7a` from AVD cache steps * Include ABI in AVD cache key * Fix options for Android test step * Fix `get_module` on Android * Exclude `armeabi-v7a` and `arm64-v8a` from testing * Add 4-byte alignment * Switch benchmark to manual dispatch * Add AArch64 scan hint * Actually update the scan_hint enum * Update README to reflect Android support * Update README to include optimizations specific to ARM * Adjust formatting * Remove `-Wno-unused-command-line-argument` workaround * Detect ISA extensions at compile time * Theoretical 32-bit Mach-O support * space --------- Co-authored-by: Imrglop <69129770+Imrglop@users.noreply.github.com>
1 parent ae41bbf commit 130664b

28 files changed

Lines changed: 899 additions & 171 deletions

.github/workflows/benchmark.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Benchmark
2+
3+
on: workflow_dispatch
4+
5+
jobs:
6+
windows:
7+
strategy:
8+
matrix:
9+
toolset: [ v145, ClangCL ]
10+
runs-on: windows-11-vs2026-arm
11+
steps:
12+
- uses: actions/checkout@v6
13+
14+
- name: CPM Cache
15+
uses: actions/cache@v5
16+
with:
17+
path: ${{github.workspace}}/.cpmcache
18+
key: cpm-${{runner.os}}-ARM64-${{hashFiles('test/CMakeLists.txt')}}
19+
restore-keys: |
20+
cpm-${{runner.os}}-ARM64-
21+
22+
- name: Check Hardware
23+
run: (Get-CimInstance Win32_Processor).Name
24+
25+
- name: Locate Visual Studio
26+
run: |
27+
$vsInstall = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath
28+
echo "VS_INSTALL_DIR=$vsInstall" >> $env:GITHUB_ENV
29+
30+
- name: Configure
31+
run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=23 -DLIBHAT_SHARED_C_LIB=ON -DLIBHAT_TESTING_SDE=OFF -A ARM64 -T ${{matrix.toolset}}
32+
33+
- name: Build
34+
run: cmake --build ${{github.workspace}}/build -j 4 --config Release --target libhat_benchmark_compare_impl libhat_benchmark_chromium
35+
36+
- name: Test
37+
working-directory: ${{github.workspace}}/build
38+
shell: cmd
39+
run: |
40+
call "${{env.VS_INSTALL_DIR}}\VC\Auxiliary\Build\vcvarsarm64.bat"
41+
ctest --verbose -C Release -R "(libhat_benchmark_compare_impl|libhat_benchmark_chromium)"

.github/workflows/cmake.yml

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,38 @@ jobs:
1818
steps:
1919
- uses: actions/checkout@v6
2020

21+
- name: CPM Cache
22+
uses: actions/cache@v5
23+
with:
24+
path: ${{github.workspace}}/.cpmcache
25+
key: cpm-${{runner.os}}-${{hashFiles('test/CMakeLists.txt')}}
26+
restore-keys: |
27+
cpm-${{runner.os}}-
28+
2129
- name: Configure
22-
run: cmake -B ${{github.workspace}}/build -DCMAKE_CXX_STANDARD=${{matrix.cxx_standard}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBHAT_TESTING=OFF
30+
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
2331

2432
- name: Build
2533
run: cmake --build ${{github.workspace}}/build -j 4
2634

35+
- name: Test
36+
working-directory: ${{github.workspace}}/build
37+
run: ctest --verbose -C ${{env.BUILD_TYPE}} -R libhat_test_.*
38+
2739
linux:
2840
strategy:
2941
matrix:
42+
target: [ x64, ARM64 ]
43+
cxx_standard: [ 20, 23 ]
3044
compiler:
3145
- { pkg: g++, exe: g++, version: 14 }
3246
- { pkg: clang, exe: clang++, version: 18 }
33-
cxx_standard: [ 20, 23 ]
34-
runs-on: ubuntu-24.04
47+
include:
48+
- target: x64
49+
os: ubuntu-26.04
50+
- target: ARM64
51+
os: ubuntu-26.04-arm
52+
runs-on: ${{matrix.os}}
3553
steps:
3654
- uses: actions/checkout@v6
3755

@@ -51,7 +69,7 @@ jobs:
5169
- name: Configure
5270
env:
5371
CXX: ${{matrix.compiler.exe}}-${{matrix.compiler.version}}
54-
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 -DLIBHAT_TESTING_SANITIZE=OFF
72+
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 -DLIBHAT_TESTING_SANITIZE=OFF
5573

5674
- name: Build
5775
run: cmake --build ${{github.workspace}}/build -j 4
@@ -60,6 +78,57 @@ jobs:
6078
working-directory: ${{github.workspace}}/build
6179
run: ctest --verbose -C ${{env.BUILD_TYPE}} -R libhat_test_.*
6280

81+
android:
82+
strategy:
83+
matrix:
84+
cxx_standard: [ 20 ]
85+
abi: [ armeabi-v7a, arm64-v8a, x86, x86_64 ]
86+
sdk: [ 24 ]
87+
runs-on: ubuntu-latest
88+
steps:
89+
- uses: actions/checkout@v6
90+
91+
- name: AVD Cache
92+
if: ${{!startsWith(matrix.abi, 'arm')}}
93+
uses: actions/cache@v5
94+
id: avd-cache
95+
with:
96+
path: |
97+
~/.android/avd/*
98+
~/.android/adb*
99+
key: avd-${{matrix.sdk}}-${{matrix.abi}}
100+
101+
- name: Configure
102+
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
103+
104+
- name: Build
105+
run: cmake --build ${{github.workspace}}/build -j 4
106+
107+
- name: Create AVD and generate snapshot for caching
108+
if: ${{!startsWith(matrix.abi, 'arm') && steps.avd-cache.outputs.cache-hit != 'true'}}
109+
uses: reactivecircus/android-emulator-runner@v2
110+
with:
111+
api-level: ${{matrix.sdk}}
112+
arch: ${{matrix.abi}}
113+
force-avd-creation: false
114+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
115+
disable-animations: false
116+
script: echo "Generated AVD snapshot for caching."
117+
118+
- name: Test
119+
if: ${{!startsWith(matrix.abi, 'arm')}}
120+
uses: reactivecircus/android-emulator-runner@v2
121+
with:
122+
api-level: ${{matrix.sdk}}
123+
arch: ${{matrix.abi}}
124+
force-avd-creation: false
125+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
126+
disable-animations: true
127+
script: |
128+
adb push ${{github.workspace}}/build/test /data/local/tmp/
129+
adb shell chmod +x /data/local/tmp/test/libhat_test_process
130+
adb shell /data/local/tmp/test/libhat_test_process
131+
63132
windows:
64133
strategy:
65134
matrix:

CMakeLists.txt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ option(LIBHAT_USE_STD_MODULE "Compile the module target using the std module" OF
2828
option(LIBHAT_EXAMPLES "Include example targets" ${PROJECT_IS_TOP_LEVEL})
2929

3030
option(LIBHAT_HINT_X86_64 "Enables support for the x86_64 scan hint, requires a small (2KB) data table" ON)
31+
option(LIBHAT_HINT_AARCH64 "Enables support for the aarch64 scan hint, requires a small (2KB) data table" ON)
3132

3233
if(LIBHAT_TESTING AND LIBHAT_TESTING_SANITIZE)
3334
if(MSVC)
@@ -42,6 +43,8 @@ set(LIBHAT_SRC
4243
src/Scanner.cpp
4344
src/System.cpp
4445

46+
src/os/mac/Process.cpp
47+
4548
src/os/linux/MemoryProtector.cpp
4649
src/os/linux/Process.cpp
4750

@@ -57,7 +60,9 @@ set(LIBHAT_SRC
5760
src/arch/x86/AVX512.cpp
5861
src/arch/x86/System.cpp
5962

60-
src/arch/arm/System.cpp)
63+
src/arch/arm/Neon.cpp
64+
src/arch/arm/System.cpp
65+
)
6166

6267
add_library(libhat STATIC ${LIBHAT_SRC})
6368
add_library(libhat::libhat ALIAS libhat)
@@ -75,10 +80,7 @@ if (MSVC)
7580
target_compile_options(libhat PRIVATE /W3 /WX)
7681
endif()
7782
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
78-
target_compile_options(libhat PRIVATE -Wall -Wextra -Wconversion -Werror
79-
# temp fix for macOS CI failing due to incorrect LIBHAT_COMPILER_X86_OPTIONS value
80-
-Wno-unused-command-line-argument
81-
)
83+
target_compile_options(libhat PRIVATE -Wall -Wextra -Wconversion -Werror)
8284
endif()
8385

8486
target_include_directories(libhat PUBLIC
@@ -90,6 +92,7 @@ target_compile_definitions(libhat PUBLIC
9092
"$<$<BOOL:${LIBHAT_DISABLE_SSE}>:LIBHAT_DISABLE_SSE>"
9193
"$<$<BOOL:${LIBHAT_DISABLE_AVX512}>:LIBHAT_DISABLE_AVX512>"
9294
"$<$<BOOL:${LIBHAT_HINT_X86_64}>:LIBHAT_HINT_X86_64>"
95+
"$<$<BOOL:${LIBHAT_HINT_AARCH64}>:LIBHAT_HINT_AARCH64>"
9396
)
9497

9598
if(LIBHAT_STATIC_C_LIB OR LIBHAT_SHARED_C_LIB)

README.md

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
A modern, high-performance library for C++20 designed around game hacking
33

44
## Feature overview
5-
- Windows x86/x64 support
6-
- Partial Linux and macOS support
75
- Vectorized scanning for byte patterns
86
- SSE 4.1 and AVX2 on x86/x64
97
- AVX-512 on x64
8+
- Neon on ARM/ARM64
109
- RAII memory protector
1110
- Convenience wrappers over OS APIs
1211
- Language bindings (C, C#, Java)
12+
- Full Windows support
13+
- Partial (WIP) Linux, macOS, and Android support
1314

1415
## Versioning
1516
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
100101

101102
Below is a summary of the current support for libhat's platform-dependent APIs:
102103

103-
| | Windows | Linux | macOS |
104-
|--------------------------------|:-------:|:-----:|:-----:|
105-
| `hat::get_system` ||||
106-
| `hat::memory_protector` ||| |
107-
| `hp::get_process_module` ||| |
108-
| `hp::get_module` ||| |
109-
| `hp::module_at` || | |
110-
| `hp::is_readable` ||| |
111-
| `hp::is_writable` ||| |
112-
| `hp::is_executable` ||| |
113-
| `hp::module::get_module_data` ||| |
114-
| `hp::module::get_section_data` || | |
115-
| `hp::module::for_each_segment` ||| |
104+
### APIs
105+
106+
| | Windows | Linux | macOS | Android |
107+
|--------------------------------|:-------:|:-----:|:-----:|:-------:|
108+
| `hat::get_system` |||||
109+
| `hat::memory_protector` ||| ||
110+
| `hp::get_process_module` |||||
111+
| `hp::get_module` |||||
112+
| `hp::module_at` || | | |
113+
| `hp::is_readable` ||| ||
114+
| `hp::is_writable` ||| ||
115+
| `hp::is_executable` ||| ||
116+
| `hp::module::get_module_data` ||| ||
117+
| `hp::module::get_section_data` || | | |
118+
| `hp::module::for_each_segment` |||||
116119

117120
## Quick start
118121
### Defining patterns
@@ -189,19 +192,24 @@ const std::byte* address = result.get();
189192
const std::byte* relative_address = result.rel(3);
190193
```
191194

192-
libhat has a few optimizations for searching for patterns in `x86_64` machine code:
195+
libhat has a few optimizations for searching for patterns in `x86_64` and `AArch64` machine code:
193196
```cpp
194197
#include <libhat/scanner.hpp>
195198

196-
// If a byte pattern matches at the start of a function, the result will be aligned on 16-bytes.
197-
// This can be indicated via the defaulted `alignment` parameter (all overloads have this parameter):
199+
// Compilers will often align the start address of a function on 16-bytes. Scanning for patterns that
200+
// match the start of a function can take advantage of this by specifying the defaulted `alignment`
201+
// parameter (all overloads have this parameter):
198202
std::span<std::byte> range = /* ... */;
199203
hat::signature_view pattern = /* ... */;
200204
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X16);
201205

202-
// Additionally, x86_64 contains a non-uniform distribution of byte pairs. By passing the `x86_64`
203-
// scan hint, the search can be based on the least common byte pair that is found in the pattern.
204-
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64);
206+
// Or, if the architecture has byte-aligned instructions (such as ARM and AArch64):
207+
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X4);
208+
209+
// Additionally, machine code contains a non-uniform distribution of bytes. By passing the respective
210+
// scan hint (either `x86_64` or `aarch64`), the search anchor can be tuned to the least frequent
211+
// bytes that are present in the pattern.
212+
hat::scan_result result = hat::find_pattern(range, pattern, hat::scan_alignment::X1, hat::scan_hint::x86_64);
205213
```
206214

207215
### Accessing members

bindings/java/src/main/java/me/zero/libhat/Hat.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public static OptionalInt findPattern(@NotNull final Signature signature, @NotNu
127127
Objects.requireNonNull(signature.handle),
128128
new Pointer(start),
129129
count,
130-
alignment.ordinal()
130+
alignment.alignment()
131131
);
132132

133133
if (result == Pointer.NULL) {
@@ -175,7 +175,7 @@ public static Optional<Pointer> findPattern(@NotNull final Signature signature,
175175
Objects.requireNonNull(signature.handle),
176176
module.handle,
177177
section,
178-
alignment.ordinal()
178+
alignment.alignment()
179179
);
180180

181181
return Optional.ofNullable(result);

bindings/java/src/main/java/me/zero/libhat/ScanAlignment.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,28 @@ public enum ScanAlignment {
77
/**
88
* No byte alignment
99
*/
10-
X1,
10+
X1(1),
11+
12+
/**
13+
* 4 byte alignment
14+
*/
15+
X4(4),
1116

1217
/**
1318
* 16 byte alignment
1419
*/
15-
X16
20+
X16(16);
21+
22+
private final int alignment;
23+
24+
ScanAlignment(final int alignment) {
25+
this.alignment = alignment;
26+
}
27+
28+
/**
29+
* @return The scan result alignment requirement, in bytes
30+
*/
31+
public final int alignment() {
32+
return this.alignment;
33+
}
1634
}

include/libhat/c/libhat.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ typedef enum libhat_status_t {
3737
} libhat_status_t;
3838

3939
typedef enum scan_alignment {
40-
scan_alignment_x1,
41-
scan_alignment_x16,
40+
scan_alignment_x1 = 1,
41+
scan_alignment_x4 = 4,
42+
scan_alignment_x16 = 16,
4243
} scan_alignment_t;
4344

4445
typedef struct signature {

include/libhat/platform.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@
1313
#error Unsupported Architecture
1414
#endif
1515

16+
#if defined(__LP64__) || defined(_M_X64)
17+
#define LIBHAT_LP64
18+
#else
19+
#define LIBHAT_LP32
20+
#endif
21+
1622
// Detect Operating System
1723
#if defined(_WIN32)
1824
#define LIBHAT_WINDOWS
1925
#elif defined(linux) || defined(__linux__) || defined(__linux)
2026
#define LIBHAT_UNIX
2127
#define LIBHAT_LINUX
28+
#if defined(__ANDROID__)
29+
#define LIBHAT_ANDROID
30+
#endif
2231
#elif defined(__APPLE__) && defined(__MACH__)
2332
#define LIBHAT_UNIX
2433
#define LIBHAT_MAC

0 commit comments

Comments
 (0)