// rexcode ยท Brendan Punsky (dotbmp@github), original author package rexcode_x86 // ============================================================================= // SECTION: 7. HIGH-PERFORMANCE ENCODER // ============================================================================= // // Ultra-fast table-driven x64 instruction encoder with: // - Zero allocations: user provides all buffers // - O(1) mnemonic lookup via enum-indexed table // - O(1) label lookup via array indexing // - Fully inlined hot path - no function call overhead // - Trivially parallelizable: encode() is pure, no shared state // // API: Single entry point `encode()` that takes: // - instructions: []Instruction to encode // - label_defs: []Label_Definition mapping label_id -> instruction_index // - code: []u8 output buffer for machine code // - relocs: []Relocation output buffer for relocations // - errors: []Error output buffer for errors // // Returns Result with counts and success status. // Unresolved labels are returned as relocations (no extern/internal distinction). import "base:intrinsics" import "core:fmt" import "core:rexcode/isa" // ----------------------------------------------------------------------------- // SECTION: 7.1 Constants // ----------------------------------------------------------------------------- MAX_INST_SIZE :: 15 // Maximum x64 instruction length // ----------------------------------------------------------------------------- // SECTION: 7.6 Core Encoding Function // ----------------------------------------------------------------------------- // encode: The single entry point for x64 instruction encoding. // // Parameters: // instructions - Array of instructions to encode // label_defs - Array mapping label_id -> instruction index. MODIFIED IN PLACE // to contain byte offsets after encoding. // code - Output buffer for machine code (must be large enough) // relocs - Dynamic array; unresolved relocations are appended // errors - Dynamic array; encoding errors are appended // resolve - If true, resolve relocations and patch code in place // base_address - Base address for absolute relocations (when resolve=true) // // Returns: // Result with code size and success status. // // After encoding, label_defs[label_id] contains the byte offset of that label. // Unresolved references (labels not in label_defs) are appended to relocs. // encode :: proc( instructions: []Instruction, label_defs: []Label_Definition, // Input: inst index. Modified to byte offset. code: []u8, relocs: ^[dynamic]Relocation, // Unresolved relocations appended here errors: ^[dynamic]Error, // Errors appended here resolve: bool = true, base_address: u64 = 0, mode: Mode = ._64, // i386 vs x86-64 mode ) -> (byte_count: u32, ok: bool) { if mode == ._16 { // Real-mode encoding is not implemented; the ModRM addressing // model differs from protected/long mode and needs a separate // emission path. See Mode enum comment in encoding_types.odin. fmt.panicf("x64.encode: Mode._16 (real mode) is not yet supported") } ok = true // Temp storage for pending relocations (before resolution) pending_relocations: [dynamic]Relocation defer delete(pending_relocations) // Temp storage for instruction byte offsets inst_offsets: [dynamic]u32 defer delete(inst_offsets) resize(&inst_offsets, len(instructions)) // ========================================================================= // PASS 1: Encode all instructions, collect relocations // ========================================================================= for &inst, instruction_index in instructions { // Record this instruction's byte offset inst_offsets[instruction_index] = byte_count // Validate operand_count bounds if inst.operand_count > 4 { append(errors, Error{u32(instruction_index), .INVALID_OPERAND_COUNT, {}}) ok = false continue } // Check buffer space if byte_count + MAX_INST_SIZE > u32(len(code)) { append(errors, Error{u32(instruction_index), .BUFFER_OVERFLOW, {}}) ok = false continue } // i386 operand validation. The following don't exist in 32-bit // protected mode and must be rejected up front so we don't // silently emit bytes that mean something else (e.g. SPL would // encode as AH in 32-bit). Catches legacy AND VEX/EVEX paths. if mode == ._32 { invalid := false for i in 0..= 4 && hw <= 7 { invalid = true; break } } case .MEMORY: m := op.mem if (mem_has_base(m) && m.base_ext) || (mem_has_index(m) && m.index_ext) { invalid = true; break } } } if invalid { append(errors, Error{u32(instruction_index), .OPERAND_MISMATCH, {}}) ok = false continue } } matched_enc: ^Encoding = nil form_index := -1 // index into ENCODE_FORMS / ENCODE_RECIPES; -1 = no recipe fast path // Pre-matched form fast-path: a typed builder that maps to a single // encoding form bakes `global_index + 1` into enc_hint, letting us skip // the O(forms) match scan entirely -- and with it the scan's branches, // which are the unpredictable ones in a varied instruction stream. Only // in long mode (the builders' target); bounds-checked; anything else // (hand-built, generic builders, i386, decode) falls back to matching. if mode == ._64 && inst.enc_hint != ENC_HINT_NONE && int(inst.enc_hint) <= len(ENCODE_FORMS) { form_index = int(inst.enc_hint) - 1 matched_enc = &ENCODE_FORMS[form_index] } else { // Resolve the form on the matcher path (memoizing cache + scan) in a // separate, non-inlined proc so this hot loop stays lean for the hint // path above. form_index is discarded: the matcher path uses the // interpreter, not the recipe -- keeping it off the shared fast-path // hook leaves the hint path's codegen untouched. err: Error_Code matched_enc, _, err = find_form(&inst, mode) if err != .NONE { append(errors, Error{u32(instruction_index), err, {}}) ok = false continue } } // Recipe fast path (hint path only): a hinted, eligible form with a literal // immediate emits straight-line from the precomputed recipe, skipping the // interpreter below (resolve scan, prefix/REX/escape selection, ModR/M // source choice). Only the hint branch sets form_index; the matcher branch // leaves it -1 and emits via the interpreter, so this hook -- and the hot // loop's codegen -- stays exactly what it was before the matcher cache. // Anything outside the envelope falls through to the interpreter, the // byte-exact source of truth. if form_index >= 0 && form_index < len(ENCODE_RECIPES) { recipe := &ENCODE_RECIPES[form_index] if recipe.flags.eligible && transmute(u8)inst.flags == 0 { // 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 } } } // ===================================================================== // ENCODE INSTRUCTION (fully inlined hot path) // ===================================================================== enc := matched_enc out := code[byte_count:] pos: u32 = 0 // Resolve every encoding slot to its user operand ONCE, and gather the // ModR/M and opcode-reg slot roles in the same pass. The emission below // indexes user_ops[slot] instead of re-deriving the mapping per pass -- // the previous code re-scanned enc.ops ~5-10x per instruction (once for // REX bits, opcode +rb, ModR/M slots, reg/rm fields, immediates), which // was a dominant per-instruction cost. user_ops: [4]^Operand mr_slot: int = -1 reg_slot: int = -1 opr_slot: int = -1 imm_slot: int = -1 has_gpr16 := false // any GPR16 operand -> 66h operand-size prefix has_spl := false // any SPL/BPL/SIL/DIL (GPR8 hw 4-7) -> forces a REX { // If the caller supplied every operand -- including the ones this form // leaves implicit (which is what the decoder produces: it materializes // AL/AX/EAX/RAX/DX/ST0/ST(i)/1) -- map form slot i directly to user // operand i, so implicit slots consume their operand and the explicit // slots stay aligned. Otherwise the implicit operands are absent, so map // only the explicit slots (skipping implicit). For non-implicit forms // the two are identical (operand_count == total), so nothing changes. total_form_ops := 0 for op in enc.ops { if op == .NONE { break }; total_form_ops += 1 } fully_explicit := int(inst.operand_count) == total_form_ops user_idx := 0 for op, i in enc.ops { if op == .NONE { break } uop: ^Operand if fully_explicit { uop = &inst.ops[i] } else if !is_implicit_op_inline(op) { if user_idx < int(inst.operand_count) { uop = &inst.ops[user_idx] } user_idx += 1 } if uop != nil { user_ops[i] = uop if uop.kind == .REGISTER { cls := reg_class(uop.reg) hw := reg_hw(uop.reg) has_gpr16 ||= cls == REG_GPR16 has_spl ||= cls == REG_GPR8 && hw >= 4 && hw <= 7 } } // Slot roles (parallel array enc.enc[i]) gathered in the same pass. #partial switch enc.enc[i] { case .MR: mr_slot = i case .REG: reg_slot = i case .OP_R: opr_slot = i case .IB, .IW, .ID, .IQ: imm_slot = i } } } has_modrm := mr_slot >= 0 || reg_slot >= 0 // MOVSX/MOVZX/MOVSXD are *widening* moves: the r/m source is a fixed // narrower width selected by the opcode (0F BE/BF/B6/B7, 63), so it must // NOT drive the operand-size (66h) prefix -- only the destination width // does. The generic "any GPR16 operand" scan above would otherwise treat a // 16-bit *source* (e.g. `movsx eax, ax`) as a request for 16-bit operand // size and emit 66h, mis-encoding it as `movsx ax, ax`. (Contrast CRC32, // whose r/m source width legitimately selects operand size -- it keeps the // generic behavior.) The destination is always op0 and always a register. #partial switch inst.mnemonic { case .MOVSX, .MOVZX, .MOVSXD: has_gpr16 = inst.operand_count > 0 && inst.ops[0].kind == .REGISTER && reg_class(inst.ops[0].reg) == REG_GPR16 } // --- Legacy Prefixes --- // // The vast majority of instructions carry no legacy prefix, so gate the // whole block on a single flags-is-zero test instead of four separate // predicted-not-taken branches per instruction. Inside, the branches are // kept (a present prefix is rare enough that the branching form beats the // branchless speculative-write one -- see git history). if transmute(u8)inst.flags != 0 { // Lock prefix (F0) if inst.flags.lock && enc.flags.lock_ok { out[pos] = 0xF0 pos += 1 } // Rep/Repne prefix #partial switch inst.flags.rep { case .REP: out[pos] = 0xF3; pos += 1 case .REPNE: out[pos] = 0xF2; pos += 1 } // Segment override if inst.flags.segment != 0 { seg_prefix := [8]u8{0, 0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65, 0} out[pos] = seg_prefix[inst.flags.segment] pos += 1 } // Address size override (67h) if inst.flags.addr32 { out[pos] = 0x67 pos += 1 } } // --- VEX/EVEX or Legacy Encoding --- #partial switch enc.flags.vex_type{ case .VEX: // VEX prefix encoding r: u8 = 1; x: u8 = 1; b: u8 = 1 vvvv: u8 = 0xF; l: u8 = 0; pp: u8 = 0; mmmmm: u8 = 1; w: u8 = 0 #partial switch enc.flags.esc { case ._0F: mmmmm = 1 case ._0F38: mmmmm = 2 case ._0F3A: mmmmm = 3 } switch enc.flags.prefix { case 1: pp = 1 // 66 case 2: pp = 2 // F3 case 3: pp = 3 // F2 } #partial switch enc.flags.vex_l { case .L1: l = 1 } #partial switch enc.flags.vex_w { case .W1: w = 1 } // Operand-driven extension bits (branchless: compute reg & mem // contributions, gate by kind, clear the inverted bit via AND-mask). for enc_type, i in enc.enc { user_op := user_ops[i] if user_op == nil { continue } is_reg := user_op.kind == .REGISTER is_mem := user_op.kind == .MEMORY m := user_op.mem reg_ext := is_reg && reg_needs_rex(user_op.reg) base_ext := is_mem && mem_has_base(m) && m.base_ext index_ext := is_mem && mem_has_index(m) && m.index_ext #partial switch enc_type { case .REG: r &= u8(!reg_ext) case .MR: b &= u8(!reg_ext) b &= u8(!base_ext) x &= u8(!index_ext) case .VVVV: vvvv = is_reg ? (~reg_hw(user_op.reg) & 0xF) : vvvv } } // 2-byte or 3-byte VEX if x == 1 && b == 1 && w == 0 && mmmmm == 1 { out[pos] = 0xC5 out[pos+1] = (r << 7) | (vvvv << 3) | (l << 2) | pp pos += 2 } else { out[pos] = 0xC4 out[pos+1] = (r << 7) | (x << 6) | (b << 5) | mmmmm out[pos+2] = (w << 7) | (vvvv << 3) | (l << 2) | pp pos += 3 } case .EVEX: // EVEX prefix encoding (4 bytes) r: u8 = 1; x: u8 = 1; b: u8 = 1; rr: u8 = 1 mm: u8 = 1; w: u8 = 0; vvvv: u8 = 0xF; pp: u8 = 0 z: u8 = 0; ll: u8 = 0; bb: u8 = 0; vvv: u8 = 1; aaa: u8 = 0 #partial switch enc.flags.esc { case ._0F: mm = 1 case ._0F38: mm = 2 case ._0F3A: mm = 3 } switch enc.flags.prefix { case 1: pp = 1 case 2: pp = 2 case 3: pp = 3 } #partial switch enc.flags.vex_l { case .L1: ll = 1 case .L2: ll = 2 } #partial switch enc.flags.vex_w { case .W1: w = 1 } for i in 0..<4 { user_op := user_ops[i] if user_op == nil { continue } is_reg := user_op.kind == .REGISTER is_mem := user_op.kind == .MEMORY m := user_op.mem hw := reg_hw(user_op.reg) // gated by is_reg below reg8 := is_reg && hw >= 8 reg16 := is_reg && hw >= 16 base_ext := is_mem && mem_has_base(m) && m.base_ext index_ext := is_mem && mem_has_index(m) && m.index_ext #partial switch enc.enc[i] { case .REG: r &= u8(!reg8) rr &= u8(!reg16) case .MR: b &= u8(!reg8) b &= u8(!base_ext) x &= u8(!index_ext) bb |= u8(is_mem && user_op.flags.broadcast != .NONE) case .VVVV: vvvv = is_reg ? (~hw & 0xF) : vvvv vvv &= u8(!reg16) case .AAA: aaa = is_reg ? (hw & 0x7) : aaa } z |= u8(user_op.flags.zeroing) } out[pos] = 0x62 out[pos+1] = (r << 7) | (x << 6) | (b << 5) | (rr << 4) | mm out[pos+2] = (w << 7) | (vvvv << 3) | 0x04 | pp out[pos+3] = (z << 7) | (ll << 5) | (bb << 4) | (vvv << 3) | aaa pos += 4 case: // Legacy encoding // Operand size override (66h) -- has_gpr16 computed in the resolve pass. // opsize_16 covers operand-less 16-bit forms (CBW/CWD/MOVSW/...) that // carry no GPR16 operand to key off (matters on the interpreter path, // e.g. `rep movsw`; the eligible no-prefix forms take the recipe path). // Fixed-ModR/M forms (ext >= 0xC0: x87 like FSTSW AX, system ops) never // take an operand-size prefix, even though an implicit accumulator // operand may look like a GPR16. needs_66 := (has_gpr16 || enc.flags.opsize_16) && enc.flags.prefix != 1 && enc.ext < 0xC0 // PREFIX_66 if needs_66 { out[pos] = 0x66 pos += 1 } // Mandatory prefix if enc.flags.prefix != 0 && !needs_66 { mand_prefix := [4]u8{0, 0x66, 0xF3, 0xF2} out[pos] = mand_prefix[enc.flags.prefix] pos += 1 } else if enc.flags.prefix != 0 && enc.flags.prefix != 1 { mand_prefix := [4]u8{0, 0x66, 0xF3, 0xF2} out[pos] = mand_prefix[enc.flags.prefix] pos += 1 } // REX prefix, straight-line from the precomputed slots (no scan over // enc.enc). Contributions are OR-masked and gated by operand kind, so // the REGISTER/MEMORY branch stays out of the hot path. rex: u8 = bmask(enc.flags.force_rex_w) & 0x48 if reg_slot >= 0 { op := user_ops[reg_slot] if op != nil { rex |= bmask(op.kind == .REGISTER && reg_needs_rex(op.reg)) & 0x44 } } if mr_slot >= 0 { op := user_ops[mr_slot] if op != nil { is_reg := op.kind == .REGISTER is_mem := op.kind == .MEMORY m := op.mem // union bytes; only used when is_mem rex |= bmask(is_reg && reg_needs_rex(op.reg)) & 0x41 rex |= bmask(is_mem && mem_has_base(m) && m.base_ext) & 0x41 rex |= bmask(is_mem && mem_has_index(m) && m.index_ext) & 0x42 } } if opr_slot >= 0 { op := user_ops[opr_slot] if op != nil { rex |= bmask(op.kind == .REGISTER && reg_needs_rex(op.reg)) & 0x41 } } // SPL/BPL/SIL/DIL (has_spl, computed in the resolve pass) force an // empty REX in long mode when no other REX bit is set. rex |= bmask(mode == ._64 && rex == 0 && has_spl) & 0x40 // 32-bit mode forbids the REX prefix entirely. If any operand // demanded REX bits (R8-R15, SPL/BPL/SIL/DIL, force_rex_w), // the instruction is not legal i386. if mode == ._32 && rex != 0 { append(errors, Error{u32(instruction_index), .OPERAND_MISMATCH, {}}) ok = false continue } if rex != 0 { out[pos] = rex pos += 1 } // Escape bytes #partial switch enc.flags.esc { case ._0F: out[pos] = 0x0F pos += 1 case ._0F38: out[pos] = 0x0F; out[pos+1] = 0x38 pos += 2 case ._0F3A: out[pos] = 0x0F; out[pos+1] = 0x3A pos += 2 } } // --- Opcode --- opcode := enc.opcode // Handle +rb/+rw/+rd/+ro (register in opcode). For x87 fixed-ModR/M // forms (opcodes 0xD8..0xDF with ext >= 0xC0), the .OP_R index goes // into the rm field of the fixed ModR/M byte instead of the opcode. x87_fixed_modrm := opcode >= 0xD8 && opcode <= 0xDF && enc.ext >= 0xC0 opr_index: u8 = 0 opr_seen := false if opr_slot >= 0 { user_op := user_ops[opr_slot] if user_op != nil && user_op.kind == .REGISTER { opr_index = reg_hw(user_op.reg) & 0x07 opr_seen = true } } if opr_seen && !x87_fixed_modrm { opcode += opr_index } out[pos] = opcode pos += 1 // --- ModR/M and SIB --- (mr_slot/reg_slot/has_modrm gathered above) if has_modrm { has_sib := false mod: u8 = 0 reg_field: u8 = 0 rm: u8 = 0 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 { reg_field = enc.ext & 0x07 } else if reg_slot >= 0 { reg_op := user_ops[reg_slot] if reg_op != nil && reg_op.kind == .REGISTER { reg_field = reg_hw(reg_op.reg) & 0x07 } } // R/M field if mr_slot >= 0 { mr_op := user_ops[mr_slot] if mr_op != nil { #partial switch mr_op.kind { case .REGISTER: mod = 0b11 rm = reg_hw(mr_op.reg) & 0x07 case .MEMORY: m := mr_op.mem if mem_is_rip_relative(m) { mod = 0b00 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 has_sib = true sib = 0b00_100_101 disp = m.disp displacement_size = 4 } else { base_hw := m.base_hw has_index := mem_has_index(m) disp_value := m.disp needs_sib := has_index || (base_hw & 0x07) == 4 has_base := mem_has_base(m) is_rbp := (base_hw & 0x07) == 5 is_zero := disp_value == 0 fits8 := disp_value >= -128 && disp_value <= 127 disp = disp_value if needs_sib { has_sib = true rm = 0b100 scale: u8 = 0 switch mem_scale(m) { case 2: scale = 1 case 4: scale = 2 case 8: scale = 3 } idx := has_index ? (m.index_hw & 0x07) : u8(0b100) base_sib := has_base ? (base_hw & 0x07) : u8(0b101) sib = (scale << 6) | (idx << 3) | base_sib // mod / disp size, branchless. No base -> [disp32] // (mod 00, size 4). Otherwise: no displacement when // zero and not RBP-like; else disp8 if it fits, else // disp32. (RBP-like base forces an explicit disp8.) no_disp := has_base && is_zero && !(has_base && is_rbp) displacement_size = !has_base ? 4 : (no_disp ? 0 : (fits8 ? 1 : 4)) mod = !has_base ? 0b00 : (no_disp ? 0b00 : (fits8 ? 0b01 : 0b10)) } else { rm = base_hw & 0x07 no_disp := is_zero && !is_rbp displacement_size = no_disp ? 0 : (fits8 ? 1 : 4) mod = no_disp ? 0b00 : (fits8 ? 0b01 : 0b10) } } } } } out[pos] = (mod << 6) | (reg_field << 3) | rm pos += 1 if has_sib { out[pos] = sib 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..>= 8 pos += 1 } } } // Fixed ModR/M for special instructions. Triggered for: // - 0F-escape forms (NOP-class, MONITOR/MWAIT, etc.) // - x87 ST(i) and special control instructions (opcodes 0xD8..0xDF) is_x87_opcode := enc.opcode >= 0xD8 && enc.opcode <= 0xDF if enc.ext >= 0xC0 && !has_modrm && (enc.flags.esc != .NONE || is_x87_opcode) { modrm_byte := enc.ext // For x87 ST(i) forms, OR the OP_R register index into the rm field if x87_fixed_modrm && opr_seen { modrm_byte = (modrm_byte & 0xF8) | opr_index } out[pos] = modrm_byte pos += 1 } // --- Immediate(s), in operand-slot order (ENTER = C8 has two: IMM16 then IMM8). --- for slot in 0 ..< 4 { if user_ops[slot] == nil { continue } user_op := user_ops[slot] #partial switch enc.enc[slot] { case .IB: #partial switch user_op.kind { case .IMMEDIATE: out[pos] = u8(user_op.immediate) pos += 1 case .RELATIVE: label_id := u32(user_op.relative) append(&pending_relocations, Relocation{byte_count + pos, label_id, 0, .REL8, 1, u16(instruction_index)}) out[pos] = 0 pos += 1 } case .IW: if user_op.kind == .IMMEDIATE { v := u16(user_op.immediate) out[pos] = u8(v); out[pos+1] = u8(v >> 8) pos += 2 } case .ID: #partial switch user_op.kind { case .IMMEDIATE: v := u32(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) pos += 4 case .RELATIVE: label_id := u32(user_op.relative) append(&pending_relocations, Relocation{byte_count + pos, 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 } case .IQ: if user_op.kind == .IMMEDIATE { if user_op.flags.imm_is_label { // movabs reg,