Skip to main content

r2smt_patch/
arm_encoding.rs

1//! Byte-level rewrites for ARM conditional branches.
2//!
3//! Both `AArch64` and `AArch32` (ARM mode) are fixed 4-byte
4//! instruction encodings, so the v0 rewrite strategy mirrors x86
5//! `nop_jcc` / `replace_jcc_with_jmp`:
6//!
7//! - **Always-false branch** → replace the 4-byte conditional
8//!   instruction with a 4-byte architectural NOP.
9//! - **Always-true branch** → assemble an unconditional `b <target>`
10//!   over the same 4 bytes.
11//!
12//! Mnemonic coverage (planner-side, classified by `plan::classify_mnemonic`):
13//!
14//! - `AArch64`: `b.<cond>` and the compare-and-branch family
15//!   `cbz` / `cbnz` / `tbz` / `tbnz`.
16//! - `AArch32` (ARM mode): `b<cond>` for the standard condition
17//!   suffixes (`eq`/`ne`/`cs`/`hs`/`cc`/`lo`/`mi`/`pl`/`vs`/`vc`/
18//!   `hi`/`ls`/`ge`/`lt`/`gt`/`le`). Unconditional `b`, link forms
19//!   `bl`/`blx`, and indirect `bx` are excluded.
20//!
21//! Encoding references:
22//!
23//! - `AArch64` NOP: `D503201F` (ARM ARM Vol. C §C6.2.182). Little-
24//!   endian byte layout: `1F 20 03 D5`.
25//! - `AArch32` NOP (ARMv6T2+): `E320F000` (ARM ARM Vol. C §A8.8.119).
26//!   Little-endian byte layout: `00 F0 20 E3`.
27//!
28//! Both NOP forms are architectural hint instructions, not the
29//! historical `mov rN, rN` idiom — they are explicitly recognised
30//! as NOPs by the CPU's instruction decoder and have zero side
31//! effects on flags or registers.
32//!
33//! Thumb-mode `AArch32` (2-byte / 4-byte mixed encoding) is **out of
34//! scope** for this rewrite; callers must reject Thumb mnemonics
35//! upstream. The slicer / planner currently classifies `b<cond>`
36//! purely on the textual mnemonic and does not yet attempt to detect
37//! Thumb vs ARM mode, so any caller invoking the planner against a
38//! Thumb function should expect the resulting plan to be byte-
39//! incorrect — fixing that is a follow-up gated on Thumb mode
40//! detection in the slicer / r2pipe adapter.
41
42use r2smt_common::{Arch, Error, Result};
43
44/// Length, in bytes, of any `AArch64` or `AArch32` (ARM-mode)
45/// instruction.
46pub const ARM_INSTRUCTION_BYTES: usize = 4;
47
48/// Canonical NOP encoding for `AArch64` (`D503201F`, little-endian).
49const AARCH64_NOP_LE: [u8; 4] = [0x1F, 0x20, 0x03, 0xD5];
50
51/// Canonical NOP encoding for `AArch32` (`E320F000`, little-endian).
52const AARCH32_NOP_LE: [u8; 4] = [0x00, 0xF0, 0x20, 0xE3];
53
54/// Length, in bytes, of a Thumb 16-bit instruction half-word.
55pub const THUMB_HALFWORD_BYTES: usize = 2;
56
57/// Thumb NOP encoding (`BF00`, little-endian). `ARMv6T2` introduced this
58/// as a proper hint instruction; older Thumb encodings fell back to
59/// `MOV r8, r8` which still functions as a NOP but is harder to
60/// recognise.
61pub const THUMB_NOP_LE: [u8; 2] = [0x00, 0xBF];
62
63/// Return the architectural NOP encoding for `arch`.
64///
65/// # Errors
66///
67/// Returns [`Error::Parse`] if `arch` is not an ARM ISA. Callers
68/// should not invoke this for x86 — the x86 NOP is a single
69/// `0x90` byte and lives in [`crate::x86_encoding`].
70pub fn arm_nop_bytes(arch: Arch) -> Result<[u8; 4]> {
71    match arch {
72        Arch::Aarch64 => Ok(AARCH64_NOP_LE),
73        Arch::Arm => Ok(AARCH32_NOP_LE),
74        other => Err(Error::parse(
75            "arm_encoding.nop",
76            format!("{other:?} is not an ARM ISA"),
77        )),
78    }
79}
80
81/// Fill `len` bytes with the architectural NOP encoding for `arch`.
82///
83/// `len` must be a multiple of [`ARM_INSTRUCTION_BYTES`]; otherwise
84/// the resulting tail bytes would be a partial instruction and the
85/// CPU would fault on execution.
86///
87/// # Errors
88///
89/// Returns [`Error::Parse`] if `arch` is not an ARM ISA or `len` is
90/// not a multiple of 4.
91pub fn arm_nop_buffer(arch: Arch, len: usize) -> Result<Vec<u8>> {
92    if len % ARM_INSTRUCTION_BYTES != 0 {
93        return Err(Error::parse(
94            "arm_encoding.nop_buffer",
95            format!("{len} is not a multiple of {ARM_INSTRUCTION_BYTES}"),
96        ));
97    }
98    let nop = arm_nop_bytes(arch)?;
99    let count = len / ARM_INSTRUCTION_BYTES;
100    let mut out = Vec::with_capacity(len);
101    for _ in 0..count {
102        out.extend_from_slice(&nop);
103    }
104    Ok(out)
105}
106
107/// Fill `len` bytes with the Thumb 16-bit NOP hint.
108///
109/// `len` must be a multiple of [`THUMB_HALFWORD_BYTES`] so the
110/// resulting buffer ends on an instruction boundary. Used by the
111/// patcher to NOP-out Thumb conditional branches whose original
112/// footprint is 2 or 4 bytes (a single Thumb half-word or a Thumb-2
113/// 32-bit branch).
114///
115/// # Errors
116///
117/// Returns [`Error::Parse`] if `len` is not a multiple of 2.
118pub fn thumb_nop_buffer(len: usize) -> Result<Vec<u8>> {
119    if len % THUMB_HALFWORD_BYTES != 0 {
120        return Err(Error::parse(
121            "arm_encoding.thumb_nop_buffer",
122            format!("{len} is not a multiple of {THUMB_HALFWORD_BYTES}"),
123        ));
124    }
125    let count = len / THUMB_HALFWORD_BYTES;
126    let mut out = Vec::with_capacity(len);
127    for _ in 0..count {
128        out.extend_from_slice(&THUMB_NOP_LE);
129    }
130    Ok(out)
131}
132
133#[cfg(test)]
134mod tests {
135    #![allow(clippy::unwrap_used)]
136
137    use super::*;
138
139    #[test]
140    fn aarch64_nop_is_canonical_hint_encoding() {
141        let nop = arm_nop_bytes(Arch::Aarch64).unwrap();
142        // D503201F in little-endian byte order.
143        assert_eq!(nop, [0x1F, 0x20, 0x03, 0xD5]);
144        // Decoded back to a u32 big-endian: 0xD503201F.
145        let word = u32::from_le_bytes(nop);
146        assert_eq!(word, 0xD503_201F);
147    }
148
149    #[test]
150    fn aarch32_nop_is_canonical_hint_encoding() {
151        let nop = arm_nop_bytes(Arch::Arm).unwrap();
152        // E320F000 in little-endian byte order.
153        assert_eq!(nop, [0x00, 0xF0, 0x20, 0xE3]);
154        let word = u32::from_le_bytes(nop);
155        assert_eq!(word, 0xE320_F000);
156    }
157
158    #[test]
159    fn arm_nop_bytes_rejects_non_arm_arch() {
160        assert!(arm_nop_bytes(Arch::X86_64).is_err());
161        assert!(arm_nop_bytes(Arch::X86).is_err());
162    }
163
164    #[test]
165    fn arm_nop_buffer_tiles_for_aarch64() {
166        let buf = arm_nop_buffer(Arch::Aarch64, 8).unwrap();
167        assert_eq!(buf.len(), 8);
168        assert_eq!(&buf[..4], &AARCH64_NOP_LE);
169        assert_eq!(&buf[4..], &AARCH64_NOP_LE);
170    }
171
172    #[test]
173    fn arm_nop_buffer_handles_single_instruction() {
174        let buf = arm_nop_buffer(Arch::Arm, 4).unwrap();
175        assert_eq!(buf, AARCH32_NOP_LE);
176    }
177
178    #[test]
179    fn arm_nop_buffer_rejects_misaligned_length() {
180        // 6 is not a multiple of 4 — partial instruction would crash
181        // the CPU at execution time.
182        assert!(arm_nop_buffer(Arch::Aarch64, 6).is_err());
183        assert!(arm_nop_buffer(Arch::Arm, 5).is_err());
184    }
185
186    #[test]
187    fn arm_nop_buffer_zero_length_yields_empty() {
188        let buf = arm_nop_buffer(Arch::Aarch64, 0).unwrap();
189        assert!(buf.is_empty());
190    }
191
192    #[test]
193    fn thumb_nop_2byte_matches_bf00() {
194        let buf = thumb_nop_buffer(2).unwrap();
195        assert_eq!(buf, vec![0x00, 0xBF]);
196    }
197
198    #[test]
199    fn thumb_nop_4byte_tiles_two_halfwords() {
200        let buf = thumb_nop_buffer(4).unwrap();
201        assert_eq!(buf, vec![0x00, 0xBF, 0x00, 0xBF]);
202    }
203
204    #[test]
205    fn thumb_nop_buffer_rejects_odd_length() {
206        assert!(thumb_nop_buffer(3).is_err());
207        assert!(thumb_nop_buffer(5).is_err());
208    }
209
210    #[test]
211    fn thumb_nop_buffer_zero_length_yields_empty() {
212        assert!(thumb_nop_buffer(0).unwrap().is_empty());
213    }
214}