commit 51a0605d7da6f4b5bdb045921c315598ab57ec06
parent a1dca02e7fb38956632f2c76acaff5f2b0fa89eb
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 10:48:39 -0700
arm32 disasm: render shifted-MOV (lsl/lsr/asr/ror/rrx) + MVN shift suffix
Diffstat:
4 files changed, 106 insertions(+), 2 deletions(-)
diff --git a/src/arch/arm32/disasm.c b/src/arch/arm32/disasm.c
@@ -327,6 +327,18 @@ static void arm32_render_mnemonic(Arm32InsnFormatter* f, const Arm32InsnDesc* d,
strbuf_putc(&f->mnem, (cc[k] == cc[0]) ? 't' : 'e');
return;
}
+ /* Shifted-MOV (ORR rn=1111) aliases to the shift mnemonic (lsl/lsr/asr/ror.w
+ * or rrx); the MVN (ORN) form keeps `mvn.w` and renders the shift as an
+ * operand suffix instead. Substitute the alias as the base mnemonic for the
+ * MOV form, then fall through to the shared IT-cond / `.w` suffix logic
+ * (`rrx` carries no `.w`, so that logic leaves it untouched). */
+ if ((Arm32Format)d->fmt == ARM_FMT_MOV_REG) {
+ u32 op4 = (word >> 21) & 0xfu; /* hw1[8:5]: 2 = MOV (ORR), 3 = MVN (ORN). */
+ if (op4 == 2u) {
+ const char* alias = arm32_mov_shift_alias(word, NULL, NULL);
+ if (alias) mn = slice_from_cstr(alias);
+ }
+ }
/* Base mnemonic, with optional IT condition suffix inserted before ".w". */
if (in_it) {
const char* ccn = arm32_cond_name(it_cond);
diff --git a/src/arch/arm32/isa.c b/src/arch/arm32/isa.c
@@ -467,6 +467,10 @@ static u32 f_modimm12(u32 w) {
return (i << 11) | (imm3 << 8) | imm8;
}
+/* Shift-type names for the DP-shift / MOV-shift / MVN-shift suffix
+ * (index = hw2[5:4]). */
+static const char* const ARM_SHIFT_NAMES[4] = {"lsl", "lsr", "asr", "ror"};
+
static void print_dp_reg(StrBuf* sb, u32 w) {
p_reg(sb, f_rd(w));
p_sep(sb);
@@ -478,10 +482,9 @@ static void print_dp_reg(StrBuf* sb, u32 w) {
u32 imm3 = (f_hw2(w) >> 12) & 7u, imm2 = (f_hw2(w) >> 6) & 3u;
u32 type = (f_hw2(w) >> 4) & 3u;
u32 sh = (imm3 << 2) | imm2;
- static const char* const SH[4] = {"lsl", "lsr", "asr", "ror"};
if (sh != 0u || type != 0u) {
p_sep(sb);
- strbuf_puts(sb, SH[type]);
+ strbuf_puts(sb, ARM_SHIFT_NAMES[type]);
strbuf_putc(sb, ' ');
p_imm(sb, (i64)(sh == 0u && type != 0u ? 32u : sh));
}
@@ -502,10 +505,55 @@ static void print_mov_imm(StrBuf* sb, u32 w) {
p_imm(sb, (i64)(i32)thumb_expand_imm_decode(f_modimm12(w)));
}
+const char* arm32_mov_shift_alias(u32 word, u32* type_out, u32* amount_out) {
+ /* hw2[14:12] = imm3, hw2[7:6] = imm2, hw2[5:4] = type. */
+ u32 imm3 = (f_hw2(word) >> 12) & 7u, imm2 = (f_hw2(word) >> 6) & 3u;
+ u32 type = (f_hw2(word) >> 4) & 3u;
+ u32 amount = (imm3 << 2) | imm2;
+ if (type_out) *type_out = type;
+ if (amount_out) *amount_out = amount;
+ if (type == 0u && amount == 0u) return NULL; /* plain mov.w rd, rm. */
+ switch (type) {
+ case 0u: return "lsl.w"; /* amount != 0 here. */
+ case 1u:
+ if (amount == 0u) amount = 32u; /* LSR #0 means #32. */
+ if (amount_out) *amount_out = amount;
+ return "lsr.w";
+ case 2u:
+ if (amount == 0u) amount = 32u; /* ASR #0 means #32. */
+ if (amount_out) *amount_out = amount;
+ return "asr.w";
+ default: /* type == 3 (ROR). */
+ return (amount == 0u) ? "rrx" : "ror.w";
+ }
+}
+
+/* MOV (ORR) form. op4 == 3 is MVN (ORN), which keeps its own mnemonic and
+ * renders the shift as a `, <sh> #n` suffix; op4 == 2 is MOV, which is aliased
+ * to the shift mnemonic (so here it prints just the operands). */
static void print_mov_reg(StrBuf* sb, u32 w) {
+ u32 op4 = (f_hw1(w) >> 5) & 0xfu;
+ u32 type = 0u, amount = 0u;
+ const char* alias = arm32_mov_shift_alias(w, &type, &amount);
p_reg(sb, f_rd(w));
p_sep(sb);
p_reg(sb, f_rm(w));
+ if (op4 == 3u) {
+ /* MVN: append the shift suffix when present (mnemonic stays mvn.w). */
+ if (alias) {
+ p_sep(sb);
+ strbuf_puts(sb, ARM_SHIFT_NAMES[type]);
+ strbuf_putc(sb, ' ');
+ p_imm(sb, (i64)amount);
+ }
+ return;
+ }
+ /* MOV (op4 == 2): the mnemonic carries the shift; append the amount unless
+ * the alias is a plain mov.w (NULL) or rrx (ROR #0 -> two operands only). */
+ if (alias && !(type == 3u && amount == 0u)) {
+ p_sep(sb);
+ p_imm(sb, (i64)amount);
+ }
}
static void print_cmp_reg(StrBuf* sb, u32 w) {
diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h
@@ -486,6 +486,16 @@ int arm32_cond_from_name(Slice s);
/* Condition field of a B<cond>.W (T3) word (disasm appends it to the mnemonic). */
u32 arm32_branch_t3_cond(u32 w);
+/* Shifted-MOV (ORR/ORN rn=1111, ARM_FMT_MOV_REG) shift decode. Returns the
+ * shift mnemonic the MOV form aliases to ("lsl.w"/"lsr.w"/"asr.w"/"ror.w", or
+ * "rrx" for ROR #0), or NULL when there is no shift (a plain `mov.w`). Decodes
+ * imm3:imm2 (hw2[14:12]/[7:6]) and type (hw2[5:4]); applies the LSR/ASR amount
+ * 0 -> 32 and ROR amount 0 -> rrx rules. *type_out gets the raw 2-bit shift
+ * type, *amount_out the rendered amount (undefined/unused for rrx). Both out
+ * pointers may be NULL. The MVN form is NOT aliased; callers reuse type_out/
+ * amount_out to append a `, <sh> #n` suffix. */
+const char* arm32_mov_shift_alias(u32 word, u32* type_out, u32* amount_out);
+
/* Render operand text for `word` into `sb` using `desc->fmt`. `vaddr` is the
* instruction address (for PC-relative branch targets); 0 if unknown. */
void arm32_print_operands(StrBuf* sb, const Arm32InsnDesc* desc, u32 word,
diff --git a/test/arch/arm32_decode_test.c b/test/arch/arm32_decode_test.c
@@ -123,6 +123,39 @@ static void decode_dp(KitCompiler* pub) {
expect_text(pub, b, 4, 0, 4, "cmp.w", "r2, r3", "cmp.w reg");
}
+/* The Thumb-2 shifted-MOV family (0xea4f....) is the ORR-based MOV-with-shift;
+ * it disassembles (per llvm-objdump --triple=thumbv7em) to the shift mnemonic
+ * (lsl/lsr/asr/ror, or rrx for ROR #0) — except a plain LSL #0, which stays
+ * `mov.w`. The MVN-with-shift form (0xea6f....) keeps its `mvn.w` mnemonic and
+ * appends a `, <sh> #n` suffix. No direct encoder, so raw words via put_t32. */
+static void decode_shifts(KitCompiler* pub) {
+ unsigned char b[4];
+ /* LSL #0 = plain mov.w (regression guard). */
+ put_t32(b, 0, 0xea4f0001u);
+ expect_text(pub, b, 4, 0, 4, "mov.w", "r0, r1", "mov-shift lsl#0 = mov.w");
+ /* LSL #3 -> lsl.w. */
+ put_t32(b, 0, 0xea4f00c1u);
+ expect_text(pub, b, 4, 0, 4, "lsl.w", "r0, r1, #3", "mov-shift lsl#3");
+ /* LSR #0 means #32. */
+ put_t32(b, 0, 0xea4f0011u);
+ expect_text(pub, b, 4, 0, 4, "lsr.w", "r0, r1, #32", "mov-shift lsr#0->32");
+ /* ASR #0 means #32. */
+ put_t32(b, 0, 0xea4f0021u);
+ expect_text(pub, b, 4, 0, 4, "asr.w", "r0, r1, #32", "mov-shift asr#0->32");
+ /* ROR #5. */
+ put_t32(b, 0, 0xea4f1071u);
+ expect_text(pub, b, 4, 0, 4, "ror.w", "r0, r1, #5", "mov-shift ror#5");
+ /* ROR #0 -> rrx (two operands, no amount, no .w). */
+ put_t32(b, 0, 0xea4f0031u);
+ expect_text(pub, b, 4, 0, 4, "rrx", "r0, r1", "mov-shift ror#0 = rrx");
+ /* MVN LSL #3 keeps mvn.w + appends the shift suffix. */
+ put_t32(b, 0, 0xea6f00c1u);
+ expect_text(pub, b, 4, 0, 4, "mvn.w", "r0, r1, lsl #3", "mvn-shift lsl#3");
+ /* MVN LSL #0 = plain mvn.w (regression guard). */
+ put_t32(b, 0, 0xea6f0001u);
+ expect_text(pub, b, 4, 0, 4, "mvn.w", "r0, r1", "mvn lsl#0 = mvn.w");
+}
+
static void decode_mul_div_ext(KitCompiler* pub) {
unsigned char b[4];
put_t32(b, 0, arm_mul(ARM_R0, ARM_R1, ARM_R2));
@@ -339,6 +372,7 @@ int main(void) {
c = new_compiler();
decode_movw_movt(c);
decode_dp(c);
+ decode_shifts(c);
decode_mul_div_ext(c);
decode_rev_clz_bitfield(c);
decode_ldst(c);