Merge pull request #7168 from odin-lang/bill/rexcode

rexcode/x86: label addressing for RIP-relative disp and movabs imm
This commit is contained in:
gingerBill
2026-07-30 23:30:47 +02:00
committed by GitHub
4 changed files with 260 additions and 19 deletions

View File

@@ -122,6 +122,7 @@ Memory :: bit_field u64 {
addr_size_override: bool | 1,
base_class: u8 | 5,
index_class: u8 | 5,
disp_is_label: bool | 1, // disp holds a label id -> REL32 relocation
}
MEM_BASE_RIP :: 30 MEM_BASE_NONE :: 31 MEM_INDEX_NONE :: 31
```
@@ -131,7 +132,17 @@ MEM_BASE_RIP :: 30 MEM_BASE_NONE :: 31 MEM_INDEX_NONE :: 31
**Convenience constructors** (current names after the in-tree refactor):
`mem_base_only(base)`, `mem_base_disp(base, disp)`,
`mem_base_index(base, index, scale)`,
`mem_base_index_disp(base, index, scale, disp)`, `mem_rip_disp(disp)`.
`mem_base_index_disp(base, index, scale, disp)`, `mem_rip_disp(disp)`,
`mem_rip_label(label_id)`.
> **Label addressing.** `mem_rip_label(label_id)` builds a RIP-relative operand
> whose displacement references a **label** rather than a literal: the encoder
> writes a placeholder disp32 and emits a `REL32` relocation for `label_id` at
> that field (addend 0), exactly as the `.RELATIVE` jump/call path does. This is
> how `lea reg, [rip + <label>]` (position-independent data addressing) is
> expressed. When the label is defined the disp resolves in place; otherwise the
> relocation is appended to the caller's `relocs`. The `disp_is_label` bit reuses
> one of `Memory`'s spare bits — no struct growth.
> ⚠️ `mem_base` is an **accessor** (returns the base `Register`), not a
> constructor — use `mem_base_only` for the no-displacement case.
@@ -158,17 +169,24 @@ Operand :: struct #packed { // 16 bytes
Broadcast :: enum u8 { NONE, B1TO2, B1TO4, B1TO8, B1TO16 } // EVEX
Operand_Flags :: bit_field u16 { // EVEX-specific
mask: u8 | 3, // opmask K1K7
zeroing: bool | 1, // merge vs zero masking
broadcast: Broadcast | 3,
er_sae: u8 | 2, // embedded rounding / SAE
mask: u8 | 3, // opmask K1K7
zeroing: bool | 1, // merge vs zero masking
broadcast: Broadcast | 3,
er_sae: u8 | 2, // embedded rounding / SAE
imm_is_label: bool | 1, // movabs imm64 holds a label id -> ABS64 reloc
}
```
### Generic operand constructors
`op_reg(r)`, `op_mem(m, size)`, `op_mem_from_parts(base, index, scale, disp, size)`,
`op_imm8/16/32/64(v)`, `op_rel8/32(offset)`, `op_label(label_id, size=4)`.
`op_imm8/16/32/64(v)`, `op_rel8/32(offset)`, `op_label(label_id, size=4)`,
`op_imm_label(label_id)`.
> `op_imm_label(label_id)` is a `movabs reg, <label>` immediate: it stays kind
> `.IMMEDIATE` (form matching unchanged) but is flagged so the encoder emits an
> `ABS64` relocation (size 8) for the imm64 instead of a literal. The label
> always selects the full imm64 form regardless of the id's numeric value.
### Typed operand constructors (compile-time class safety)

View File

@@ -178,8 +178,14 @@ encode :: proc(
if form_index >= 0 && form_index < len(ENCODE_RECIPES) {
recipe := &ENCODE_RECIPES[form_index]
if recipe.flags.eligible && transmute(u8)inst.flags == 0 {
imm_lit := recipe.imm_op < 0 || inst.ops[recipe.imm_op].kind == .IMMEDIATE
if imm_lit {
// The recipe emitter is contextless and cannot append relocations,
// so a labeled immediate or a labeled memory displacement must fall
// back to the interpreter (which emits the relocation).
imm_lit := recipe.imm_op < 0 ||
(inst.ops[recipe.imm_op].kind == .IMMEDIATE && !inst.ops[recipe.imm_op].flags.imm_is_label)
rm_lit := recipe.rm_op < 0 ||
inst.ops[recipe.rm_op].kind != .MEMORY || !inst.ops[recipe.rm_op].mem.disp_is_label
if imm_lit && rm_lit {
byte_count += emit_recipe(recipe, &inst, code[byte_count:])
continue
}
@@ -545,6 +551,8 @@ encode :: proc(
sib: u8 = 0
disp: i32 = 0
displacement_size: u8 = 0
disp_is_label := false // disp32 references a label -> emit REL32
disp_label_id: u32 = 0
// Reg field
if enc.flags.modrm_reg_ext {
@@ -572,6 +580,11 @@ encode :: proc(
rm = 0b101
disp = m.disp
displacement_size = 4
// A RIP-relative disp can carry a label id instead of a
// literal: emit a REL32 relocation at the disp32 offset
// (mirrors the .RELATIVE -> REL32 jump/call path).
disp_is_label = m.disp_is_label
disp_label_id = u32(m.disp)
} else if !mem_has_base(m) && !mem_has_index(m) {
mod = 0b00
rm = 0b100
@@ -633,13 +646,21 @@ encode :: proc(
pos += 1
}
// Displacement: bounded little-endian emit. Kept as a counted loop
// (0/1/4 trips, highly predictable per code pattern) so no buffer
// tail-slack is needed and no bytes are written past the real size.
for _ in 0..<displacement_size {
out[pos] = u8(disp & 0xFF)
disp >>= 8
pos += 1
// Displacement. A labeled disp emits a placeholder disp32 and records a
// REL32 relocation at its byte offset (addend 0). Otherwise a bounded
// little-endian emit -- kept as a counted loop (0/1/4 trips, highly
// predictable per code pattern) so no buffer tail-slack is needed and no
// bytes are written past the real size.
if disp_is_label {
append(&pending_relocations, Relocation{byte_count + pos, disp_label_id, 0, .REL32, 4, u16(instruction_index)})
out[pos] = 0; out[pos+1] = 0; out[pos+2] = 0; out[pos+3] = 0
pos += 4
} else {
for _ in 0..<displacement_size {
out[pos] = u8(disp & 0xFF)
disp >>= 8
pos += 1
}
}
}
@@ -693,10 +714,19 @@ encode :: proc(
}
case .IQ:
if user_op.kind == .IMMEDIATE {
v := u64(user_op.immediate)
out[pos] = u8(v); out[pos+1] = u8(v >> 8); out[pos+2] = u8(v >> 16); out[pos+3] = u8(v >> 24)
out[pos+4] = u8(v >> 32); out[pos+5] = u8(v >> 40); out[pos+6] = u8(v >> 48); out[pos+7] = u8(v >> 56)
pos += 8
if user_op.flags.imm_is_label {
// movabs reg, <label>: placeholder imm64 + ABS64 relocation.
label_id := u32(user_op.immediate)
append(&pending_relocations, Relocation{byte_count + pos, label_id, 0, .ABS64, 8, u16(instruction_index)})
out[pos] = 0; out[pos+1] = 0; out[pos+2] = 0; out[pos+3] = 0
out[pos+4] = 0; out[pos+5] = 0; out[pos+6] = 0; out[pos+7] = 0
pos += 8
} else {
v := u64(user_op.immediate)
out[pos] = u8(v); out[pos+1] = u8(v >> 8); out[pos+2] = u8(v >> 16); out[pos+3] = u8(v >> 24)
out[pos+4] = u8(v >> 32); out[pos+5] = u8(v >> 40); out[pos+6] = u8(v >> 48); out[pos+7] = u8(v >> 56)
pos += 8
}
}
}
}
@@ -946,6 +976,12 @@ mem_matches_inline :: #force_inline proc "contextless" (op: ^Operand, op_type: O
}
imm_matches_inline :: #force_inline proc "contextless" (op: ^Operand, op_type: Operand_Type) -> bool {
// A labeled immediate is an address placeholder (movabs): it must select the
// full imm64 form regardless of the label id's numeric value, so a small id
// doesn't collapse to a shorter `mov r64, imm32` and lose the ABS64 slot.
if op.flags.imm_is_label {
return op_type == .IMM64
}
// Match based on whether the VALUE fits in the encoding's immediate size.
// x64 immediates are interpreted as both signed and unsigned depending on context:
// - ADD r32, imm8sx: sign-extended, so -1 becomes 0xFFFFFFFF

View File

@@ -37,6 +37,11 @@ Memory :: bit_field u64 {
addr_size_override: bool | 1,
base_class: u8 | 5,
index_class: u8 | 5,
// When set, `disp` holds a label id (not a literal displacement): the
// encoder emits a REL32 relocation at the disp32's byte offset instead of
// the literal, so a RIP-relative operand can reference a label. Uses one of
// Memory's 4 spare bits -- no growth, and one bit-test on the hot path.
disp_is_label: bool | 1,
}
@(require_results)
@@ -76,6 +81,10 @@ mem_make :: proc "contextless" (base: Register, index: Register, scale: u8, disp
}
mem.disp = disp
// Must be explicitly cleared: `mem` starts uninitialized (`= ---`) and the
// encoder reads this on the hot path -- garbage here would spuriously turn a
// literal displacement into a label reference. mem_rip_label sets it after.
mem.disp_is_label = false
return mem
}
@@ -150,6 +159,20 @@ mem_rip_disp :: #force_inline proc "contextless" (disp: i32) -> Memory {
return mem_make(RIP, NONE, 1, disp, NONE)
}
// RIP-relative memory operand whose displacement references a label rather than
// a literal. The encoder writes a placeholder disp32 and emits a REL32
// relocation for `label_id` at that field's byte offset -- the native way to
// express `lea reg, [rip + <label>]` (position-independent data addressing).
// The relocation carries addend 0; any within-section offset is supplied by the
// downstream object/JIT layer when it resolves the label.
@(require_results)
mem_rip_label :: #force_inline proc "contextless" (label_id: u32) -> Memory {
m := mem_make(RIP, NONE, 1, 0, NONE)
m.disp = i32(label_id)
m.disp_is_label = true
return m
}
// -----------------------------------------------------------------------------
// SECTION: 2.5 Operand struct and Flags
// -----------------------------------------------------------------------------
@@ -184,6 +207,10 @@ Operand_Flags :: bit_field u16 {
zeroing: bool | 1, // merge (0) vs zero (1) masking
broadcast: Broadcast | 3, // broadcast mode (see Broadcast enum)
er_sae: u8 | 2, // embedded rounding / SAE (0=none, 1=RN-SAE, 2=RD-SAE, 3=RU-SAE/RZ-SAE)
// When set on a movabs (IQ) immediate operand, `Operand.immediate` holds a
// label id: the encoder writes a placeholder imm64 and emits an ABS64
// relocation at its byte offset. Uses one of Operand_Flags' spare bits.
imm_is_label: bool | 1,
}
// -----------------------------------------------------------------------------
@@ -255,6 +282,16 @@ op_label :: #force_inline proc "contextless" (label_id: u32, size: u8 = 4) -> Op
return Operand{relative = i64(label_id), kind = .RELATIVE, size = size}
}
// A movabs imm64 that references a label: the immediate stays kind .IMMEDIATE
// (so form matching is unchanged) but is flagged as a label, so the encoder
// emits an ABS64 relocation for it instead of a literal. Use for
// `movabs reg, <label>` (absolute addressing; the target address is filled in
// at relocation time).
@(require_results)
op_imm_label :: #force_inline proc "contextless" (label_id: u32) -> Operand {
return Operand{immediate = i64(label_id), kind = .IMMEDIATE, size = 8, flags = {imm_is_label = true}}
}
// -----------------------------------------------------------------------------
// SECTION: 2.7 Typed Operand Constructors (compile-time type safety)
// -----------------------------------------------------------------------------

View File

@@ -3369,6 +3369,153 @@ run_i386_tests :: proc() {
}
}
// =============================================================================
// SECTION 11: LABEL ADDRESSING (RIP-relative disp + movabs imm)
// =============================================================================
//
// Native label references for a memory displacement (REL32) and a movabs imm64
// (ABS64) -- the encoder emits the same relocations it already does for
// jump/call targets, so a RIP-relative `lea reg, [rip + <label>]` and an
// absolute `movabs reg, <label>` can be expressed directly.
read_i32_le :: proc(b: []u8, off: int) -> i32 {
return i32(u32(b[off]) | (u32(b[off+1]) << 8) | (u32(b[off+2]) << 16) | (u32(b[off+3]) << 24))
}
run_label_addressing_tests :: proc() {
before := g_stats.failed
// -- lea rax, [rip + L], L DEFINED: disp32 must resolve to L - end_of_lea ----
{
// insts: 0=lea rax,[rip+L] 1=ret 2=ret L (id 0) at inst index 2.
insts := []x86.Instruction{
{mnemonic = .LEA, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_mem(x86.mem_rip_label(0), 8), {}, {}}},
x86.inst_none(.RET),
x86.inst_none(.RET),
}
labels := [1]x86.Label_Definition{x86.Label_Definition(2)} // L -> inst index 2
code: [64]u8
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
defer delete(relocs); defer delete(errs)
n, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
// lea rax,[rip+disp32] = 48 8D 05 dd dd dd dd (7 bytes); disp32 at offset 3.
disp := read_i32_le(code[:], 3)
target := i32(labels[0]) // now a byte offset
want := target - 7 // next_pc = end of the 7-byte lea
prefix_ok := code[0] == 0x48 && code[1] == 0x8D && code[2] == 0x05
if ok && len(relocs) == 0 && prefix_ok && disp == want && n == 9 {
fmt.printf("%s[PASS]%s lea [rip+L] resolved: disp32=%d (L@%d)\n", GREEN, RESET, disp, target)
g_stats.passed += 1
} else {
fmt.printf("%s[FAIL]%s lea [rip+L] resolved: ok=%v relocs=%d prefix_ok=%v disp=%d want=%d n=%d\n",
RED, RESET, ok, len(relocs), prefix_ok, disp, want, n)
g_stats.failed += 1
}
}
// -- lea rax, [rip + L], L UNDEFINED: a REL32 reloc at the disp32 offset ------
{
insts := []x86.Instruction{
{mnemonic = .LEA, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_mem(x86.mem_rip_label(0), 8), {}, {}}},
}
labels := [1]x86.Label_Definition{x86.LABEL_UNDEFINED}
code: [64]u8
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
defer delete(relocs); defer delete(errs)
_, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
r := len(relocs) == 1 ? relocs[0] : x86.Relocation{}
good := ok && len(relocs) == 1 &&
r.type == .REL32 && r.size == 4 && r.offset == 3 && r.label_id == 0 && r.addend == 0
// placeholder disp must be zero until relocation
good &&= read_i32_le(code[:], 3) == 0
if good {
fmt.printf("%s[PASS]%s lea [rip+L] unresolved: REL32 @off=%d size=%d label=%d\n",
GREEN, RESET, r.offset, r.size, r.label_id)
g_stats.passed += 1
} else {
fmt.printf("%s[FAIL]%s lea [rip+L] unresolved: ok=%v relocs=%d %v\n", RED, RESET, ok, len(relocs), r)
g_stats.failed += 1
}
}
// -- movabs rax, L: an ABS64 reloc at the imm64 offset -----------------------
{
insts := []x86.Instruction{
{mnemonic = .MOV, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_imm_label(0), {}, {}}},
}
labels := [1]x86.Label_Definition{x86.LABEL_UNDEFINED}
code: [64]u8
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
defer delete(relocs); defer delete(errs)
n, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
r := len(relocs) == 1 ? relocs[0] : x86.Relocation{}
// movabs rax, imm64 = 48 B8 <imm64> (10 bytes); imm64 at offset 2.
prefix_ok := n == 10 && code[0] == 0x48 && code[1] == 0xB8
good := ok && len(relocs) == 1 &&
r.type == .ABS64 && r.size == 8 && r.offset == 2 && r.label_id == 0 && prefix_ok
if good {
fmt.printf("%s[PASS]%s movabs rax, L: ABS64 @off=%d size=%d (48 B8 ...)\n",
GREEN, RESET, r.offset, r.size)
g_stats.passed += 1
} else {
fmt.printf("%s[FAIL]%s movabs rax, L: ok=%v n=%d relocs=%d prefix_ok=%v %v\n",
RED, RESET, ok, n, len(relocs), prefix_ok, r)
g_stats.failed += 1
}
}
// -- lea [rip+L] with a nonzero-distance L, executed end-to-end --------------
// Prove the resolved displacement actually points at the datum: lay a known
// dword right after a `lea rax,[rip+L]; mov eax,[rax]; ret`, and check the
// loaded value comes back. The disp is derived from the real layout, not baked.
{
DATUM :: i32(0x5A17C0DE)
insts := []x86.Instruction{
// 0: lea rax, [rip + L]
{mnemonic = .LEA, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_mem(x86.mem_rip_label(0), 8), {}, {}}},
// 1: mov eax, [rax] (load the datum L points at)
{mnemonic = .MOV, operand_count = 2, ops = {x86.op_reg(x86.EAX), x86.op_mem(x86.mem_base_only(x86.RAX), 4), {}, {}}},
// 2: ret
x86.inst_none(.RET),
// 3: filler that reserves >=4 bytes for the datum; its first 4 bytes are
// overwritten below. Never executed (the ret at inst 2 returns first).
x86.inst_r_i(.MOV, x86.EAX, 0x1111_1111, 4),
}
labels := [1]x86.Label_Definition{x86.Label_Definition(3)} // L at inst index 3
code: [64]u8
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
defer delete(relocs); defer delete(errs)
n, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
// L's byte offset is where inst 3 starts; the lea's disp resolved to it.
// Overwrite those 4 bytes with the datum the load should return.
datum_off := int(labels[0])
v := u32(DATUM)
code[datum_off] = u8(v); code[datum_off+1] = u8(v >> 8); code[datum_off+2] = u8(v >> 16); code[datum_off+3] = u8(v >> 24)
exec := alloc_exec(4096)
defer free_exec(exec)
copy(exec, code[:n])
fn := transmute(proc "c" () -> i64)raw_data(exec)
got := i32(fn())
if ok && len(relocs) == 0 && got == DATUM {
fmt.printf("%s[PASS]%s lea [rip+L] loads datum: got 0x%08X\n", GREEN, RESET, u32(got))
g_stats.passed += 1
} else {
fmt.printf("%s[FAIL]%s lea [rip+L] loads datum: ok=%v relocs=%d got 0x%08X want 0x%08X\n",
RED, RESET, ok, len(relocs), u32(got), u32(DATUM))
g_stats.failed += 1
}
}
if g_stats.failed == before {
fmt.printf("%s[PASS]%s label addressing (REL32 disp + ABS64 movabs)\n", GREEN, RESET)
}
}
// =============================================================================
// MAIN
// =============================================================================
@@ -3413,6 +3560,9 @@ main :: proc() {
log_header("LARGE FUNCTION TESTS")
run_large_tests()
log_header("LABEL ADDRESSING TESTS")
run_label_addressing_tests()
}
log_header("DECODE-ONLY TESTS")