commit cc7a68d9dd2cd09a8fbf02e115f49030fc99d5e3
parent 34b5294b427e90ed1a00dda03759deab3f203ddf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 13:56:34 -0700
arm32/asm: assemble general add/sub rd, rn, {#imm | rm}
The standalone/inline-asm assembler had no row for the 3-operand non-flag
add/sub: `add` resolved to the 2-operand hi-register form (plus the sp-adjust
rows) and `sub` to the sp-adjust row, so a third operand was silently dropped or
misread. `add r4, r4, #1` assembled as `add r4, r4` (= r4*2) and `sub r4, r4,
#1` as `sub sp, sp, #0` — corrupting any inline asm that used them (e.g. the
`+r` inout `add %0, %0, #1`).
Add a shared arm_asm_addsub_third encoder, reached from both the HIREG_16 (add)
and ADJSP_16 (sub) handlers once rd/rn are parsed. It picks the densest legal
Thumb-2 form: a register third operand → 32-bit add.w/sub.w; an immediate →
16-bit sp-adjust / add-from-sp, else 32-bit modified-immediate add.w/sub.w, else
12-bit addw/subw; a negative immediate flips the operation. The 2-operand hi-
register `add rdn, rm` and the `sub rd, #imm` shorthand are preserved.
Encodings byte-match llvm-mc (thumbv7m) across the immediate, 3-register, sp,
modimm, and imm12 forms.
Diffstat:
| M | src/arch/arm32/asm.c | | | 76 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------ |
1 file changed, 58 insertions(+), 18 deletions(-)
diff --git a/src/arch/arm32/asm.c b/src/arch/arm32/asm.c
@@ -348,6 +348,42 @@ static const Arm32InsnDesc* resolve_mnemonic(AsmDriver* d, Slice mn,
asm_driver_panic(d, "arm32 asm: unsupported instruction");
}
+/* Encode the third operand of a general `add`/`sub rd, rn, <#imm | rm>` once rd
+ * and rn are parsed and the separating comma consumed. There is no 16-bit
+ * non-flag add/sub-immediate or 3-register form, so a register third operand
+ * takes the 32-bit add.w/sub.w, and an immediate takes the densest of: the
+ * 16-bit sp-adjust / add-from-sp forms, the 32-bit modified-immediate
+ * (add.w/sub.w), then the 12-bit addw/subw. A negative immediate flips the
+ * operation. The 16-bit hi-register `add rdn, rm` (2 operands) is handled at the
+ * call site, not here. */
+static void arm_asm_addsub_third(AsmDriver* d, int is_sub, u32 rd, u32 rn) {
+ i64 simm;
+ u32 mag, out12;
+ if (peek_is_reg(d)) {
+ u32 rm = parse_reg(d);
+ emit_t32(d, is_sub ? arm_sub_reg(rd, rn, rm) : arm_add_reg(rd, rn, rm));
+ return;
+ }
+ simm = parse_imm(d);
+ if (simm < 0) {
+ is_sub = !is_sub;
+ simm = -simm;
+ }
+ mag = (u32)simm;
+ if (rd == 13u && rn == 13u && (mag & 3u) == 0u && (mag >> 2) <= 0x7fu) {
+ emit_t16(d, (u16)((is_sub ? 0xb080u : 0xb000u) | ((mag >> 2) & 0x7fu)));
+ } else if (!is_sub && rn == 13u && rd <= 7u && (mag & 3u) == 0u &&
+ (mag >> 2) <= 0xffu) {
+ emit_t16(d, (u16)(0xa800u | ((rd & 7u) << 8) | ((mag >> 2) & 0xffu)));
+ } else if (thumb_expand_imm_encode(mag, &out12)) {
+ emit_t32(d, arm_dp_imm(is_sub ? 13u : 8u, 0u, rd, rn, out12));
+ } else if (mag <= 0xfffu) {
+ emit_t32(d, is_sub ? arm_sub_imm12(rd, rn, mag) : arm_add_imm12(rd, rn, mag));
+ } else {
+ asm_driver_panic(d, "arm32 asm: add/sub immediate not encodable");
+ }
+}
+
/* Encode + emit one instruction for the matched descriptor. */
static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) {
Slice mn = desc->mnemonic;
@@ -881,18 +917,14 @@ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) {
return;
}
rm = parse_reg(d);
- /* `add rd, sp, #imm` / `add sp, sp, #imm` (a third immediate operand
- * through SP) use the SP-relative encodings: ADD rd,sp,#imm8*4 (T1,
- * 0xA800, rd<=r7) or ADD sp,sp,#imm7*4 (T2, 0xB000). The hi-register form
- * below has no immediate and would drop it. */
- if (slice_eq_cstr(mn, "add") && rm == 13u && asm_driver_eat_comma(d)) {
- u32 imm = (u32)parse_imm(d);
- if (rdn == 13u)
- emit_t16(d, (u16)(0xb000u | ((imm / 4u) & 0x7fu)));
- else if (rdn <= 7u)
- emit_t16(d, (u16)(0xa800u | ((rdn & 7u) << 8) | ((imm / 4u) & 0xffu)));
- else
- asm_driver_panic(d, "arm32 asm: add sp immediate needs add.w");
+ /* A third operand means the general `add rd, rn, <#imm | rm>` (rd=rdn,
+ * rn=rm): there is no 16-bit non-flag add-immediate or 3-register form, so
+ * route it through the shared encoder (which also covers the sp-relative
+ * forms). Without a third operand this is the 2-operand hi-register
+ * `add rdn, rm` below; the old SP-only special case dropped every other
+ * immediate. */
+ if (slice_eq_cstr(mn, "add") && asm_driver_eat_comma(d)) {
+ arm_asm_addsub_third(d, 0, rdn, rm);
return;
}
emit_t16(d, (u16)((desc->match & 0xff00u) | (((rdn >> 3) & 1u) << 7) |
@@ -958,13 +990,21 @@ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) {
return;
}
case ARM_FMT_ADJSP_16: {
- u32 imm7;
- (void)parse_reg(d); /* sp */
- expect_comma(d);
- (void)parse_reg(d); /* sp */
+ /* Plain `sub` (and the unreachable plain-`add` row) land here. Parse the
+ * real registers and route through the shared add/sub encoder, which picks
+ * the 16-bit sp-adjust when rd==rn==sp. Accept the 2-operand shorthand
+ * `sub rd, #imm` (== `sub rd, rd, #imm`); the old code assumed sp/sp and
+ * silently mis-encoded `sub rN, rN, #imm` as `sub sp, sp, #...`. */
+ int is_sub = slice_eq_cstr(mn, "sub");
+ u32 rd = parse_reg(d), rn;
expect_comma(d);
- imm7 = ((u32)parse_imm(d) / 4u) & 0x7fu;
- emit_t16(d, (u16)((desc->match & 0xff80u) | imm7));
+ if (peek_is_reg(d)) {
+ rn = parse_reg(d);
+ expect_comma(d);
+ } else {
+ rn = rd;
+ }
+ arm_asm_addsub_third(d, is_sub, rd, rn);
return;
}
case ARM_FMT_PUSHPOP_16: {