use serde::Serialize;
use crate::error::Error;
use crate::gobin::{Arch, GoBinary, SectionKind};
use crate::itabs::Itab;
use crate::pclntab::{Function, Pclntab};
use crate::Result;
const ITAB_HEADER_BYTES: u64 = 40;
const ITAB_SLOT_BYTES: u64 = 8;
#[derive(Debug, Clone)]
pub enum Target {
Function(String),
Address(u64),
ItabMethod { itab_addr: u64, method_index: usize },
}
#[derive(Debug, Clone, Serialize)]
pub struct CallSite {
pub call_site: u64,
pub caller_name: Option<String>,
pub caller_addr: Option<u64>,
pub kind: CallKind,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CallKind {
Direct,
IndirectItab {
itab_addr: u64,
method_index: usize,
method_name: Option<String>,
},
}
pub fn find(
bin: &GoBinary,
pcln: &Pclntab<'_>,
itabs: &[Itab],
target: &Target,
) -> Result<Vec<CallSite>> {
if !bin.little_endian {
return Err(Error::Xrefs(
"callsites scan is little-endian only today".into(),
));
}
if !matches!(bin.arch, Arch::X86_64 | Arch::Aarch64) {
return Err(Error::Xrefs(format!(
"callsites scan supports amd64 and arm64 today; got {:?}",
bin.arch
)));
}
let functions = pcln.functions()?;
let text = pick_text(bin)?;
let text_start = text.addr;
let text_bytes = &bin.bytes[text.file_offset..text.file_offset + text.file_size];
let scan_direct = match bin.arch {
Arch::X86_64 => scan_direct_amd64,
Arch::Aarch64 => scan_direct_arm64,
_ => unreachable!("arch gate above"),
};
let scan_indirect = match bin.arch {
Arch::X86_64 => scan_indirect_itab_for_slot_amd64,
Arch::Aarch64 => scan_indirect_itab_for_slot_arm64,
_ => unreachable!("arch gate above"),
};
let mut out = Vec::new();
match target {
Target::Function(name) => {
let target_addr = resolve_function(name, &functions)?;
scan_direct(text_bytes, text_start, target_addr, pcln, &mut out);
for it in itabs {
for (idx, m) in it.methods.iter().enumerate() {
if m.concrete_fn == target_addr {
scan_indirect(text_bytes, text_start, it, idx, pcln, &mut out);
}
}
}
}
Target::Address(addr) => {
scan_direct(text_bytes, text_start, *addr, pcln, &mut out);
for it in itabs {
for (idx, m) in it.methods.iter().enumerate() {
if m.concrete_fn == *addr {
scan_indirect(text_bytes, text_start, it, idx, pcln, &mut out);
}
}
}
}
Target::ItabMethod {
itab_addr,
method_index,
} => {
let it = itabs
.iter()
.find(|it| it.addr == *itab_addr)
.ok_or_else(|| {
Error::Xrefs(format!(
"no itab recovered at 0x{itab_addr:x}; --xref Target::ItabMethod \
requires the address to match a recovered itab base"
))
})?;
scan_indirect(text_bytes, text_start, it, *method_index, pcln, &mut out);
}
}
Ok(out)
}
fn resolve_function(name: &str, functions: &[Function]) -> Result<u64> {
functions
.iter()
.find(|f| f.name == name)
.map(|f| f.address)
.ok_or_else(|| Error::Xrefs(format!("no function named {name:?} in pclntab")))
}
fn pick_text(bin: &GoBinary) -> Result<&crate::gobin::Section> {
bin.sections
.iter()
.find(|s| s.kind == SectionKind::Text && s.name.ends_with(".text"))
.or_else(|| {
bin.sections
.iter()
.filter(|s| s.kind == SectionKind::Text)
.max_by_key(|s| s.file_size)
})
.ok_or_else(|| Error::Xrefs("no text section".into()))
}
fn scan_direct_amd64(
text_bytes: &[u8],
text_start: u64,
target_addr: u64,
pcln: &Pclntab<'_>,
out: &mut Vec<CallSite>,
) {
let mut i = 0usize;
while i + 5 <= text_bytes.len() {
if text_bytes[i] == 0xE8 {
let rel = i32::from_le_bytes(text_bytes[i + 1..i + 5].try_into().unwrap());
let call_site_va = text_start + i as u64;
let call_next_va = call_site_va + 5;
let resolved = call_next_va.wrapping_add(rel as i64 as u64);
if resolved == target_addr {
let caller = pcln.lookup(call_site_va);
out.push(CallSite {
call_site: call_site_va,
caller_name: caller.as_ref().map(|f| f.name.clone()),
caller_addr: caller.as_ref().map(|f| f.address),
kind: CallKind::Direct,
});
}
}
i += 1;
}
}
#[cfg(test)]
pub(crate) fn encode_call_rip_rel(instruction_va: u64, effective_addr: u64) -> [u8; 6] {
let next_pc = instruction_va + 6;
let disp = (effective_addr as i64).wrapping_sub(next_pc as i64) as i32;
let d = disp.to_le_bytes();
[0xff, 0x15, d[0], d[1], d[2], d[3]]
}
fn scan_indirect_itab_for_slot_amd64(
text_bytes: &[u8],
text_start: u64,
itab: &Itab,
method_index: usize,
pcln: &Pclntab<'_>,
out: &mut Vec<CallSite>,
) {
let slot_va = itab
.addr
.wrapping_add(ITAB_HEADER_BYTES)
.wrapping_add((method_index as u64).wrapping_mul(ITAB_SLOT_BYTES));
let method_name = itab
.methods
.get(method_index)
.map(|m| m.interface_method.clone());
let mut i = 0usize;
while i + 6 <= text_bytes.len() {
if text_bytes[i] == 0xff && text_bytes[i + 1] == 0x15 {
let disp = i32::from_le_bytes(text_bytes[i + 2..i + 6].try_into().unwrap());
let call_site_va = text_start + i as u64;
let next_pc = call_site_va + 6;
let resolved = next_pc.wrapping_add(disp as i64 as u64);
if resolved == slot_va {
let caller = pcln.lookup(call_site_va);
out.push(CallSite {
call_site: call_site_va,
caller_name: caller.as_ref().map(|f| f.name.clone()),
caller_addr: caller.as_ref().map(|f| f.address),
kind: CallKind::IndirectItab {
itab_addr: itab.addr,
method_index,
method_name: method_name.clone(),
},
});
}
}
i += 1;
}
}
fn scan_direct_arm64(
text_bytes: &[u8],
text_start: u64,
target_addr: u64,
pcln: &Pclntab<'_>,
out: &mut Vec<CallSite>,
) {
let mut i = 0usize;
while i + 4 <= text_bytes.len() {
let instr = u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap());
if (instr >> 26) == 0b100101 {
let imm26 = instr & 0x03ff_ffff;
let signed = if imm26 & (1 << 25) != 0 {
(imm26 | 0xfc00_0000) as i32
} else {
imm26 as i32
};
let offset_bytes = (signed as i64).wrapping_mul(4);
let call_site_va = text_start + i as u64;
let resolved = call_site_va.wrapping_add(offset_bytes as u64);
if resolved == target_addr {
let caller = pcln.lookup(call_site_va);
out.push(CallSite {
call_site: call_site_va,
caller_name: caller.as_ref().map(|f| f.name.clone()),
caller_addr: caller.as_ref().map(|f| f.address),
kind: CallKind::Direct,
});
}
}
i += 4; }
}
fn scan_indirect_itab_for_slot_arm64(
text_bytes: &[u8],
text_start: u64,
itab: &Itab,
method_index: usize,
pcln: &Pclntab<'_>,
out: &mut Vec<CallSite>,
) {
let slot_offset_into_itab = ITAB_HEADER_BYTES + (method_index as u64) * ITAB_SLOT_BYTES;
let method_name = itab
.methods
.get(method_index)
.map(|m| m.interface_method.clone());
let mut i = 0usize;
while i + 8 <= text_bytes.len() {
let ldr = u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap());
let blr = u32::from_le_bytes(text_bytes[i + 4..i + 8].try_into().unwrap());
if (ldr >> 22) != 0b11_1110_0101 {
i += 4;
continue;
}
if (blr & 0xff_ff_fc_1f) != 0xd6_3f_00_00 {
i += 4;
continue;
}
let ldr_rt = ldr & 0b11111;
let _ldr_rn = (ldr >> 5) & 0b11111;
let ldr_imm12 = (ldr >> 10) & 0xfff;
let load_offset = (ldr_imm12 as u64) * 8;
let blr_rn = (blr >> 5) & 0b11111;
if blr_rn != ldr_rt {
i += 4;
continue;
}
if load_offset != slot_offset_into_itab {
i += 4;
continue;
}
let call_site_va = text_start + (i + 4) as u64;
let caller = pcln.lookup(call_site_va);
out.push(CallSite {
call_site: call_site_va,
caller_name: caller.as_ref().map(|f| f.name.clone()),
caller_addr: caller.as_ref().map(|f| f.address),
kind: CallKind::IndirectItab {
itab_addr: itab.addr,
method_index,
method_name: method_name.clone(),
},
});
i += 8;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::itabs::{Itab, ItabMethod};
fn itab_at(addr: u64, methods: Vec<(String, u64)>) -> Itab {
Itab {
addr,
interface_name: "*main.stage".into(),
concrete_name: "*main.xorMask".into(),
hash: 0,
incomplete: false,
methods: methods
.into_iter()
.map(|(n, f)| ItabMethod {
interface_method: n,
concrete_fn: f,
})
.collect(),
stdlib_interface: None,
}
}
#[test]
fn encode_call_rip_rel_round_trips_through_the_scanner() {
let text_start = 0x401000u64;
let itab = itab_at(0x500000, vec![("Apply".into(), 0x402000)]);
let slot_va = itab.addr + ITAB_HEADER_BYTES; let call_instr_va = 0x401100u64;
let encoded = encode_call_rip_rel(call_instr_va, slot_va);
let mut text_bytes = vec![0x90u8; 0x200]; let offset = (call_instr_va - text_start) as usize;
text_bytes[offset..offset + 6].copy_from_slice(&encoded);
let mut i = 0usize;
let mut hits = Vec::new();
while i + 6 <= text_bytes.len() {
if text_bytes[i] == 0xff && text_bytes[i + 1] == 0x15 {
let disp = i32::from_le_bytes(text_bytes[i + 2..i + 6].try_into().unwrap());
let pc = text_start + i as u64;
let next = pc + 6;
let resolved = next.wrapping_add(disp as i64 as u64);
if resolved == slot_va {
hits.push(pc);
}
}
i += 1;
}
assert_eq!(
hits.len(),
1,
"expected exactly one hit for the synthetic encoding"
);
assert_eq!(
hits[0], call_instr_va,
"hit must point at the encoded instruction"
);
}
#[test]
fn target_itabmethod_resolves_slot_va_correctly() {
let itab = itab_at(
0x500000,
vec![
("Read".into(), 0x402000),
("Write".into(), 0x402100),
("Close".into(), 0x402200),
],
);
for (idx, expected_slot) in [(0, 0x500028), (1, 0x500030), (2, 0x500038)] {
let computed = itab
.addr
.wrapping_add(ITAB_HEADER_BYTES)
.wrapping_add((idx as u64).wrapping_mul(ITAB_SLOT_BYTES));
assert_eq!(
computed, expected_slot,
"slot {idx} of itab at 0x{:x} should be at 0x{:x}",
itab.addr, expected_slot
);
}
}
#[test]
fn itab_constants_match_runtime_layout() {
assert_eq!(ITAB_HEADER_BYTES, 40, "5 pointers x 8 bytes");
assert_eq!(ITAB_SLOT_BYTES, 8, "amd64 pointer width");
}
}