Pin zhook as a URL dependency instead of a relative path
build.zig.zon pointed at ../zhook, so a clone of this repo alone could not build. Pin the Codeberg tarball for f1b252e (current zhook master) with its hash; zig fetches it automatically. Verified by building with no sibling zhook checkout present.
This commit is contained in:
@@ -274,14 +274,14 @@ what you do with them on someone else's server is between you and that server's
|
||||
|
||||
### Building
|
||||
|
||||
Requires Zig 0.16 and a checkout of [zhook](https://codeberg.org/marcelinevq/zhook)
|
||||
as a sibling directory - `build.zig.zon` refers to it by relative path:
|
||||
Requires Zig 0.16. The only dependency is
|
||||
[zhook](https://codeberg.org/marcelinevq/zhook), the x86 inline hooking library,
|
||||
pinned in `build.zig.zon` and fetched automatically:
|
||||
|
||||
```sh
|
||||
git clone https://codeberg.org/marcelinevq/zhook
|
||||
git clone https://codeberg.org/MarcelineVQ/WeirdUtils
|
||||
cd WeirdUtils
|
||||
zig build # zig-out/bin/weirdutils.dll
|
||||
zig build # zig-out/bin/weirdutils.dll
|
||||
zig build all-variants -Doptimize=ReleaseSmall # + one DLL per module
|
||||
```
|
||||
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@
|
||||
.fingerprint = 0x54f0a9542562d318,
|
||||
.dependencies = .{
|
||||
.zhook = .{
|
||||
.path = "../zhook",
|
||||
.url = "https://codeberg.org/marcelinevq/zhook/archive/f1b252ed61ad839f00310c386761d068f293ad0f.tar.gz",
|
||||
.hash = "zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// x86 length disassembler — standalone, no dependencies
|
||||
const x86dis_mod = b.addModule("x86dis", .{
|
||||
.root_source_file = b.path("src/x86dis.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// zhook — x86-32 inline hooking with auto-sizing trampolines
|
||||
const zhook_mod = b.addModule("zhook", .{
|
||||
.root_source_file = b.path("src/zhook.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
zhook_mod.addImport("x86dis", x86dis_mod);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.{
|
||||
.name = .zhook,
|
||||
.version = "0.1.0",
|
||||
.fingerprint = 0xf05c933a601259a4,
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
//! Minimal x86 (32-bit) length disassembler.
|
||||
//!
|
||||
//! Faithful port of Vyacheslav Patkov's Hacker Disassembler Engine 32 (HDE32)
|
||||
//! Only computes instruction length + flags needed for relocation (F_RELATIVE).
|
||||
//! ~470 bytes of table data, compiles to ~1-2 KB.
|
||||
//!
|
||||
//! Standalone module with no OS dependencies — pure computation on byte slices.
|
||||
//! Can be used independently of the hook library for any x86 disassembly task.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// ── public flags ───────────────────────────────────────────────────────
|
||||
|
||||
/// Instruction has a ModR/M byte.
|
||||
pub const F_MODRM: u32 = 0x00000001;
|
||||
/// Instruction has a SIB (Scale-Index-Base) byte.
|
||||
pub const F_SIB: u32 = 0x00000002;
|
||||
/// Instruction has an 8-bit immediate operand.
|
||||
pub const F_IMM8: u32 = 0x00000004;
|
||||
/// Instruction has a 16-bit immediate operand.
|
||||
pub const F_IMM16: u32 = 0x00000008;
|
||||
/// Instruction has a 32-bit immediate operand.
|
||||
pub const F_IMM32: u32 = 0x00000010;
|
||||
/// Instruction has an 8-bit displacement.
|
||||
pub const F_DISP8: u32 = 0x00000020;
|
||||
/// Instruction has a 16-bit displacement.
|
||||
pub const F_DISP16: u32 = 0x00000040;
|
||||
/// Instruction has a 32-bit displacement.
|
||||
pub const F_DISP32: u32 = 0x00000080;
|
||||
/// Instruction contains a relative offset (CALL/JMP/Jcc). Indicates the
|
||||
/// immediate is PC-relative and must be relocated if the instruction is moved.
|
||||
pub const F_RELATIVE: u32 = 0x00000100;
|
||||
/// Decoding failed — the byte sequence is not a valid x86 instruction.
|
||||
pub const F_ERROR: u32 = 0x00001000;
|
||||
|
||||
// ── internal cflags ────────────────────────────────────────────────────
|
||||
const C_MODRM: u8 = 0x01;
|
||||
const C_IMM8: u8 = 0x02;
|
||||
const C_IMM16: u8 = 0x04;
|
||||
const C_IMM_P66: u8 = 0x10;
|
||||
const C_REL8: u8 = 0x20;
|
||||
const C_REL32: u8 = 0x40;
|
||||
const C_GROUP: u8 = 0x80;
|
||||
const C_ERROR: u8 = 0xff;
|
||||
|
||||
const PRE_NONE: u8 = 0x01;
|
||||
const PRE_66: u8 = 0x08;
|
||||
const PRE_67: u8 = 0x10;
|
||||
|
||||
const DELTA_OPCODES: usize = 0x4a;
|
||||
|
||||
// HDE32 opcode table — verbatim from table32.h
|
||||
const hde32_table = [_]u8{
|
||||
0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3,
|
||||
0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xac, 0xaa, 0xb2, 0xaa, 0x9f, 0x9f,
|
||||
0x9f, 0x9f, 0xb5, 0xa3, 0xa3, 0xa4, 0xaa, 0xaa, 0xba, 0xaa, 0x96, 0xaa, 0xa8, 0xaa, 0xc3,
|
||||
0xc3, 0x96, 0x96, 0xb7, 0xae, 0xd6, 0xbd, 0xa3, 0xc5, 0xa3, 0xa3, 0x9f, 0xc3, 0x9c, 0xaa,
|
||||
0xaa, 0xac, 0xaa, 0xbf, 0x03, 0x7f, 0x11, 0x7f, 0x01, 0x7f, 0x01, 0x3f, 0x01, 0x01, 0x90,
|
||||
0x82, 0x7d, 0x97, 0x59, 0x59, 0x59, 0x59, 0x59, 0x7f, 0x59, 0x59, 0x60, 0x7d, 0x7f, 0x7f,
|
||||
0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x9a, 0x88, 0x7d,
|
||||
0x59, 0x50, 0x50, 0x50, 0x50, 0x59, 0x59, 0x59, 0x59, 0x61, 0x94, 0x61, 0x9e, 0x59, 0x59,
|
||||
0x85, 0x59, 0x92, 0xa3, 0x60, 0x60, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59,
|
||||
0x59, 0x59, 0x9f, 0x01, 0x03, 0x01, 0x04, 0x03, 0xd5, 0x03, 0xcc, 0x01, 0xbc, 0x03, 0xf0,
|
||||
0x10, 0x10, 0x10, 0x10, 0x50, 0x50, 0x50, 0x50, 0x14, 0x20, 0x20, 0x20, 0x20, 0x01, 0x01,
|
||||
0x01, 0x01, 0xc4, 0x02, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0xc0, 0xc2, 0x10, 0x11,
|
||||
0x02, 0x03, 0x11, 0x03, 0x03, 0x04, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x00, 0xc6, 0xc8,
|
||||
0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xca,
|
||||
0x01, 0x01, 0x01, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc0, 0xc2, 0x01, 0x01, 0x03, 0x01, 0xff,
|
||||
0xff, 0x01, 0x00, 0x03, 0xc4, 0xc4, 0xc6, 0x03, 0x01, 0x01, 0x01, 0xff, 0x03, 0x03, 0x03,
|
||||
0xc8, 0x40, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x33, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xbf, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
|
||||
0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x7f, 0x00, 0x00, 0xff, 0x4a, 0x4a, 0x4a, 0x4a, 0x4b, 0x52, 0x4a, 0x4a, 0x4a, 0x4a, 0x4f,
|
||||
0x4c, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x55, 0x45, 0x40, 0x4a, 0x4a, 0x4a,
|
||||
0x45, 0x59, 0x4d, 0x46, 0x4a, 0x5d, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a,
|
||||
0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x61, 0x63, 0x67, 0x4e, 0x4a, 0x4a, 0x6b, 0x6d, 0x4a, 0x4a,
|
||||
0x45, 0x6d, 0x4a, 0x4a, 0x44, 0x45, 0x4a, 0x4a, 0x00, 0x00, 0x00, 0x02, 0x0d, 0x06, 0x06,
|
||||
0x06, 0x06, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x00, 0x06, 0x06, 0x02, 0x06,
|
||||
0x00, 0x0a, 0x0a, 0x07, 0x07, 0x06, 0x02, 0x05, 0x05, 0x02, 0x02, 0x00, 0x00, 0x04, 0x04,
|
||||
0x04, 0x04, 0x00, 0x00, 0x00, 0x0e, 0x05, 0x06, 0x06, 0x06, 0x01, 0x06, 0x00, 0x00, 0x08,
|
||||
0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x30, 0x00, 0x80, 0x01, 0x82, 0x01,
|
||||
0x86, 0x00, 0xf6, 0xcf, 0xfe, 0x3f, 0xab, 0x00, 0xb0, 0x00, 0xb1, 0x00, 0xb3, 0x00, 0xba,
|
||||
0xf8, 0xbb, 0x00, 0xc0, 0x00, 0xc1, 0x00, 0xc7, 0xbf, 0x62, 0xff, 0x00, 0x8d, 0xff, 0x00,
|
||||
0xc4, 0xff, 0x00, 0xc5, 0xff, 0x00,
|
||||
};
|
||||
|
||||
/// Decoded instruction descriptor. Contains the length and metadata flags
|
||||
/// needed for instruction relocation — no operand values are extracted.
|
||||
pub const Insn = struct {
|
||||
/// Total instruction length in bytes (including prefixes, opcode, ModRM, SIB, disp, imm).
|
||||
len: u8,
|
||||
/// Bitmask of `F_*` flags describing the instruction's encoding.
|
||||
flags: u32,
|
||||
/// Primary opcode byte (after any prefixes). For two-byte opcodes (0F xx), this is 0x0F.
|
||||
opcode: u8,
|
||||
/// Secondary opcode byte for two-byte instructions (the byte after 0F). Zero for single-byte opcodes.
|
||||
opcode2: u8,
|
||||
};
|
||||
|
||||
/// Decode the instruction at `code`, returning its length and flags.
|
||||
///
|
||||
/// Handles all x86-32 prefixes, one- and two-byte opcodes, ModRM/SIB,
|
||||
/// displacement, and immediate fields. Sets `F_RELATIVE` on instructions
|
||||
/// with PC-relative operands (E8 CALL, E9 JMP, 0F 8x Jcc, short jumps).
|
||||
///
|
||||
/// Does not read beyond the instruction boundary — safe to use on
|
||||
/// pointers to live code without over-reading into adjacent instructions.
|
||||
/// Returns `F_ERROR` in flags if the byte sequence is invalid; `len` is
|
||||
/// clamped to 15 (the x86 maximum instruction length).
|
||||
pub fn decode(code: [*]const u8) Insn {
|
||||
var result = Insn{ .len = 0, .flags = 0, .opcode = 0, .opcode2 = 0 };
|
||||
|
||||
var p: usize = 0;
|
||||
var pref: u8 = 0;
|
||||
var disp_size: u8 = 0;
|
||||
|
||||
// ── prefixes ──
|
||||
var prefix_count: u8 = 16;
|
||||
prefix_loop: while (prefix_count > 0) : (prefix_count -= 1) {
|
||||
switch (code[p]) {
|
||||
0xf3, 0xf2 => pref |= if (code[p] == 0xf3) 0x04 else 0x02,
|
||||
0xf0 => pref |= 0x20, // PRE_LOCK
|
||||
0x26, 0x2e, 0x36, 0x3e, 0x64, 0x65 => pref |= 0x40, // PRE_SEG
|
||||
0x66 => pref |= PRE_66,
|
||||
0x67 => pref |= PRE_67,
|
||||
else => break :prefix_loop,
|
||||
}
|
||||
p += 1;
|
||||
}
|
||||
|
||||
result.flags = @as(u32, pref) << 23;
|
||||
|
||||
if (pref == 0) pref |= PRE_NONE;
|
||||
|
||||
// ── opcode ──
|
||||
var ht_base: usize = 0;
|
||||
var c = code[p];
|
||||
p += 1;
|
||||
result.opcode = c;
|
||||
|
||||
if (c == 0x0f) {
|
||||
// two-byte opcode
|
||||
result.opcode2 = code[p];
|
||||
c = code[p];
|
||||
p += 1;
|
||||
ht_base = DELTA_OPCODES;
|
||||
} else if (c >= 0xa0 and c <= 0xa3) {
|
||||
// MOV moffs — address-size prefix swaps operand-size behavior
|
||||
if (pref & PRE_67 != 0)
|
||||
pref |= PRE_66
|
||||
else
|
||||
pref &= ~PRE_66;
|
||||
}
|
||||
|
||||
const opcode = c;
|
||||
|
||||
// ── two-level table lookup: ht[ht[opcode/4] + (opcode%4)] ──
|
||||
var cflags: u8 = blk: {
|
||||
const idx1 = ht_base + @as(usize, opcode / 4);
|
||||
if (idx1 >= hde32_table.len) break :blk C_ERROR;
|
||||
const idx2 = ht_base + @as(usize, hde32_table[idx1]) + @as(usize, opcode % 4);
|
||||
if (idx2 >= hde32_table.len) break :blk C_ERROR;
|
||||
break :blk hde32_table[idx2];
|
||||
};
|
||||
|
||||
if (cflags == C_ERROR) {
|
||||
result.flags |= F_ERROR;
|
||||
cflags = 0;
|
||||
if ((opcode & 0xfd) == 0x24) // (opcode & -3) == 0x24
|
||||
cflags +%= 1;
|
||||
}
|
||||
|
||||
// ── group resolution ──
|
||||
var x: u8 = 0;
|
||||
if (cflags & C_GROUP != 0) {
|
||||
const group_idx = ht_base + @as(usize, cflags & 0x7f);
|
||||
if (group_idx + 1 < hde32_table.len) {
|
||||
const t = std.mem.readInt(u16, hde32_table[group_idx..][0..2], .little);
|
||||
cflags = @truncate(t);
|
||||
x = @truncate(t >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── modrm ──
|
||||
if (cflags & C_MODRM != 0) {
|
||||
result.flags |= F_MODRM;
|
||||
const modrm = code[p];
|
||||
p += 1;
|
||||
const m_mod = modrm >> 6;
|
||||
const m_rm: u8 = modrm & 7;
|
||||
const m_reg: u3 = @truncate((modrm & 0x3f) >> 3);
|
||||
|
||||
// F6 TEST imm8 / F7 TEST imm16/32
|
||||
if (m_reg <= 1) {
|
||||
if (opcode == 0xf6)
|
||||
cflags |= C_IMM8;
|
||||
if (opcode == 0xf7)
|
||||
cflags |= C_IMM_P66;
|
||||
}
|
||||
|
||||
// displacement
|
||||
switch (m_mod) {
|
||||
0 => {
|
||||
if (pref & PRE_67 != 0) {
|
||||
if (m_rm == 6) disp_size = 2;
|
||||
} else {
|
||||
if (m_rm == 5) disp_size = 4;
|
||||
}
|
||||
},
|
||||
1 => disp_size = 1,
|
||||
2 => {
|
||||
disp_size = 2;
|
||||
if (pref & PRE_67 == 0)
|
||||
disp_size = 4;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// SIB byte
|
||||
if (m_mod != 3 and m_rm == 4 and (pref & PRE_67 == 0)) {
|
||||
result.flags |= F_SIB;
|
||||
const sib = code[p];
|
||||
p += 1;
|
||||
if ((sib & 7) == 5 and (m_mod & 1) == 0)
|
||||
disp_size = 4;
|
||||
}
|
||||
|
||||
// displacement bytes
|
||||
switch (disp_size) {
|
||||
1 => {
|
||||
result.flags |= F_DISP8;
|
||||
p += 1;
|
||||
},
|
||||
2 => {
|
||||
result.flags |= F_DISP16;
|
||||
p += 2;
|
||||
},
|
||||
4 => {
|
||||
result.flags |= F_DISP32;
|
||||
p += 4;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// ── immediates ──
|
||||
if (cflags & C_IMM_P66 != 0) {
|
||||
if (cflags & C_REL32 != 0) {
|
||||
if (pref & PRE_66 != 0) {
|
||||
result.flags |= F_IMM16 | F_RELATIVE;
|
||||
p += 2;
|
||||
// disasm_done — skip remaining immediate checks
|
||||
result.len = @intCast(p);
|
||||
if (result.len > 15) {
|
||||
result.flags |= F_ERROR;
|
||||
result.len = 15;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// fall through to rel32_ok below
|
||||
} else {
|
||||
if (pref & PRE_66 != 0) {
|
||||
result.flags |= F_IMM16;
|
||||
p += 2;
|
||||
} else {
|
||||
result.flags |= F_IMM32;
|
||||
p += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cflags & C_IMM16 != 0) {
|
||||
if (result.flags & F_IMM32 != 0) {
|
||||
result.flags |= F_IMM16;
|
||||
} else if (result.flags & F_IMM16 != 0) {
|
||||
// F_2IMM16
|
||||
} else {
|
||||
result.flags |= F_IMM16;
|
||||
}
|
||||
p += 2;
|
||||
}
|
||||
if (cflags & C_IMM8 != 0) {
|
||||
result.flags |= F_IMM8;
|
||||
p += 1;
|
||||
}
|
||||
|
||||
if (cflags & C_REL32 != 0) {
|
||||
result.flags |= F_IMM32 | F_RELATIVE;
|
||||
p += 4;
|
||||
} else if (cflags & C_REL8 != 0) {
|
||||
result.flags |= F_IMM8 | F_RELATIVE;
|
||||
p += 1;
|
||||
}
|
||||
|
||||
result.len = @intCast(p);
|
||||
if (result.len > 15) {
|
||||
result.flags |= F_ERROR;
|
||||
result.len = 15;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
test "push ebp" {
|
||||
const d = decode(&[_]u8{ 0x55, 0xCC });
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "mov ebp, esp" {
|
||||
// 8B EC (or 89 E5)
|
||||
const d = decode(&[_]u8{ 0x8B, 0xEC });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
}
|
||||
|
||||
test "call rel32" {
|
||||
const d = decode(&[_]u8{ 0xE8, 0x78, 0x56, 0x34, 0x12 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
try std.testing.expect(d.flags & F_IMM32 != 0);
|
||||
}
|
||||
|
||||
test "jmp rel32" {
|
||||
const d = decode(&[_]u8{ 0xE9, 0x00, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "sub esp, imm8" {
|
||||
// 83 EC 10
|
||||
const d = decode(&[_]u8{ 0x83, 0xEC, 0x10 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_IMM8 != 0);
|
||||
}
|
||||
|
||||
test "mov eax, [ebp+8]" {
|
||||
// 8B 45 08
|
||||
const d = decode(&[_]u8{ 0x8B, 0x45, 0x08 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "jz rel32 (0F 84)" {
|
||||
const d = decode(&[_]u8{ 0x0F, 0x84, 0x10, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "nop" {
|
||||
const d = decode(&[_]u8{0x90});
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "ret" {
|
||||
const d = decode(&[_]u8{0xC3});
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "short jmp EB" {
|
||||
const d = decode(&[_]u8{ 0xEB, 0x05 });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
try std.testing.expect(d.flags & F_IMM8 != 0);
|
||||
}
|
||||
|
||||
test "short jcc 74 (jz rel8)" {
|
||||
const d = decode(&[_]u8{ 0x74, 0x0A });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "mov eax, imm32" {
|
||||
const d = decode(&[_]u8{ 0xB8, 0x44, 0x33, 0x22, 0x11 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "push imm32" {
|
||||
const d = decode(&[_]u8{ 0x68, 0x44, 0x33, 0x22, 0x11 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "push imm8" {
|
||||
// 6A 01
|
||||
const d = decode(&[_]u8{ 0x6A, 0x01 });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
}
|
||||
|
||||
test "mov [ebp-4], eax" {
|
||||
// 89 45 FC
|
||||
const d = decode(&[_]u8{ 0x89, 0x45, 0xFC });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "lea eax, [ecx+edx*4+8]" {
|
||||
// 8D 44 91 08
|
||||
const d = decode(&[_]u8{ 0x8D, 0x44, 0x91, 0x08 });
|
||||
try std.testing.expectEqual(@as(u8, 4), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_SIB != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "mov [disp32], eax" {
|
||||
// A3 xx xx xx xx
|
||||
const d = decode(&[_]u8{ 0xA3, 0x00, 0x10, 0x40, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "sub esp, imm32" {
|
||||
// 81 EC 00 01 00 00
|
||||
const d = decode(&[_]u8{ 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
}
|
||||
|
||||
test "test eax, imm32 (F7 C0)" {
|
||||
// F7 C0 FF 00 00 00 = test eax, 0xFF
|
||||
const d = decode(&[_]u8{ 0xF7, 0xC0, 0xFF, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
}
|
||||
|
||||
test "ret imm16" {
|
||||
// C2 04 00
|
||||
const d = decode(&[_]u8{ 0xC2, 0x04, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
//! x86-32 inline hooking library for Windows DLL injection.
|
||||
//!
|
||||
//! Provides auto-sizing detours with instruction relocation, type-safe
|
||||
//! `Detour(FnType)` wrappers, memory read/write helpers, rel32 arithmetic,
|
||||
//! a generic `fastcall` caller, and Windows virtual memory API re-exports.
|
||||
//!
|
||||
//! ## Type-safe API (Detour) — recommended
|
||||
//!
|
||||
//! ```zig
|
||||
//! const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
//! const CheckFile = fn (u32, u32, u32) callconv(fc) u32;
|
||||
//! var hook: Detour(CheckFile) = .{};
|
||||
//! hook.attach(0x654DD0, &myDetour);
|
||||
//! // Inside detour: hook.callOriginal(.{ filename, flags, output });
|
||||
//! hook.detach();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Low-level API (GenericHook)
|
||||
//!
|
||||
//! ```zig
|
||||
//! var my_hook: GenericHook = .{};
|
||||
//! if (my_hook.install(0x401000, @intFromPtr(&myDetour)) == .ok) {
|
||||
//! const orig = my_hook.getTrampoline(OrigFnType);
|
||||
//! _ = orig();
|
||||
//! }
|
||||
//! my_hook.remove();
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const x86dis = @import("x86dis");
|
||||
|
||||
// =============================================================================
|
||||
// Windows API
|
||||
// =============================================================================
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
/// Memory protection constant: page is readable, writable, and executable.
|
||||
pub const PAGE_EXECUTE_READWRITE: u32 = 0x40;
|
||||
/// Memory allocation type: commit physical storage for the region.
|
||||
pub const MEM_COMMIT: u32 = 0x1000;
|
||||
/// Memory free type: release the region (decommit + free address space).
|
||||
pub const MEM_RELEASE: u32 = 0x8000;
|
||||
|
||||
extern "kernel32" fn VirtualProtect(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
flNewProtect: u32,
|
||||
lpflOldProtect: *u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
/// Allocate or reserve virtual memory. Used to create RWX trampoline pages.
|
||||
pub extern "kernel32" fn VirtualAlloc(
|
||||
lpAddress: ?*anyopaque,
|
||||
dwSize: usize,
|
||||
flAllocationType: u32,
|
||||
flProtect: u32,
|
||||
) callconv(WINAPI) ?[*]u8;
|
||||
|
||||
/// Release virtual memory previously allocated with `VirtualAlloc`.
|
||||
pub extern "kernel32" fn VirtualFree(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
dwFreeType: u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
// =============================================================================
|
||||
// Memory helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Read a value of type `T` from an arbitrary memory address (unaligned).
|
||||
pub fn readMem(comptime T: type, addr: usize) T {
|
||||
return @as(*align(1) const T, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
/// Write raw bytes to an arbitrary memory address. No protection change —
|
||||
/// caller must ensure the page is writable (or use `writeProtected`).
|
||||
pub fn writeMem(addr: usize, bytes: []const u8) void {
|
||||
const dest: [*]u8 = @ptrFromInt(addr);
|
||||
for (bytes, 0..) |b, i| {
|
||||
dest[i] = b;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write bytes to a potentially read-only/executable page. Temporarily sets
|
||||
/// PAGE_EXECUTE_READWRITE, writes, then restores the original protection.
|
||||
pub fn writeProtected(addr: usize, bytes: []const u8) void {
|
||||
var old: u32 = 0;
|
||||
_ = VirtualProtect(@ptrFromInt(addr), bytes.len, PAGE_EXECUTE_READWRITE, &old);
|
||||
writeMem(addr, bytes);
|
||||
_ = VirtualProtect(@ptrFromInt(addr), bytes.len, old, &old);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Rel32 helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Resolve the absolute target of an E8 (CALL) or E9 (JMP) at `addr`.
|
||||
/// Reads the signed rel32 operand at addr+1 and computes addr+5+offset.
|
||||
pub fn rel32Target(addr: usize) usize {
|
||||
const offset: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 1)).*);
|
||||
return (addr + 5) +% offset;
|
||||
}
|
||||
|
||||
/// Write a rel32 displacement into dest[0..4] such that a JMP/CALL
|
||||
/// from address `from` reaches `to`. Displacement = to - (from + 4).
|
||||
pub fn writeRel32(dest: [*]u8, from: usize, to: usize) void {
|
||||
std.mem.writeInt(u32, dest[0..4], to -% (from + 4), .little);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Calling convention aliases and call helper
|
||||
// =============================================================================
|
||||
|
||||
/// x86-32 calling convention shorthands for use with `callconv()`.
|
||||
///
|
||||
/// Once Zig supports `@Type` for function types, these can be used to build
|
||||
/// convention-specific wrappers (stdcall/thiscall/fastcall/cdecl) that inject
|
||||
/// the callconv automatically, removing the need for `callconv(hook.cc.*)`.
|
||||
///
|
||||
/// ```zig
|
||||
/// const result = hook.call(fn (u32, u32) callconv(hook.cc.thiscall) i32, 0x6061E0, .{ player, unit });
|
||||
/// ```
|
||||
pub const cc = struct {
|
||||
pub const stdcall: std.builtin.CallingConvention = .{ .x86_stdcall = .{} };
|
||||
pub const thiscall: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
||||
pub const fastcall: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
pub const cdecl: std.builtin.CallingConvention = .c;
|
||||
};
|
||||
|
||||
/// Call a function at `addr` using a typed function pointer cast.
|
||||
///
|
||||
/// The function type `F` must include an explicit `callconv`. Supports all
|
||||
/// x86-32 conventions (stdcall, thiscall, fastcall, cdecl). Uses `.never_tail`
|
||||
/// to prevent tail-call optimization that would corrupt callee-cleanup stacks.
|
||||
///
|
||||
/// ```zig
|
||||
/// const result = hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, 0x464870, .{ lo, hi });
|
||||
/// ```
|
||||
pub fn call(comptime F: type, addr: usize, args: anytype) FnReturnType(F) {
|
||||
const func: *const F = @ptrFromInt(addr);
|
||||
return @call(.never_tail, func, args);
|
||||
}
|
||||
|
||||
fn FnReturnType(comptime F: type) type {
|
||||
return @typeInfo(F).@"fn".return_type orelse void;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GenericHook — low-level auto-sizing hook
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const JMP_SIZE: usize = 5; // E9 + rel32
|
||||
const MAX_STOLEN: usize = 32;
|
||||
const ALLOC_SIZE: usize = 64; // trampoline only
|
||||
|
||||
/// Auto-sizing inline hook that uses the x86 length disassembler to determine
|
||||
/// how many prologue bytes to steal, then copies and relocates them into a
|
||||
/// trampoline. Handles hook chaining (detects an existing E9 JMP at the target)
|
||||
/// and expands short jumps to near jumps during relocation.
|
||||
///
|
||||
/// For type-safe hooking with automatic calling convention handling, use
|
||||
/// `Detour(FnType)` instead — it wraps `GenericHook` and adds compile-time
|
||||
/// type checking.
|
||||
pub const GenericHook = struct {
|
||||
/// Base of the allocated RWX page, or null if not yet prepared.
|
||||
mem: ?[*]u8 = null,
|
||||
/// Address of the trampoline entry point (calls the original code).
|
||||
trampoline: usize = 0,
|
||||
/// Address of the hooked function's entry point.
|
||||
target: usize = 0,
|
||||
/// Number of bytes stolen from the target prologue (>= 5 for E9 JMP).
|
||||
stolen_size: usize = 0,
|
||||
/// Original prologue bytes, saved for restoration on `remove()`.
|
||||
saved_bytes: [MAX_STOLEN]u8 = undefined,
|
||||
|
||||
/// Result of a hook operation.
|
||||
pub const Error = enum {
|
||||
/// Success.
|
||||
ok,
|
||||
/// `VirtualAlloc` failed to allocate executable memory.
|
||||
alloc_failed,
|
||||
/// The length disassembler returned `F_ERROR` or a zero-length instruction.
|
||||
disasm_error,
|
||||
/// Could not steal enough bytes for a 5-byte JMP before hitting `MAX_STOLEN`.
|
||||
prologue_too_short,
|
||||
/// Prologue contains a relative instruction that cannot be relocated
|
||||
/// (e.g. LOOP, JECXZ — only CALL/JMP/Jcc are supported).
|
||||
unsupported_relocation,
|
||||
};
|
||||
|
||||
/// Convenience: `prepare` + `activate` in one call.
|
||||
pub fn install(self: *GenericHook, target: usize, detour_addr: usize) Error {
|
||||
const err = self.prepare(target);
|
||||
if (err != .ok) return err;
|
||||
self.activate(detour_addr);
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Phase 1: disassemble prologue, allocate trampoline, copy + relocate.
|
||||
///
|
||||
/// Walks the target's prologue with `x86dis.decode` until at least 5 bytes
|
||||
/// are covered, allocates an RWX page, copies the stolen bytes into a
|
||||
/// trampoline, relocates any relative instructions (E8/E9/Jcc/short jumps),
|
||||
/// and appends a JMP back to the remainder of the original function.
|
||||
///
|
||||
/// If the target already starts with an E9 JMP (another hook), chains
|
||||
/// through it: the trampoline jumps to the existing detour rather than
|
||||
/// copying the prologue.
|
||||
///
|
||||
/// Does NOT patch the target — call `activate()` after to write the JMP.
|
||||
pub fn prepare(self: *GenericHook, target: usize) Error {
|
||||
if (self.mem != null) return .ok;
|
||||
|
||||
const src: [*]const u8 = @ptrFromInt(target);
|
||||
|
||||
// ── determine how many bytes to steal ──
|
||||
var stolen: usize = 0;
|
||||
while (stolen < JMP_SIZE) {
|
||||
const insn = x86dis.decode(src + stolen);
|
||||
if (insn.flags & x86dis.F_ERROR != 0) return .disasm_error;
|
||||
if (insn.len == 0) return .disasm_error;
|
||||
stolen += insn.len;
|
||||
if (stolen > MAX_STOLEN) return .prologue_too_short;
|
||||
}
|
||||
|
||||
// ── allocate ──
|
||||
const mem = VirtualAlloc(null, ALLOC_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return .alloc_failed;
|
||||
|
||||
self.mem = mem;
|
||||
self.target = target;
|
||||
self.stolen_size = stolen;
|
||||
self.trampoline = @intFromPtr(mem);
|
||||
|
||||
@memcpy(self.saved_bytes[0..stolen], src[0..stolen]);
|
||||
|
||||
// ── check if already hooked (E9 at target) — chain through ──
|
||||
if (src[0] == 0xE9) {
|
||||
const other_detour = rel32Target(target);
|
||||
mem[0] = 0xE9;
|
||||
writeRel32(mem + 1, self.trampoline + 1, other_detour);
|
||||
return .ok;
|
||||
}
|
||||
|
||||
// ── build trampoline: copy + relocate ──
|
||||
var t_pos: usize = 0;
|
||||
var s_pos: usize = 0;
|
||||
|
||||
while (s_pos < stolen) {
|
||||
const insn = x86dis.decode(src + s_pos);
|
||||
const op = insn.opcode;
|
||||
const src_addr = target + s_pos;
|
||||
const dst_addr = self.trampoline + t_pos;
|
||||
|
||||
if (insn.flags & x86dis.F_RELATIVE != 0) {
|
||||
if (op == 0xE8 or op == 0xE9) {
|
||||
// CALL/JMP rel32
|
||||
const abs = rel32Target(src_addr);
|
||||
mem[t_pos] = op;
|
||||
writeRel32(mem + t_pos + 1, dst_addr + 1, abs);
|
||||
t_pos += 5;
|
||||
} else if (op == 0x0F and insn.opcode2 >= 0x80 and insn.opcode2 <= 0x8F) {
|
||||
// Jcc rel32 (0F 80-8F)
|
||||
const abs = jcc32Target(src_addr);
|
||||
mem[t_pos] = 0x0F;
|
||||
mem[t_pos + 1] = insn.opcode2;
|
||||
writeRel32(mem + t_pos + 2, dst_addr + 2, abs);
|
||||
t_pos += 6;
|
||||
} else if (op >= 0x70 and op <= 0x7F) {
|
||||
// Short Jcc → expand to near Jcc (0F 8x)
|
||||
const offset = @as(i8, @bitCast(src[s_pos + 1]));
|
||||
const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset);
|
||||
mem[t_pos] = 0x0F;
|
||||
mem[t_pos + 1] = op + 0x10;
|
||||
writeRel32(mem + t_pos + 2, dst_addr + 2, abs);
|
||||
t_pos += 6;
|
||||
} else if (op == 0xEB) {
|
||||
// Short JMP → expand to near JMP (E9)
|
||||
const offset = @as(i8, @bitCast(src[s_pos + 1]));
|
||||
const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset);
|
||||
mem[t_pos] = 0xE9;
|
||||
writeRel32(mem + t_pos + 1, dst_addr + 1, abs);
|
||||
t_pos += 5;
|
||||
} else {
|
||||
// LOOP/JECXZ or unknown — cannot trivially expand
|
||||
self.cleanup();
|
||||
return .unsupported_relocation;
|
||||
}
|
||||
} else {
|
||||
// Non-relative — copy verbatim
|
||||
@memcpy(mem[t_pos .. t_pos + insn.len], src[s_pos .. s_pos + insn.len]);
|
||||
t_pos += insn.len;
|
||||
}
|
||||
s_pos += insn.len;
|
||||
}
|
||||
|
||||
// ── JMP back to original code after stolen bytes ──
|
||||
mem[t_pos] = 0xE9;
|
||||
writeRel32(
|
||||
mem + t_pos + 1,
|
||||
self.trampoline + t_pos + 1,
|
||||
target + stolen,
|
||||
);
|
||||
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Phase 2: write the E9 JMP patch at the target.
|
||||
/// Any excess stolen bytes beyond the 5-byte JMP are filled with NOPs.
|
||||
pub fn activate(self: *GenericHook, detour_addr: usize) void {
|
||||
var patch: [MAX_STOLEN]u8 = .{0x90} ** MAX_STOLEN;
|
||||
patch[0] = 0xE9;
|
||||
writeRel32(patch[1..5], self.target + 1, detour_addr);
|
||||
writeProtected(self.target, patch[0..self.stolen_size]);
|
||||
}
|
||||
|
||||
/// Restore original bytes and free trampoline memory.
|
||||
pub fn remove(self: *GenericHook) void {
|
||||
if (self.mem == null) return;
|
||||
writeProtected(self.target, self.saved_bytes[0..self.stolen_size]);
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
|
||||
/// Get trampoline as a typed function pointer.
|
||||
pub fn getTrampoline(self: *const GenericHook, comptime T: type) T {
|
||||
return @ptrFromInt(self.trampoline);
|
||||
}
|
||||
|
||||
fn cleanup(self: *GenericHook) void {
|
||||
if (self.mem) |m| {
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(m)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Detour(FnType) — HadesMem-style type-safe generic hook
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Comptime-generic typed detour — the recommended primary API for hooking.
|
||||
///
|
||||
/// Declare the target function's type once; get type-checked `attach`,
|
||||
/// `callOriginal`, and `detach` with no manual pointer casts. Wraps
|
||||
/// `GenericHook` for auto-sizing and relocation.
|
||||
///
|
||||
/// Supports all x86-32 calling conventions: `stdcall`, `cdecl`, `thiscall`,
|
||||
/// and `fastcall` (requires patched Zig 0.16 with `inreg` fix for correct
|
||||
/// x86 fastcall codegen). Uses `.never_tail` internally to prevent tail-call
|
||||
/// optimization that would corrupt the stack with callee-cleanup conventions.
|
||||
///
|
||||
/// ```zig
|
||||
/// const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
/// const CheckFile = fn (u32, u32, u32) callconv(fc) u32;
|
||||
/// var hook: Detour(CheckFile) = .{};
|
||||
/// _ = hook.attach(0x654DD0, &myDetour);
|
||||
/// // in detour: hook.callOriginal(.{ filename, flags, output });
|
||||
/// hook.detach();
|
||||
/// ```
|
||||
pub fn Detour(comptime TargetFnType: type) type {
|
||||
const FnInfo = @typeInfo(TargetFnType).@"fn";
|
||||
const TargetFnPtr = *const TargetFnType;
|
||||
const ReturnType = FnInfo.return_type orelse void;
|
||||
|
||||
return struct {
|
||||
/// The underlying `GenericHook` managing the trampoline and patch.
|
||||
inner: GenericHook = .{},
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Hook the function at `target` to redirect to `detour`.
|
||||
/// The `detour` function pointer must match `TargetFnType` exactly.
|
||||
pub fn attach(self: *Self, target: usize, detour: TargetFnPtr) GenericHook.Error {
|
||||
const err = self.inner.prepare(target);
|
||||
if (err != .ok) return err;
|
||||
self.inner.activate(@intFromPtr(detour));
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Call the original (pre-hook) function through the trampoline.
|
||||
/// Pass args as a tuple: `.{ arg1, arg2, arg3 }`.
|
||||
///
|
||||
/// Uses `.never_tail` to prevent tail-call optimization, which would
|
||||
/// corrupt the stack with callee-cleanup conventions (stdcall/fastcall/
|
||||
/// thiscall) — a tail call reuses the caller's stack frame, but the
|
||||
/// callee pops args itself, leaving ESP pointing at garbage on return.
|
||||
pub fn callOriginal(self: *const Self, args: anytype) ReturnType {
|
||||
const orig: *const TargetFnType = @ptrFromInt(self.inner.trampoline);
|
||||
return @call(.never_tail, orig, args);
|
||||
}
|
||||
|
||||
/// Unhook: restore original bytes, free trampoline.
|
||||
pub fn detach(self: *Self) void {
|
||||
self.inner.remove();
|
||||
}
|
||||
|
||||
/// Get the trampoline as a raw function pointer of the target type.
|
||||
///
|
||||
/// Unlike `callOriginal`, calling through this pointer does NOT force
|
||||
/// `.never_tail` — the compiler may tail-call optimize, which corrupts
|
||||
/// the stack for callee-cleanup conventions. Prefer `callOriginal`
|
||||
/// unless you specifically need the function pointer (e.g. to pass as
|
||||
/// a callback).
|
||||
pub fn original(self: *const Self) *const TargetFnType {
|
||||
return @ptrFromInt(self.inner.trampoline);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolve absolute target of a Jcc rel32 (0F 8x xx xx xx xx) — 6 byte insn.
|
||||
fn jcc32Target(addr: usize) usize {
|
||||
const disp: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 2)).*);
|
||||
return (addr + 6) +% disp;
|
||||
}
|
||||
Reference in New Issue
Block a user