Skip to content

Commit e5266e5

Browse files
committed
Fix x86_64 call replay trampolines
1 parent 43971fa commit e5266e5

3 files changed

Lines changed: 41 additions & 5 deletions

File tree

src/arch/x86_64/trampoline.zig

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,19 @@ const TrampolineEmitter = struct {
4242
}
4343

4444
fn emitAbsolutePush(self: *TrampolineEmitter, value: u64) HookError!void {
45-
// `push qword ptr [rip+0]` followed by an inline literal. This is used
46-
// to synthesize the architectural "return address push" side effect of
47-
// a displaced `call`.
48-
const opcode = [_]u8{ 0xFF, 0x35, 0x00, 0x00, 0x00, 0x00 };
45+
// x86_64 has no `push imm64`. We therefore synthesize one without
46+
// permanently clobbering architectural state:
47+
// push rax
48+
// movabs rax, imm64
49+
// xchg qword ptr [rsp], rax
50+
//
51+
// After the final `xchg`, the stack top contains `value` and `rax`
52+
// regains its original pre-push contents.
4953
const literal = std.mem.toBytes(std.mem.nativeToLittle(u64, value));
50-
try self.emit(opcode[0..]);
54+
try self.emit(&.{0x50});
55+
try self.emit(&.{ 0x48, 0xB8 });
5156
try self.emit(literal[0..]);
57+
try self.emit(&.{ 0x48, 0x87, 0x04, 0x24 });
5258
}
5359

5460
fn emitStackPointerIndirectJump(

tests/api_integration.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const zighook = @import("zighook");
55

66
extern fn demo_add_target(a: i32, b: i32) callconv(.c) i32;
77
extern fn demo_add_patchpoint() callconv(.c) void;
8+
extern fn demo_direct_call_target(a: i32, b: i32) callconv(.c) i32;
9+
extern fn demo_direct_call_patchpoint() callconv(.c) void;
810
extern fn demo_stack_call_target(a: i32, b: i32) callconv(.c) i32;
911
extern fn demo_stack_call_patchpoint() callconv(.c) void;
1012
extern fn demo_prepatched_target() callconv(.c) i32;
@@ -67,6 +69,19 @@ test "x86_64 instrument replays stack-pointer indirect calls" {
6769
try std.testing.expect(saved.slice().len > 0);
6870
}
6971

72+
test "x86_64 instrument replays direct calls" {
73+
if (builtin.cpu.arch != .x86_64) return;
74+
75+
const patchpoint_addr: u64 = @intFromPtr(&demo_direct_call_patchpoint);
76+
77+
defer zighook.unhook(patchpoint_addr) catch {};
78+
79+
_ = try zighook.instrument(patchpoint_addr, signalReplay42);
80+
try std.testing.expectEqual(@as(i32, 42), demo_direct_call_target(1, 2));
81+
const saved = zighook.original_instruction(patchpoint_addr) orelse return error.TestUnexpectedResult;
82+
try std.testing.expect(saved.slice().len > 0);
83+
}
84+
7085
test "instrument_no_original skips the displaced instruction on both backends" {
7186
const patchpoint_addr: u64 = @intFromPtr(&demo_add_patchpoint);
7287

tests/support/runtime_targets_x86_64.S

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ SYM(demo_add_patchpoint):
1515
leal (%rdi,%rsi), %eax
1616
ret
1717

18+
// Replay test for ordinary direct calls. The patchpoint is the `call rel32`
19+
// itself so the execute-original trampoline must synthesize the pushed return
20+
// address before transferring control to the final callee.
21+
.globl SYM(demo_direct_call_target)
22+
.globl SYM(demo_direct_call_patchpoint)
23+
SYM(demo_direct_call_target):
24+
SYM(demo_direct_call_patchpoint):
25+
call SYM(demo_direct_call_impl)
26+
ret
27+
28+
.globl SYM(demo_direct_call_impl)
29+
SYM(demo_direct_call_impl):
30+
leal (%rdi,%rsi), %eax
31+
ret
32+
1833
// Replay test for stack-pointer-based indirect calls. The patchpoint is the
1934
// indirect call through the top-of-stack function pointer slot.
2035
.globl SYM(demo_stack_call_target)

0 commit comments

Comments
 (0)