Skip to content

Commit 3ac96c3

Browse files
committed
Add cross-platform AArch64 context backends
1 parent bc48ec7 commit 3ac96c3

23 files changed

Lines changed: 922 additions & 318 deletions

File tree

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,37 @@ jobs:
2727
- name: Zig test
2828
run: zig build test
2929

30+
- name: Cross compile Linux AArch64 core and payload
31+
run: |
32+
set -euo pipefail
33+
zig build-lib -dynamic -target aarch64-linux-musl -OReleaseFast \
34+
-femit-bin=/tmp/libzighook_linux.so \
35+
src/root.zig \
36+
-lc
37+
zig build-lib -dynamic -target aarch64-linux-musl -OReleaseFast \
38+
-femit-bin=/tmp/prepatched_hook_linux.so \
39+
--dep zighook \
40+
-Mroot=examples/prepatched_inline_hook/hook.zig \
41+
-Mzighook=src/root.zig \
42+
-lc
43+
44+
- name: Cross compile iOS AArch64 core and payload
45+
run: |
46+
set -euo pipefail
47+
IOS_SDK="$(xcrun --sdk iphoneos --show-sdk-path)"
48+
zig build-lib -dynamic -target aarch64-ios -OReleaseFast \
49+
-femit-bin=/tmp/libzighook_ios.dylib \
50+
src/root.zig \
51+
-L"$IOS_SDK/usr/lib" \
52+
-lc
53+
zig build-lib -dynamic -target aarch64-ios -OReleaseFast \
54+
-femit-bin=/tmp/prepatched_hook_ios.dylib \
55+
--dep zighook \
56+
-Mroot=examples/prepatched_inline_hook/hook.zig \
57+
-Mzighook=src/root.zig \
58+
-L"$IOS_SDK/usr/lib" \
59+
-lc
60+
3061
- name: Example inline_hook_signal
3162
working-directory: examples/inline_hook_signal
3263
run: |

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
Current scope:
66

77
- macOS
8-
- Apple Silicon / AArch64
8+
- iOS
9+
- Linux
10+
- Android
11+
- AArch64 / ARM64
912

1013
Implemented APIs:
1114

@@ -24,15 +27,30 @@ The current backend supports:
2427
- signal-based entry hooks
2528
- strict execute-original replay for common AArch64 PC-relative instructions
2629
- public callback access to AArch64 FP/SIMD state (`fpregs.v[i]`, `fpregs.named.v0..v31`, `fpsr`, `fpcr`)
27-
- constructor-based dylib payloads for `DYLD_INSERT_LIBRARIES` / later Mach-O insertion workflows
30+
- constructor-based payloads for both Mach-O (`__mod_init_func`) and ELF (`.init_array`)
2831

2932
## Status
3033

31-
This repository currently targets the first backend slice only:
34+
Implemented AArch64 platform backends:
3235

33-
- `aarch64-apple-darwin`
36+
- `aarch64-macos`
37+
- `aarch64-ios`
38+
- `aarch64-linux`
39+
- `aarch64-linux-android` at the code/backend level via the Linux-family signal path
3440

35-
It is usable for local experiments on Apple Silicon macOS, but it is not yet at full feature/platform parity with the Rust crate.
41+
Verification status:
42+
43+
- macOS AArch64: runtime-tested locally and in CI
44+
- Linux AArch64: cross-compiled core library and ELF payload locally
45+
- iOS AArch64: cross-compiled core dylib and Mach-O payload locally
46+
- Android AArch64: compiled core/payload objects against a local NDK sysroot
47+
48+
Deployment model by platform:
49+
50+
- macOS: runtime patching or prepatched trap sites, usually with `DYLD_INSERT_LIBRARIES`
51+
- Linux: runtime patching or prepatched trap sites, usually with `LD_PRELOAD` or `patchelf`
52+
- iOS: recommended prepatched trap sites plus inserted dylib + re-sign
53+
- Android: Linux-family backend plus sidecar `.so`, typically loaded via patched ELF metadata / app packaging
3654

3755
Current execute-original replay whitelist for PC-relative AArch64 instructions:
3856

@@ -94,6 +112,10 @@ zig build-lib -dynamic -OReleaseFast -femit-bin=hook.dylib \
94112
DYLD_INSERT_LIBRARIES=$PWD/hook.dylib ./target
95113
```
96114

115+
For Linux, iOS, and Android deployment flows, see:
116+
117+
- `docs/platform-workflows.md`
118+
97119
Available examples:
98120

99121
- `inline_hook_signal`: function-entry trap hook, expected output `result=42`
@@ -106,6 +128,7 @@ CI runs these exact per-directory build commands and compares exact stdout.
106128

107129
See:
108130

131+
- `docs/platform-workflows.md`
109132
- `examples/README.md`
110133
- each `examples/*/README.md`
111134

build.zig.zon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,6 @@
7878
"src",
7979
"tests",
8080
"examples",
81+
"docs",
8182
},
8283
}

docs/platform-workflows.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# AArch64 platform workflows
2+
3+
This repository now implements AArch64 backends for:
4+
5+
- macOS
6+
- iOS
7+
- Linux
8+
- Android
9+
10+
The core trap/replay engine is shared across all of them. The platform-specific
11+
differences are mostly:
12+
13+
- how executable pages are patched
14+
- how `sigaction` / `ucontext_t` machine state is remapped
15+
- how the sidecar payload is injected into the target process
16+
17+
The example `hook.zig` payloads automatically pick the right constructor
18+
section:
19+
20+
- Mach-O (`macOS`, `iOS`): `__DATA,__mod_init_func`
21+
- ELF (`Linux`, `Android`): `.init_array`
22+
23+
## Linux AArch64
24+
25+
Recommended workflows:
26+
27+
- `LD_PRELOAD` for local experiments
28+
- `patchelf` for persistent sidecar loading
29+
30+
Build a sidecar payload from any example directory:
31+
32+
```bash
33+
cc -O3 -DNDEBUG -Wl,-export-dynamic -o target target.c
34+
35+
zig build-lib -dynamic -target aarch64-linux-musl -OReleaseFast -femit-bin=hook.so \
36+
--dep zighook \
37+
-Mroot=hook.zig \
38+
-Mzighook=../../src/root.zig \
39+
-lc
40+
```
41+
42+
Run with preload:
43+
44+
```bash
45+
LD_PRELOAD=$PWD/hook.so ./target
46+
```
47+
48+
Or patch the ELF loader metadata with `patchelf` / equivalent tooling and ship
49+
the sidecar `.so` next to the target binary.
50+
51+
## iOS AArch64
52+
53+
Recommended workflow:
54+
55+
- use `prepatched.*`
56+
- patch `brk #0` into the app binary offline
57+
- ship a sidecar dylib inside `Frameworks/`
58+
- inject the load command with `insert-dylib`
59+
- re-sign the entire app bundle and install
60+
61+
`prepatched_inline_hook` is the best template example for this deployment mode.
62+
63+
Build the payload dylib:
64+
65+
```bash
66+
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
67+
68+
zig build-lib -dynamic -target aarch64-ios -OReleaseFast -femit-bin=hook.dylib \
69+
--dep zighook \
70+
-Mroot=hook.zig \
71+
-Mzighook=../../src/root.zig \
72+
-L"$IOS_SDK/usr/lib" \
73+
-lc
74+
```
75+
76+
Typical packaging flow afterwards:
77+
78+
1. Copy `hook.dylib` into `MyApp.app/Frameworks/`
79+
2. Use `insert-dylib` to add a load command to the app Mach-O
80+
3. Re-sign the full app bundle
81+
4. Install and run
82+
83+
This repository's iOS support is therefore oriented around **prepatched trap
84+
sites plus inserted dylibs**, matching the workflow described above.
85+
86+
## Android AArch64
87+
88+
Recommended workflow:
89+
90+
- use a sidecar `.so`
91+
- patch the target ELF / native binary with `patch-elf` or equivalent
92+
- let the app or native packaging flow provide the final shared-library link
93+
94+
The Android backend shares the same Linux-family AArch64 signal/context code
95+
path. In local verification, the Android target successfully compiled to object
96+
files against an installed NDK sysroot.
97+
98+
Example compile-to-object smoke commands:
99+
100+
```bash
101+
ANDROID_NDK=$HOME/Library/Android/sdk/ndk/29.0.13113456
102+
ANDROID_SYSROOT=$(find "$ANDROID_NDK/toolchains/llvm/prebuilt" -maxdepth 1 -mindepth 1 -type d | head -n 1)/sysroot
103+
104+
zig build-obj -target aarch64-linux-android -OReleaseFast \
105+
--sysroot "$ANDROID_SYSROOT" \
106+
src/root.zig \
107+
-lc
108+
109+
zig build-obj -target aarch64-linux-android -OReleaseFast \
110+
--dep zighook \
111+
-Mroot=hook.zig \
112+
-Mzighook=../../src/root.zig \
113+
--sysroot "$ANDROID_SYSROOT" \
114+
-lc
115+
```
116+
117+
On the machine used for this implementation, Zig 0.15.2 did not provide a
118+
fully self-contained Android libc link for the final shared library, so the
119+
expected final `.so` link should be driven by the NDK / app build system.
120+
121+
## Verification status
122+
123+
- macOS AArch64: native runtime tests and examples executed locally
124+
- Linux AArch64: core shared library and ELF payload cross-compiled locally
125+
- iOS AArch64: core dylib and Mach-O payload cross-compiled locally
126+
- Android AArch64: core library and payload compiled to object files against a
127+
local NDK sysroot

examples/README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# zighook examples
22

33
These examples mirror the intent of the Rust `sighook` demos, but are currently
4-
implemented only for the first completed backend slice:
4+
implemented for the first completed AArch64 backend family:
55

6-
- **OS:** macOS
7-
- **Architecture:** Apple Silicon / AArch64
6+
- **OS:** macOS / iOS / Linux / Android
7+
- **Architecture:** AArch64 / ARM64
88

99
Each example directory is intentionally a standalone mini-project with exactly:
1010

@@ -15,7 +15,12 @@ Each example directory is intentionally a standalone mini-project with exactly:
1515
The root `build.zig` does not build examples. That is deliberate: the example
1616
directories are meant to show the real commands needed to compile a release C
1717
target, compile a release Zig hook dylib, and inject that dylib with
18-
`DYLD_INSERT_LIBRARIES`.
18+
platform-appropriate sidecar loading.
19+
20+
The example payloads now auto-select the constructor section:
21+
22+
- Mach-O (`macOS`, `iOS`): `__DATA,__mod_init_func`
23+
- ELF (`Linux`, `Android`): `.init_array`
1924

2025
Common build pattern from inside an example directory:
2126

@@ -31,6 +36,11 @@ zig build-lib -dynamic -OReleaseFast -femit-bin=hook.dylib \
3136
DYLD_INSERT_LIBRARIES=$PWD/hook.dylib ./target
3237
```
3338

39+
That exact command sequence is still the canonical **macOS runtime smoke**.
40+
For Linux / iOS / Android deployment workflows, see:
41+
42+
- `../docs/platform-workflows.md`
43+
3444
Available examples:
3545

3646
- `inline_hook_signal`: function-entry trap hook, expected output `result=42`
@@ -40,4 +50,5 @@ Available examples:
4050
- `prepatched_inline_hook`: use `prepatched.inline_hook(...)` on a binary that already contains `brk`, expected output `result=77`
4151

4252
Each example README contains the exact commands and expected output. CI executes
43-
the same commands directly and compares stdout against those documented values.
53+
the macOS runtime commands directly and compares stdout against those
54+
documented values.
Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1+
const builtin = @import("builtin");
12
const zighook = @import("zighook");
2-
const c = @cImport({
3-
@cInclude("dlfcn.h");
4-
});
3+
4+
const init_section = switch (builtin.os.tag) {
5+
.macos, .ios => "__DATA,__mod_init_func",
6+
.linux => ".init_array",
7+
else => @compileError("example payload constructors are only implemented for Mach-O and ELF targets."),
8+
};
9+
10+
extern fn dlsym(handle: ?*anyopaque, symbol: [*:0]const u8) ?*anyopaque;
11+
12+
fn rtldDefault() ?*anyopaque {
13+
return switch (builtin.os.tag) {
14+
.macos, .ios => @ptrFromInt(@as(usize, @bitCast(@as(isize, -2)))),
15+
.linux => null,
16+
else => @compileError("RTLD_DEFAULT is only implemented for Mach-O and ELF targets."),
17+
};
18+
}
519

620
fn onHit(_: u64, ctx: *zighook.HookContext) callconv(.c) void {
721
ctx.regs.named.x0 = 42;
822
}
923

1024
fn install() callconv(.c) void {
11-
const symbol = c.dlsym(c.RTLD_DEFAULT, "target_add");
25+
const symbol = dlsym(rtldDefault(), "target_add");
1226
if (symbol == null) return;
1327
_ = zighook.inline_hook(@intFromPtr(symbol.?), onHit) catch {};
1428
}
1529

1630
const InitFn = *const fn () callconv(.c) void;
17-
pub export const example_init: InitFn linksection("__DATA,__mod_init_func") = &install;
31+
pub export const example_init: InitFn linksection(init_section) = &install;
Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1+
const builtin = @import("builtin");
12
const zighook = @import("zighook");
2-
const c = @cImport({
3-
@cInclude("dlfcn.h");
4-
});
3+
4+
const init_section = switch (builtin.os.tag) {
5+
.macos, .ios => "__DATA,__mod_init_func",
6+
.linux => ".init_array",
7+
else => @compileError("example payload constructors are only implemented for Mach-O and ELF targets."),
8+
};
9+
10+
extern fn dlsym(handle: ?*anyopaque, symbol: [*:0]const u8) ?*anyopaque;
11+
12+
fn rtldDefault() ?*anyopaque {
13+
return switch (builtin.os.tag) {
14+
.macos, .ios => @ptrFromInt(@as(usize, @bitCast(@as(isize, -2)))),
15+
.linux => null,
16+
else => @compileError("RTLD_DEFAULT is only implemented for Mach-O and ELF targets."),
17+
};
18+
}
519

620
fn onHit(_: u64, ctx: *zighook.HookContext) callconv(.c) void {
721
ctx.regs.named.x0 = 99;
822
}
923

1024
fn install() callconv(.c) void {
11-
const symbol = c.dlsym(c.RTLD_DEFAULT, "target_add_patchpoint");
25+
const symbol = dlsym(rtldDefault(), "target_add_patchpoint");
1226
if (symbol == null) return;
1327
_ = zighook.instrument_no_original(@intFromPtr(symbol.?), onHit) catch {};
1428
}
1529

1630
const InitFn = *const fn () callconv(.c) void;
17-
pub export const example_init: InitFn linksection("__DATA,__mod_init_func") = &install;
31+
pub export const example_init: InitFn linksection(init_section) = &install;

examples/instrument_unhook_restore/hook.zig

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1+
const builtin = @import("builtin");
12
const zighook = @import("zighook");
2-
const c = @cImport({
3-
@cInclude("dlfcn.h");
4-
});
3+
4+
const init_section = switch (builtin.os.tag) {
5+
.macos, .ios => "__DATA,__mod_init_func",
6+
.linux => ".init_array",
7+
else => @compileError("example payload constructors are only implemented for Mach-O and ELF targets."),
8+
};
9+
10+
extern fn dlsym(handle: ?*anyopaque, symbol: [*:0]const u8) ?*anyopaque;
11+
12+
fn rtldDefault() ?*anyopaque {
13+
return switch (builtin.os.tag) {
14+
.macos, .ios => @ptrFromInt(@as(usize, @bitCast(@as(isize, -2)))),
15+
.linux => null,
16+
else => @compileError("RTLD_DEFAULT is only implemented for Mach-O and ELF targets."),
17+
};
18+
}
519

620
var patchpoint_addr: u64 = 0;
721

@@ -10,7 +24,7 @@ fn onHit(_: u64, ctx: *zighook.HookContext) callconv(.c) void {
1024
}
1125

1226
fn install() callconv(.c) void {
13-
const symbol = c.dlsym(c.RTLD_DEFAULT, "target_add_patchpoint");
27+
const symbol = dlsym(rtldDefault(), "target_add_patchpoint");
1428
if (symbol == null) return;
1529

1630
patchpoint_addr = @intFromPtr(symbol.?);
@@ -23,4 +37,4 @@ pub export fn zighook_example_unhook() callconv(.c) void {
2337
}
2438

2539
const InitFn = *const fn () callconv(.c) void;
26-
pub export const example_init: InitFn linksection("__DATA,__mod_init_func") = &install;
40+
pub export const example_init: InitFn linksection(init_section) = &install;

0 commit comments

Comments
 (0)