1#![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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct LiftedReturn {
94 pub insns_consumed: usize,
95 pub value: u64,
96}
97
98#[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 let ret = insns.get(i - 1)?;
123 if ret.original_bytes.as_slice() != [0xc3] {
124 return None;
125 }
126 i -= 1;
127
128 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 let setter = &insns[i - 1].original_bytes;
142 let value = match setter.as_slice() {
143 [0xb8, b0, b1, b2, b3] => u64::from(u32::from_le_bytes([*b0, *b1, *b2, *b3])),
145 [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#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct LiftedPrologue {
160 pub insns_consumed: usize,
162 pub kind: &'static str,
166}
167
168#[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 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 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 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 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 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 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 if !has_endbr && saves_count == 0 && !has_frame && !has_sub {
281 return None;
282 }
283 let _ = push_start; let kind = match (has_endbr, saves_count > 0, has_frame, has_sub) {
286 (true, false, false, false) => "std-noframe",
288 (true, false, false, true) => "thin",
290 (false, false, false, true) => "thin-no-cf",
291 (true, false, true, _) => "std",
294 (false, false, true, _) => "std-no-cf",
295 (true, true, false, false) => "saves-cf",
297 (false, true, false, false) => "saves",
298 (true, true, true, _) => "saves-std",
300 (false, true, true, _) => "saves-std-no-cf",
301 (_, true, false, true) => "saves-thin",
303 (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#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct LiftedEpilogue {
317 pub insns_consumed: usize,
319 pub kind: &'static str,
322}
323
324#[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 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 if is_ret_bare {
357 let two_kind = match prev.as_slice() {
358 [0xc9] => Some("std"), [0x5d] => Some("std-pop-rbp"), _ => None,
361 };
362 if let Some(kind) = two_kind {
363 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 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 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 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#[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 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#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct LiftedIfBranchHead {
502 pub insns_consumed: usize,
505 pub cond_text: String,
508 pub cond_bytes: Vec<u8>,
511 pub jcc_target: u64,
514}
515
516#[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 let target = jcc.iced.near_branch_target();
540 if target == 0 {
541 return None;
542 }
543
544 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 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 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#[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
619fn 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 return None;
648 }
649 if stable >= 8 {
650 let off = u64::try_from(stable).ok()?;
651 return Some(format!("arg_{off:x}"));
652 }
653 let off = u64::try_from(-stable).ok()?;
656 Some(format!("var_{off:x}"))
657}
658
659#[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#[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#[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#[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#[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 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#[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 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 Je => "!=",
841 Jne => "==",
842 Jl => ">=",
845 Jle => ">",
846 Jg => "<=",
847 Jge => "<",
848 Jb => ">=u",
851 Jbe => ">u",
852 Ja => "<=u",
853 Jae => "<u",
854 _ => return None,
855 })
856}
857
858fn 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#[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)] fn 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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
1228pub enum VerifyAsm {
1229 Match,
1231 Diverged { canonical: String },
1234 Undecodable,
1236 MultipleInsns { count: usize },
1238}
1239
1240#[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
1279fn 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#[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#[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#[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
1357pub 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#[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#[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
1427pub 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
1442pub 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#[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
1477pub 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#[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
1530pub 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
1572pub 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#[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#[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#[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#[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#[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
1674pub 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 let cmp_modrm = 0xc0 | (7 << 3) | reg_code; 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 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 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
1747fn 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 "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 #[test]
1773 fn encode_msmpeg4_driverproc_switch() {
1774 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, 0x0f, 0x87, 0x52, 0x03, 0x00, 0x00, 0xff, 0x24, 0x8d, 0x6a, 0x24, 0x20, 0x1c, ]
1784 );
1785 }
1786
1787 #[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 let bytes = [
1800 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, ];
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 #[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 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, 0x55, 0x48, 0x89, 0xe5, 0x31, 0xc0, 0x5d, 0xc3, ];
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]; 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]; 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]; 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 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]; 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 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 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 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 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 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 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 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 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 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 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 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 let bytes = [0xf3, 0x0f, 0x1e, 0xfa, 0x31, 0xc0, 0xc3]; 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 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 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 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 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 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 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 assert!(try_lift_return_via_jmp(&insns, 0x9999).is_none());
2107 }
2108
2109 #[test]
2110 fn lift_return_via_jmp_rejects_non_setter() {
2111 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 let bytes = [0x90, 0xb8, 0x00, 0x00, 0x00, 0x00, 0xc3]; 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 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 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 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 assert_eq!(lifted.cond_text, "eax != 0");
2165 }
2166
2167 #[test]
2168 fn lift_if_branch_head_rejects_unconditional_jmp() {
2169 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 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 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 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 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}