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}