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:
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, ®, 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