use std::collections::HashMap;
use crate::arch::x86::{Bitness, lift_function};
use crate::ast::{Field, Item, Module, UdFile, Value};
use crate::format::pe::{
COFF_SYM_CLASS_EXTERNAL, COFF_SYM_CLASS_STATIC, CoffSymbol, IMAGE_FILE_MACHINE_AMD64,
IMAGE_FILE_MACHINE_I386, PeExport, PeFile, PeKind,
};
use crate::translate::decompile::build_function;
#[must_use]
pub fn decompile_pe(pe: &PeFile) -> UdFile {
let module = build_pe_module(pe);
let items = build_pe_items(pe);
UdFile { module, items }
}
#[must_use]
pub fn decompile_pe_to_text(pe: &PeFile) -> String {
crate::ast::emit(&decompile_pe(pe))
}
#[allow(clippy::too_many_lines)]
fn build_pe_module(pe: &PeFile) -> Module {
let arch = match pe.coff.machine {
IMAGE_FILE_MACHINE_I386 => "i386",
IMAGE_FILE_MACHINE_AMD64 => "x86_64",
_ => "unknown",
};
let bits = match pe.kind {
PeKind::Pe32 => 32,
PeKind::Pe32Plus => 64,
};
let coff_block = Value::Block(vec![
field("machine", Value::Int(u64::from(pe.coff.machine))),
field(
"number_of_sections",
Value::Int(u64::from(pe.coff.number_of_sections)),
),
field(
"time_date_stamp",
Value::Int(u64::from(pe.coff.time_date_stamp)),
),
field(
"pointer_to_symbol_table",
Value::Int(u64::from(pe.coff.pointer_to_symbol_table)),
),
field(
"number_of_symbols",
Value::Int(u64::from(pe.coff.number_of_symbols)),
),
field(
"size_of_optional_header",
Value::Int(u64::from(pe.coff.size_of_optional_header)),
),
field(
"characteristics",
Value::Int(u64::from(pe.coff.characteristics)),
),
]);
let sections_value = Value::List(
pe.sections
.iter()
.enumerate()
.map(|(i, s)| {
let name = pe.section_name(i).unwrap_or_default().to_string();
Value::Block(vec![
field("name", Value::String(name)),
field("name_raw", byte_list(&s.name)),
field("virtual_size", Value::Int(u64::from(s.virtual_size))),
field("virtual_address", Value::Int(u64::from(s.virtual_address))),
field(
"size_of_raw_data",
Value::Int(u64::from(s.size_of_raw_data)),
),
field(
"pointer_to_raw_data",
Value::Int(u64::from(s.pointer_to_raw_data)),
),
field(
"pointer_to_relocations",
Value::Int(u64::from(s.pointer_to_relocations)),
),
field(
"pointer_to_linenumbers",
Value::Int(u64::from(s.pointer_to_linenumbers)),
),
field(
"number_of_relocations",
Value::Int(u64::from(s.number_of_relocations)),
),
field(
"number_of_linenumbers",
Value::Int(u64::from(s.number_of_linenumbers)),
),
field("characteristics", Value::Int(u64::from(s.characteristics))),
])
})
.collect(),
);
let functions_value = Value::List(build_function_blocks(pe));
let dos_block = build_dos_block(&pe.dos);
let dos_stub_value = byte_list(&pe.dos_stub);
let optional_value = pe
.optional
.as_ref()
.map_or(Value::Block(Vec::new()), build_optional_block);
let data_directories_value = Value::List(
pe.data_directories
.iter()
.map(|dd| {
Value::Block(vec![
field("virtual_address", Value::Int(u64::from(dd.virtual_address))),
field("size", Value::Int(u64::from(dd.size))),
])
})
.collect(),
);
let build = Value::Block(vec![
field("e_lfanew", Value::Int(u64::from(pe.e_lfanew))),
field("file_size", Value::Int(pe.file_size())),
field("dos", dos_block),
field("dos_stub", dos_stub_value),
field("coff", coff_block),
field("optional", optional_value),
field("data_directories", data_directories_value),
field("sections", sections_value),
field("functions", functions_value),
field("vtables", Value::List(scan_vtables(pe))),
]);
Module {
fields: vec![
field("arch", Value::String(arch.into())),
field("format", Value::String("pe".into())),
field("bits", Value::Int(bits)),
field("endian", Value::String("little".into())),
field("build", build),
],
}
}
fn scan_vtables(pe: &PeFile) -> Vec<Value> {
let mut out: Vec<Value> = Vec::new();
let bits = match pe.kind {
PeKind::Pe32 => 32,
PeKind::Pe32Plus => 64,
};
let ptr_size = if bits == 64 { 8 } else { 4 };
let mut text_ranges: Vec<(u64, u64)> = Vec::new();
for sh in &pe.sections {
if sh.characteristics & 0x2000_0000 != 0 {
let start = pe.image_base + u64::from(sh.virtual_address);
let size = u64::from(sh.virtual_size.max(sh.size_of_raw_data));
text_ranges.push((start, start + size));
}
}
let in_text = |va: u64| text_ranges.iter().any(|(s, e)| va >= *s && va < *e);
for (idx, sh) in pe.sections.iter().enumerate() {
let is_data =
sh.characteristics & 0x4000_0000 != 0 && sh.characteristics & 0x2000_0000 == 0;
if !is_data {
continue;
}
let Some(data) = pe.section_data(idx) else {
continue;
};
let sec_base = pe.image_base + u64::from(sh.virtual_address);
let mut i = 0usize;
while i + ptr_size <= data.len() {
let raw = &data[i..i + ptr_size];
let val = if bits == 64 {
u64::from_le_bytes(raw.try_into().unwrap_or([0; 8]))
} else {
u64::from(u32::from_le_bytes(raw[..4].try_into().unwrap_or([0; 4])))
};
if val != 0 && in_text(val) {
let run_start = i;
let mut count = 0usize;
while i + ptr_size <= data.len() {
let raw = &data[i..i + ptr_size];
let val = if bits == 64 {
u64::from_le_bytes(raw.try_into().unwrap_or([0; 8]))
} else {
u64::from(u32::from_le_bytes(raw[..4].try_into().unwrap_or([0; 4])))
};
if val == 0 || !in_text(val) {
break;
}
count += 1;
i += ptr_size;
}
if count >= 2 {
out.push(Value::Block(vec![
field("addr", Value::Int(sec_base + run_start as u64)),
field("count", Value::Int(count as u64)),
]));
}
} else {
i += ptr_size;
}
}
}
out
}
fn build_pe_items(pe: &PeFile) -> Vec<Item> {
let bytes = pe.raw_bytes();
let mut items: Vec<Item> = Vec::new();
let funcs_by_section = group_functions_by_section(pe);
let mut regions: Vec<(usize, usize, usize)> = Vec::new();
for (idx, s) in pe.sections.iter().enumerate() {
let start = s.pointer_to_raw_data as usize;
let size = s.size_of_raw_data as usize;
if size == 0 {
continue; }
let end = start.saturating_add(size);
if end > bytes.len() {
continue; }
regions.push((start, end, idx));
}
regions.sort_unstable_by_key(|(start, _, _)| *start);
let header_end = pe_header_region_end(pe);
let mut cursor = header_end.min(bytes.len());
for (start, end, sec_idx) in ®ions {
if *start > cursor {
items.push(Item::Raw {
addr: cursor as u64,
bytes: bytes[cursor..*start].to_vec(),
});
}
if let Some(funcs) = funcs_by_section.get(sec_idx) {
emit_section_with_function_split(pe, *sec_idx, *start, *end, funcs, bytes, &mut items);
} else {
items.push(Item::Raw {
addr: *start as u64,
bytes: bytes[*start..*end].to_vec(),
});
}
cursor = *end;
}
if cursor < bytes.len() {
items.push(Item::Raw {
addr: cursor as u64,
bytes: bytes[cursor..].to_vec(),
});
}
items
}
struct PeFunctionRecord {
name: String,
section_offset: u32,
rva: u32,
}
fn group_functions_by_section(
pe: &PeFile,
) -> std::collections::HashMap<usize, Vec<PeFunctionRecord>> {
let mut out: std::collections::HashMap<usize, Vec<PeFunctionRecord>> =
std::collections::HashMap::new();
for sym in pe.coff_symbols() {
if !is_code_function(pe, &sym) {
continue;
}
#[allow(clippy::cast_sign_loss)]
let idx = (sym.section_number - 1) as usize;
let Some(sh) = pe.sections.get(idx) else {
continue;
};
let rva = sh.virtual_address.wrapping_add(sym.value);
out.entry(idx).or_default().push(PeFunctionRecord {
name: sym.name,
section_offset: sym.value,
rva,
});
}
add_pe_entry_point(pe, &mut out);
add_pe_exports(pe, &mut out);
add_pe_call_graph_targets(pe, &mut out);
for v in out.values_mut() {
v.sort_by_key(|f| f.section_offset);
v.dedup_by(|a, b| a.section_offset == b.section_offset && a.name == b.name);
}
out
}
const IMAGE_FILE_DLL: u16 = 0x2000;
fn rva_to_section(pe: &PeFile, rva: u32) -> Option<(usize, u32)> {
for (idx, sh) in pe.sections.iter().enumerate() {
let start = sh.virtual_address;
let size = sh.virtual_size.max(sh.size_of_raw_data);
if let Some(end) = start.checked_add(size)
&& rva >= start
&& rva < end
{
return Some((idx, rva - start));
}
}
None
}
fn add_pe_entry_point(
pe: &PeFile,
out: &mut std::collections::HashMap<usize, Vec<PeFunctionRecord>>,
) {
let ep = pe.address_of_entry_point;
if ep == 0 {
return;
}
let Some((idx, off)) = rva_to_section(pe, ep) else {
return;
};
let already = out.get(&idx).is_some_and(|v| v.iter().any(|f| f.rva == ep));
if already {
return;
}
let name = if pe.coff.characteristics & IMAGE_FILE_DLL != 0 {
"DllMain".to_string()
} else {
"_start".to_string()
};
out.entry(idx).or_default().push(PeFunctionRecord {
name,
section_offset: off,
rva: ep,
});
}
fn add_pe_exports(pe: &PeFile, out: &mut std::collections::HashMap<usize, Vec<PeFunctionRecord>>) {
for export in pe.exports() {
let Some((idx, off)) = rva_to_section(pe, export.rva) else {
continue;
};
let already = out
.get(&idx)
.is_some_and(|v| v.iter().any(|f| f.rva == export.rva));
if already {
continue;
}
let name = export_function_name(&export);
out.entry(idx).or_default().push(PeFunctionRecord {
name,
section_offset: off,
rva: export.rva,
});
}
}
const MAX_FUNCTION_BYTES: usize = 65536;
fn add_pe_call_graph_targets(
pe: &PeFile,
out: &mut std::collections::HashMap<usize, Vec<PeFunctionRecord>>,
) {
let bitness = match pe.coff.machine {
IMAGE_FILE_MACHINE_I386 => Bitness::Bits32,
IMAGE_FILE_MACHINE_AMD64 => Bitness::Bits64,
_ => return,
};
let mut known: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut worklist: Vec<u32> = Vec::new();
for fns in out.values() {
for f in fns {
if known.insert(f.rva) {
worklist.push(f.rva);
}
}
}
let raw = pe.raw_bytes();
while let Some(rva) = worklist.pop() {
let Some((sec_idx, _)) = rva_to_section(pe, rva) else {
continue;
};
let Some(sec) = pe.sections.get(sec_idx) else {
continue;
};
let Some(start_off) = pe.rva_to_file_offset(rva) else {
continue;
};
let sec_raw_end_rva = sec.virtual_address.saturating_add(sec.size_of_raw_data);
let in_section = (sec_raw_end_rva.saturating_sub(rva)) as usize;
let read_len = in_section.min(MAX_FUNCTION_BYTES);
let slice = &raw[start_off..start_off.saturating_add(read_len).min(raw.len())];
let insns = crate::arch::x86::decode_tolerant(bitness, slice, u64::from(rva));
if insns.is_empty() {
continue;
}
let Ok(lifted) = lift_function(format!("seed_{rva:x}"), &insns) else {
continue;
};
for block in &lifted.blocks {
for ins in &block.insns {
let Some(target_u64) = crate::arch::x86::direct_call_target(&ins.iced) else {
continue;
};
let Ok(target_rva) = u32::try_from(target_u64) else {
continue;
};
let Some((tgt_sec, tgt_off)) = rva_to_section(pe, target_rva) else {
continue;
};
if tgt_sec != sec_idx {
continue;
}
if known.insert(target_rva) {
out.entry(tgt_sec).or_default().push(PeFunctionRecord {
name: format!("sub_{target_rva:x}"),
section_offset: tgt_off,
rva: target_rva,
});
worklist.push(target_rva);
}
}
}
}
}
fn export_function_name(export: &PeExport) -> String {
export
.name
.clone()
.unwrap_or_else(|| format!("Ord_{}", export.ordinal))
}
fn emit_section_with_function_split(
pe: &PeFile,
sec_idx: usize,
start: usize,
end: usize,
funcs: &[PeFunctionRecord],
bytes: &[u8],
items: &mut Vec<Item>,
) {
let _ = &pe.sections[sec_idx]; let section_size = (end - start) as u32;
let first_off = funcs
.first()
.map_or(section_size, |f| f.section_offset.min(section_size));
if first_off > 0 {
let lo = start;
let hi = start + first_off as usize;
items.push(Item::Raw {
addr: lo as u64,
bytes: bytes[lo..hi].to_vec(),
});
}
let bitness = match pe.coff.machine {
IMAGE_FILE_MACHINE_I386 => Some(Bitness::Bits32),
IMAGE_FILE_MACHINE_AMD64 => Some(Bitness::Bits64),
_ => None,
};
let name_at = pe_name_at(pe);
for (i, f) in funcs.iter().enumerate() {
let off = f.section_offset.min(section_size);
let next_off = funcs
.get(i + 1)
.map_or(section_size, |n| n.section_offset.min(section_size));
if next_off <= off {
continue; }
let lo = start + off as usize;
let hi = start + next_off as usize;
let func_bytes = &bytes[lo..hi];
if let Some(bn) = bitness
&& let Some(emitted) =
lift_pe_function(pe, &f.name, lo as u64, f.rva, bn, func_bytes, &name_at)
{
items.extend(emitted);
continue;
}
let section_name = pe.section_name(sec_idx).unwrap_or_default();
items.push(Item::Comment(format!(
"fn {} at {section_name}+0x{off:x} (rva 0x{:x}, {} bytes)",
f.name,
f.rva,
hi - lo,
)));
items.push(Item::Raw {
addr: lo as u64,
bytes: func_bytes.to_vec(),
});
}
let last_end = funcs.last().map_or(0u32, |f| {
let off = f.section_offset.min(section_size);
off.max(section_size)
});
if (last_end as usize) < (end - start) {
let lo = start + last_end as usize;
items.push(Item::Raw {
addr: lo as u64,
bytes: bytes[lo..end].to_vec(),
});
}
}
fn field(name: &str, value: Value) -> Field {
Field {
name: name.to_string(),
value,
}
}
fn byte_list(bs: &[u8]) -> Value {
Value::List(bs.iter().map(|b| Value::Int(u64::from(*b))).collect())
}
fn pe_header_region_end(pe: &PeFile) -> usize {
const PE_SIG_SIZE: usize = 4;
const COFF_HEADER_SIZE: usize = 20;
let opt_size = pe.coff.size_of_optional_header as usize;
let pe_off = pe.e_lfanew as usize;
let opt_off = pe_off + PE_SIG_SIZE + COFF_HEADER_SIZE;
let sec_off = opt_off + opt_size;
sec_off + pe.sections.len() * crate::format::pe::SECTION_HEADER_SIZE
}
fn build_dos_block(dos: &crate::format::pe::DosHeader) -> Value {
Value::Block(vec![
field("e_magic", byte_list(&dos.e_magic)),
field("e_cblp", Value::Int(u64::from(dos.e_cblp))),
field("e_cp", Value::Int(u64::from(dos.e_cp))),
field("e_crlc", Value::Int(u64::from(dos.e_crlc))),
field("e_cparhdr", Value::Int(u64::from(dos.e_cparhdr))),
field("e_minalloc", Value::Int(u64::from(dos.e_minalloc))),
field("e_maxalloc", Value::Int(u64::from(dos.e_maxalloc))),
field("e_ss", Value::Int(u64::from(dos.e_ss))),
field("e_sp", Value::Int(u64::from(dos.e_sp))),
field("e_csum", Value::Int(u64::from(dos.e_csum))),
field("e_ip", Value::Int(u64::from(dos.e_ip))),
field("e_cs", Value::Int(u64::from(dos.e_cs))),
field("e_lfarlc", Value::Int(u64::from(dos.e_lfarlc))),
field("e_ovno", Value::Int(u64::from(dos.e_ovno))),
field(
"e_res",
Value::List(
dos.e_res
.iter()
.map(|n| Value::Int(u64::from(*n)))
.collect(),
),
),
field("e_oemid", Value::Int(u64::from(dos.e_oemid))),
field("e_oeminfo", Value::Int(u64::from(dos.e_oeminfo))),
field(
"e_res2",
Value::List(
dos.e_res2
.iter()
.map(|n| Value::Int(u64::from(*n)))
.collect(),
),
),
field("e_lfanew", Value::Int(u64::from(dos.e_lfanew))),
])
}
fn build_optional_block(opt: &crate::format::pe::OptionalHeader) -> Value {
Value::Block(vec![
field("magic", Value::Int(u64::from(opt.magic))),
field(
"major_linker_version",
Value::Int(u64::from(opt.major_linker_version)),
),
field(
"minor_linker_version",
Value::Int(u64::from(opt.minor_linker_version)),
),
field("size_of_code", Value::Int(u64::from(opt.size_of_code))),
field(
"size_of_initialized_data",
Value::Int(u64::from(opt.size_of_initialized_data)),
),
field(
"size_of_uninitialized_data",
Value::Int(u64::from(opt.size_of_uninitialized_data)),
),
field(
"address_of_entry_point",
Value::Int(u64::from(opt.address_of_entry_point)),
),
field("base_of_code", Value::Int(u64::from(opt.base_of_code))),
field("base_of_data", Value::Int(u64::from(opt.base_of_data))),
field("image_base", Value::Int(opt.image_base)),
field(
"section_alignment",
Value::Int(u64::from(opt.section_alignment)),
),
field("file_alignment", Value::Int(u64::from(opt.file_alignment))),
field(
"major_operating_system_version",
Value::Int(u64::from(opt.major_operating_system_version)),
),
field(
"minor_operating_system_version",
Value::Int(u64::from(opt.minor_operating_system_version)),
),
field(
"major_image_version",
Value::Int(u64::from(opt.major_image_version)),
),
field(
"minor_image_version",
Value::Int(u64::from(opt.minor_image_version)),
),
field(
"major_subsystem_version",
Value::Int(u64::from(opt.major_subsystem_version)),
),
field(
"minor_subsystem_version",
Value::Int(u64::from(opt.minor_subsystem_version)),
),
field(
"win32_version_value",
Value::Int(u64::from(opt.win32_version_value)),
),
field("size_of_image", Value::Int(u64::from(opt.size_of_image))),
field(
"size_of_headers",
Value::Int(u64::from(opt.size_of_headers)),
),
field("check_sum", Value::Int(u64::from(opt.check_sum))),
field("subsystem", Value::Int(u64::from(opt.subsystem))),
field(
"dll_characteristics",
Value::Int(u64::from(opt.dll_characteristics)),
),
field(
"size_of_stack_reserve",
Value::Int(opt.size_of_stack_reserve),
),
field("size_of_stack_commit", Value::Int(opt.size_of_stack_commit)),
field("size_of_heap_reserve", Value::Int(opt.size_of_heap_reserve)),
field("size_of_heap_commit", Value::Int(opt.size_of_heap_commit)),
field("loader_flags", Value::Int(u64::from(opt.loader_flags))),
field(
"number_of_rva_and_sizes",
Value::Int(u64::from(opt.number_of_rva_and_sizes)),
),
])
}
fn pe_name_at(pe: &PeFile) -> HashMap<u64, String> {
let mut out = HashMap::new();
for sym in pe.coff_symbols() {
if !is_code_function(pe, &sym) {
continue;
}
#[allow(clippy::cast_sign_loss)]
let idx = (sym.section_number - 1) as usize;
let Some(sh) = pe.sections.get(idx) else {
continue;
};
let rva = u64::from(sh.virtual_address.wrapping_add(sym.value));
out.insert(rva, sym.name);
}
let mut name_count: HashMap<String, u32> = HashMap::new();
for imp in pe.imports() {
if let Some(name) = &imp.name {
*name_count.entry(name.clone()).or_default() += 1;
}
}
for imp in pe.imports() {
let label = match (&imp.name, imp.ordinal) {
(Some(name), _) => {
if name_count.get(name).copied().unwrap_or(0) > 1 && !imp.dll_name.is_empty() {
let stem = imp
.dll_name
.strip_suffix(".dll")
.or_else(|| imp.dll_name.strip_suffix(".DLL"))
.unwrap_or(&imp.dll_name);
format!("{stem}!{name}")
} else {
name.clone()
}
}
(None, Some(ord)) => {
let stem = imp
.dll_name
.strip_suffix(".dll")
.or_else(|| imp.dll_name.strip_suffix(".DLL"))
.unwrap_or(&imp.dll_name);
format!("{stem}@{ord}")
}
(None, None) => continue,
};
out.insert(imp.iat_va, label);
}
out
}
fn lift_pe_function(
pe: &PeFile,
name: &str,
file_offset: u64,
rva: u32,
bitness: Bitness,
func_bytes: &[u8],
name_at: &HashMap<u64, String>,
) -> Option<Vec<Item>> {
let insns = crate::arch::x86::decode_tolerant(bitness, func_bytes, u64::from(rva));
if insns.is_empty() {
return None;
}
let consumed: usize = insns.iter().map(|i| i.original_bytes.len()).sum();
let lifted = lift_function(name.to_string(), &insns).ok()?;
let fn_decl = build_function::build_function(&lifted, None, name_at, pe);
let mut out = Vec::with_capacity(2);
out.push(Item::Function(crate::ast::FnDecl {
addr: Some(file_offset),
name: name.to_string(),
attrs: fn_decl.attrs,
signature: fn_decl.signature,
locals: fn_decl.locals,
body: fn_decl.body,
}));
if consumed < func_bytes.len() {
out.push(Item::Raw {
addr: file_offset + consumed as u64,
bytes: func_bytes[consumed..].to_vec(),
});
}
Some(out)
}
fn is_emittable_function_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first == '_' || first.is_ascii_alphabetic()) {
return false;
}
chars.all(|c| c == '_' || c == '.' || c.is_ascii_alphanumeric())
}
fn build_function_blocks(pe: &PeFile) -> Vec<Value> {
pe.coff_symbols()
.iter()
.filter(|s| is_code_function(pe, s))
.map(|s| {
#[allow(clippy::cast_sign_loss)]
let section_idx = (s.section_number - 1) as usize;
let section_va = pe
.sections
.get(section_idx)
.map_or(0u32, |sh| sh.virtual_address);
let rva = section_va.wrapping_add(s.value);
#[allow(clippy::cast_sign_loss)]
let section = s.section_number as u16;
Value::Block(vec![
field("name", Value::String(s.name.clone())),
field("rva", Value::Int(u64::from(rva))),
field("section", Value::Int(u64::from(section))),
field("section_offset", Value::Int(u64::from(s.value))),
field("storage_class", Value::Int(u64::from(s.storage_class))),
])
})
.collect()
}
fn is_code_function(pe: &PeFile, sym: &CoffSymbol) -> bool {
if sym.section_number <= 0 {
return false;
}
if !is_emittable_function_name(&sym.name) {
return false;
}
#[allow(clippy::cast_sign_loss)]
let section_idx = (sym.section_number - 1) as usize;
let Some(sh) = pe.sections.get(section_idx) else {
return false;
};
let is_executable = sh.characteristics & 0x2000_0000 != 0;
if !is_executable {
return false;
}
if sym.is_function() {
return true;
}
matches!(
sym.storage_class,
COFF_SYM_CLASS_EXTERNAL | COFF_SYM_CLASS_STATIC
)
}