kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit d095314213d32055bf1f738b97bd8e36cbd8b536
parent 5cb35a4c2f608f36882882771ab3ddc82739aa05
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 29 May 2026 15:15:33 -0700

x64 asm: encode SIB index/scale, bare (%rip), and ALU/MOV memory-store forms

Adds full AT&T memory-operand parsing (disp(%base,%index,scale), index-only,
and bare/numeric (%rip)) plus reg->mem and imm->mem ALU/MOV store forms to the
standalone assembler. Reuses x64_pack_mem_sib; LEA/MOV store paths refactored
to shared opcode-parameterized emit helpers. Corpus: x64_memop_{sib_load,rip,
alu_store,mov_store}, byte-verified vs llvm-mc. Symbolic sym(%rip)/@reloc forms
remain for the reloc-operator-syntax foundation.

Diffstat:
Msrc/arch/x64/asm.c | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/arch/x64/isa.h | 23+++++++++++++++++++++++
Atest/asm/encode/x64_memop_alu_store.expected.hex | 1+
Atest/asm/encode/x64_memop_alu_store.s | 6++++++
Atest/asm/encode/x64_memop_alu_store.targets | 1+
Atest/asm/encode/x64_memop_mov_store.expected.hex | 1+
Atest/asm/encode/x64_memop_mov_store.s | 5+++++
Atest/asm/encode/x64_memop_mov_store.targets | 1+
Atest/asm/encode/x64_memop_rip.expected.hex | 1+
Atest/asm/encode/x64_memop_rip.s | 3+++
Atest/asm/encode/x64_memop_rip.targets | 1+
Atest/asm/encode/x64_memop_sib_load.expected.hex | 1+
Atest/asm/encode/x64_memop_sib_load.s | 4++++
Atest/asm/encode/x64_memop_sib_load.targets | 1+
14 files changed, 228 insertions(+), 18 deletions(-)

diff --git a/src/arch/x64/asm.c b/src/arch/x64/asm.c @@ -39,7 +39,11 @@ typedef struct X64AsmOperand { u8 base; u8 high8; u8 seg; - u8 no_base; + u8 no_base; /* MEM: segment-prefixed absolute, no base register */ + u8 index; /* MEM SIB: index register (valid when has_index) */ + u8 scale; /* MEM SIB: log2 of scale ∈ {0,1,2,3} → 1/2/4/8 */ + u8 has_index; /* MEM: SIB index present */ + u8 rip_relative; /* MEM: bare (%rip)/disp(%rip) form */ u8 pad[1]; i64 imm; i32 disp; @@ -124,6 +128,8 @@ static int x64_segment_prefix_from_name(AsmDriver* d, Sym s, u8* prefix_out) { return 0; } +static void expect_comma(AsmDriver* d); + static u32 parse_reg(AsmDriver* d, u32* width_out, u32* high8_out) { AsmTok t; u32 reg; @@ -137,6 +143,76 @@ static u32 parse_reg(AsmDriver* d, u32* width_out, u32* high8_out) { return reg; } +/* True if the symbol names the instruction pointer ("rip"). */ +static int x64_ident_is_rip(AsmDriver* d, Sym s) { + Slice sl = pool_slice(asm_driver_pool(d), s); + return sl.s && sl.len == 3 && sl.s[0] == 'r' && sl.s[1] == 'i' && + sl.s[2] == 'p'; +} + +/* Convert a SIB scale literal (1/2/4/8) to its log2 (0/1/2/3). */ +static u32 x64_scale_to_log2(AsmDriver* d, i64 scale) { + switch (scale) { + case 1: + return 0u; + case 2: + return 1u; + case 4: + return 2u; + case 8: + return 3u; + default: + asm_driver_panic(d, "x64 asm: memory scale must be 1, 2, 4, or 8"); + } +} + +/* Parse the body of a memory operand once the leading '(' has been + * consumed: '%base[,%index,scale])', '%rip)', or ',%index,scale)'. + * Fills base/index/scale/has_index/rip_relative on `op` and eats the + * closing ')'. */ +static void parse_mem_paren_body(AsmDriver* d, X64AsmOperand* op) { + AsmTok t = asm_driver_peek(d); + if (asm_driver_tok_is_punct(t, '%')) { + /* Peek the register name to detect the RIP-relative form. */ + AsmTok ident; + (void)asm_driver_next(d); + ident = asm_driver_next(d); + if (ident.kind != ASM_TOK_IDENT) + asm_driver_panic(d, "x64 asm: bad register"); + if (x64_ident_is_rip(d, ident.v.ident)) { + op->rip_relative = 1; + asm_driver_expect_punct(d, ')', "')' in x64 memory operand"); + return; + } + { + u32 reg = 0; + if (!x64_reg_from_name(d, ident.v.ident, &reg, NULL, NULL)) + asm_driver_panic(d, "x64 asm: bad register"); + op->base = (u8)reg; + } + /* Optional ',%index,scale'. */ + if (asm_driver_eat_comma(d)) { + op->index = (u8)parse_reg(d, NULL, NULL); + op->has_index = 1; + expect_comma(d); + op->scale = (u8)x64_scale_to_log2(d, asm_driver_parse_const(d)); + } + asm_driver_expect_punct(d, ')', "')' in x64 memory operand"); + return; + } + /* Index-only form: '(,%index,scale)' — base omitted. */ + if (asm_driver_eat_comma(d)) { + op->no_base = 1; + op->index = (u8)parse_reg(d, NULL, NULL); + op->has_index = 1; + expect_comma(d); + op->scale = (u8)x64_scale_to_log2(d, asm_driver_parse_const(d)); + asm_driver_expect_punct(d, ')', "')' in x64 memory operand"); + return; + } + asm_driver_panic(d, "x64 asm: expected register in memory operand"); +} + static X64AsmOperand parse_operand(AsmDriver* d) { X64AsmOperand op; AsmTok t; @@ -196,38 +272,65 @@ static X64AsmOperand parse_operand(AsmDriver* d) { op.disp = (i32)asm_driver_parse_const(d); } asm_driver_expect_punct(d, '(', "'(' in x64 memory operand"); - op.base = (u8)parse_reg(d, NULL, NULL); - asm_driver_expect_punct(d, ')', "')' in x64 memory operand"); + parse_mem_paren_body(d, &op); return op; } static u32 x64_pack_rex_mem_operand(u8* out, int w, u32 reg, X64AsmOperand mem) { + /* RIP-relative carries no base/index registers (rm=101, no SIB). */ + if (mem.rip_relative) return x64_pack_rex(out, w, reg, 0, 0u); + /* SIB forms supply REX.X from the index register (and REX.B from base + * unless the base is omitted in the index-only form). */ + if (mem.has_index) + return x64_pack_rex(out, w, reg, mem.index, mem.no_base ? 0u : mem.base); return x64_pack_rex(out, w, reg, 0, mem.no_base ? 0u : mem.base); } static u32 x64_pack_mem_operand(u8* out, u32 reg, X64AsmOperand mem) { + if (mem.rip_relative) return x64_pack_mem_rip(out, reg, mem.disp); + if (mem.has_index) { + /* Index-only form (no base): mod=00 with SIB.base=101 → disp32. */ + if (mem.no_base) { + out[0] = x64_modrm(0u, reg, X64_MODRM_RM_SIB); + out[1] = x64_sib(mem.scale, mem.index, X64_SIB_NO_BASE); + return 2u + x64_put_u32le(out + 2, (u32)mem.disp); + } + return x64_pack_mem_sib(out, reg, mem.base, mem.index, mem.scale, mem.disp); + } if (mem.no_base) { - out[0] = x64_modrm(0u, reg, 4u); - out[1] = x64_sib(0u, 4u, 5u); + out[0] = x64_modrm(0u, reg, X64_MODRM_RM_SIB); + out[1] = x64_sib(0u, X64_SIB_NO_INDEX, X64_SIB_NO_BASE); return 2u + x64_put_u32le(out + 2, (u32)mem.disp); } return x64_pack_mem(out, reg, mem.base, mem.disp); } -static void emit_mov_load_operand(MCEmitter* mc, u32 size, u32 dst, - X64AsmOperand src) { +/* reg ← mem with an explicit single-byte opcode (e.g. 0x8B MOV, 0x8D LEA). + * Routes the full memory-operand variety (plain / SIB / RIP / segment) + * through the shared pack helpers. */ +static void emit_reg_mem_operand(MCEmitter* mc, u32 size, u8 opc, u32 dst, + X64AsmOperand src) { u8 buf[16]; u32 n = 0; if (size == 2u) buf[n++] = X64_OPSIZE_PFX; if (src.seg) buf[n++] = src.seg; n += x64_pack_rex_mem_operand(buf + n, size == 8u, dst, src); - buf[n++] = X64_OPC_MOV_R_RM; + buf[n++] = opc; n += x64_pack_mem_operand(buf + n, dst, src); mc->emit_bytes(mc, buf, n); } -static void emit_mov_store_operand(MCEmitter* mc, u32 size, u32 src, +static void emit_mov_load_operand(MCEmitter* mc, u32 size, u32 dst, + X64AsmOperand src) { + emit_reg_mem_operand(mc, size, X64_OPC_MOV_R_RM, dst, src); +} + +/* reg → mem store with an explicit reg-to-r/m opcode. Used by MOV + * (0x89/0x88) and the ALU /r stores (ADD 0x01, OR 0x09, AND 0x21, + * SUB 0x29, XOR 0x31, CMP 0x39). The register operand occupies the + * ModR/M reg field; the memory operand the r/m field. */ +static void emit_reg_store_operand(MCEmitter* mc, u32 size, u8 opc, u32 src, X64AsmOperand dst, int force_rex) { u8 buf[16]; u32 n = 0; @@ -238,11 +341,46 @@ static void emit_mov_store_operand(MCEmitter* mc, u32 size, u32 src, dst.no_base ? 0u : dst.base); else n += x64_pack_rex_mem_operand(buf + n, size == 8u, src, dst); - buf[n++] = size == 1u ? X64_OPC_MOV_RM_R8 : X64_OPC_MOV_RM_R; + buf[n++] = opc; n += x64_pack_mem_operand(buf + n, src, dst); mc->emit_bytes(mc, buf, n); } +static void emit_mov_store_operand(MCEmitter* mc, u32 size, u32 src, + X64AsmOperand dst, int force_rex) { + emit_reg_store_operand(mc, size, + size == 1u ? X64_OPC_MOV_RM_R8 : X64_OPC_MOV_RM_R, src, + dst, force_rex); +} + +/* imm → mem store via a group-1 /digit opcode (group-1 ALU 0x80/0x81/0x83, + * or MOV C6/C7). `opc8`/`opc32` select the 8-bit-immediate vs + * 32-bit-immediate (sign-extended) opcode; pass equal values when the + * encoding has no imm8 short form (e.g. MOV). `imm8` forces the short + * form when the immediate fits. */ +static void emit_rm_imm_store_operand(AsmDriver* d, MCEmitter* mc, u32 size, + u8 opc8, u8 opc32, u32 sub, + X64AsmOperand dst, i64 imm, + int allow_i8) { + u8 buf[16]; + u32 n = 0; + int use_i8 = allow_i8 && imm_fits_i8(imm); + if (!use_i8 && !imm_fits_i32(imm) && size != 1u) + asm_driver_panic(d, "x64 asm: immediate out of range"); + if (size == 2u) buf[n++] = X64_OPSIZE_PFX; + if (dst.seg) buf[n++] = dst.seg; + n += x64_pack_rex_mem_operand(buf + n, size == 8u, 0, dst); + buf[n++] = use_i8 ? opc8 : opc32; + n += x64_pack_mem_operand(buf + n, sub, dst); + if (size == 1u) + buf[n++] = (u8)imm; + else if (use_i8) + buf[n++] = (u8)(i8)imm; + else + n += x64_put_u32le(buf + n, (u32)(i32)imm); + mc->emit_bytes(mc, buf, n); +} + static void expect_comma(AsmDriver* d) { if (!asm_driver_eat_comma(d)) asm_driver_panic(d, "x64 asm: expected ','"); } @@ -651,9 +789,16 @@ static void parse_alu_rr(X64ParseCtx* p) { /* Immediate source → not an ALU_RR encoding. Redirect to the * ALU_RM_IMM row for this mnemonic. */ - if (src.kind == X64_ASM_OP_IMM && dst.kind == X64_ASM_OP_REG) { + if (src.kind == X64_ASM_OP_IMM && + (dst.kind == X64_ASM_OP_REG || dst.kind == X64_ASM_OP_MEM)) { const X64InsnDesc* imm_row = find_alu_imm_row(p->desc->mnemonic); if (!imm_row) asm_driver_panic(p->d, "x64 asm: no alu-imm row"); + if (dst.kind == X64_ASM_OP_MEM) { + emit_rm_imm_store_operand(p->d, p->mc, p->width, X64_OPC_ALU_IMM8, + X64_OPC_ALU_IMM32, imm_row->modrm_reg, dst, + src.imm, 1); + return; + } if (imm_fits_i8(src.imm)) emit_alu_imm8(p->mc, width_to_w(p->width), imm_row->modrm_reg, dst.reg, (i8)src.imm); @@ -711,6 +856,15 @@ static void parse_alu_rr(X64ParseCtx* p) { emit_mov_load_operand(p->mc, p->width, dst.reg, src); return; } + /* ALU reg → mem store (add/or/and/sub/xor/cmp %reg, mem): the reg-to-r/m + * /r opcode (opc[0]) with a memory ModR/M. The byte form clears the + * opcode's W bit (e.g. ADD r/m,r 0x01 → r/m8,r8 0x00). */ + if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_MEM) { + u8 op = p->width == 1u ? (u8)(p->desc->opc[0] & ~1u) : p->desc->opc[0]; + emit_reg_store_operand(p->mc, p->width, op, src.reg, dst, + p->width == 1u && byte_reg_needs_rex(&src)); + return; + } asm_driver_panic(p->d, "x64 asm: unsupported alu_rr form"); } @@ -720,8 +874,19 @@ static void parse_mov_ri(X64ParseCtx* p) { src = parse_operand(p->d); expect_comma(p->d); dst = parse_operand(p->d); - if (src.kind != X64_ASM_OP_IMM || dst.kind != X64_ASM_OP_REG) + if (src.kind != X64_ASM_OP_IMM || + (dst.kind != X64_ASM_OP_REG && dst.kind != X64_ASM_OP_MEM)) asm_driver_panic(p->d, "x64 asm: mov-imm form"); + /* MOV $imm → mem: C6 /0 (byte) or C7 /0 (32/64 sign-extended imm32). */ + if (dst.kind == X64_ASM_OP_MEM) { + if (p->width != 8u && !imm_fits_i32(src.imm)) + asm_driver_panic(p->d, "x64 asm: mov immediate out of range"); + emit_rm_imm_store_operand( + p->d, p->mc, p->width, X64_OPC_MOV_RM_IMM8, + p->width == 1u ? X64_OPC_MOV_RM_IMM8 : X64_OPC_MOV_RM_IMM32, + X64_MOV_RM_IMM_SUB, dst, src.imm, 0); + return; + } if (p->width != 4u && p->width != 8u) asm_driver_panic(p->d, "x64 asm: mov imm only supports l/q forms"); x64_emit_load_imm(p->mc, p->width == 8u ? 1 : 0, dst.reg, src.imm); @@ -738,15 +903,11 @@ static void parse_mov_rm_load(X64ParseCtx* p) { if (p->desc->opc[0] == 0x8Du) { if (src.kind != X64_ASM_OP_MEM || dst.kind != X64_ASM_OP_REG) asm_driver_panic(p->d, "x64 asm: lea form"); - emit_lea(p->mc, dst.reg, src.base, src.disp); + emit_reg_mem_operand(p->mc, p->width, X64_OPC_LEA, dst.reg, src); return; } if (src.kind == X64_ASM_OP_MEM && dst.kind == X64_ASM_OP_REG) { - if (p->width == 2u) { - emit_mov_load_operand(p->mc, p->width, dst.reg, src); - } else { - emit_mov_load_operand(p->mc, p->width, dst.reg, src); - } + emit_mov_load_operand(p->mc, p->width, dst.reg, src); return; } if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_REG) { diff --git a/src/arch/x64/isa.h b/src/arch/x64/isa.h @@ -208,6 +208,11 @@ u32 x64_decode_prefixes(const u8* bytes, u32 len, X64DecodeCtx* ctx); /* MOV r, imm — B8+rd. */ #define X64_OPC_MOV_RI 0xB8u +/* MOV r/m, imm — C6 /0 (byte) and C7 /0 (32/64 sign-extended imm32). */ +#define X64_OPC_MOV_RM_IMM8 0xC6u +#define X64_OPC_MOV_RM_IMM32 0xC7u +#define X64_MOV_RM_IMM_SUB 0u + /* IMUL r, r/m (two-byte) and IMUL r, r/m, imm. */ #define X64_OPC_IMUL_2B 0xAFu /* preceded by 0x0F */ #define X64_OPC_IMUL_IMM8 0x6Bu @@ -390,6 +395,17 @@ static inline u8 x64_sib(u32 scale, u32 index, u32 base) { return (u8)(((scale & 3u) << 6) | ((index & 7u) << 3) | (base & 7u)); } +/* ModR/M r/m encodings with special meaning: + * rm=100 → SIB byte follows. + * rm=101 with mod=00 → RIP-relative (disp32) or, in SIB.base, disp32-only. */ +#define X64_MODRM_RM_SIB 4u +#define X64_MODRM_RM_RIP_DISP32 5u + +/* SIB.index=100 means "no index". */ +#define X64_SIB_NO_INDEX 4u +/* SIB.base=101 with mod=00 means "no base" (disp32 only). */ +#define X64_SIB_NO_BASE 5u + /* Pick ModR/M.mod from a (base,disp) memory operand: * 0 → [base] (only if disp==0 and (base&7)!=5) * 1 → [base + disp8] @@ -413,6 +429,13 @@ static inline u32 x64_put_u64le(u8* out, u64 v) { return 8u; } +/* Pack a bare RIP-relative memory operand `[rip + disp32]` (no symbol). + * ModR/M mod=00, rm=101, followed by disp32; no SIB. */ +static inline u32 x64_pack_mem_rip(u8* out, u32 reg, i32 disp) { + out[0] = x64_modrm(0u, reg, X64_MODRM_RM_RIP_DISP32); + return 1u + x64_put_u32le(out + 1, (u32)disp); +} + /* Pack a memory operand (ModR/M + optional SIB + optional disp) for the * `reg` operand and `[base + disp]` r/m operand. Returns bytes written. */ static inline u32 x64_pack_mem(u8* out, u32 reg, u32 base, i32 disp) { diff --git a/test/asm/encode/x64_memop_alu_store.expected.hex b/test/asm/encode/x64_memop_alu_store.expected.hex @@ -0,0 +1 @@ +4801431049290848315608488343080549832c240ac3 diff --git a/test/asm/encode/x64_memop_alu_store.s b/test/asm/encode/x64_memop_alu_store.s @@ -0,0 +1,6 @@ +addq %rax,16(%rbx) +subq %rcx,(%r8) +xorq %rdx,8(%rsi) +addq $5,8(%rbx) +subq $10,(%r12) +ret diff --git a/test/asm/encode/x64_memop_alu_store.targets b/test/asm/encode/x64_memop_alu_store.targets @@ -0,0 +1 @@ +x64 diff --git a/test/asm/encode/x64_memop_mov_store.expected.hex b/test/asm/encode/x64_memop_mov_store.expected.hex @@ -0,0 +1 @@ +c7000700000048c743082a000000488904514289748f10c3 diff --git a/test/asm/encode/x64_memop_mov_store.s b/test/asm/encode/x64_memop_mov_store.s @@ -0,0 +1,5 @@ +movl $7,(%rax) +movq $42,8(%rbx) +movq %rax,(%rcx,%rdx,2) +movl %esi,16(%rdi,%r9,4) +ret diff --git a/test/asm/encode/x64_memop_mov_store.targets b/test/asm/encode/x64_memop_mov_store.targets @@ -0,0 +1 @@ +x64 diff --git a/test/asm/encode/x64_memop_rip.expected.hex b/test/asm/encode/x64_memop_rip.expected.hex @@ -0,0 +1 @@ +488d0500000000488b0d10000000c3 diff --git a/test/asm/encode/x64_memop_rip.s b/test/asm/encode/x64_memop_rip.s @@ -0,0 +1,3 @@ +leaq (%rip),%rax +movq 16(%rip),%rcx +ret diff --git a/test/asm/encode/x64_memop_rip.targets b/test/asm/encode/x64_memop_rip.targets @@ -0,0 +1 @@ +x64 diff --git a/test/asm/encode/x64_memop_sib_load.expected.hex b/test/asm/encode/x64_memop_sib_load.expected.hex @@ -0,0 +1 @@ +488b1488488b44fb084a8b0c48c3 diff --git a/test/asm/encode/x64_memop_sib_load.s b/test/asm/encode/x64_memop_sib_load.s @@ -0,0 +1,4 @@ +movq (%rax,%rcx,4),%rdx +movq 8(%rbx,%rdi,8),%rax +movq (%rax,%r9,2),%rcx +ret diff --git a/test/asm/encode/x64_memop_sib_load.targets b/test/asm/encode/x64_memop_sib_load.targets @@ -0,0 +1 @@ +x64