Skip to main content

ud_arch_x86/
lib.rs

1//! x86 architecture backend.
2//!
3//! Phase 1 scope: decode an x86 byte sequence into structured instructions
4//! (via [`iced_x86`]), and provide two distinct emission paths:
5//!
6//! * [`emit_preserved`] — concatenate each instruction's original bytes
7//!   captured at decode time. This is byte-identical by construction
8//!   and is what the round-trip contract is built on.
9//! * [`reencode_via_iced`] — feed the structured [`Instruction`]s back
10//!   through `BlockEncoder`. This is *not* byte-identical for all real
11//!   inputs: iced canonicalizes redundant prefixes (e.g. drops a `66`
12//!   data16 override on a NOP that doesn't need it), so for compiler-
13//!   emitted alignment NOPs and `.plt` padding the bytes will differ.
14//!   Useful for "I edited an instruction" workflows in later phases,
15//!   not for round-trip.
16//!
17//! 16- and 32-bit modes are exposed through [`Bitness`] and the same
18//! API; the round-trip property is identical.
19
20#![allow(clippy::cast_possible_truncation)]
21
22use iced_x86::{
23    BlockEncoder, BlockEncoderOptions, Decoder, DecoderOptions, Formatter, InstructionBlock,
24    IntelFormatter,
25};
26pub use iced_x86::{
27    CodeSize, FlowControl, Instruction, InstructionInfoFactory, MemorySize, Mnemonic, OpAccess,
28    OpKind, Register, UsedRegister,
29};
30use ud_core::VAddr;
31use ud_ir::ArchInsn;
32
33mod assemble;
34mod call_site;
35mod codec;
36mod encode_text;
37mod expr;
38mod lift;
39mod prologue_codec;
40pub use assemble::{assemble_intel, AssembleError};
41pub use call_site::{
42    detect_post_call_spill, identify_call_sites, ArgValue, CallSite, PostCallSpill,
43};
44pub use codec::{register, X86Codec};
45pub use encode_text::{encode_cmp_or_test, encode_head_from_cond_text};
46pub use expr::{try_lift_value_block, ExprRenderCtx, LiftedValueBlock, ValueExpr};
47pub use lift::{lift_function, LiftError};
48pub use prologue_codec::{
49    decode_epilogue, decode_prologue, default_epilogue, default_prologue, encode_epilogue,
50    encode_prologue, epilogue_roundtrips, prologue_roundtrips, CodecBits, ProfileInputs,
51    StructuredEpilogue, StructuredPrologue,
52};
53
54/// If `insn` is a direct (relative) `call` whose target is statically
55/// known, return that target's virtual address. Indirect calls
56/// (`call rax`, `call [rip+…]`) and non-call instructions return
57/// `None`.
58///
59/// Lets downstream code annotate call sites with the destination
60/// function's name without depending on iced directly.
61#[must_use]
62pub fn direct_call_target(insn: &Instruction) -> Option<u64> {
63    match insn.flow_control() {
64        FlowControl::Call => Some(insn.near_branch_target()),
65        _ => None,
66    }
67}
68
69/// Heuristic: does this instruction terminate a function?
70/// Returns, unconditional branches (direct or indirect), and the
71/// rare hardware traps (`int`, `ud2`, etc.) all qualify.
72/// Conditional branches don't — they can still flow into later
73/// instructions in the same function.
74///
75/// Used by the PE / ELF function-discovery passes to bound linear
76/// decoding of a newly-seeded function.
77#[must_use]
78pub fn is_function_terminator(insn: &Instruction) -> bool {
79    matches!(
80        insn.flow_control(),
81        FlowControl::Return
82            | FlowControl::UnconditionalBranch
83            | FlowControl::IndirectBranch
84            | FlowControl::Interrupt
85            | FlowControl::Exception,
86    )
87}
88
89/// One recognised return-with-literal pattern at the tail of a
90/// function: how many trailing instructions matched, and the literal
91/// integer value the function returns.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct LiftedReturn {
94    pub insns_consumed: usize,
95    pub value: u64,
96}
97
98/// Try to recognize the trailing instructions of `insns` as a return
99/// with a known integer literal: the canonical SysV-x64 epilogue
100/// patterns gcc emits at `-O0`.
101///
102/// Recognised forms (working backwards from `ret`):
103///
104/// * `mov eax, IMM32; [pop rbp | leave;] ret`
105/// * `xor eax, eax;   [pop rbp | leave;] ret`
106/// * `mov eax, IMM32; ret`
107/// * `xor eax, eax;   ret`
108///
109/// Only matches when the trailing instruction is a `ret` (`0xc3`); the
110/// caller is expected to invoke this only on a function's last block.
111/// Returns `None` when no pattern fits.
112#[must_use]
113pub fn try_lift_return_pattern(insns: &[DecodedInsn]) -> Option<LiftedReturn> {
114    if insns.is_empty() {
115        return None;
116    }
117
118    let mut i = insns.len();
119
120    // Last instruction must be `ret` (0xc3). Iret / retf etc. are
121    // beyond v0 scope.
122    let ret = insns.get(i - 1)?;
123    if ret.original_bytes.as_slice() != [0xc3] {
124        return None;
125    }
126    i -= 1;
127
128    // Optional epilogue: pop rbp (0x5d) or leave (0xc9).
129    if i > 0 {
130        let prev = &insns[i - 1].original_bytes;
131        if prev.as_slice() == [0x5d] || prev.as_slice() == [0xc9] {
132            i -= 1;
133        }
134    }
135
136    if i == 0 {
137        return None;
138    }
139
140    // The instruction setting the return value.
141    let setter = &insns[i - 1].original_bytes;
142    let value = match setter.as_slice() {
143        // mov eax, imm32 — opcode 0xB8, 4 little-endian bytes follow.
144        [0xb8, b0, b1, b2, b3] => u64::from(u32::from_le_bytes([*b0, *b1, *b2, *b3])),
145        // xor eax, eax — clears eax to zero.
146        [0x31, 0xc0] => 0,
147        _ => return None,
148    };
149    i -= 1;
150
151    Some(LiftedReturn {
152        insns_consumed: insns.len() - i,
153        value,
154    })
155}
156
157/// One recognised SysV-x64 prologue at the entry of a function.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct LiftedPrologue {
160    /// Number of leading instructions matched.
161    pub insns_consumed: usize,
162    /// Symbolic kind name. Today: `"std"` (full standard prologue with
163    /// `endbr64` and a frame), `"std-no-cf"` (no `endbr64`),
164    /// `"std-noframe"` (just `endbr64`).
165    pub kind: &'static str,
166}
167
168/// Try to recognize the leading instructions of `insns` as a
169/// canonical SysV-x64 prologue at function entry.
170///
171/// Recognised forms (greediest first):
172///
173/// * `endbr64; push rbp; mov rbp, rsp; sub rsp, IMM` → `"std"`
174///   (full prologue with cf-protection and a stack frame)
175/// * `push rbp; mov rbp, rsp; sub rsp, IMM` → `"std-no-cf"`
176///   (older toolchain or `-fno-cf-protection`)
177/// * `endbr64; push rbp; mov rbp, rsp` → `"std"` (frame, no
178///   stack-allocated locals)
179/// * `push rbp; mov rbp, rsp` → `"std-no-cf"`
180/// * `endbr64` alone at the top of a leaf function → `"std-noframe"`
181///
182/// Bytes are matched literally — instruction-encoding choices are
183/// pinned, so the lifter only fires on the exact byte sequences gcc
184/// emits at `-O0`. Other compilers / optimization levels add new
185/// patterns to this DB.
186#[must_use]
187pub fn try_lift_prologue_pattern(insns: &[DecodedInsn]) -> Option<LiftedPrologue> {
188    let endbr64: &[u8] = &[0xf3, 0x0f, 0x1e, 0xfa];
189    let endbr32: &[u8] = &[0xf3, 0x0f, 0x1e, 0xfb];
190    // `mov ebp, esp` has two equivalent encodings: the
191    // destination-form `0x89 0xe5` (mov r/m32, r32) and the
192    // source-form `0x8b 0xec` (mov r32, r/m32). gcc / clang emit
193    // the former; MSVC/Watcom often emit the latter. The 64-bit
194    // forms add a REX.W prefix.
195    let mov_rbp_rsp_64_dst: &[u8] = &[0x48, 0x89, 0xe5];
196    let mov_rbp_rsp_64_src: &[u8] = &[0x48, 0x8b, 0xec];
197    let mov_ebp_esp_32_dst: &[u8] = &[0x89, 0xe5];
198    let mov_ebp_esp_32_src: &[u8] = &[0x8b, 0xec];
199    let is_mov_bp_sp = |b: &[u8]| {
200        b == mov_rbp_rsp_64_dst
201            || b == mov_rbp_rsp_64_src
202            || b == mov_ebp_esp_32_dst
203            || b == mov_ebp_esp_32_src
204    };
205
206    let bytes_at = |i: usize| insns.get(i).map(|d| d.original_bytes.as_slice());
207
208    // Step 1: optional indirect-branch landing pad (Intel CET).
209    let (has_endbr, mut start) = match bytes_at(0) {
210        Some(b) if b == endbr64 || b == endbr32 => (true, 1),
211        _ => (false, 0),
212    };
213
214    // Step 2: leading run of single-byte `push reg` (opcodes
215    // 0x50..=0x57 → push eax..edi/r8..r15-low). Windows i386 routinely
216    // saves several callee-saved registers (ebx, esi, edi) before any
217    // frame setup; gcc/msvc on x86_64 can do the same with rbx/r12-r15.
218    let push_start = start;
219    let mut pushes: Vec<u8> = Vec::new();
220    while let Some(b) = bytes_at(start) {
221        if b.len() == 1 && (0x50..=0x57).contains(&b[0]) {
222            pushes.push(b[0]);
223            start += 1;
224        } else {
225            break;
226        }
227    }
228
229    // Step 3: frame setup. When the final push was `push ebp/rbp`
230    // (opcode 0x55) AND it is immediately followed by `mov ebp, esp`,
231    // attribute that last push to the frame rather than to saves.
232    // Otherwise every push is a save.
233    let mut has_frame = false;
234    if matches!(pushes.last(), Some(&0x55)) {
235        if let Some(b) = bytes_at(start) {
236            if is_mov_bp_sp(b) {
237                has_frame = true;
238                start += 1;
239            }
240        }
241    }
242    let mut saves_count = pushes.len() - usize::from(has_frame);
243
244    // Step 4: optional stack-locals reservation. Only meaningful when a
245    // frame was set up (or no frame and no saves — the bare CRT-stub
246    // `_init`/`_fini` idiom); a `sub esp, IMM` mid-function isn't
247    // really a prologue.
248    let sub_matched = matches!(
249        bytes_at(start),
250        Some(
251            &[0x48, 0x83, 0xec, _]
252                | &[0x48, 0x81, 0xec, _, _, _, _]
253                | &[0x83, 0xec, _]
254                | &[0x81, 0xec, _, _, _, _]
255        )
256    );
257    let has_sub = sub_matched && (has_frame || saves_count == 0);
258    if has_sub {
259        start += 1;
260    }
261
262    // Step 5: additional callee-save pushes that come *after* the
263    // frame setup. The MSVC i386 prologue often looks like
264    // `push ebp; mov ebp,esp; push esi; push edi` — the frame is
265    // set up first, then the function spills the extra callee-saved
266    // registers it'll need.
267    if has_frame {
268        while let Some(b) = bytes_at(start) {
269            if b.len() == 1 && (0x50..=0x57).contains(&b[0]) {
270                saves_count += 1;
271                start += 1;
272            } else {
273                break;
274            }
275        }
276    }
277
278    // Step 5: classify. If nothing was consumed beyond the (optional)
279    // endbr, there's nothing to lift.
280    if !has_endbr && saves_count == 0 && !has_frame && !has_sub {
281        return None;
282    }
283    let _ = push_start; // kept for future use when emitting save-list text
284
285    let kind = match (has_endbr, saves_count > 0, has_frame, has_sub) {
286        // Pure endbr leaf.
287        (true, false, false, false) => "std-noframe",
288        // CRT-stub `sub esp, N; ...; add esp, N; ret` with optional CET.
289        (true, false, false, true) => "thin",
290        (false, false, false, true) => "thin-no-cf",
291        // Frame-only (no saves). `sub esp, IMM` doesn't change the
292        // kind label — it's still a standard frame.
293        (true, false, true, _) => "std",
294        (false, false, true, _) => "std-no-cf",
295        // Saves-only (no frame).
296        (true, true, false, false) => "saves-cf",
297        (false, true, false, false) => "saves",
298        // Saves + frame.
299        (true, true, true, _) => "saves-std",
300        (false, true, true, _) => "saves-std-no-cf",
301        // Saves + sub-without-frame: unusual; label it.
302        (_, true, false, true) => "saves-thin",
303        // Guarded by the `!has_endbr && saves_count == 0 && !has_frame &&
304        // !has_sub` early-return above.
305        (false, false, false, false) => unreachable!("nothing-to-lift case already returned None"),
306    };
307    Some(LiftedPrologue {
308        insns_consumed: start,
309        kind,
310    })
311}
312
313/// One recognised SysV-x64 epilogue at the tail of a function or
314/// shared between branches.
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct LiftedEpilogue {
317    /// Number of trailing instructions matched.
318    pub insns_consumed: usize,
319    /// Symbolic kind: `"std"` for `leave; ret`, `"std-pop-rbp"` for
320    /// `pop rbp; ret`.
321    pub kind: &'static str,
322}
323
324/// Try to recognize the trailing instructions of `insns` as a stack-
325/// frame-tearing-down epilogue:
326///
327/// * `leave; ret`                  → `"std"`
328/// * `pop rbp; ret`                → `"std-pop-rbp"`
329/// * `add rsp/esp, IMM; ret`       → `"thin"`
330/// * `add rsp/esp, IMM; pop rbp; ret` → `"thin-pop-rbp"`
331///
332/// Used when [`try_lift_return_pattern`] doesn't fire (e.g. the last
333/// block has no value-setter — the return value was set in an earlier
334/// block by a non-trivial expression). Lifting just the epilogue still
335/// removes the boilerplate from the decompile output.
336#[must_use]
337pub fn try_lift_epilogue_pattern(insns: &[DecodedInsn]) -> Option<LiftedEpilogue> {
338    if insns.is_empty() {
339        return None;
340    }
341    let last = insns.last()?;
342    let last_b = last.original_bytes.as_slice();
343    // The trailing instruction must terminate the function:
344    //   c3       ret
345    //   c2 ww ww ret IMM16 (stdcall callee-cleans-up)
346    let is_ret_bare = last_b == [0xc3];
347    let is_ret_imm = matches!(last_b, [0xc2, _, _]);
348    if !is_ret_bare && !is_ret_imm {
349        return None;
350    }
351
352    if insns.len() >= 2 {
353        let prev = &insns[insns.len() - 2].original_bytes;
354
355        // Two-instruction tails: leave/ret, pop rbp/ret.
356        if is_ret_bare {
357            let two_kind = match prev.as_slice() {
358                [0xc9] => Some("std"),         // leave
359                [0x5d] => Some("std-pop-rbp"), // pop rbp
360                _ => None,
361            };
362            if let Some(kind) = two_kind {
363                // Check if it can be widened to a thin form (`add rsp, IMM`
364                // immediately before the `pop rbp`/`leave`). Only for
365                // `pop rbp`, since `leave` already restores rsp.
366                if kind == "std-pop-rbp" && insns.len() >= 3 {
367                    let before_pop = insns[insns.len() - 3].original_bytes.as_slice();
368                    if is_add_rsp_imm(before_pop) {
369                        return Some(LiftedEpilogue {
370                            insns_consumed: 3,
371                            kind: "thin-pop-rbp",
372                        });
373                    }
374                }
375                return Some(LiftedEpilogue {
376                    insns_consumed: 2,
377                    kind,
378                });
379            }
380
381            // `add rsp, IMM; ret` — bare CRT-stub epilogue.
382            if is_add_rsp_imm(prev.as_slice()) {
383                return Some(LiftedEpilogue {
384                    insns_consumed: 2,
385                    kind: "thin",
386                });
387            }
388        }
389    }
390
391    // Generic "restore saved registers + return" epilogue: a tail of
392    // pop-like instructions followed by `ret` or `ret IMM16`. Counts
393    // every preceding insn that's a `pop reg` (single-byte opcodes
394    // 0x58..=0x5f), `leave` (0xc9), or `add rsp/esp, IMM` (the manual
395    // stack-cleanup form). Common on Windows i386 (callee-saves
396    // ebx/esi/edi + leave) and useful any time a function exits
397    // through more than one block with the same teardown sequence.
398    let mut restore_count = 0usize;
399    if insns.len() >= 2 {
400        for i in (0..insns.len() - 1).rev() {
401            let b = insns[i].original_bytes.as_slice();
402            let is_pop_reg = b.len() == 1 && (0x58..=0x5f).contains(&b[0]);
403            let is_leave = b == [0xc9];
404            let is_add_rsp = is_add_rsp_imm(b);
405            if is_pop_reg || is_leave || is_add_rsp {
406                restore_count += 1;
407            } else {
408                break;
409            }
410        }
411    }
412    if restore_count >= 1 {
413        let kind = if is_ret_imm { "saves-imm" } else { "saves" };
414        return Some(LiftedEpilogue {
415            insns_consumed: restore_count + 1,
416            kind,
417        });
418    }
419
420    // Bare ret (or ret IMM16) with nothing in front to lift — still
421    // worth labelling. Makes function exits land on `@epilogue`
422    // consistently whether there's a frame teardown or not, and the
423    // `ret-imm` kind tells you stdcall is in play.
424    if is_ret_imm {
425        return Some(LiftedEpilogue {
426            insns_consumed: 1,
427            kind: "ret-imm",
428        });
429    }
430    if is_ret_bare {
431        return Some(LiftedEpilogue {
432            insns_consumed: 1,
433            kind: "ret",
434        });
435    }
436
437    None
438}
439
440fn is_add_rsp_imm(bytes: &[u8]) -> bool {
441    matches!(
442        bytes,
443        [0x48, 0x83, 0xc4, _]
444            | [0x48, 0x81, 0xc4, _, _, _, _]
445            | [0x83, 0xc4, _]
446            | [0x81, 0xc4, _, _, _, _]
447    )
448}
449
450/// Try to recognize the trailing instructions of `insns` as a
451/// "return with literal, then jump to a shared epilogue" pattern.
452///
453/// gcc emits this for functions with multiple return sites and a
454/// single `leave; ret` (or `pop rbp; ret`) tail block. Each
455/// non-final block ends with `mov eax, IMM; jmp <epilogue_addr>` (or
456/// `xor eax, eax; jmp <epilogue_addr>`); the lifter matches that
457/// pattern and folds it into a single `Stmt::Return`. The shared
458/// epilogue itself is left as `@asm` since it doesn't carry a value
459/// — it's just `[leave|pop rbp;] ret`.
460///
461/// Recognised forms (working backwards from the final `jmp`):
462///
463/// * `mov eax, IMM32; jmp short rel8 -> epilogue_addr`
464/// * `mov eax, IMM32; jmp near rel32 -> epilogue_addr`
465/// * `xor eax, eax;   jmp short rel8 -> epilogue_addr`
466/// * `xor eax, eax;   jmp near rel32 -> epilogue_addr`
467///
468/// Only matches when the final instruction is a direct jump whose
469/// `near_branch_target()` equals `epilogue_addr`.
470#[must_use]
471pub fn try_lift_return_via_jmp(insns: &[DecodedInsn], epilogue_addr: u64) -> Option<LiftedReturn> {
472    if insns.len() < 2 {
473        return None;
474    }
475
476    let last = insns.last()?;
477    if last.iced.flow_control() != FlowControl::UnconditionalBranch {
478        return None;
479    }
480    if last.iced.near_branch_target() != epilogue_addr {
481        return None;
482    }
483
484    // The instruction before the jmp must be a value-setter.
485    let setter = &insns[insns.len() - 2].original_bytes;
486    let value = match setter.as_slice() {
487        [0xb8, b0, b1, b2, b3] => u64::from(u32::from_le_bytes([*b0, *b1, *b2, *b3])),
488        [0x31, 0xc0] => 0,
489        _ => return None,
490    };
491
492    Some(LiftedReturn {
493        insns_consumed: 2,
494        value,
495    })
496}
497
498/// One recognised `cmp/test + jcc` pair at the tail of a basic block,
499/// suitable for lifting into a structured `if/else` directive.
500#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct LiftedIfBranchHead {
502    /// Number of trailing instructions matched. Always 2 in v0
503    /// (one comparison + one conditional jump).
504    pub insns_consumed: usize,
505    /// Human-readable form of the head, e.g.
506    /// `"cmp dword ptr [rbp-4],1; jne short 11F6h"`.
507    pub cond_text: String,
508    /// Raw encoded bytes of the comparison and conditional jump,
509    /// concatenated. The lower path emits these unchanged.
510    pub cond_bytes: Vec<u8>,
511    /// Absolute virtual address the jcc transfers control to when
512    /// taken. Used to find the "taken" basic block in the CFG.
513    pub jcc_target: u64,
514}
515
516/// Try to recognise the trailing two instructions of `insns` as a
517/// `cmp` (or `test`) followed by a direct conditional jump.
518///
519/// Returns `None` when the pair doesn't fit; in particular the jcc
520/// must be a *direct* conditional branch with a constant near target —
521/// indirect / unsupported jcc forms aren't lifted because we can't
522/// statically point at a "taken" block address.
523///
524/// The function does not look further back than the last two
525/// instructions; chained comparisons or short-circuit boolean rebuilds
526/// are out of scope for v0.
527#[must_use]
528pub fn try_lift_if_branch_head(insns: &[DecodedInsn]) -> Option<LiftedIfBranchHead> {
529    if insns.len() < 2 {
530        return None;
531    }
532    let jcc = insns.last()?;
533    if jcc.iced.flow_control() != FlowControl::ConditionalBranch {
534        return None;
535    }
536    // Only direct jcc — `near_branch_target()` is meaningful only for
537    // direct near branches; indirect forms (rare for jcc but possible
538    // in obfuscated code) would return 0 or a stale value.
539    let target = jcc.iced.near_branch_target();
540    if target == 0 {
541        return None;
542    }
543
544    // Adjacent cmp/test: the canonical shape. Consume both
545    // instructions; the IfBranch owns their bytes.
546    let cmp_idx = insns.len() - 2;
547    let cmp = &insns[cmp_idx];
548    if matches!(cmp.iced.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
549        let cond_text = render_cond_source(&cmp.iced, &jcc.iced);
550        let mut cond_bytes =
551            Vec::with_capacity(cmp.original_bytes.len() + jcc.original_bytes.len());
552        cond_bytes.extend_from_slice(&cmp.original_bytes);
553        cond_bytes.extend_from_slice(&jcc.original_bytes);
554        return Some(LiftedIfBranchHead {
555            insns_consumed: 2,
556            cond_text,
557            cond_bytes,
558            jcc_target: target,
559        });
560    }
561
562    // Separated cmp/test: scan backward through flag-preserving
563    // instructions (mov, lea, push, pop, etc.) for the last cmp/test.
564    // If one is found, the IfBranch owns only the jcc's bytes — the
565    // cmp/test stays as @asm at its original position in the block.
566    // This lets the if-detection fire even when the compiler
567    // interleaved unrelated setup between the comparison and the
568    // branch (a common optimised-codegen shape).
569    let mut probe = insns.len() - 2;
570    loop {
571        let ins = &insns[probe];
572        if matches!(ins.iced.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
573            let cond_text = render_cond_source(&ins.iced, &jcc.iced);
574            return Some(LiftedIfBranchHead {
575                insns_consumed: 1,
576                cond_text,
577                cond_bytes: jcc.original_bytes.clone(),
578                jcc_target: target,
579            });
580        }
581        // Any instruction that writes flags poisons the comparison —
582        // its result reaches the jcc before the older cmp/test does.
583        if ins.iced.rflags_modified() != 0 {
584            return None;
585        }
586        if probe == 0 {
587            return None;
588        }
589        probe -= 1;
590    }
591}
592
593/// Rename a memory operand to its source-language slot name —
594/// handling both frame-pointer (`[ebp+N]`) and stack-pointer
595/// (`[esp+N]`, with an optional SP delta context) forms.
596///
597/// For SP-relative accesses, `sp_delta` is the signed change in ESP
598/// from function entry at the instruction that contains the
599/// operand. The stable slot offset is `sp_delta + disp + 4` — the
600/// `+4` normalises onto the same coordinate system as the EBP-based
601/// names (where `arg_8` = first arg = `entry_ESP + 4`).
602///
603/// Returns `None` for shapes the renamer doesn't recognise (indexed
604/// addressing, non-ebp/esp bases, bare operands, mismatched
605/// addressing modes); the caller falls back to the raw text. The
606/// no-context [`rename_operand_if_slot`] is a thin wrapper that
607/// passes `sp_delta = None`.
608#[must_use]
609pub fn rename_operand_with_ctx(text: &str, sp_delta: Option<i64>) -> Option<String> {
610    if let Some(name) = rename_ebp_slot(text) {
611        return Some(name);
612    }
613    if let Some(delta) = sp_delta {
614        return rename_esp_slot(text, delta);
615    }
616    None
617}
618
619/// SP-relative slot rename. The caller-supplied `sp_delta` is the
620/// SP change relative to function entry at the point of the
621/// access; `[esp+disp]` at `sp_delta = -12` lands on
622/// `entry_ESP + 8`, which is `arg_c` in the EBP-relative
623/// convention.
624fn rename_esp_slot(text: &str, sp_delta: i64) -> Option<String> {
625    let core = text.strip_prefix("dword ptr ").unwrap_or(text);
626    let core = core.strip_prefix("qword ptr ").unwrap_or(core);
627    let inner = core
628        .strip_prefix('[')
629        .and_then(|s| s.strip_suffix(']'))?
630        .trim();
631    let disp: i64 = if inner == "esp" {
632        0
633    } else if let Some(rest) = inner.strip_prefix("esp+") {
634        let off = parse_unsigned_disp(rest.trim())?;
635        i64::try_from(off).ok()?
636    } else if let Some(rest) = inner.strip_prefix("esp-") {
637        let off = parse_unsigned_disp(rest.trim())?;
638        -(i64::try_from(off).ok()?)
639    } else {
640        return None;
641    };
642    let stable = sp_delta + disp + 4;
643    if stable == 0 || stable == 4 {
644        // Saved EBP slot / return address slot — internal, not a
645        // source-language variable. Keep the raw text so the reader
646        // can tell it's the ABI's spill, not user data.
647        return None;
648    }
649    if stable >= 8 {
650        let off = u64::try_from(stable).ok()?;
651        return Some(format!("arg_{off:x}"));
652    }
653    // stable < 0 — local slot (negative offsets from the conceptual
654    // EBP-frame origin).
655    let off = u64::try_from(-stable).ok()?;
656    Some(format!("var_{off:x}"))
657}
658
659/// Compute SP delta at every instruction in `insns`. The first
660/// instruction sees delta = 0 (function entry: SP points at the
661/// return address). Subsequent deltas accumulate stack effects
662/// from `push` / `pop` / `enter` / `leave` (via iced's built-in
663/// `stack_pointer_increment`) plus the arithmetic forms `sub
664/// esp/rsp, IMM` / `add esp/rsp, IMM` which iced doesn't model.
665///
666/// Each entry maps an instruction's IP to its delta. The map is
667/// keyed by IP because patterns get instructions by reference and
668/// IP is the cheap stable identifier.
669#[must_use]
670pub fn compute_sp_delta_table(insns: &[DecodedInsn]) -> std::collections::HashMap<u64, i64> {
671    use std::collections::HashMap;
672    let mut out: HashMap<u64, i64> = HashMap::with_capacity(insns.len());
673    let mut delta: i64 = 0;
674    for ins in insns {
675        out.insert(ins.iced.ip(), delta);
676        delta = delta.saturating_add(sp_change_for(&ins.iced));
677    }
678    out
679}
680
681/// Per-instruction stack-pointer change. Uses iced's intrinsic
682/// `stack_pointer_increment` for push / pop / call / ret / enter /
683/// leave; falls back to operand inspection for `sub esp, IMM` and
684/// `add esp, IMM` which iced doesn't compute.
685#[must_use]
686pub fn sp_change_for(insn: &Instruction) -> i64 {
687    let intrinsic = i64::from(insn.stack_pointer_increment());
688    if intrinsic != 0 {
689        return intrinsic;
690    }
691    match insn.mnemonic() {
692        Mnemonic::Sub | Mnemonic::Add => {
693            if insn.op0_kind() != OpKind::Register {
694                return 0;
695            }
696            let r = insn.op0_register();
697            if r != Register::ESP && r != Register::RSP {
698                return 0;
699            }
700            let imm = match insn.op1_kind() {
701                OpKind::Immediate8to32 | OpKind::Immediate8to64 | OpKind::Immediate8 => {
702                    #[allow(clippy::cast_possible_wrap)]
703                    let v = i64::from(insn.immediate8() as i8);
704                    v
705                }
706                OpKind::Immediate32 | OpKind::Immediate32to64 => {
707                    #[allow(clippy::cast_possible_wrap)]
708                    let v = i64::from(insn.immediate32() as i32);
709                    v
710                }
711                _ => return 0,
712            };
713            if insn.mnemonic() == Mnemonic::Sub {
714                -imm
715            } else {
716                imm
717            }
718        }
719        _ => 0,
720    }
721}
722
723/// Rename a frame-pointer memory operand to its source-language slot
724/// name. `[ebp+N]` becomes `arg_<hex>` and `[ebp-N]` becomes
725/// `var_<hex>` — the offset (always positive) is the hex part of the
726/// name, matching the Ghidra/IDA convention. Bare `[ebp]` (offset 0)
727/// renders as `var_0`. Anything else (indexed addressing, non-EBP
728/// bases, non-memory operands, …) returns `None` so the caller can
729/// fall back to the raw text.
730///
731/// The mapping is purely syntactic — there is no ABI inference yet,
732/// just "slot at `[ebp+N]`" gets a stable name. The encoder in
733/// `encode_text` recognises both forms so byte derivation continues
734/// to work when the cond/operand text uses the named slot.
735#[must_use]
736pub fn rename_ebp_slot(text: &str) -> Option<String> {
737    let core = text.strip_prefix("dword ptr ").unwrap_or(text);
738    let core = core.strip_prefix("qword ptr ").unwrap_or(core);
739    let inner = core
740        .strip_prefix('[')
741        .and_then(|s| s.strip_suffix(']'))?
742        .trim();
743    if inner == "ebp" {
744        return Some("var_0".into());
745    }
746    if let Some(rest) = inner.strip_prefix("ebp+") {
747        let offset = parse_unsigned_disp(rest.trim())?;
748        return Some(format!("arg_{offset:x}"));
749    }
750    if let Some(rest) = inner.strip_prefix("ebp-") {
751        let offset = parse_unsigned_disp(rest.trim())?;
752        return Some(format!("var_{offset:x}"));
753    }
754    None
755}
756
757/// Apply [`rename_ebp_slot`] when it matches; otherwise return the
758/// input unchanged. Useful as a one-call helper for places that want
759/// to rename operands without branching on the result.
760#[must_use]
761pub fn rename_operand_if_slot(text: &str) -> String {
762    rename_ebp_slot(text).unwrap_or_else(|| text.to_string())
763}
764
765/// Apply [`rename_operand_with_ctx`] (which handles both `[ebp+N]`
766/// and `[esp+N]` with the supplied SP delta) and return the renamed
767/// text — or the original input unchanged when no rename rule
768/// matches.
769#[must_use]
770pub fn rename_operand_in_ctx(text: &str, sp_delta: Option<i64>) -> String {
771    rename_operand_with_ctx(text, sp_delta).unwrap_or_else(|| text.to_string())
772}
773
774fn parse_unsigned_disp(s: &str) -> Option<u64> {
775    // Reject anything that's not a plain integer literal — we don't
776    // want to rename `[ebp+esi*4]` or other indexed forms.
777    if s.is_empty() || !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
778        return None;
779    }
780    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
781        return u64::from_str_radix(hex, 16).ok();
782    }
783    if let Some(hex) = s.strip_suffix('h').or_else(|| s.strip_suffix('H')) {
784        return u64::from_str_radix(hex, 16).ok();
785    }
786    s.parse::<u64>().ok()
787}
788
789/// Render a `cmp` / `test` + `jcc` pair as a C-style relational
790/// expression evaluated against the source-language `if`'s body.
791///
792/// The body of an `if (cond) { … }` runs on the JCC's *not-taken*
793/// path, so we invert the jcc's branch sense to get the body-side
794/// operator. Worked examples:
795///
796/// * `test esi,esi; jne …`   →  `"esi == 0"`     (jne taken when ≠0
797///   so body runs when =0)
798/// * `cmp eax,5; jl …`        →  `"eax >= 5"`     (jl is *signed*
799///   less-than; body runs on ≥)
800/// * `cmp ebx,ecx; jb …`      →  `"ebx >=u ecx"`  (`jb` is unsigned;
801///   `u` suffix marks the comparison)
802///
803/// Falls back to the literal assembly form (`"cmp X,Y; jcc T"`) when
804/// the jcc isn't one we have a clean source-level mapping for (e.g.
805/// `js`, `jp`, `jo`). The crate-internal `encode_text` encoder
806/// accepts the high-level form and continues to auto-derive
807/// `head_bytes`.
808#[must_use]
809pub fn render_cond_source(cmp: &Instruction, jcc: &Instruction) -> String {
810    let cmp_text = format_intel(cmp);
811    let Some((lhs_raw, rhs_raw)) = split_cmp_test_operands(&cmp_text) else {
812        return format!("{cmp_text}; {}", format_intel(jcc));
813    };
814    let is_test = cmp.mnemonic() == Mnemonic::Test;
815    let Some(op) = body_operator_from_jcc(jcc.mnemonic()) else {
816        return format!("{cmp_text}; {}", format_intel(jcc));
817    };
818    let lhs = rename_operand_if_slot(&lhs_raw);
819    let rhs = rename_operand_if_slot(&rhs_raw);
820    // `test reg, reg` AND-s the register with itself — the result is
821    // the register's value, so the jcc effectively compares the
822    // register against zero (ZF for `==/!=`, SF for signed
823    // ordering). Render that as `reg <op> 0` instead of the
824    // redundant `reg <op> reg`. The unsigned-suffix form drops
825    // because unsigned compare-with-zero is always trivially true
826    // or false; the comparison is meaningful only in the signed
827    // interpretation. (Renames don't matter here — the same-register
828    // check fires on the raw text, before renaming.)
829    if is_test && lhs_raw == rhs_raw {
830        let signed_op = op.strip_suffix('u').unwrap_or(op);
831        return format!("{lhs} {signed_op} 0");
832    }
833    format!("{lhs} {op} {rhs}")
834}
835
836fn body_operator_from_jcc(jcc: Mnemonic) -> Option<&'static str> {
837    use Mnemonic::{Ja, Jae, Jb, Jbe, Je, Jg, Jge, Jl, Jle, Jne};
838    Some(match jcc {
839        // ZF tests — same in signed and unsigned interpretations.
840        Je => "!=",
841        Jne => "==",
842        // Signed magnitude tests. The jcc takes when its name
843        // describes the relation; body runs on the inverse.
844        Jl => ">=",
845        Jle => ">",
846        Jg => "<=",
847        Jge => "<",
848        // Unsigned magnitude tests. `u`-suffixed operators tell the
849        // reader (and the encoder) the comparison is unsigned.
850        Jb => ">=u",
851        Jbe => ">u",
852        Ja => "<=u",
853        Jae => "<u",
854        _ => return None,
855    })
856}
857
858/// Split the operand portion of `format_intel(cmp_or_test)` into a
859/// `(lhs, rhs)` pair. Strips the `cmp ` / `test ` prefix; splits on
860/// the first top-level comma (commas inside `[…]` don't count, even
861/// though Intel syntax doesn't actually use them inside memory
862/// operands — be defensive).
863fn split_cmp_test_operands(formatted: &str) -> Option<(String, String)> {
864    let rest = formatted
865        .strip_prefix("cmp ")
866        .or_else(|| formatted.strip_prefix("test "))?;
867    let mut depth = 0i32;
868    for (i, ch) in rest.char_indices() {
869        match ch {
870            '(' | '[' => depth += 1,
871            ')' | ']' => depth -= 1,
872            ',' if depth == 0 => {
873                let (l, r) = rest.split_at(i);
874                return Some((l.trim().to_string(), r[1..].trim().to_string()));
875            }
876            _ => {}
877        }
878    }
879    None
880}
881
882/// If `insn` is an "argument spill" — `mov [rbp+disp], REG` (or
883/// `movss/movsd [rbp+disp], xmm`) where `REG` is one of the SysV
884/// x86-64 argument-passing registers — return that argument's
885/// 0-based index.
886///
887/// SysV x86-64 calling convention:
888///
889/// | Arg index | Int / ptr | Float (xmm) |
890/// |-----------|-----------|-------------|
891/// | 0         | rdi/edi/di/dil | xmm0  |
892/// | 1         | rsi/esi/si/sil | xmm1  |
893/// | 2         | rdx/edx/dx/dl  | xmm2  |
894/// | 3         | rcx/ecx/cx/cl  | xmm3  |
895/// | 4         | r8/r8d/r8w/r8b | xmm4  |
896/// | 5         | r9/r9d/r9w/r9b | xmm5  |
897///
898/// Used by the decompiler to annotate `mov [rbp-N], edi` (etc.) at
899/// function entry with the corresponding parameter name from DWARF.
900#[must_use]
901pub fn arg_spill_index(insn: &Instruction) -> Option<u32> {
902    let m = insn.mnemonic();
903    if !matches!(m, Mnemonic::Mov | Mnemonic::Movss | Mnemonic::Movsd) {
904        return None;
905    }
906    if insn.op_count() < 2 {
907        return None;
908    }
909    if insn.op0_kind() != OpKind::Memory {
910        return None;
911    }
912    if insn.memory_base() != Register::RBP {
913        return None;
914    }
915    if insn.op1_kind() != OpKind::Register {
916        return None;
917    }
918    sysv_arg_index(insn.op_register(1))
919}
920
921#[allow(clippy::match_same_arms)] // each line is one register width
922fn sysv_arg_index(reg: Register) -> Option<u32> {
923    Some(match reg {
924        Register::RDI | Register::EDI | Register::DI | Register::DIL | Register::XMM0 => 0,
925        Register::RSI | Register::ESI | Register::SI | Register::SIL | Register::XMM1 => 1,
926        Register::RDX | Register::EDX | Register::DX | Register::DL | Register::XMM2 => 2,
927        Register::RCX | Register::ECX | Register::CX | Register::CL | Register::XMM3 => 3,
928        Register::R8 | Register::R8D | Register::R8W | Register::R8L | Register::XMM4 => 4,
929        Register::R9 | Register::R9D | Register::R9W | Register::R9L | Register::XMM5 => 5,
930        _ => return None,
931    })
932}
933
934/// Like [`direct_call_target`], but for unconditional direct branches
935/// (`jmp rel32` / `jmp short rel8`). Useful for spotting tail calls
936/// when the target lives in another discovered function.
937#[must_use]
938pub fn direct_unconditional_branch_target(insn: &Instruction) -> Option<u64> {
939    match insn.flow_control() {
940        FlowControl::UnconditionalBranch => Some(insn.near_branch_target()),
941        _ => None,
942    }
943}
944
945/// Read the memory operand's displacement as a signed 64-bit value,
946/// honouring the addressing mode's actual width.
947///
948/// iced's `memory_displacement64()` returns the displacement
949/// "extended to 64 bits" but in 32-bit addressing mode (e.g. when
950/// the base is `EBP`/`ESP`) it doesn't sign-extend small negative
951/// displacements past bit 31 — `[ebp-0x20]` comes back as
952/// `0x00000000_ffffffe0` rather than `0xffffffff_ffffffe0`. We
953/// detect 32-bit addressing via the base register and re-extend
954/// from i32 in that case.
955#[must_use]
956#[allow(clippy::cast_possible_wrap)]
957pub fn signed_memory_displacement(insn: &Instruction) -> i64 {
958    let raw = insn.memory_displacement64();
959    let is_32bit_addressing = matches!(
960        insn.memory_base(),
961        Register::EAX
962            | Register::EBX
963            | Register::ECX
964            | Register::EDX
965            | Register::ESI
966            | Register::EDI
967            | Register::EBP
968            | Register::ESP
969            | Register::EIP
970    );
971    if is_32bit_addressing {
972        i64::from(raw as i32)
973    } else {
974        raw as i64
975    }
976}
977
978/// If `insn` is `add/sub dword/qword ptr [rbp/ebp+disp], IMM` —
979/// a stack-frame local being incremented or decremented by a
980/// literal — return `(slot, op, value)` where `op` is `"+="` for
981/// `add` and `"-="` for `sub`.
982#[must_use]
983pub fn match_local_arith_immediate(insn: &Instruction) -> Option<(i64, &'static str, i64)> {
984    let op = match insn.mnemonic() {
985        Mnemonic::Add => "+=",
986        Mnemonic::Sub => "-=",
987        _ => return None,
988    };
989    if insn.op_count() != 2 {
990        return None;
991    }
992    if insn.op0_kind() != OpKind::Memory {
993        return None;
994    }
995    if !matches!(insn.memory_base(), Register::RBP | Register::EBP) {
996        return None;
997    }
998    if insn.memory_index() != Register::None {
999        return None;
1000    }
1001    #[allow(clippy::cast_possible_wrap)]
1002    let value = match insn.op1_kind() {
1003        OpKind::Immediate8 => i64::from(insn.immediate8() as i8),
1004        OpKind::Immediate16 => i64::from(insn.immediate16() as i16),
1005        OpKind::Immediate32 => i64::from(insn.immediate32() as i32),
1006        OpKind::Immediate64 => insn.immediate64() as i64,
1007        OpKind::Immediate8to16 => i64::from(insn.immediate8to16()),
1008        OpKind::Immediate8to32 => i64::from(insn.immediate8to32()),
1009        OpKind::Immediate8to64 => insn.immediate8to64(),
1010        OpKind::Immediate32to64 => insn.immediate32to64(),
1011        _ => return None,
1012    };
1013    Some((signed_memory_displacement(insn), op, value))
1014}
1015
1016/// If the instruction window starts with a recognised compound
1017/// stack-slot pattern (`[rbp+dst] op= [rbp+src]`), return
1018/// `(consumed, dst, op, src)`.
1019///
1020/// Two shapes are handled:
1021///
1022/// * 2-insn (`add`, `sub`, `and`, `or`, `xor`) —
1023///   `mov reg, [rbp+src]; <op> [rbp+dst], reg`.
1024///   Possible because these ops have a memory-destination form.
1025///
1026/// * 3-insn (`imul`) —
1027///   `mov reg, [rbp+dst]; imul reg, [rbp+src]; mov [rbp+dst], reg`.
1028///   `imul` has no memory-destination form, so the compiler routes
1029///   through a register.
1030///
1031/// The temp register must be the same across the window, the memory
1032/// operands must all be `[rbp/ebp+disp]` with no index, and (for the
1033/// 3-insn form) the dst displacement must match between the load and
1034/// the store-back. Returns `None` for any other shape.
1035#[must_use]
1036pub fn match_local_compound(insns: &[Instruction]) -> Option<(usize, i64, &'static str, i64)> {
1037    if insns.len() >= 2 {
1038        let i0 = &insns[0];
1039        let i1 = &insns[1];
1040        if let Some(out) = match_compound_two(i0, i1) {
1041            return Some(out);
1042        }
1043    }
1044    if insns.len() >= 3 {
1045        let i0 = &insns[0];
1046        let i1 = &insns[1];
1047        let i2 = &insns[2];
1048        if let Some(out) = match_compound_three(i0, i1, i2) {
1049            return Some(out);
1050        }
1051    }
1052    None
1053}
1054
1055fn is_rbp_local(insn: &Instruction) -> bool {
1056    insn.op_count() == 2
1057        && matches!(insn.memory_base(), Register::RBP | Register::EBP)
1058        && insn.memory_index() == Register::None
1059}
1060
1061fn match_compound_two(
1062    i0: &Instruction,
1063    i1: &Instruction,
1064) -> Option<(usize, i64, &'static str, i64)> {
1065    if i0.mnemonic() != Mnemonic::Mov {
1066        return None;
1067    }
1068    if i0.op_count() != 2 || i0.op0_kind() != OpKind::Register || i0.op1_kind() != OpKind::Memory {
1069        return None;
1070    }
1071    if !is_rbp_local(i0) {
1072        return None;
1073    }
1074    let op = match i1.mnemonic() {
1075        Mnemonic::Add => "+=",
1076        Mnemonic::Sub => "-=",
1077        Mnemonic::And => "&=",
1078        Mnemonic::Or => "|=",
1079        Mnemonic::Xor => "^=",
1080        _ => return None,
1081    };
1082    if i1.op_count() != 2 || i1.op0_kind() != OpKind::Memory || i1.op1_kind() != OpKind::Register {
1083        return None;
1084    }
1085    if !is_rbp_local(i1) {
1086        return None;
1087    }
1088    if i0.op0_register() != i1.op1_register() {
1089        return None;
1090    }
1091    let src = signed_memory_displacement(i0);
1092    let dst = signed_memory_displacement(i1);
1093    Some((2, dst, op, src))
1094}
1095
1096fn match_compound_three(
1097    i0: &Instruction,
1098    i1: &Instruction,
1099    i2: &Instruction,
1100) -> Option<(usize, i64, &'static str, i64)> {
1101    if i0.mnemonic() != Mnemonic::Mov {
1102        return None;
1103    }
1104    if i0.op_count() != 2 || i0.op0_kind() != OpKind::Register || i0.op1_kind() != OpKind::Memory {
1105        return None;
1106    }
1107    if !is_rbp_local(i0) {
1108        return None;
1109    }
1110    let op = match i1.mnemonic() {
1111        Mnemonic::Imul => "*=",
1112        _ => return None,
1113    };
1114    if i1.op_count() != 2 || i1.op0_kind() != OpKind::Register || i1.op1_kind() != OpKind::Memory {
1115        return None;
1116    }
1117    if !is_rbp_local(i1) {
1118        return None;
1119    }
1120    if i0.op0_register() != i1.op0_register() {
1121        return None;
1122    }
1123    if i2.mnemonic() != Mnemonic::Mov {
1124        return None;
1125    }
1126    if i2.op_count() != 2 || i2.op0_kind() != OpKind::Memory || i2.op1_kind() != OpKind::Register {
1127        return None;
1128    }
1129    if !is_rbp_local(i2) {
1130        return None;
1131    }
1132    if i2.op1_register() != i0.op0_register() {
1133        return None;
1134    }
1135    let dst_load = signed_memory_displacement(i0);
1136    let src = signed_memory_displacement(i1);
1137    let dst_store = signed_memory_displacement(i2);
1138    if dst_load != dst_store {
1139        return None;
1140    }
1141    Some((3, dst_load, op, src))
1142}
1143
1144/// If `insn` is a `mov [rbp/ebp+disp], IMM` — a stack-frame local
1145/// being initialised or assigned a literal — return the signed
1146/// displacement and the immediate value. The displacement honours
1147/// 32-bit addressing mode (so `[ebp-0x8]` round-trips as `-8`,
1148/// not `0xffff_fff8`).
1149#[must_use]
1150pub fn match_local_set_immediate(insn: &Instruction) -> Option<(i64, i64)> {
1151    if insn.mnemonic() != Mnemonic::Mov {
1152        return None;
1153    }
1154    if insn.op_count() != 2 {
1155        return None;
1156    }
1157    if insn.op0_kind() != OpKind::Memory {
1158        return None;
1159    }
1160    if !matches!(insn.memory_base(), Register::RBP | Register::EBP) {
1161        return None;
1162    }
1163    if insn.memory_index() != Register::None {
1164        return None;
1165    }
1166    #[allow(clippy::cast_possible_wrap)]
1167    let value = match insn.op1_kind() {
1168        OpKind::Immediate8 => i64::from(insn.immediate8() as i8),
1169        OpKind::Immediate16 => i64::from(insn.immediate16() as i16),
1170        OpKind::Immediate32 => i64::from(insn.immediate32() as i32),
1171        OpKind::Immediate64 => insn.immediate64() as i64,
1172        OpKind::Immediate8to16 => i64::from(insn.immediate8to16()),
1173        OpKind::Immediate8to32 => i64::from(insn.immediate8to32()),
1174        OpKind::Immediate8to64 => insn.immediate8to64(),
1175        OpKind::Immediate32to64 => insn.immediate32to64(),
1176        _ => return None,
1177    };
1178    Some((signed_memory_displacement(insn), value))
1179}
1180
1181/// If `insn` is a `lea reg, [rip+disp]` (compiler-typical
1182/// "load address of a global / string-constant"), return the absolute
1183/// virtual address of the target. Returns `None` for non-`lea`s, or
1184/// for `lea`s with a non-RIP base or a non-trivial index.
1185///
1186/// Iced computes the rip-relative target for us in
1187/// `memory_displacement64()` when the operand's base is `RIP`.
1188#[must_use]
1189pub fn direct_lea_rip_target(insn: &Instruction) -> Option<u64> {
1190    if insn.mnemonic() != Mnemonic::Lea {
1191        return None;
1192    }
1193    if insn.op_count() != 2 {
1194        return None;
1195    }
1196    if insn.op0_kind() != OpKind::Register {
1197        return None;
1198    }
1199    if insn.op1_kind() != OpKind::Memory {
1200        return None;
1201    }
1202    if insn.memory_base() != Register::RIP {
1203        return None;
1204    }
1205    if insn.memory_index() != Register::None {
1206        return None;
1207    }
1208    Some(insn.memory_displacement64())
1209}
1210
1211/// Format `insn` as Intel-syntax assembly text, suitable for embedding
1212/// inside an `@asm("...")` directive in a `.ud` file.
1213///
1214/// Goes through iced's [`IntelFormatter`] with default options so the
1215/// output matches the ubiquitous Intel-syntax convention (`mov rax,
1216/// rbx`, source on the right). Fresh formatter per call: deterministic,
1217/// no shared mutable state.
1218#[must_use]
1219pub fn format_intel(insn: &Instruction) -> String {
1220    let mut formatter = IntelFormatter::new();
1221    let mut out = String::new();
1222    formatter.format(insn, &mut out);
1223    out
1224}
1225
1226/// Result of [`verify_intel_text`].
1227#[derive(Debug, Clone, PartialEq, Eq)]
1228pub enum VerifyAsm {
1229    /// `text` matches the canonical Intel-syntax form for `bytes`.
1230    Match,
1231    /// `text` and the canonical form diverge; the canonical form is
1232    /// what `bytes` actually decode to.
1233    Diverged { canonical: String },
1234    /// `bytes` couldn't be decoded as a single x86 instruction.
1235    Undecodable,
1236    /// `bytes` decoded to multiple instructions instead of one.
1237    MultipleInsns { count: usize },
1238}
1239
1240/// Decode `bytes` as a single x86 instruction at `rip`, format it via
1241/// [`format_intel`], and compare against `text` (after a light
1242/// normalization that ignores case and folds whitespace runs).
1243///
1244/// Returns [`VerifyAsm::Match`] when the user's text agrees with what
1245/// the bytes actually encode — i.e. nothing has been edited away from
1246/// the canonical form. A divergence is the cleanest signal we have
1247/// today that a user edited the text without updating the bytes (or
1248/// vice versa); a stricter "would the edited text re-encode to the
1249/// same length" check needs a text assembler we don't ship yet.
1250#[must_use]
1251pub fn verify_intel_text(bitness: Bitness, text: &str, bytes: &[u8], rip: u64) -> VerifyAsm {
1252    let mut decoder = Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
1253    let mut count = 0usize;
1254    let mut first: Option<Instruction> = None;
1255    while decoder.can_decode() {
1256        let insn = decoder.decode();
1257        if insn.is_invalid() {
1258            return VerifyAsm::Undecodable;
1259        }
1260        if first.is_none() {
1261            first = Some(insn);
1262        }
1263        count += 1;
1264    }
1265    let Some(insn) = first else {
1266        return VerifyAsm::Undecodable;
1267    };
1268    if count != 1 {
1269        return VerifyAsm::MultipleInsns { count };
1270    }
1271    let canonical = format_intel(&insn);
1272    if normalize(text) == normalize(&canonical) {
1273        VerifyAsm::Match
1274    } else {
1275        VerifyAsm::Diverged { canonical }
1276    }
1277}
1278
1279/// Lower-case + strip all whitespace. The canonical form iced emits is
1280/// inconsistent about spaces (no space after a comma in operands; space
1281/// between mnemonic and first operand), and we don't want benign user
1282/// whitespace edits to surface as warnings — only the actual tokens.
1283fn normalize(s: &str) -> String {
1284    let mut out = String::with_capacity(s.len());
1285    for c in s.chars() {
1286        if !c.is_ascii_whitespace() {
1287            out.extend(c.to_lowercase());
1288        }
1289    }
1290    out
1291}
1292
1293/// Errors produced by decode / encode / round-trip helpers.
1294#[derive(Debug, thiserror::Error)]
1295pub enum Error {
1296    #[error("instruction decoder rejected bytes at offset {offset}")]
1297    DecodeFailed { offset: usize },
1298
1299    #[error("encoder rejected instructions: {0}")]
1300    Encode(String),
1301
1302    #[error("round-trip diverged at offset {offset}: expected 0x{expected:02x}, got 0x{got:02x}")]
1303    ByteMismatch {
1304        offset: usize,
1305        expected: u8,
1306        got: u8,
1307    },
1308
1309    #[error("round-trip length mismatch: input was {input} bytes, output is {output}")]
1310    LengthMismatch { input: usize, output: usize },
1311}
1312
1313pub type Result<T, E = Error> = std::result::Result<T, E>;
1314
1315/// Bitness of an x86 decode/encode pass.
1316#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1317pub enum Bitness {
1318    Bits16,
1319    Bits32,
1320    Bits64,
1321}
1322
1323impl Bitness {
1324    fn as_u32(self) -> u32 {
1325        match self {
1326            Self::Bits16 => 16,
1327            Self::Bits32 => 32,
1328            Self::Bits64 => 64,
1329        }
1330    }
1331}
1332
1333/// A single decoded instruction together with the exact bytes it
1334/// occupied in the source buffer.
1335///
1336/// `iced` is the structured form, useful for analysis (operand kinds,
1337/// branch targets, register usage, etc.). `original_bytes` is the byte
1338/// slice from the input — used by [`emit_preserved`] to re-emit a
1339/// byte-identical copy regardless of any encoding choices iced would
1340/// pick if asked to encode the structured form.
1341#[derive(Debug, Clone)]
1342pub struct DecodedInsn {
1343    pub iced: Instruction,
1344    pub original_bytes: Vec<u8>,
1345}
1346
1347impl ArchInsn for DecodedInsn {
1348    fn addr(&self) -> VAddr {
1349        VAddr(self.iced.ip())
1350    }
1351
1352    fn original_bytes(&self) -> &[u8] {
1353        &self.original_bytes
1354    }
1355}
1356
1357/// Decode `bytes` as a contiguous x86 instruction stream starting at
1358/// virtual address `rip`. Captures each instruction's exact bytes for
1359/// later byte-faithful re-emission.
1360///
1361/// Stops only when the buffer is exhausted. An invalid instruction is
1362/// a hard error — for code containing data-in-code, slice the
1363/// executable regions before calling.
1364pub fn decode(bitness: Bitness, bytes: &[u8], rip: u64) -> Result<Vec<DecodedInsn>> {
1365    let mut decoder = Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
1366    let mut out = Vec::new();
1367    while decoder.can_decode() {
1368        let pos = decoder.position();
1369        let insn = decoder.decode();
1370        if insn.is_invalid() {
1371            return Err(Error::DecodeFailed { offset: pos });
1372        }
1373        let len = insn.len();
1374        let end = pos.saturating_add(len);
1375        if end > bytes.len() {
1376            return Err(Error::DecodeFailed { offset: pos });
1377        }
1378        out.push(DecodedInsn {
1379            iced: insn,
1380            original_bytes: bytes[pos..end].to_vec(),
1381        });
1382    }
1383    Ok(out)
1384}
1385
1386/// Like [`decode`] but tolerates a decoder failure mid-stream: on
1387/// hitting an invalid byte the walk stops and returns every
1388/// instruction successfully decoded up to that point plus the
1389/// failure offset. Used by function-discovery passes that scan
1390/// past data-in-code regions (e.g. jump-table embedded inside a
1391/// `.text` section).
1392#[must_use]
1393pub fn decode_tolerant(bitness: Bitness, bytes: &[u8], rip: u64) -> Vec<DecodedInsn> {
1394    let mut decoder = Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
1395    let mut out = Vec::new();
1396    while decoder.can_decode() {
1397        let pos = decoder.position();
1398        let insn = decoder.decode();
1399        if insn.is_invalid() {
1400            break;
1401        }
1402        let len = insn.len();
1403        let end = pos.saturating_add(len);
1404        if end > bytes.len() {
1405            break;
1406        }
1407        out.push(DecodedInsn {
1408            iced: insn,
1409            original_bytes: bytes[pos..end].to_vec(),
1410        });
1411    }
1412    out
1413}
1414
1415/// Re-emit a decoded instruction stream using each instruction's
1416/// preserved original bytes. Byte-identical by construction.
1417#[must_use]
1418pub fn emit_preserved(insns: &[DecodedInsn]) -> Vec<u8> {
1419    let total: usize = insns.iter().map(|i| i.original_bytes.len()).sum();
1420    let mut out = Vec::with_capacity(total);
1421    for insn in insns {
1422        out.extend_from_slice(&insn.original_bytes);
1423    }
1424    out
1425}
1426
1427/// Re-encode the structured instructions through iced's `BlockEncoder`.
1428///
1429/// **Warning**: this does not preserve redundant encoding choices. If
1430/// the input used a non-canonical encoding (redundant prefixes,
1431/// alignment NOPs with `66` data16 overrides, larger-than-necessary
1432/// displacement sizes), the output will differ from the input. Use
1433/// [`emit_preserved`] for byte-identical round-trip.
1434pub fn reencode_via_iced(bitness: Bitness, insns: &[DecodedInsn], rip: u64) -> Result<Vec<u8>> {
1435    let iced_insns: Vec<Instruction> = insns.iter().map(|i| i.iced).collect();
1436    let block = InstructionBlock::new(&iced_insns, rip);
1437    let result = BlockEncoder::encode(bitness.as_u32(), block, BlockEncoderOptions::NONE)
1438        .map_err(|e| Error::Encode(e.to_string()))?;
1439    Ok(result.code_buffer)
1440}
1441
1442/// Decode `bytes` and re-emit via [`emit_preserved`]; verify the result
1443/// equals the input. This is the format-agnostic round-trip property
1444/// for the x86 backend, and it must hold for every byte sequence we
1445/// claim to support.
1446pub fn roundtrip_bytes(bitness: Bitness, bytes: &[u8], rip: u64) -> Result<Vec<DecodedInsn>> {
1447    let insns = decode(bitness, bytes, rip)?;
1448    let emitted = emit_preserved(&insns);
1449    if emitted.len() != bytes.len() {
1450        return Err(Error::LengthMismatch {
1451            input: bytes.len(),
1452            output: emitted.len(),
1453        });
1454    }
1455    if let Some((offset, (&expected, &got))) = bytes
1456        .iter()
1457        .zip(&emitted)
1458        .enumerate()
1459        .find(|(_, (a, b))| a != b)
1460    {
1461        return Err(Error::ByteMismatch {
1462            offset,
1463            expected,
1464            got,
1465        });
1466    }
1467    Ok(insns)
1468}
1469
1470/// Errors emitted by [`encode_jmp`].
1471#[derive(Debug, thiserror::Error)]
1472pub enum JumpEncodeError {
1473    #[error("jmp rel32 target out of i32 range: from=0x{from:x} to=0x{to:x}")]
1474    OutOfRange { from: u64, to: u64 },
1475}
1476
1477/// Encode an unconditional `jmp` from `source_ip` (the address of
1478/// the jmp instruction itself) to `target`.
1479///
1480/// * `wide=false` and the displacement fits in `i8`: `jmp rel8`
1481///   (2 bytes, opcode `0xeb`).
1482/// * otherwise (`wide=true`, or `i8` displacement doesn't fit):
1483///   `jmp rel32` (5 bytes, opcode `0xe9`).
1484///
1485/// The `wide` flag exists because compilers don't always pick
1486/// the shortest encoding — most do, but MSVC in particular
1487/// occasionally emits `jmp rel32` when `jmp rel8` would fit.
1488/// Setting `wide=true` reproduces that choice for round-trip
1489/// fidelity on unedited inputs. Edits that push a target beyond
1490/// `i8` reach auto-promote to `rel32` regardless of the flag.
1491pub fn encode_jmp(
1492    source_ip: u64,
1493    target: u64,
1494    wide: bool,
1495) -> std::result::Result<Vec<u8>, JumpEncodeError> {
1496    if !wide {
1497        let after_rel8 = source_ip.wrapping_add(2);
1498        let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
1499        if (-128..=127).contains(&rel8) {
1500            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1501            let imm = rel8 as i8 as u8;
1502            return Ok(vec![0xeb, imm]);
1503        }
1504    }
1505    let after_rel32 = source_ip.wrapping_add(5);
1506    let rel = i128::from(target).wrapping_sub(i128::from(after_rel32));
1507    let rel32 = i32::try_from(rel).map_err(|_| JumpEncodeError::OutOfRange {
1508        from: source_ip,
1509        to: target,
1510    })?;
1511    let mut out = Vec::with_capacity(5);
1512    out.push(0xe9);
1513    out.extend_from_slice(&rel32.to_le_bytes());
1514    Ok(out)
1515}
1516
1517/// Pre-computed byte size of [`encode_jmp`]'s output.
1518#[must_use]
1519pub fn encoded_jmp_size(source_ip: u64, target: u64, wide: bool) -> usize {
1520    if !wide {
1521        let after_rel8 = source_ip.wrapping_add(2);
1522        let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
1523        if (-128..=127).contains(&rel8) {
1524            return 2;
1525        }
1526    }
1527    5
1528}
1529
1530/// Encode a conditional `jcc` from `source_ip` (the address of
1531/// the jcc instruction itself) to `target`.
1532///
1533/// `cond_code` is the low nibble of the jcc opcode (0..=15):
1534///
1535/// * 0x0 = jo,  0x1 = jno,  0x2 = jb/jnae,  0x3 = jae/jnb
1536/// * 0x4 = je,  0x5 = jne,  0x6 = jbe,      0x7 = ja
1537/// * 0x8 = js,  0x9 = jns,  0xA = jp,       0xB = jnp
1538/// * 0xC = jl,  0xD = jge,  0xE = jle,      0xF = jg
1539///
1540/// * `wide=false` and the displacement fits in `i8`: `jcc rel8`
1541///   (2 bytes, opcode `0x70 | cond`).
1542/// * otherwise: `jcc rel32` (6 bytes, opcodes `0x0F 0x80 | cond`).
1543pub fn encode_jcc(
1544    source_ip: u64,
1545    target: u64,
1546    cond_code: u8,
1547    wide: bool,
1548) -> std::result::Result<Vec<u8>, JumpEncodeError> {
1549    let cc = cond_code & 0x0f;
1550    if !wide {
1551        let after_rel8 = source_ip.wrapping_add(2);
1552        let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
1553        if (-128..=127).contains(&rel8) {
1554            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1555            let imm = rel8 as i8 as u8;
1556            return Ok(vec![0x70 | cc, imm]);
1557        }
1558    }
1559    let after_rel32 = source_ip.wrapping_add(6);
1560    let rel = i128::from(target).wrapping_sub(i128::from(after_rel32));
1561    let rel32 = i32::try_from(rel).map_err(|_| JumpEncodeError::OutOfRange {
1562        from: source_ip,
1563        to: target,
1564    })?;
1565    let mut out = Vec::with_capacity(6);
1566    out.push(0x0f);
1567    out.push(0x80 | cc);
1568    out.extend_from_slice(&rel32.to_le_bytes());
1569    Ok(out)
1570}
1571
1572/// Encode `call rel32` from `source_ip` to `target` — 5 bytes
1573/// (`0xe8` + i32). Direct near calls on x86 are always rel32 in
1574/// 32- and 64-bit modes, so there's no narrow/wide choice here.
1575pub fn encode_call_rel32(
1576    source_ip: u64,
1577    target: u64,
1578) -> std::result::Result<Vec<u8>, JumpEncodeError> {
1579    let after = source_ip.wrapping_add(5);
1580    let rel = i128::from(target).wrapping_sub(i128::from(after));
1581    let rel32 = i32::try_from(rel).map_err(|_| JumpEncodeError::OutOfRange {
1582        from: source_ip,
1583        to: target,
1584    })?;
1585    let mut out = Vec::with_capacity(5);
1586    out.push(0xe8);
1587    out.extend_from_slice(&rel32.to_le_bytes());
1588    Ok(out)
1589}
1590
1591/// Pre-computed byte size of [`encode_jcc`]'s output.
1592#[must_use]
1593pub fn encoded_jcc_size(source_ip: u64, target: u64, wide: bool) -> usize {
1594    if !wide {
1595        let after_rel8 = source_ip.wrapping_add(2);
1596        let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
1597        if (-128..=127).contains(&rel8) {
1598            return 2;
1599        }
1600    }
1601    6
1602}
1603
1604/// Extract the jcc condition code (0..=15) from an already-
1605/// decoded jcc's opcode bytes. Returns `None` when `bytes` isn't
1606/// a recognised jcc encoding.
1607#[must_use]
1608pub fn jcc_cond_code_from_bytes(bytes: &[u8]) -> Option<u8> {
1609    match bytes {
1610        [op, ..] if (0x70..=0x7f).contains(op) => Some(op - 0x70),
1611        [0x0f, op, ..] if (0x80..=0x8f).contains(op) => Some(op - 0x80),
1612        _ => None,
1613    }
1614}
1615
1616/// Symbolic name for a jcc condition code, lowercase.
1617#[must_use]
1618pub fn jcc_cond_name(cond_code: u8) -> &'static str {
1619    match cond_code & 0x0f {
1620        0x0 => "jo",
1621        0x1 => "jno",
1622        0x2 => "jb",
1623        0x3 => "jae",
1624        0x4 => "je",
1625        0x5 => "jne",
1626        0x6 => "jbe",
1627        0x7 => "ja",
1628        0x8 => "js",
1629        0x9 => "jns",
1630        0xa => "jp",
1631        0xb => "jnp",
1632        0xc => "jl",
1633        0xd => "jge",
1634        0xe => "jle",
1635        _ => "jg",
1636    }
1637}
1638
1639/// Inverse of [`jcc_cond_name`].
1640#[must_use]
1641pub fn jcc_cond_code_from_name(name: &str) -> Option<u8> {
1642    Some(match name {
1643        "jo" => 0x0,
1644        "jno" => 0x1,
1645        "jb" | "jc" | "jnae" => 0x2,
1646        "jae" | "jnb" | "jnc" => 0x3,
1647        "je" | "jz" => 0x4,
1648        "jne" | "jnz" => 0x5,
1649        "jbe" | "jna" => 0x6,
1650        "ja" | "jnbe" => 0x7,
1651        "js" => 0x8,
1652        "jns" => 0x9,
1653        "jp" | "jpe" => 0xa,
1654        "jnp" | "jpo" => 0xb,
1655        "jl" | "jnge" => 0xc,
1656        "jge" | "jnl" => 0xd,
1657        "jle" | "jng" => 0xe,
1658        "jg" | "jnle" => 0xf,
1659        _ => return None,
1660    })
1661}
1662
1663/// Errors emitted by [`encode_msvc_jmp_table_dispatch`].
1664#[derive(Debug, thiserror::Error)]
1665pub enum SwitchEncodeError {
1666    #[error("unsupported selector register {0:?} (expected eax/ecx/edx/ebx/esi/edi/ebp)")]
1667    UnsupportedSelector(String),
1668    #[error("case count {0} doesn't fit in u32")]
1669    TooManyCases(usize),
1670    #[error("ja rel32 target out of i32 range: cmp_ip={cmp_ip:#x} default={default:#x}")]
1671    JaOutOfRange { cmp_ip: u64, default: u64 },
1672}
1673
1674/// Encode an MSVC-style switch dispatch sequence:
1675///
1676/// ```text
1677/// cmp <reg>, <max>           ; 3 bytes (imm8 if max≤127) or 6 (imm32)
1678/// ja <default>               ; 6 bytes (rel32)
1679/// jmp dword ptr [<reg>*4 + <table_va>]  ; 7 bytes
1680/// ```
1681///
1682/// `cmp_ip` is the absolute address where the cmp instruction
1683/// will live — used to compute the `ja` rel32. `cases` is the
1684/// number of jump-table entries; the bounds-check uses
1685/// `MAX = cases - 1`. Returns the encoded bytes.
1686///
1687/// This is the inverse of the dispatch-recogniser in
1688/// `ud-decompile`'s `try_switch_pair`. Together they form the
1689/// round-trip: structural Switch → bytes → structural Switch.
1690pub fn encode_msvc_jmp_table_dispatch(
1691    selector: &str,
1692    cases: usize,
1693    default_addr: u64,
1694    table_va: u64,
1695    cmp_ip: u64,
1696) -> std::result::Result<Vec<u8>, SwitchEncodeError> {
1697    let reg_code = gpr32_code(selector)
1698        .ok_or_else(|| SwitchEncodeError::UnsupportedSelector(selector.into()))?;
1699    let max_value = u32::try_from(cases.saturating_sub(1))
1700        .map_err(|_| SwitchEncodeError::TooManyCases(cases))?;
1701
1702    let mut out = Vec::with_capacity(16);
1703
1704    // cmp REG, imm
1705    let cmp_modrm = 0xc0 | (7 << 3) | reg_code; // mod=11, reg=/7, r/m=reg_code
1706    if max_value <= 0x7f {
1707        out.push(0x83);
1708        out.push(cmp_modrm);
1709        out.push(max_value as u8);
1710    } else {
1711        out.push(0x81);
1712        out.push(cmp_modrm);
1713        out.extend_from_slice(&max_value.to_le_bytes());
1714    }
1715
1716    // ja rel32 — target = default; rel = default - (cmp_ip + cmp_len + 6)
1717    let cmp_len = out.len() as u64;
1718    let ja_end = cmp_ip
1719        .checked_add(cmp_len)
1720        .and_then(|x| x.checked_add(6))
1721        .ok_or(SwitchEncodeError::JaOutOfRange {
1722            cmp_ip,
1723            default: default_addr,
1724        })?;
1725    let rel = i128::from(default_addr) - i128::from(ja_end);
1726    let rel32 = i32::try_from(rel).map_err(|_| SwitchEncodeError::JaOutOfRange {
1727        cmp_ip,
1728        default: default_addr,
1729    })?;
1730    out.push(0x0f);
1731    out.push(0x87);
1732    out.extend_from_slice(&rel32.to_le_bytes());
1733
1734    // jmp dword ptr [REG*4 + TABLE_VA]
1735    //   opcode = ff
1736    //   ModR/M = 00_100_100 (mod=00 SIB-follows, reg=/4, r/m=100 SIB)
1737    //   SIB    = 10_<reg_code>_101  (scale=2 bits = *4, index=reg, base=101 no-base+disp32)
1738    //   DISP32 = table_va (LE)
1739    out.push(0xff);
1740    out.push(0x24);
1741    out.push(0x80 | (reg_code << 3) | 0x05);
1742    out.extend_from_slice(&(table_va as u32).to_le_bytes());
1743
1744    Ok(out)
1745}
1746
1747/// 32-bit GPR register-code lookup by name. Returns `None` for
1748/// names that don't map to a usable index/r-m register in the
1749/// switch-dispatch encoding above.
1750fn gpr32_code(name: &str) -> Option<u8> {
1751    match name.trim().to_ascii_lowercase().as_str() {
1752        "eax" => Some(0),
1753        "ecx" => Some(1),
1754        "edx" => Some(2),
1755        "ebx" => Some(3),
1756        // esp (4) can't be an index register in SIB; rejected.
1757        "ebp" => Some(5),
1758        "esi" => Some(6),
1759        "edi" => Some(7),
1760        _ => None,
1761    }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766    use super::*;
1767
1768    /// Encoding regression: the dispatch we lift from the msmpeg4
1769    /// codec's `DriverProc` switch round-trips. Verifies the
1770    /// `cmp ecx, 9; ja 0x23e8; jmp [ecx*4+0x1c20246a]` form
1771    /// re-emits to the exact bytes the compiler put down.
1772    #[test]
1773    fn encode_msmpeg4_driverproc_switch() {
1774        // cmp_ip = 0x208d (where the cmp lives in the original
1775        // .text). default = 0x23e8. table_va = 0x1c20246a.
1776        let bytes = encode_msvc_jmp_table_dispatch("ecx", 10, 0x23e8, 0x1c20_246a, 0x208d).unwrap();
1777        assert_eq!(
1778            bytes,
1779            vec![
1780                0x83, 0xf9, 0x09, // cmp ecx, 9
1781                0x0f, 0x87, 0x52, 0x03, 0x00, 0x00, // ja 0x23e8 (rel32 = 0x352)
1782                0xff, 0x24, 0x8d, 0x6a, 0x24, 0x20, 0x1c, // jmp [ecx*4 + 0x1c20246a]
1783            ]
1784        );
1785    }
1786
1787    /// `endbr64`: 0xf3 0x0f 0x1e 0xfa — gcc with -fcf-protection emits
1788    /// this at every function entry, so the fixtures all start with it.
1789    #[test]
1790    fn endbr64_roundtrips() {
1791        let bytes = [0xf3, 0x0f, 0x1e, 0xfa];
1792        let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1793        assert_eq!(insns.len(), 1);
1794    }
1795
1796    #[test]
1797    fn prologue_roundtrips() {
1798        // push rbp; mov rbp, rsp; sub rsp, 0x20
1799        let bytes = [
1800            0x55, // push rbp
1801            0x48, 0x89, 0xe5, // mov rbp, rsp
1802            0x48, 0x83, 0xec, 0x20, // sub rsp, 0x20
1803        ];
1804        let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1805        assert_eq!(insns.len(), 3);
1806    }
1807
1808    #[test]
1809    fn short_jump_roundtrips() {
1810        let bytes = [0xeb, 0x05];
1811        roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1812    }
1813
1814    #[test]
1815    fn near_jump_roundtrips() {
1816        let bytes = [0xe9, 0x34, 0x12, 0x00, 0x00];
1817        roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1818    }
1819
1820    #[test]
1821    fn call_rel32_roundtrips() {
1822        let bytes = [0xe8, 0x80, 0x00, 0x00, 0x00];
1823        roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1824    }
1825
1826    #[test]
1827    fn xor_zero_idiom_roundtrips() {
1828        let bytes = [0x48, 0x31, 0xc0];
1829        roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1830    }
1831
1832    /// Multi-byte NOP with the redundant 66 data16 prefix — the exact
1833    /// pattern compilers use for alignment, and the one that exposed
1834    /// iced's canonicalization issue. emit_preserved must keep it
1835    /// verbatim; reencode_via_iced is allowed to drop the 66.
1836    #[test]
1837    fn multibyte_nop_with_data16_prefix_preserved() {
1838        let bytes = [0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00];
1839        let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1840        assert_eq!(insns.len(), 1);
1841        assert_eq!(insns[0].original_bytes, bytes);
1842
1843        // iced's encoder drops the redundant prefix; that's fine and
1844        // documented — the emit_preserved path is what guards round-trip.
1845        let reencoded = reencode_via_iced(Bitness::Bits64, &insns, 0x1000).unwrap();
1846        assert!(
1847            reencoded.len() <= bytes.len(),
1848            "iced should produce a shorter or equal canonical encoding"
1849        );
1850    }
1851
1852    #[test]
1853    fn small_function_roundtrips() {
1854        let bytes = [
1855            0xf3, 0x0f, 0x1e, 0xfa, // endbr64
1856            0x55, // push rbp
1857            0x48, 0x89, 0xe5, // mov rbp, rsp
1858            0x31, 0xc0, // xor eax, eax
1859            0x5d, // pop rbp
1860            0xc3, // ret
1861        ];
1862        let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
1863        assert_eq!(insns.len(), 6);
1864    }
1865
1866    #[test]
1867    fn verify_matches_canonical_form() {
1868        let bytes = [0xc3]; // ret
1869        match verify_intel_text(Bitness::Bits64, "ret", &bytes, 0x1000) {
1870            VerifyAsm::Match => {}
1871            other => panic!("expected Match, got {other:?}"),
1872        }
1873    }
1874
1875    #[test]
1876    fn verify_tolerates_whitespace_and_case() {
1877        let bytes = [0x48, 0x89, 0xd8]; // mov rax, rbx
1878        match verify_intel_text(Bitness::Bits64, "MOV  RAX,   RBX", &bytes, 0x1000) {
1879            VerifyAsm::Match => {}
1880            other => panic!("expected Match, got {other:?}"),
1881        }
1882    }
1883
1884    #[test]
1885    fn verify_diverges_when_text_disagrees() {
1886        let bytes = [0xc3]; // ret
1887        match verify_intel_text(Bitness::Bits64, "nop", &bytes, 0x1000) {
1888            VerifyAsm::Diverged { canonical } => {
1889                assert_eq!(canonical, "ret");
1890            }
1891            other => panic!("expected Diverged, got {other:?}"),
1892        }
1893    }
1894
1895    #[test]
1896    fn verify_rejects_multi_insn_byte_sequence() {
1897        // ret; ret  — two instructions in one @asm line is wrong
1898        let bytes = [0xc3, 0xc3];
1899        let result = verify_intel_text(Bitness::Bits64, "ret", &bytes, 0x1000);
1900        assert!(matches!(result, VerifyAsm::MultipleInsns { count: 2 }));
1901    }
1902
1903    #[test]
1904    fn verify_rejects_undecodable_bytes() {
1905        let bytes = [0x06]; // invalid in 64-bit mode
1906        let result = verify_intel_text(Bitness::Bits64, "ret", &bytes, 0x1000);
1907        assert!(matches!(result, VerifyAsm::Undecodable));
1908    }
1909
1910    #[test]
1911    fn lift_return_recognizes_mov_eax_pop_rbp_ret() {
1912        // mov eax, 0; pop rbp; ret
1913        let bytes = [0xb8, 0x00, 0x00, 0x00, 0x00, 0x5d, 0xc3];
1914        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1915        let lifted = try_lift_return_pattern(&insns).unwrap();
1916        assert_eq!(lifted.value, 0);
1917        assert_eq!(lifted.insns_consumed, 3);
1918    }
1919
1920    #[test]
1921    fn lift_return_recognizes_xor_zero_pop_rbp_ret() {
1922        // xor eax, eax; pop rbp; ret
1923        let bytes = [0x31, 0xc0, 0x5d, 0xc3];
1924        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1925        let lifted = try_lift_return_pattern(&insns).unwrap();
1926        assert_eq!(lifted.value, 0);
1927        assert_eq!(lifted.insns_consumed, 3);
1928    }
1929
1930    #[test]
1931    fn lift_return_recognizes_mov_eax_leave_ret() {
1932        // mov eax, 1; leave; ret
1933        let bytes = [0xb8, 0x01, 0x00, 0x00, 0x00, 0xc9, 0xc3];
1934        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1935        let lifted = try_lift_return_pattern(&insns).unwrap();
1936        assert_eq!(lifted.value, 1);
1937        assert_eq!(lifted.insns_consumed, 3);
1938    }
1939
1940    #[test]
1941    fn lift_return_recognizes_mov_ret_without_epilogue() {
1942        // mov eax, 42; ret
1943        let bytes = [0xb8, 0x2a, 0x00, 0x00, 0x00, 0xc3];
1944        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1945        let lifted = try_lift_return_pattern(&insns).unwrap();
1946        assert_eq!(lifted.value, 0x2a);
1947        assert_eq!(lifted.insns_consumed, 2);
1948    }
1949
1950    #[test]
1951    fn lift_return_rejects_bare_ret() {
1952        // ret only — no value-setter, no pattern
1953        let bytes = [0xc3];
1954        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1955        assert!(try_lift_return_pattern(&insns).is_none());
1956    }
1957
1958    #[test]
1959    fn lift_return_rejects_unrecognized_setter() {
1960        // mov rax, rbx; ret — not a literal value
1961        let bytes = [0x48, 0x89, 0xd8, 0xc3];
1962        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1963        assert!(try_lift_return_pattern(&insns).is_none());
1964    }
1965
1966    #[test]
1967    fn lift_epilogue_recognizes_leave_ret() {
1968        let bytes = [0xc9, 0xc3];
1969        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1970        let lifted = try_lift_epilogue_pattern(&insns).unwrap();
1971        assert_eq!(lifted.kind, "std");
1972        assert_eq!(lifted.insns_consumed, 2);
1973    }
1974
1975    #[test]
1976    fn lift_epilogue_recognizes_pop_rbp_ret() {
1977        let bytes = [0x5d, 0xc3];
1978        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1979        let lifted = try_lift_epilogue_pattern(&insns).unwrap();
1980        assert_eq!(lifted.kind, "std-pop-rbp");
1981        assert_eq!(lifted.insns_consumed, 2);
1982    }
1983
1984    #[test]
1985    fn lift_epilogue_bare_ret_is_minimal_kind() {
1986        // A standalone `ret` lifts as the minimal `"ret"` epilogue —
1987        // no register restore to fold in, but the kind tells the
1988        // reader they're at a function exit rather than mid-flow.
1989        let bytes = [0xc3];
1990        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
1991        let lifted = try_lift_epilogue_pattern(&insns).expect("bare ret should lift");
1992        assert_eq!(lifted.kind, "ret");
1993        assert_eq!(lifted.insns_consumed, 1);
1994    }
1995
1996    #[test]
1997    fn lift_epilogue_non_teardown_predecessor_lifts_only_the_ret() {
1998        // `mov rax, rbx; ret` — the mov isn't part of an epilogue
1999        // (it's a value materialisation that landed before the
2000        // exit), so the lift consumes only the trailing ret.
2001        let bytes = [0x48, 0x89, 0xd8, 0xc3];
2002        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2003        let lifted = try_lift_epilogue_pattern(&insns).expect("ret should lift");
2004        assert_eq!(lifted.kind, "ret");
2005        assert_eq!(lifted.insns_consumed, 1);
2006    }
2007
2008    #[test]
2009    fn lift_prologue_full_std() {
2010        // endbr64; push rbp; mov rbp,rsp; sub rsp,0x10
2011        let bytes = [
2012            0xf3, 0x0f, 0x1e, 0xfa, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x10,
2013        ];
2014        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2015        let lifted = try_lift_prologue_pattern(&insns).unwrap();
2016        assert_eq!(lifted.kind, "std");
2017        assert_eq!(lifted.insns_consumed, 4);
2018    }
2019
2020    #[test]
2021    fn lift_prologue_without_sub() {
2022        // endbr64; push rbp; mov rbp,rsp (no sub rsp)
2023        let bytes = [0xf3, 0x0f, 0x1e, 0xfa, 0x55, 0x48, 0x89, 0xe5];
2024        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2025        let lifted = try_lift_prologue_pattern(&insns).unwrap();
2026        assert_eq!(lifted.kind, "std");
2027        assert_eq!(lifted.insns_consumed, 3);
2028    }
2029
2030    #[test]
2031    fn lift_prologue_no_cf_protection() {
2032        // push rbp; mov rbp,rsp; sub rsp,0x20  (no endbr64)
2033        let bytes = [0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20];
2034        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2035        let lifted = try_lift_prologue_pattern(&insns).unwrap();
2036        assert_eq!(lifted.kind, "std-no-cf");
2037        assert_eq!(lifted.insns_consumed, 3);
2038    }
2039
2040    #[test]
2041    fn lift_prologue_noframe() {
2042        // Lone endbr64 (leaf function, no frame setup)
2043        let bytes = [0xf3, 0x0f, 0x1e, 0xfa, 0x31, 0xc0, 0xc3]; // endbr64; xor eax,eax; ret
2044        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2045        let lifted = try_lift_prologue_pattern(&insns).unwrap();
2046        assert_eq!(lifted.kind, "std-noframe");
2047        assert_eq!(lifted.insns_consumed, 1);
2048    }
2049
2050    #[test]
2051    fn lift_prologue_rejects_nonstandard() {
2052        // mov rax, rbx — not a prologue
2053        let bytes = [0x48, 0x89, 0xd8];
2054        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2055        assert!(try_lift_prologue_pattern(&insns).is_none());
2056    }
2057
2058    #[test]
2059    fn arg_spill_recognizes_int_register_to_stack() {
2060        // mov [rbp-4], edi
2061        let bytes = [0x89, 0x7d, 0xfc];
2062        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2063        assert_eq!(arg_spill_index(&insns[0].iced), Some(0));
2064    }
2065
2066    #[test]
2067    fn arg_spill_recognizes_xmm_register_to_stack() {
2068        // movsd [rbp-0x10], xmm0
2069        let bytes = [0xf2, 0x0f, 0x11, 0x45, 0xf0];
2070        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2071        assert_eq!(arg_spill_index(&insns[0].iced), Some(0));
2072    }
2073
2074    #[test]
2075    fn arg_spill_rejects_non_arg_register() {
2076        // mov [rbp-4], eax (eax is not a SysV arg register)
2077        let bytes = [0x89, 0x45, 0xfc];
2078        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2079        assert_eq!(arg_spill_index(&insns[0].iced), None);
2080    }
2081
2082    #[test]
2083    fn arg_spill_rejects_non_rbp_dst() {
2084        // mov [rax-4], edi (not rbp-relative)
2085        let bytes = [0x89, 0x78, 0xfc];
2086        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2087        assert_eq!(arg_spill_index(&insns[0].iced), None);
2088    }
2089
2090    #[test]
2091    fn lift_return_via_jmp_recognizes_mov_jmp_short() {
2092        // mov eax, 1; jmp short +0x14
2093        // Block at 0x1000, jmp short rel8 lands at 0x1009 + signed 0x14 = 0x101d
2094        let bytes = [0xb8, 0x01, 0x00, 0x00, 0x00, 0xeb, 0x14];
2095        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2096        let lifted = try_lift_return_via_jmp(&insns, 0x101b).unwrap();
2097        assert_eq!(lifted.value, 1);
2098        assert_eq!(lifted.insns_consumed, 2);
2099    }
2100
2101    #[test]
2102    fn lift_return_via_jmp_rejects_wrong_target() {
2103        let bytes = [0xb8, 0x01, 0x00, 0x00, 0x00, 0xeb, 0x14];
2104        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2105        // pass a non-matching epilogue address
2106        assert!(try_lift_return_via_jmp(&insns, 0x9999).is_none());
2107    }
2108
2109    #[test]
2110    fn lift_return_via_jmp_rejects_non_setter() {
2111        // mov rax, rbx; jmp +5
2112        let bytes = [0x48, 0x89, 0xd8, 0xeb, 0x05];
2113        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2114        let target = insns.last().unwrap().iced.near_branch_target();
2115        assert!(try_lift_return_via_jmp(&insns, target).is_none());
2116    }
2117
2118    #[test]
2119    fn lift_return_only_consumes_tail() {
2120        // some_other_insn; mov eax, 0; ret
2121        let bytes = [0x90, 0xb8, 0x00, 0x00, 0x00, 0x00, 0xc3]; // nop, mov, ret
2122        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2123        let lifted = try_lift_return_pattern(&insns).unwrap();
2124        assert_eq!(lifted.insns_consumed, 2);
2125    }
2126
2127    #[test]
2128    fn invalid_bytes_fail_decode() {
2129        let bytes = [0x06];
2130        assert!(matches!(
2131            roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000),
2132            Err(Error::DecodeFailed { .. })
2133        ));
2134    }
2135
2136    #[test]
2137    fn lift_if_branch_head_recognizes_cmp_jne() {
2138        // cmp dword ptr [rbp-4], 1; jne short 0x1007 (rel8 = +1)
2139        // Block at 0x1000: cmp is 4 bytes (ends at 0x1004), jne short
2140        // is 2 bytes at 0x1004; rel8=1 lands at 0x1004+2+1 = 0x1007.
2141        let bytes = [0x83, 0x7d, 0xfc, 0x01, 0x75, 0x01];
2142        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2143        let lifted = try_lift_if_branch_head(&insns).expect("should match");
2144        assert_eq!(lifted.insns_consumed, 2);
2145        assert_eq!(lifted.jcc_target, 0x1007);
2146        assert_eq!(lifted.cond_bytes, bytes.to_vec());
2147        // `cmp X,1; jne` → body runs when X == 1 → `"X == 1"`.
2148        assert!(
2149            lifted.cond_text.contains("=="),
2150            "got cond_text: {}",
2151            lifted.cond_text
2152        );
2153    }
2154
2155    #[test]
2156    fn lift_if_branch_head_recognizes_test_je() {
2157        // test eax, eax; je short 0x1004 (off=0)
2158        let bytes = [0x85, 0xc0, 0x74, 0x00];
2159        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2160        let lifted = try_lift_if_branch_head(&insns).expect("should match");
2161        assert_eq!(lifted.insns_consumed, 2);
2162        assert_eq!(lifted.jcc_target, 0x1004);
2163        // `test eax,eax; je` → body runs when eax != 0 → `"eax != 0"`.
2164        assert_eq!(lifted.cond_text, "eax != 0");
2165    }
2166
2167    #[test]
2168    fn lift_if_branch_head_rejects_unconditional_jmp() {
2169        // cmp eax,0; jmp +5 — second insn is not a conditional branch
2170        let bytes = [0x83, 0xf8, 0x00, 0xeb, 0x05];
2171        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2172        assert!(try_lift_if_branch_head(&insns).is_none());
2173    }
2174
2175    #[test]
2176    fn direct_lea_rip_target_resolves_rip_relative_load() {
2177        // lea rax, [rip+0x10]  encoded as 48 8d 05 10 00 00 00 (7 bytes).
2178        // Block at 0x1000; rip-after-this-insn = 0x1007; target = 0x1017.
2179        let bytes = [0x48, 0x8d, 0x05, 0x10, 0x00, 0x00, 0x00];
2180        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2181        assert_eq!(direct_lea_rip_target(&insns[0].iced), Some(0x1017));
2182    }
2183
2184    #[test]
2185    fn direct_lea_rip_target_rejects_non_lea() {
2186        // mov rax, [rip+0x10]  (also rip-relative but not lea)
2187        let bytes = [0x48, 0x8b, 0x05, 0x10, 0x00, 0x00, 0x00];
2188        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2189        assert_eq!(direct_lea_rip_target(&insns[0].iced), None);
2190    }
2191
2192    #[test]
2193    fn direct_lea_rip_target_rejects_non_rip_base() {
2194        // lea rax, [rbx+0x10] — base is rbx, not rip.
2195        let bytes = [0x48, 0x8d, 0x43, 0x10];
2196        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2197        assert_eq!(direct_lea_rip_target(&insns[0].iced), None);
2198    }
2199
2200    #[test]
2201    fn lift_if_branch_head_rejects_non_compare_predecessor() {
2202        // mov eax, ebx; je short +5 — first insn is not cmp/test
2203        let bytes = [0x89, 0xd8, 0x74, 0x05];
2204        let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
2205        assert!(try_lift_if_branch_head(&insns).is_none());
2206    }
2207}