use crate::error::Error;
use crate::gobin::GoBinary;
use crate::pclntab::{FuncEntry, Pclntab};
use crate::Result;
#[derive(Debug, Clone, Copy)]
pub struct InlinedCall {
pub func_id: u8,
pub name_off: i32,
pub parent_pc: i32,
pub start_line: i32,
}
const FUNCDATA_INLTREE: usize = 3;
const PCDATA_INLTREE_INDEX: usize = 2;
const MAX_INLINE_ENTRIES: usize = 65_536;
pub fn decode_inline_tree(
bin: &GoBinary,
pcln: &Pclntab,
func: &FuncEntry,
) -> Result<Vec<InlinedCall>> {
let data = pcln.data();
let le = pcln.little_endian();
let func_start = pcln.funcdata_off() + func.func_off;
if func_start + 44 > data.len() {
return Ok(vec![]);
}
let npcdata = read_u32(data, func_start + 28, le)? as usize;
let nfuncdata = data[func_start + 43] as usize;
if nfuncdata <= FUNCDATA_INLTREE || npcdata <= PCDATA_INLTREE_INDEX {
return Ok(vec![]);
}
let pcdata_off = func_start + 44;
let pcdata_inltree_off = read_u32(data, pcdata_off + PCDATA_INLTREE_INDEX * 4, le)? as usize;
let max_idx = max_pcvalue(
pcln_pctab(pcln),
pcdata_inltree_off,
u32::try_from(func.size).unwrap_or(u32::MAX),
);
if max_idx < 0 {
return Ok(vec![]);
}
let n_entries = (max_idx as usize).saturating_add(1).min(MAX_INLINE_ENTRIES);
let funcdata_arr_off = func_start + 44 + npcdata * 4;
let inltree_slot_off = funcdata_arr_off + FUNCDATA_INLTREE * 4;
if inltree_slot_off + 4 > data.len() {
return Ok(vec![]);
}
let inltree_funcdata = read_u32(data, inltree_slot_off, le)?;
if inltree_funcdata == u32::MAX {
return Ok(vec![]);
}
let Some(gofunc) = pcln.gofunc() else {
return Err(Error::ModuleData(
"inline-tree decoding requires gofunc base; \
call Pclntab::with_gofunc(moduledata.gofunc) first"
.to_string(),
));
};
let inltree_addr = gofunc.wrapping_add(inltree_funcdata as u64);
let mut out = Vec::with_capacity(n_entries);
for i in 0..n_entries {
let entry_addr = inltree_addr.wrapping_add((i as u64) * 16);
let Some(bytes) = bin.read_at_addr(entry_addr, 16) else {
break;
};
let func_id = bytes[0];
let name_off = i32::from_le_bytes(bytes[4..8].try_into().unwrap());
let parent_pc = i32::from_le_bytes(bytes[8..12].try_into().unwrap());
let start_line = i32::from_le_bytes(bytes[12..16].try_into().unwrap());
let entry = InlinedCall {
func_id,
name_off,
parent_pc,
start_line,
};
if entry.parent_pc != -1 {
if entry.parent_pc < 0 {
return Err(Error::ModuleData(format!(
"inline tree for {} has negative parent_pc {} at index {}",
func.name, entry.parent_pc, i,
)));
}
let pc = entry.parent_pc as u64;
if pc >= func.size {
return Err(Error::ModuleData(format!(
"inline tree for {} has parent_pc 0x{:x} >= func.size 0x{:x} at index {}",
func.name, pc, func.size, i,
)));
}
}
out.push(entry);
}
Ok(out)
}
pub fn resolve_leaf_name(pcln: &Pclntab, leaf: &InlinedCall) -> Option<String> {
if leaf.name_off == 0 {
return None;
}
if leaf.name_off < 0 {
return None;
}
let off = leaf.name_off as usize;
let abs = pcln.funcname_off().checked_add(off)?;
if abs >= pcln.data().len() {
return None;
}
let name = pcln.read_name_at(off).ok()?;
if name.is_empty() {
None
} else {
Some(name)
}
}
fn pcln_pctab<'a>(pcln: &'a Pclntab<'_>) -> &'a [u8] {
let off = pcln.pctab_off();
let data = pcln.data();
if off >= data.len() {
&[]
} else {
&data[off..]
}
}
fn max_pcvalue(table: &[u8], start: usize, pc_limit: u32) -> i64 {
if start >= table.len() {
return -1;
}
let mut value: i64 = -1;
let mut pc: u32 = 0;
let mut pos = start;
let mut max_seen: i64 = -1;
let mut iter_guard = 0;
while pos < table.len() && iter_guard < 1_000_000 {
iter_guard += 1;
let (uval, n) = match read_varint(&table[pos..]) {
Some(v) => v,
None => return max_seen,
};
if uval == 0 && pc != 0 {
return max_seen;
}
pos += n;
let delta = zig_zag(uval);
value = value.wrapping_add(delta);
let (pc_delta, n2) = match read_varint(&table[pos..]) {
Some(v) => v,
None => return max_seen,
};
pos += n2;
pc = pc.wrapping_add(pc_delta as u32);
if value > max_seen {
max_seen = value;
}
if pc >= pc_limit {
return max_seen;
}
}
max_seen
}
fn zig_zag(u: u64) -> i64 {
((u >> 1) as i64) ^ -((u & 1) as i64)
}
fn read_varint(buf: &[u8]) -> Option<(u64, usize)> {
const MAX_BYTES: usize = 10;
let mut result: u64 = 0;
let mut shift = 0;
for (i, &b) in buf.iter().take(MAX_BYTES).enumerate() {
result |= ((b & 0x7f) as u64) << shift;
if b & 0x80 == 0 {
return Some((result, i + 1));
}
shift += 7;
if shift >= 64 {
return None;
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
pub addr: u64,
pub name: String,
pub kind: NodeKind,
}
impl Node {
const ANONYMOUS_ADDR_BIT: u64 = 1 << 63;
pub fn is_anonymous_addr(addr: u64) -> bool {
addr & Self::ANONYMOUS_ADDR_BIT != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeKind {
Physical,
AnonymousInline {
parent: u64,
start_line: i32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edge {
pub from: u64,
pub to: u64,
pub call_site: u64,
pub kind: EdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeKind {
Inlined,
Direct,
}
#[derive(Debug, Clone, Default)]
pub struct CallGraph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
}
impl CallGraph {
pub fn node(&self, addr: u64) -> Option<&Node> {
self.nodes.iter().find(|n| n.addr == addr)
}
pub fn edges_from(&self, from: u64) -> impl Iterator<Item = &Edge> {
self.edges.iter().filter(move |e| e.from == from)
}
pub fn edges_to(&self, to: u64) -> impl Iterator<Item = &Edge> {
self.edges.iter().filter(move |e| e.to == to)
}
}
pub fn inline_callgraph(bin: &GoBinary, pcln: &Pclntab) -> Result<CallGraph> {
use std::collections::HashMap;
let funcs = pcln.functions_with_offsets()?;
let mut graph = CallGraph::default();
let mut named_nodes: HashMap<String, u64> = HashMap::new();
let mut anon_nodes: HashMap<(u64, i32, i32), u64> = HashMap::new();
let mut edge_keys: std::collections::HashSet<(u64, u64, u64)> =
std::collections::HashSet::new();
let mut next_anon: u64 = 1;
for f in &funcs {
graph.nodes.push(Node {
addr: f.address,
name: f.name.clone(),
kind: NodeKind::Physical,
});
named_nodes.insert(f.name.clone(), f.address);
}
for f in &funcs {
let entries = match decode_inline_tree(bin, pcln, f) {
Ok(v) => v,
Err(_) => continue,
};
for entry in &entries {
if entry.parent_pc == -1 {
continue;
}
let call_site = f.address.wrapping_add(entry.parent_pc as u64);
let resolved = resolve_leaf_name(pcln, entry);
let callee_addr = match resolved {
Some(name) => *named_nodes.entry(name.clone()).or_insert_with(|| {
let addr = Node::ANONYMOUS_ADDR_BIT | next_anon;
next_anon += 1;
graph.nodes.push(Node {
addr,
name,
kind: NodeKind::AnonymousInline {
parent: f.address,
start_line: entry.start_line,
},
});
addr
}),
None => {
let key = (f.address, entry.parent_pc, entry.start_line);
*anon_nodes.entry(key).or_insert_with(|| {
let addr = Node::ANONYMOUS_ADDR_BIT | next_anon;
next_anon += 1;
graph.nodes.push(Node {
addr,
name: String::new(),
kind: NodeKind::AnonymousInline {
parent: f.address,
start_line: entry.start_line,
},
});
addr
})
}
};
let ek = (f.address, callee_addr, call_site);
if edge_keys.insert(ek) {
graph.edges.push(Edge {
from: f.address,
to: callee_addr,
call_site,
kind: EdgeKind::Inlined,
});
}
}
}
Ok(graph)
}
fn read_u32(buf: &[u8], off: usize, little_endian: bool) -> Result<u32> {
if off + 4 > buf.len() {
return Err(Error::ShortRead {
wanted: 4,
offset: off,
available: buf.len().saturating_sub(off),
});
}
let s = &buf[off..off + 4];
Ok(if little_endian {
u32::from_le_bytes(s.try_into().unwrap())
} else {
u32::from_be_bytes(s.try_into().unwrap())
})
}