#![allow(clippy::cast_possible_truncation)]
use iced_x86::{
BlockEncoder, BlockEncoderOptions, Decoder, DecoderOptions, Formatter, InstructionBlock,
IntelFormatter,
};
pub use iced_x86::{
CodeSize, FlowControl, Instruction, InstructionInfoFactory, MemorySize, Mnemonic, OpAccess,
OpKind, Register, UsedRegister,
};
use ud_core::VAddr;
use ud_ir::ArchInsn;
mod assemble;
mod call_site;
mod codec;
mod encode_text;
mod expr;
mod lift;
mod prologue_codec;
pub use assemble::{assemble_intel, AssembleError};
pub use call_site::{
detect_post_call_spill, identify_call_sites, ArgValue, CallSite, PostCallSpill,
};
pub use codec::{register, X86Codec};
pub use encode_text::{encode_cmp_or_test, encode_head_from_cond_text};
pub use expr::{try_lift_value_block, ExprRenderCtx, LiftedValueBlock, ValueExpr};
pub use lift::{lift_function, LiftError};
pub use prologue_codec::{
decode_epilogue, decode_prologue, default_epilogue, default_prologue, encode_epilogue,
encode_prologue, epilogue_roundtrips, prologue_roundtrips, CodecBits, ProfileInputs,
StructuredEpilogue, StructuredPrologue,
};
#[must_use]
pub fn direct_call_target(insn: &Instruction) -> Option<u64> {
match insn.flow_control() {
FlowControl::Call => Some(insn.near_branch_target()),
_ => None,
}
}
#[must_use]
pub fn is_function_terminator(insn: &Instruction) -> bool {
matches!(
insn.flow_control(),
FlowControl::Return
| FlowControl::UnconditionalBranch
| FlowControl::IndirectBranch
| FlowControl::Interrupt
| FlowControl::Exception,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LiftedReturn {
pub insns_consumed: usize,
pub value: u64,
}
#[must_use]
pub fn try_lift_return_pattern(insns: &[DecodedInsn]) -> Option<LiftedReturn> {
if insns.is_empty() {
return None;
}
let mut i = insns.len();
let ret = insns.get(i - 1)?;
if ret.original_bytes.as_slice() != [0xc3] {
return None;
}
i -= 1;
if i > 0 {
let prev = &insns[i - 1].original_bytes;
if prev.as_slice() == [0x5d] || prev.as_slice() == [0xc9] {
i -= 1;
}
}
if i == 0 {
return None;
}
let setter = &insns[i - 1].original_bytes;
let value = match setter.as_slice() {
[0xb8, b0, b1, b2, b3] => u64::from(u32::from_le_bytes([*b0, *b1, *b2, *b3])),
[0x31, 0xc0] => 0,
_ => return None,
};
i -= 1;
Some(LiftedReturn {
insns_consumed: insns.len() - i,
value,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiftedPrologue {
pub insns_consumed: usize,
pub kind: &'static str,
}
#[must_use]
pub fn try_lift_prologue_pattern(insns: &[DecodedInsn]) -> Option<LiftedPrologue> {
let endbr64: &[u8] = &[0xf3, 0x0f, 0x1e, 0xfa];
let endbr32: &[u8] = &[0xf3, 0x0f, 0x1e, 0xfb];
let mov_rbp_rsp_64_dst: &[u8] = &[0x48, 0x89, 0xe5];
let mov_rbp_rsp_64_src: &[u8] = &[0x48, 0x8b, 0xec];
let mov_ebp_esp_32_dst: &[u8] = &[0x89, 0xe5];
let mov_ebp_esp_32_src: &[u8] = &[0x8b, 0xec];
let is_mov_bp_sp = |b: &[u8]| {
b == mov_rbp_rsp_64_dst
|| b == mov_rbp_rsp_64_src
|| b == mov_ebp_esp_32_dst
|| b == mov_ebp_esp_32_src
};
let bytes_at = |i: usize| insns.get(i).map(|d| d.original_bytes.as_slice());
let (has_endbr, mut start) = match bytes_at(0) {
Some(b) if b == endbr64 || b == endbr32 => (true, 1),
_ => (false, 0),
};
let push_start = start;
let mut pushes: Vec<u8> = Vec::new();
while let Some(b) = bytes_at(start) {
if b.len() == 1 && (0x50..=0x57).contains(&b[0]) {
pushes.push(b[0]);
start += 1;
} else {
break;
}
}
let mut has_frame = false;
if matches!(pushes.last(), Some(&0x55)) {
if let Some(b) = bytes_at(start) {
if is_mov_bp_sp(b) {
has_frame = true;
start += 1;
}
}
}
let mut saves_count = pushes.len() - usize::from(has_frame);
let sub_matched = matches!(
bytes_at(start),
Some(
&[0x48, 0x83, 0xec, _]
| &[0x48, 0x81, 0xec, _, _, _, _]
| &[0x83, 0xec, _]
| &[0x81, 0xec, _, _, _, _]
)
);
let has_sub = sub_matched && (has_frame || saves_count == 0);
if has_sub {
start += 1;
}
if has_frame {
while let Some(b) = bytes_at(start) {
if b.len() == 1 && (0x50..=0x57).contains(&b[0]) {
saves_count += 1;
start += 1;
} else {
break;
}
}
}
if !has_endbr && saves_count == 0 && !has_frame && !has_sub {
return None;
}
let _ = push_start;
let kind = match (has_endbr, saves_count > 0, has_frame, has_sub) {
(true, false, false, false) => "std-noframe",
(true, false, false, true) => "thin",
(false, false, false, true) => "thin-no-cf",
(true, false, true, _) => "std",
(false, false, true, _) => "std-no-cf",
(true, true, false, false) => "saves-cf",
(false, true, false, false) => "saves",
(true, true, true, _) => "saves-std",
(false, true, true, _) => "saves-std-no-cf",
(_, true, false, true) => "saves-thin",
(false, false, false, false) => unreachable!("nothing-to-lift case already returned None"),
};
Some(LiftedPrologue {
insns_consumed: start,
kind,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiftedEpilogue {
pub insns_consumed: usize,
pub kind: &'static str,
}
#[must_use]
pub fn try_lift_epilogue_pattern(insns: &[DecodedInsn]) -> Option<LiftedEpilogue> {
if insns.is_empty() {
return None;
}
let last = insns.last()?;
let last_b = last.original_bytes.as_slice();
let is_ret_bare = last_b == [0xc3];
let is_ret_imm = matches!(last_b, [0xc2, _, _]);
if !is_ret_bare && !is_ret_imm {
return None;
}
if insns.len() >= 2 {
let prev = &insns[insns.len() - 2].original_bytes;
if is_ret_bare {
let two_kind = match prev.as_slice() {
[0xc9] => Some("std"), [0x5d] => Some("std-pop-rbp"), _ => None,
};
if let Some(kind) = two_kind {
if kind == "std-pop-rbp" && insns.len() >= 3 {
let before_pop = insns[insns.len() - 3].original_bytes.as_slice();
if is_add_rsp_imm(before_pop) {
return Some(LiftedEpilogue {
insns_consumed: 3,
kind: "thin-pop-rbp",
});
}
}
return Some(LiftedEpilogue {
insns_consumed: 2,
kind,
});
}
if is_add_rsp_imm(prev.as_slice()) {
return Some(LiftedEpilogue {
insns_consumed: 2,
kind: "thin",
});
}
}
}
let mut restore_count = 0usize;
if insns.len() >= 2 {
for i in (0..insns.len() - 1).rev() {
let b = insns[i].original_bytes.as_slice();
let is_pop_reg = b.len() == 1 && (0x58..=0x5f).contains(&b[0]);
let is_leave = b == [0xc9];
let is_add_rsp = is_add_rsp_imm(b);
if is_pop_reg || is_leave || is_add_rsp {
restore_count += 1;
} else {
break;
}
}
}
if restore_count >= 1 {
let kind = if is_ret_imm { "saves-imm" } else { "saves" };
return Some(LiftedEpilogue {
insns_consumed: restore_count + 1,
kind,
});
}
if is_ret_imm {
return Some(LiftedEpilogue {
insns_consumed: 1,
kind: "ret-imm",
});
}
if is_ret_bare {
return Some(LiftedEpilogue {
insns_consumed: 1,
kind: "ret",
});
}
None
}
fn is_add_rsp_imm(bytes: &[u8]) -> bool {
matches!(
bytes,
[0x48, 0x83, 0xc4, _]
| [0x48, 0x81, 0xc4, _, _, _, _]
| [0x83, 0xc4, _]
| [0x81, 0xc4, _, _, _, _]
)
}
#[must_use]
pub fn try_lift_return_via_jmp(insns: &[DecodedInsn], epilogue_addr: u64) -> Option<LiftedReturn> {
if insns.len() < 2 {
return None;
}
let last = insns.last()?;
if last.iced.flow_control() != FlowControl::UnconditionalBranch {
return None;
}
if last.iced.near_branch_target() != epilogue_addr {
return None;
}
let setter = &insns[insns.len() - 2].original_bytes;
let value = match setter.as_slice() {
[0xb8, b0, b1, b2, b3] => u64::from(u32::from_le_bytes([*b0, *b1, *b2, *b3])),
[0x31, 0xc0] => 0,
_ => return None,
};
Some(LiftedReturn {
insns_consumed: 2,
value,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiftedIfBranchHead {
pub insns_consumed: usize,
pub cond_text: String,
pub cond_bytes: Vec<u8>,
pub jcc_target: u64,
}
#[must_use]
pub fn try_lift_if_branch_head(insns: &[DecodedInsn]) -> Option<LiftedIfBranchHead> {
if insns.len() < 2 {
return None;
}
let jcc = insns.last()?;
if jcc.iced.flow_control() != FlowControl::ConditionalBranch {
return None;
}
let target = jcc.iced.near_branch_target();
if target == 0 {
return None;
}
let cmp_idx = insns.len() - 2;
let cmp = &insns[cmp_idx];
if matches!(cmp.iced.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
let cond_text = render_cond_source(&cmp.iced, &jcc.iced);
let mut cond_bytes =
Vec::with_capacity(cmp.original_bytes.len() + jcc.original_bytes.len());
cond_bytes.extend_from_slice(&cmp.original_bytes);
cond_bytes.extend_from_slice(&jcc.original_bytes);
return Some(LiftedIfBranchHead {
insns_consumed: 2,
cond_text,
cond_bytes,
jcc_target: target,
});
}
let mut probe = insns.len() - 2;
loop {
let ins = &insns[probe];
if matches!(ins.iced.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
let cond_text = render_cond_source(&ins.iced, &jcc.iced);
return Some(LiftedIfBranchHead {
insns_consumed: 1,
cond_text,
cond_bytes: jcc.original_bytes.clone(),
jcc_target: target,
});
}
if ins.iced.rflags_modified() != 0 {
return None;
}
if probe == 0 {
return None;
}
probe -= 1;
}
}
#[must_use]
pub fn rename_operand_with_ctx(text: &str, sp_delta: Option<i64>) -> Option<String> {
if let Some(name) = rename_ebp_slot(text) {
return Some(name);
}
if let Some(delta) = sp_delta {
return rename_esp_slot(text, delta);
}
None
}
fn rename_esp_slot(text: &str, sp_delta: i64) -> Option<String> {
let core = text.strip_prefix("dword ptr ").unwrap_or(text);
let core = core.strip_prefix("qword ptr ").unwrap_or(core);
let inner = core
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))?
.trim();
let disp: i64 = if inner == "esp" {
0
} else if let Some(rest) = inner.strip_prefix("esp+") {
let off = parse_unsigned_disp(rest.trim())?;
i64::try_from(off).ok()?
} else if let Some(rest) = inner.strip_prefix("esp-") {
let off = parse_unsigned_disp(rest.trim())?;
-(i64::try_from(off).ok()?)
} else {
return None;
};
let stable = sp_delta + disp + 4;
if stable == 0 || stable == 4 {
return None;
}
if stable >= 8 {
let off = u64::try_from(stable).ok()?;
return Some(format!("arg_{off:x}"));
}
let off = u64::try_from(-stable).ok()?;
Some(format!("var_{off:x}"))
}
#[must_use]
pub fn compute_sp_delta_table(insns: &[DecodedInsn]) -> std::collections::HashMap<u64, i64> {
use std::collections::HashMap;
let mut out: HashMap<u64, i64> = HashMap::with_capacity(insns.len());
let mut delta: i64 = 0;
for ins in insns {
out.insert(ins.iced.ip(), delta);
delta = delta.saturating_add(sp_change_for(&ins.iced));
}
out
}
#[must_use]
pub fn sp_change_for(insn: &Instruction) -> i64 {
let intrinsic = i64::from(insn.stack_pointer_increment());
if intrinsic != 0 {
return intrinsic;
}
match insn.mnemonic() {
Mnemonic::Sub | Mnemonic::Add => {
if insn.op0_kind() != OpKind::Register {
return 0;
}
let r = insn.op0_register();
if r != Register::ESP && r != Register::RSP {
return 0;
}
let imm = match insn.op1_kind() {
OpKind::Immediate8to32 | OpKind::Immediate8to64 | OpKind::Immediate8 => {
#[allow(clippy::cast_possible_wrap)]
let v = i64::from(insn.immediate8() as i8);
v
}
OpKind::Immediate32 | OpKind::Immediate32to64 => {
#[allow(clippy::cast_possible_wrap)]
let v = i64::from(insn.immediate32() as i32);
v
}
_ => return 0,
};
if insn.mnemonic() == Mnemonic::Sub {
-imm
} else {
imm
}
}
_ => 0,
}
}
#[must_use]
pub fn rename_ebp_slot(text: &str) -> Option<String> {
let core = text.strip_prefix("dword ptr ").unwrap_or(text);
let core = core.strip_prefix("qword ptr ").unwrap_or(core);
let inner = core
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))?
.trim();
if inner == "ebp" {
return Some("var_0".into());
}
if let Some(rest) = inner.strip_prefix("ebp+") {
let offset = parse_unsigned_disp(rest.trim())?;
return Some(format!("arg_{offset:x}"));
}
if let Some(rest) = inner.strip_prefix("ebp-") {
let offset = parse_unsigned_disp(rest.trim())?;
return Some(format!("var_{offset:x}"));
}
None
}
#[must_use]
pub fn rename_operand_if_slot(text: &str) -> String {
rename_ebp_slot(text).unwrap_or_else(|| text.to_string())
}
#[must_use]
pub fn rename_operand_in_ctx(text: &str, sp_delta: Option<i64>) -> String {
rename_operand_with_ctx(text, sp_delta).unwrap_or_else(|| text.to_string())
}
fn parse_unsigned_disp(s: &str) -> Option<u64> {
if s.is_empty() || !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
return u64::from_str_radix(hex, 16).ok();
}
if let Some(hex) = s.strip_suffix('h').or_else(|| s.strip_suffix('H')) {
return u64::from_str_radix(hex, 16).ok();
}
s.parse::<u64>().ok()
}
#[must_use]
pub fn render_cond_source(cmp: &Instruction, jcc: &Instruction) -> String {
let cmp_text = format_intel(cmp);
let Some((lhs_raw, rhs_raw)) = split_cmp_test_operands(&cmp_text) else {
return format!("{cmp_text}; {}", format_intel(jcc));
};
let is_test = cmp.mnemonic() == Mnemonic::Test;
let Some(op) = body_operator_from_jcc(jcc.mnemonic()) else {
return format!("{cmp_text}; {}", format_intel(jcc));
};
let lhs = rename_operand_if_slot(&lhs_raw);
let rhs = rename_operand_if_slot(&rhs_raw);
if is_test && lhs_raw == rhs_raw {
let signed_op = op.strip_suffix('u').unwrap_or(op);
return format!("{lhs} {signed_op} 0");
}
format!("{lhs} {op} {rhs}")
}
fn body_operator_from_jcc(jcc: Mnemonic) -> Option<&'static str> {
use Mnemonic::{Ja, Jae, Jb, Jbe, Je, Jg, Jge, Jl, Jle, Jne};
Some(match jcc {
Je => "!=",
Jne => "==",
Jl => ">=",
Jle => ">",
Jg => "<=",
Jge => "<",
Jb => ">=u",
Jbe => ">u",
Ja => "<=u",
Jae => "<u",
_ => return None,
})
}
fn split_cmp_test_operands(formatted: &str) -> Option<(String, String)> {
let rest = formatted
.strip_prefix("cmp ")
.or_else(|| formatted.strip_prefix("test "))?;
let mut depth = 0i32;
for (i, ch) in rest.char_indices() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
',' if depth == 0 => {
let (l, r) = rest.split_at(i);
return Some((l.trim().to_string(), r[1..].trim().to_string()));
}
_ => {}
}
}
None
}
#[must_use]
pub fn arg_spill_index(insn: &Instruction) -> Option<u32> {
let m = insn.mnemonic();
if !matches!(m, Mnemonic::Mov | Mnemonic::Movss | Mnemonic::Movsd) {
return None;
}
if insn.op_count() < 2 {
return None;
}
if insn.op0_kind() != OpKind::Memory {
return None;
}
if insn.memory_base() != Register::RBP {
return None;
}
if insn.op1_kind() != OpKind::Register {
return None;
}
sysv_arg_index(insn.op_register(1))
}
#[allow(clippy::match_same_arms)] fn sysv_arg_index(reg: Register) -> Option<u32> {
Some(match reg {
Register::RDI | Register::EDI | Register::DI | Register::DIL | Register::XMM0 => 0,
Register::RSI | Register::ESI | Register::SI | Register::SIL | Register::XMM1 => 1,
Register::RDX | Register::EDX | Register::DX | Register::DL | Register::XMM2 => 2,
Register::RCX | Register::ECX | Register::CX | Register::CL | Register::XMM3 => 3,
Register::R8 | Register::R8D | Register::R8W | Register::R8L | Register::XMM4 => 4,
Register::R9 | Register::R9D | Register::R9W | Register::R9L | Register::XMM5 => 5,
_ => return None,
})
}
#[must_use]
pub fn direct_unconditional_branch_target(insn: &Instruction) -> Option<u64> {
match insn.flow_control() {
FlowControl::UnconditionalBranch => Some(insn.near_branch_target()),
_ => None,
}
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn signed_memory_displacement(insn: &Instruction) -> i64 {
let raw = insn.memory_displacement64();
let is_32bit_addressing = matches!(
insn.memory_base(),
Register::EAX
| Register::EBX
| Register::ECX
| Register::EDX
| Register::ESI
| Register::EDI
| Register::EBP
| Register::ESP
| Register::EIP
);
if is_32bit_addressing {
i64::from(raw as i32)
} else {
raw as i64
}
}
#[must_use]
pub fn match_local_arith_immediate(insn: &Instruction) -> Option<(i64, &'static str, i64)> {
let op = match insn.mnemonic() {
Mnemonic::Add => "+=",
Mnemonic::Sub => "-=",
_ => return None,
};
if insn.op_count() != 2 {
return None;
}
if insn.op0_kind() != OpKind::Memory {
return None;
}
if !matches!(insn.memory_base(), Register::RBP | Register::EBP) {
return None;
}
if insn.memory_index() != Register::None {
return None;
}
#[allow(clippy::cast_possible_wrap)]
let value = match insn.op1_kind() {
OpKind::Immediate8 => i64::from(insn.immediate8() as i8),
OpKind::Immediate16 => i64::from(insn.immediate16() as i16),
OpKind::Immediate32 => i64::from(insn.immediate32() as i32),
OpKind::Immediate64 => insn.immediate64() as i64,
OpKind::Immediate8to16 => i64::from(insn.immediate8to16()),
OpKind::Immediate8to32 => i64::from(insn.immediate8to32()),
OpKind::Immediate8to64 => insn.immediate8to64(),
OpKind::Immediate32to64 => insn.immediate32to64(),
_ => return None,
};
Some((signed_memory_displacement(insn), op, value))
}
#[must_use]
pub fn match_local_compound(insns: &[Instruction]) -> Option<(usize, i64, &'static str, i64)> {
if insns.len() >= 2 {
let i0 = &insns[0];
let i1 = &insns[1];
if let Some(out) = match_compound_two(i0, i1) {
return Some(out);
}
}
if insns.len() >= 3 {
let i0 = &insns[0];
let i1 = &insns[1];
let i2 = &insns[2];
if let Some(out) = match_compound_three(i0, i1, i2) {
return Some(out);
}
}
None
}
fn is_rbp_local(insn: &Instruction) -> bool {
insn.op_count() == 2
&& matches!(insn.memory_base(), Register::RBP | Register::EBP)
&& insn.memory_index() == Register::None
}
fn match_compound_two(
i0: &Instruction,
i1: &Instruction,
) -> Option<(usize, i64, &'static str, i64)> {
if i0.mnemonic() != Mnemonic::Mov {
return None;
}
if i0.op_count() != 2 || i0.op0_kind() != OpKind::Register || i0.op1_kind() != OpKind::Memory {
return None;
}
if !is_rbp_local(i0) {
return None;
}
let op = match i1.mnemonic() {
Mnemonic::Add => "+=",
Mnemonic::Sub => "-=",
Mnemonic::And => "&=",
Mnemonic::Or => "|=",
Mnemonic::Xor => "^=",
_ => return None,
};
if i1.op_count() != 2 || i1.op0_kind() != OpKind::Memory || i1.op1_kind() != OpKind::Register {
return None;
}
if !is_rbp_local(i1) {
return None;
}
if i0.op0_register() != i1.op1_register() {
return None;
}
let src = signed_memory_displacement(i0);
let dst = signed_memory_displacement(i1);
Some((2, dst, op, src))
}
fn match_compound_three(
i0: &Instruction,
i1: &Instruction,
i2: &Instruction,
) -> Option<(usize, i64, &'static str, i64)> {
if i0.mnemonic() != Mnemonic::Mov {
return None;
}
if i0.op_count() != 2 || i0.op0_kind() != OpKind::Register || i0.op1_kind() != OpKind::Memory {
return None;
}
if !is_rbp_local(i0) {
return None;
}
let op = match i1.mnemonic() {
Mnemonic::Imul => "*=",
_ => return None,
};
if i1.op_count() != 2 || i1.op0_kind() != OpKind::Register || i1.op1_kind() != OpKind::Memory {
return None;
}
if !is_rbp_local(i1) {
return None;
}
if i0.op0_register() != i1.op0_register() {
return None;
}
if i2.mnemonic() != Mnemonic::Mov {
return None;
}
if i2.op_count() != 2 || i2.op0_kind() != OpKind::Memory || i2.op1_kind() != OpKind::Register {
return None;
}
if !is_rbp_local(i2) {
return None;
}
if i2.op1_register() != i0.op0_register() {
return None;
}
let dst_load = signed_memory_displacement(i0);
let src = signed_memory_displacement(i1);
let dst_store = signed_memory_displacement(i2);
if dst_load != dst_store {
return None;
}
Some((3, dst_load, op, src))
}
#[must_use]
pub fn match_local_set_immediate(insn: &Instruction) -> Option<(i64, i64)> {
if insn.mnemonic() != Mnemonic::Mov {
return None;
}
if insn.op_count() != 2 {
return None;
}
if insn.op0_kind() != OpKind::Memory {
return None;
}
if !matches!(insn.memory_base(), Register::RBP | Register::EBP) {
return None;
}
if insn.memory_index() != Register::None {
return None;
}
#[allow(clippy::cast_possible_wrap)]
let value = match insn.op1_kind() {
OpKind::Immediate8 => i64::from(insn.immediate8() as i8),
OpKind::Immediate16 => i64::from(insn.immediate16() as i16),
OpKind::Immediate32 => i64::from(insn.immediate32() as i32),
OpKind::Immediate64 => insn.immediate64() as i64,
OpKind::Immediate8to16 => i64::from(insn.immediate8to16()),
OpKind::Immediate8to32 => i64::from(insn.immediate8to32()),
OpKind::Immediate8to64 => insn.immediate8to64(),
OpKind::Immediate32to64 => insn.immediate32to64(),
_ => return None,
};
Some((signed_memory_displacement(insn), value))
}
#[must_use]
pub fn direct_lea_rip_target(insn: &Instruction) -> Option<u64> {
if insn.mnemonic() != Mnemonic::Lea {
return None;
}
if insn.op_count() != 2 {
return None;
}
if insn.op0_kind() != OpKind::Register {
return None;
}
if insn.op1_kind() != OpKind::Memory {
return None;
}
if insn.memory_base() != Register::RIP {
return None;
}
if insn.memory_index() != Register::None {
return None;
}
Some(insn.memory_displacement64())
}
#[must_use]
pub fn format_intel(insn: &Instruction) -> String {
let mut formatter = IntelFormatter::new();
let mut out = String::new();
formatter.format(insn, &mut out);
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyAsm {
Match,
Diverged { canonical: String },
Undecodable,
MultipleInsns { count: usize },
}
#[must_use]
pub fn verify_intel_text(bitness: Bitness, text: &str, bytes: &[u8], rip: u64) -> VerifyAsm {
let mut decoder = Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
let mut count = 0usize;
let mut first: Option<Instruction> = None;
while decoder.can_decode() {
let insn = decoder.decode();
if insn.is_invalid() {
return VerifyAsm::Undecodable;
}
if first.is_none() {
first = Some(insn);
}
count += 1;
}
let Some(insn) = first else {
return VerifyAsm::Undecodable;
};
if count != 1 {
return VerifyAsm::MultipleInsns { count };
}
let canonical = format_intel(&insn);
if normalize(text) == normalize(&canonical) {
VerifyAsm::Match
} else {
VerifyAsm::Diverged { canonical }
}
}
fn normalize(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if !c.is_ascii_whitespace() {
out.extend(c.to_lowercase());
}
}
out
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("instruction decoder rejected bytes at offset {offset}")]
DecodeFailed { offset: usize },
#[error("encoder rejected instructions: {0}")]
Encode(String),
#[error("round-trip diverged at offset {offset}: expected 0x{expected:02x}, got 0x{got:02x}")]
ByteMismatch {
offset: usize,
expected: u8,
got: u8,
},
#[error("round-trip length mismatch: input was {input} bytes, output is {output}")]
LengthMismatch { input: usize, output: usize },
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Bitness {
Bits16,
Bits32,
Bits64,
}
impl Bitness {
fn as_u32(self) -> u32 {
match self {
Self::Bits16 => 16,
Self::Bits32 => 32,
Self::Bits64 => 64,
}
}
}
#[derive(Debug, Clone)]
pub struct DecodedInsn {
pub iced: Instruction,
pub original_bytes: Vec<u8>,
}
impl ArchInsn for DecodedInsn {
fn addr(&self) -> VAddr {
VAddr(self.iced.ip())
}
fn original_bytes(&self) -> &[u8] {
&self.original_bytes
}
}
pub fn decode(bitness: Bitness, bytes: &[u8], rip: u64) -> Result<Vec<DecodedInsn>> {
let mut decoder = Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
let mut out = Vec::new();
while decoder.can_decode() {
let pos = decoder.position();
let insn = decoder.decode();
if insn.is_invalid() {
return Err(Error::DecodeFailed { offset: pos });
}
let len = insn.len();
let end = pos.saturating_add(len);
if end > bytes.len() {
return Err(Error::DecodeFailed { offset: pos });
}
out.push(DecodedInsn {
iced: insn,
original_bytes: bytes[pos..end].to_vec(),
});
}
Ok(out)
}
#[must_use]
pub fn decode_tolerant(bitness: Bitness, bytes: &[u8], rip: u64) -> Vec<DecodedInsn> {
let mut decoder = Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
let mut out = Vec::new();
while decoder.can_decode() {
let pos = decoder.position();
let insn = decoder.decode();
if insn.is_invalid() {
break;
}
let len = insn.len();
let end = pos.saturating_add(len);
if end > bytes.len() {
break;
}
out.push(DecodedInsn {
iced: insn,
original_bytes: bytes[pos..end].to_vec(),
});
}
out
}
#[must_use]
pub fn emit_preserved(insns: &[DecodedInsn]) -> Vec<u8> {
let total: usize = insns.iter().map(|i| i.original_bytes.len()).sum();
let mut out = Vec::with_capacity(total);
for insn in insns {
out.extend_from_slice(&insn.original_bytes);
}
out
}
pub fn reencode_via_iced(bitness: Bitness, insns: &[DecodedInsn], rip: u64) -> Result<Vec<u8>> {
let iced_insns: Vec<Instruction> = insns.iter().map(|i| i.iced).collect();
let block = InstructionBlock::new(&iced_insns, rip);
let result = BlockEncoder::encode(bitness.as_u32(), block, BlockEncoderOptions::NONE)
.map_err(|e| Error::Encode(e.to_string()))?;
Ok(result.code_buffer)
}
pub fn roundtrip_bytes(bitness: Bitness, bytes: &[u8], rip: u64) -> Result<Vec<DecodedInsn>> {
let insns = decode(bitness, bytes, rip)?;
let emitted = emit_preserved(&insns);
if emitted.len() != bytes.len() {
return Err(Error::LengthMismatch {
input: bytes.len(),
output: emitted.len(),
});
}
if let Some((offset, (&expected, &got))) = bytes
.iter()
.zip(&emitted)
.enumerate()
.find(|(_, (a, b))| a != b)
{
return Err(Error::ByteMismatch {
offset,
expected,
got,
});
}
Ok(insns)
}
#[derive(Debug, thiserror::Error)]
pub enum JumpEncodeError {
#[error("jmp rel32 target out of i32 range: from=0x{from:x} to=0x{to:x}")]
OutOfRange { from: u64, to: u64 },
}
pub fn encode_jmp(
source_ip: u64,
target: u64,
wide: bool,
) -> std::result::Result<Vec<u8>, JumpEncodeError> {
if !wide {
let after_rel8 = source_ip.wrapping_add(2);
let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
if (-128..=127).contains(&rel8) {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let imm = rel8 as i8 as u8;
return Ok(vec![0xeb, imm]);
}
}
let after_rel32 = source_ip.wrapping_add(5);
let rel = i128::from(target).wrapping_sub(i128::from(after_rel32));
let rel32 = i32::try_from(rel).map_err(|_| JumpEncodeError::OutOfRange {
from: source_ip,
to: target,
})?;
let mut out = Vec::with_capacity(5);
out.push(0xe9);
out.extend_from_slice(&rel32.to_le_bytes());
Ok(out)
}
#[must_use]
pub fn encoded_jmp_size(source_ip: u64, target: u64, wide: bool) -> usize {
if !wide {
let after_rel8 = source_ip.wrapping_add(2);
let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
if (-128..=127).contains(&rel8) {
return 2;
}
}
5
}
pub fn encode_jcc(
source_ip: u64,
target: u64,
cond_code: u8,
wide: bool,
) -> std::result::Result<Vec<u8>, JumpEncodeError> {
let cc = cond_code & 0x0f;
if !wide {
let after_rel8 = source_ip.wrapping_add(2);
let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
if (-128..=127).contains(&rel8) {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let imm = rel8 as i8 as u8;
return Ok(vec![0x70 | cc, imm]);
}
}
let after_rel32 = source_ip.wrapping_add(6);
let rel = i128::from(target).wrapping_sub(i128::from(after_rel32));
let rel32 = i32::try_from(rel).map_err(|_| JumpEncodeError::OutOfRange {
from: source_ip,
to: target,
})?;
let mut out = Vec::with_capacity(6);
out.push(0x0f);
out.push(0x80 | cc);
out.extend_from_slice(&rel32.to_le_bytes());
Ok(out)
}
pub fn encode_call_rel32(
source_ip: u64,
target: u64,
) -> std::result::Result<Vec<u8>, JumpEncodeError> {
let after = source_ip.wrapping_add(5);
let rel = i128::from(target).wrapping_sub(i128::from(after));
let rel32 = i32::try_from(rel).map_err(|_| JumpEncodeError::OutOfRange {
from: source_ip,
to: target,
})?;
let mut out = Vec::with_capacity(5);
out.push(0xe8);
out.extend_from_slice(&rel32.to_le_bytes());
Ok(out)
}
#[must_use]
pub fn encoded_jcc_size(source_ip: u64, target: u64, wide: bool) -> usize {
if !wide {
let after_rel8 = source_ip.wrapping_add(2);
let rel8 = i128::from(target).wrapping_sub(i128::from(after_rel8));
if (-128..=127).contains(&rel8) {
return 2;
}
}
6
}
#[must_use]
pub fn jcc_cond_code_from_bytes(bytes: &[u8]) -> Option<u8> {
match bytes {
[op, ..] if (0x70..=0x7f).contains(op) => Some(op - 0x70),
[0x0f, op, ..] if (0x80..=0x8f).contains(op) => Some(op - 0x80),
_ => None,
}
}
#[must_use]
pub fn jcc_cond_name(cond_code: u8) -> &'static str {
match cond_code & 0x0f {
0x0 => "jo",
0x1 => "jno",
0x2 => "jb",
0x3 => "jae",
0x4 => "je",
0x5 => "jne",
0x6 => "jbe",
0x7 => "ja",
0x8 => "js",
0x9 => "jns",
0xa => "jp",
0xb => "jnp",
0xc => "jl",
0xd => "jge",
0xe => "jle",
_ => "jg",
}
}
#[must_use]
pub fn jcc_cond_code_from_name(name: &str) -> Option<u8> {
Some(match name {
"jo" => 0x0,
"jno" => 0x1,
"jb" | "jc" | "jnae" => 0x2,
"jae" | "jnb" | "jnc" => 0x3,
"je" | "jz" => 0x4,
"jne" | "jnz" => 0x5,
"jbe" | "jna" => 0x6,
"ja" | "jnbe" => 0x7,
"js" => 0x8,
"jns" => 0x9,
"jp" | "jpe" => 0xa,
"jnp" | "jpo" => 0xb,
"jl" | "jnge" => 0xc,
"jge" | "jnl" => 0xd,
"jle" | "jng" => 0xe,
"jg" | "jnle" => 0xf,
_ => return None,
})
}
#[derive(Debug, thiserror::Error)]
pub enum SwitchEncodeError {
#[error("unsupported selector register {0:?} (expected eax/ecx/edx/ebx/esi/edi/ebp)")]
UnsupportedSelector(String),
#[error("case count {0} doesn't fit in u32")]
TooManyCases(usize),
#[error("ja rel32 target out of i32 range: cmp_ip={cmp_ip:#x} default={default:#x}")]
JaOutOfRange { cmp_ip: u64, default: u64 },
}
pub fn encode_msvc_jmp_table_dispatch(
selector: &str,
cases: usize,
default_addr: u64,
table_va: u64,
cmp_ip: u64,
) -> std::result::Result<Vec<u8>, SwitchEncodeError> {
let reg_code = gpr32_code(selector)
.ok_or_else(|| SwitchEncodeError::UnsupportedSelector(selector.into()))?;
let max_value = u32::try_from(cases.saturating_sub(1))
.map_err(|_| SwitchEncodeError::TooManyCases(cases))?;
let mut out = Vec::with_capacity(16);
let cmp_modrm = 0xc0 | (7 << 3) | reg_code; if max_value <= 0x7f {
out.push(0x83);
out.push(cmp_modrm);
out.push(max_value as u8);
} else {
out.push(0x81);
out.push(cmp_modrm);
out.extend_from_slice(&max_value.to_le_bytes());
}
let cmp_len = out.len() as u64;
let ja_end = cmp_ip
.checked_add(cmp_len)
.and_then(|x| x.checked_add(6))
.ok_or(SwitchEncodeError::JaOutOfRange {
cmp_ip,
default: default_addr,
})?;
let rel = i128::from(default_addr) - i128::from(ja_end);
let rel32 = i32::try_from(rel).map_err(|_| SwitchEncodeError::JaOutOfRange {
cmp_ip,
default: default_addr,
})?;
out.push(0x0f);
out.push(0x87);
out.extend_from_slice(&rel32.to_le_bytes());
out.push(0xff);
out.push(0x24);
out.push(0x80 | (reg_code << 3) | 0x05);
out.extend_from_slice(&(table_va as u32).to_le_bytes());
Ok(out)
}
fn gpr32_code(name: &str) -> Option<u8> {
match name.trim().to_ascii_lowercase().as_str() {
"eax" => Some(0),
"ecx" => Some(1),
"edx" => Some(2),
"ebx" => Some(3),
"ebp" => Some(5),
"esi" => Some(6),
"edi" => Some(7),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_msmpeg4_driverproc_switch() {
let bytes = encode_msvc_jmp_table_dispatch("ecx", 10, 0x23e8, 0x1c20_246a, 0x208d).unwrap();
assert_eq!(
bytes,
vec![
0x83, 0xf9, 0x09, 0x0f, 0x87, 0x52, 0x03, 0x00, 0x00, 0xff, 0x24, 0x8d, 0x6a, 0x24, 0x20, 0x1c, ]
);
}
#[test]
fn endbr64_roundtrips() {
let bytes = [0xf3, 0x0f, 0x1e, 0xfa];
let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(insns.len(), 1);
}
#[test]
fn prologue_roundtrips() {
let bytes = [
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, ];
let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(insns.len(), 3);
}
#[test]
fn short_jump_roundtrips() {
let bytes = [0xeb, 0x05];
roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
}
#[test]
fn near_jump_roundtrips() {
let bytes = [0xe9, 0x34, 0x12, 0x00, 0x00];
roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
}
#[test]
fn call_rel32_roundtrips() {
let bytes = [0xe8, 0x80, 0x00, 0x00, 0x00];
roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
}
#[test]
fn xor_zero_idiom_roundtrips() {
let bytes = [0x48, 0x31, 0xc0];
roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
}
#[test]
fn multibyte_nop_with_data16_prefix_preserved() {
let bytes = [0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00];
let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(insns.len(), 1);
assert_eq!(insns[0].original_bytes, bytes);
let reencoded = reencode_via_iced(Bitness::Bits64, &insns, 0x1000).unwrap();
assert!(
reencoded.len() <= bytes.len(),
"iced should produce a shorter or equal canonical encoding"
);
}
#[test]
fn small_function_roundtrips() {
let bytes = [
0xf3, 0x0f, 0x1e, 0xfa, 0x55, 0x48, 0x89, 0xe5, 0x31, 0xc0, 0x5d, 0xc3, ];
let insns = roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(insns.len(), 6);
}
#[test]
fn verify_matches_canonical_form() {
let bytes = [0xc3]; match verify_intel_text(Bitness::Bits64, "ret", &bytes, 0x1000) {
VerifyAsm::Match => {}
other => panic!("expected Match, got {other:?}"),
}
}
#[test]
fn verify_tolerates_whitespace_and_case() {
let bytes = [0x48, 0x89, 0xd8]; match verify_intel_text(Bitness::Bits64, "MOV RAX, RBX", &bytes, 0x1000) {
VerifyAsm::Match => {}
other => panic!("expected Match, got {other:?}"),
}
}
#[test]
fn verify_diverges_when_text_disagrees() {
let bytes = [0xc3]; match verify_intel_text(Bitness::Bits64, "nop", &bytes, 0x1000) {
VerifyAsm::Diverged { canonical } => {
assert_eq!(canonical, "ret");
}
other => panic!("expected Diverged, got {other:?}"),
}
}
#[test]
fn verify_rejects_multi_insn_byte_sequence() {
let bytes = [0xc3, 0xc3];
let result = verify_intel_text(Bitness::Bits64, "ret", &bytes, 0x1000);
assert!(matches!(result, VerifyAsm::MultipleInsns { count: 2 }));
}
#[test]
fn verify_rejects_undecodable_bytes() {
let bytes = [0x06]; let result = verify_intel_text(Bitness::Bits64, "ret", &bytes, 0x1000);
assert!(matches!(result, VerifyAsm::Undecodable));
}
#[test]
fn lift_return_recognizes_mov_eax_pop_rbp_ret() {
let bytes = [0xb8, 0x00, 0x00, 0x00, 0x00, 0x5d, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_return_pattern(&insns).unwrap();
assert_eq!(lifted.value, 0);
assert_eq!(lifted.insns_consumed, 3);
}
#[test]
fn lift_return_recognizes_xor_zero_pop_rbp_ret() {
let bytes = [0x31, 0xc0, 0x5d, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_return_pattern(&insns).unwrap();
assert_eq!(lifted.value, 0);
assert_eq!(lifted.insns_consumed, 3);
}
#[test]
fn lift_return_recognizes_mov_eax_leave_ret() {
let bytes = [0xb8, 0x01, 0x00, 0x00, 0x00, 0xc9, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_return_pattern(&insns).unwrap();
assert_eq!(lifted.value, 1);
assert_eq!(lifted.insns_consumed, 3);
}
#[test]
fn lift_return_recognizes_mov_ret_without_epilogue() {
let bytes = [0xb8, 0x2a, 0x00, 0x00, 0x00, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_return_pattern(&insns).unwrap();
assert_eq!(lifted.value, 0x2a);
assert_eq!(lifted.insns_consumed, 2);
}
#[test]
fn lift_return_rejects_bare_ret() {
let bytes = [0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(try_lift_return_pattern(&insns).is_none());
}
#[test]
fn lift_return_rejects_unrecognized_setter() {
let bytes = [0x48, 0x89, 0xd8, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(try_lift_return_pattern(&insns).is_none());
}
#[test]
fn lift_epilogue_recognizes_leave_ret() {
let bytes = [0xc9, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_epilogue_pattern(&insns).unwrap();
assert_eq!(lifted.kind, "std");
assert_eq!(lifted.insns_consumed, 2);
}
#[test]
fn lift_epilogue_recognizes_pop_rbp_ret() {
let bytes = [0x5d, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_epilogue_pattern(&insns).unwrap();
assert_eq!(lifted.kind, "std-pop-rbp");
assert_eq!(lifted.insns_consumed, 2);
}
#[test]
fn lift_epilogue_bare_ret_is_minimal_kind() {
let bytes = [0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_epilogue_pattern(&insns).expect("bare ret should lift");
assert_eq!(lifted.kind, "ret");
assert_eq!(lifted.insns_consumed, 1);
}
#[test]
fn lift_epilogue_non_teardown_predecessor_lifts_only_the_ret() {
let bytes = [0x48, 0x89, 0xd8, 0xc3];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_epilogue_pattern(&insns).expect("ret should lift");
assert_eq!(lifted.kind, "ret");
assert_eq!(lifted.insns_consumed, 1);
}
#[test]
fn lift_prologue_full_std() {
let bytes = [
0xf3, 0x0f, 0x1e, 0xfa, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x10,
];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_prologue_pattern(&insns).unwrap();
assert_eq!(lifted.kind, "std");
assert_eq!(lifted.insns_consumed, 4);
}
#[test]
fn lift_prologue_without_sub() {
let bytes = [0xf3, 0x0f, 0x1e, 0xfa, 0x55, 0x48, 0x89, 0xe5];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_prologue_pattern(&insns).unwrap();
assert_eq!(lifted.kind, "std");
assert_eq!(lifted.insns_consumed, 3);
}
#[test]
fn lift_prologue_no_cf_protection() {
let bytes = [0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_prologue_pattern(&insns).unwrap();
assert_eq!(lifted.kind, "std-no-cf");
assert_eq!(lifted.insns_consumed, 3);
}
#[test]
fn lift_prologue_noframe() {
let bytes = [0xf3, 0x0f, 0x1e, 0xfa, 0x31, 0xc0, 0xc3]; let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_prologue_pattern(&insns).unwrap();
assert_eq!(lifted.kind, "std-noframe");
assert_eq!(lifted.insns_consumed, 1);
}
#[test]
fn lift_prologue_rejects_nonstandard() {
let bytes = [0x48, 0x89, 0xd8];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(try_lift_prologue_pattern(&insns).is_none());
}
#[test]
fn arg_spill_recognizes_int_register_to_stack() {
let bytes = [0x89, 0x7d, 0xfc];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(arg_spill_index(&insns[0].iced), Some(0));
}
#[test]
fn arg_spill_recognizes_xmm_register_to_stack() {
let bytes = [0xf2, 0x0f, 0x11, 0x45, 0xf0];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(arg_spill_index(&insns[0].iced), Some(0));
}
#[test]
fn arg_spill_rejects_non_arg_register() {
let bytes = [0x89, 0x45, 0xfc];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(arg_spill_index(&insns[0].iced), None);
}
#[test]
fn arg_spill_rejects_non_rbp_dst() {
let bytes = [0x89, 0x78, 0xfc];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(arg_spill_index(&insns[0].iced), None);
}
#[test]
fn lift_return_via_jmp_recognizes_mov_jmp_short() {
let bytes = [0xb8, 0x01, 0x00, 0x00, 0x00, 0xeb, 0x14];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_return_via_jmp(&insns, 0x101b).unwrap();
assert_eq!(lifted.value, 1);
assert_eq!(lifted.insns_consumed, 2);
}
#[test]
fn lift_return_via_jmp_rejects_wrong_target() {
let bytes = [0xb8, 0x01, 0x00, 0x00, 0x00, 0xeb, 0x14];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(try_lift_return_via_jmp(&insns, 0x9999).is_none());
}
#[test]
fn lift_return_via_jmp_rejects_non_setter() {
let bytes = [0x48, 0x89, 0xd8, 0xeb, 0x05];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let target = insns.last().unwrap().iced.near_branch_target();
assert!(try_lift_return_via_jmp(&insns, target).is_none());
}
#[test]
fn lift_return_only_consumes_tail() {
let bytes = [0x90, 0xb8, 0x00, 0x00, 0x00, 0x00, 0xc3]; let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_return_pattern(&insns).unwrap();
assert_eq!(lifted.insns_consumed, 2);
}
#[test]
fn invalid_bytes_fail_decode() {
let bytes = [0x06];
assert!(matches!(
roundtrip_bytes(Bitness::Bits64, &bytes, 0x1000),
Err(Error::DecodeFailed { .. })
));
}
#[test]
fn lift_if_branch_head_recognizes_cmp_jne() {
let bytes = [0x83, 0x7d, 0xfc, 0x01, 0x75, 0x01];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_if_branch_head(&insns).expect("should match");
assert_eq!(lifted.insns_consumed, 2);
assert_eq!(lifted.jcc_target, 0x1007);
assert_eq!(lifted.cond_bytes, bytes.to_vec());
assert!(
lifted.cond_text.contains("=="),
"got cond_text: {}",
lifted.cond_text
);
}
#[test]
fn lift_if_branch_head_recognizes_test_je() {
let bytes = [0x85, 0xc0, 0x74, 0x00];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let lifted = try_lift_if_branch_head(&insns).expect("should match");
assert_eq!(lifted.insns_consumed, 2);
assert_eq!(lifted.jcc_target, 0x1004);
assert_eq!(lifted.cond_text, "eax != 0");
}
#[test]
fn lift_if_branch_head_rejects_unconditional_jmp() {
let bytes = [0x83, 0xf8, 0x00, 0xeb, 0x05];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(try_lift_if_branch_head(&insns).is_none());
}
#[test]
fn direct_lea_rip_target_resolves_rip_relative_load() {
let bytes = [0x48, 0x8d, 0x05, 0x10, 0x00, 0x00, 0x00];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(direct_lea_rip_target(&insns[0].iced), Some(0x1017));
}
#[test]
fn direct_lea_rip_target_rejects_non_lea() {
let bytes = [0x48, 0x8b, 0x05, 0x10, 0x00, 0x00, 0x00];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(direct_lea_rip_target(&insns[0].iced), None);
}
#[test]
fn direct_lea_rip_target_rejects_non_rip_base() {
let bytes = [0x48, 0x8d, 0x43, 0x10];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert_eq!(direct_lea_rip_target(&insns[0].iced), None);
}
#[test]
fn lift_if_branch_head_rejects_non_compare_predecessor() {
let bytes = [0x89, 0xd8, 0x74, 0x05];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(try_lift_if_branch_head(&insns).is_none());
}
}