use serde::Serialize;
use crate::error::Error;
use crate::gobin::GoBinary;
use crate::Result;
pub const MAGIC_1_20: u32 = 0xfffffff1;
pub const MAGIC_1_18: u32 = 0xfffffff0;
pub fn unsupported_pre118_magic(magic: u32) -> Option<&'static str> {
match magic {
0xfffffffb => Some("Go 1.2 to 1.15"),
0xfffffffa => Some("Go 1.16 or 1.17"),
_ => None,
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Function {
pub address: u64,
pub name: String,
pub file: Option<String>,
pub start_line: Option<u32>,
}
#[derive(Clone)]
pub struct Pclntab<'a> {
data: &'a [u8],
little_endian: bool,
magic: u32,
quantum: u8,
ptrsize: u8,
nfunc: u64,
text_start: u64,
funcname_off: usize,
cu_off: usize,
filetab_off: usize,
pctab_off: usize,
funcdata_off: usize,
gofunc: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct FuncEntry {
pub address: u64,
pub name: String,
pub func_off: usize,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct InlineFrame {
pub name: String,
pub file: Option<String>,
pub line: Option<u32>,
pub inlined: bool,
}
impl<'a> Pclntab<'a> {
pub fn parse(bin: &'a GoBinary) -> Result<Self> {
let data = bin.pclntab_slice();
let little_endian = bin.little_endian;
if data.len() < 8 {
return Err(Error::BadPclntab {
offset: bin.pclntab_offset,
reason: format!("header truncated: only {} bytes", data.len()),
});
}
let magic = read_u32(data, 0, little_endian)?;
if let Some(version) = unsupported_pre118_magic(magic) {
return Err(Error::UnsupportedPclntabVersion { magic, version });
}
if data[4] != 0 || data[5] != 0 {
return Err(Error::BadPclntab {
offset: bin.pclntab_offset,
reason: format!(
"expected zero pad at [4..6], got {:02x} {:02x}",
data[4], data[5]
),
});
}
let quantum = data[6];
let ptrsize = data[7];
if !matches!(quantum, 1 | 2 | 4) {
return Err(Error::BadPclntab {
offset: bin.pclntab_offset,
reason: format!("invalid quantum {quantum}, expected 1, 2, or 4"),
});
}
if !matches!(ptrsize, 4 | 8) {
return Err(Error::BadPclntab {
offset: bin.pclntab_offset,
reason: format!("invalid ptrsize {ptrsize}, expected 4 or 8"),
});
}
let ps = ptrsize as usize;
let read_uptr = |off: usize| read_uintptr(data, off, ps, little_endian);
let nfunc = read_uptr(8)?;
let _nfiles = read_uptr(8 + ps)?;
let text_start = read_uptr(8 + 2 * ps)?;
let funcname_off = read_uptr(8 + 3 * ps)? as usize;
let cu_off = read_uptr(8 + 4 * ps)? as usize;
let filetab_off = read_uptr(8 + 5 * ps)? as usize;
let pctab_off = read_uptr(8 + 6 * ps)? as usize;
let funcdata_off = read_uptr(8 + 7 * ps)? as usize;
for (name, off) in [
("funcnameOffset", funcname_off),
("cuOffset", cu_off),
("filetabOffset", filetab_off),
("pctabOffset", pctab_off),
("funcdataOffset", funcdata_off),
] {
if off >= data.len() {
return Err(Error::BadPclntab {
offset: bin.pclntab_offset,
reason: format!(
"{name}=0x{off:x} is past end of pclntab (0x{:x})",
data.len()
),
});
}
}
let text_start = if text_start == 0 {
bin.text_addr
} else {
text_start
};
Ok(Pclntab {
data,
little_endian,
magic,
quantum,
ptrsize,
nfunc,
text_start,
funcname_off,
cu_off,
filetab_off,
pctab_off,
funcdata_off,
gofunc: None,
})
}
pub fn with_gofunc(mut self, gofunc: u64) -> Self {
self.gofunc = Some(gofunc);
self
}
pub fn magic(&self) -> u32 {
self.magic
}
pub fn magic_is_official(&self) -> bool {
self.magic == MAGIC_1_20 || self.magic == MAGIC_1_18
}
pub fn nfunc(&self) -> u64 {
self.nfunc
}
pub fn data(&self) -> &[u8] {
self.data
}
pub fn little_endian(&self) -> bool {
self.little_endian
}
pub fn funcdata_off(&self) -> usize {
self.funcdata_off
}
pub fn funcname_off(&self) -> usize {
self.funcname_off
}
pub fn pctab_off(&self) -> usize {
self.pctab_off
}
pub fn gofunc(&self) -> Option<u64> {
self.gofunc
}
pub fn read_name_at(&self, name_off: usize) -> Result<String> {
self.read_name(name_off)
}
pub fn lookup(&self, pc: u64) -> Option<Function> {
let n = self.nfunc as usize;
let entry_size = 8usize;
if pc < self.text_start {
return None;
}
let pc_off = (pc - self.text_start) as u32;
let read_off = |i: usize| -> Option<u32> {
let entry = self.funcdata_off + i * entry_size;
read_u32(self.data, entry, self.little_endian).ok()
};
let mut lo = 0usize;
let mut hi = n;
while lo < hi {
let mid = (lo + hi) / 2;
let mid_off = read_off(mid)?;
if mid_off <= pc_off {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo == 0 {
return None;
}
let idx = lo - 1;
let text_off = read_off(idx)?;
let next_off = read_off(idx + 1).unwrap_or(u32::MAX);
if pc_off >= next_off {
return None;
}
let entry = self.funcdata_off + idx * entry_size;
let func_off = read_u32(self.data, entry + 4, self.little_endian).ok()? as usize;
self.read_function(text_off, func_off).ok()
}
pub fn lookup_inline(&self, bin: &crate::gobin::GoBinary, pc: u64) -> Vec<InlineFrame> {
let mut frames = Vec::new();
let Some(leaf) = self.lookup(pc) else {
return frames;
};
let physical_frame = InlineFrame {
name: leaf.name.clone(),
file: leaf.file.clone(),
line: leaf.start_line,
inlined: false,
};
let Some(gofunc) = self.gofunc else {
frames.push(physical_frame);
return frames;
};
let n = self.nfunc as usize;
let pc_off = match pc.checked_sub(self.text_start) {
Some(v) => v as u32,
None => {
frames.push(physical_frame);
return frames;
}
};
let read_text_off =
|i: usize| read_u32(self.data, self.funcdata_off + i * 8, self.little_endian).ok();
let mut lo = 0;
let mut hi = n;
while lo < hi {
let mid = (lo + hi) / 2;
match read_text_off(mid) {
Some(o) if o <= pc_off => lo = mid + 1,
_ => hi = mid,
}
}
if lo == 0 {
frames.push(physical_frame);
return frames;
}
let idx = lo - 1;
let Some(func_off) = read_u32(
self.data,
self.funcdata_off + idx * 8 + 4,
self.little_endian,
)
.ok() else {
frames.push(physical_frame);
return frames;
};
let func_start = self.funcdata_off + func_off as usize;
if func_start + 44 > self.data.len() {
frames.push(physical_frame);
return frames;
}
let npcdata = match read_u32(self.data, func_start + 28, self.little_endian) {
Ok(v) => v as usize,
Err(_) => {
frames.push(physical_frame);
return frames;
}
};
let nfuncdata = self.data[func_start + 43] as usize;
let pcdata_off = func_start + 44;
let funcdata_arr_off = pcdata_off + npcdata * 4;
const PCDATA_INLTREE_INDEX: usize = 2;
const FUNCDATA_INLTREE: usize = 3;
if nfuncdata <= FUNCDATA_INLTREE || npcdata <= PCDATA_INLTREE_INDEX {
frames.push(physical_frame);
return frames;
}
let inltree_funcdata = match read_u32(
self.data,
funcdata_arr_off + FUNCDATA_INLTREE * 4,
self.little_endian,
) {
Ok(v) => v,
Err(_) => {
frames.push(physical_frame);
return frames;
}
};
if inltree_funcdata == u32::MAX {
frames.push(physical_frame);
return frames;
}
let inltree_addr = gofunc.wrapping_add(inltree_funcdata as u64);
let pcdata_inltree_off = match read_u32(
self.data,
pcdata_off + PCDATA_INLTREE_INDEX * 4,
self.little_endian,
) {
Ok(v) => v as usize,
Err(_) => {
frames.push(physical_frame);
return frames;
}
};
let func_entry_pc = self.text_start + read_text_off(idx).unwrap_or(0) as u64;
let target_off = (pc - func_entry_pc) as u32;
let mut current_idx =
pcvalue_at(&self.data[self.pctab_off..], pcdata_inltree_off, target_off);
const MAX_INLINE_DEPTH: usize = 32;
const MAX_INLTREE_INDEX: i64 = 65_536;
let mut current_pc_off = target_off;
let mut iter_guard = 0;
while current_idx >= 0 && iter_guard < MAX_INLINE_DEPTH {
if current_idx > MAX_INLTREE_INDEX {
break;
}
iter_guard += 1;
let entry_addr = inltree_addr + (current_idx as u64) * 16;
let Some(entry) = bin.read_at_addr(entry_addr, 16) else {
break;
};
let name_off = i32::from_le_bytes(entry[4..8].try_into().unwrap()) as usize;
let parent_pc = i32::from_le_bytes(entry[8..12].try_into().unwrap());
let start_line = i32::from_le_bytes(entry[12..16].try_into().unwrap());
let name = self
.read_name(name_off)
.unwrap_or_else(|_| format!("inlined@{current_idx}"));
let file_and_line = self.file_and_line_for_pc(func_start, current_pc_off);
frames.push(InlineFrame {
name,
file: file_and_line.0,
line: file_and_line.1.or(if start_line > 0 {
Some(start_line as u32)
} else {
None
}),
inlined: true,
});
current_pc_off = parent_pc as u32;
current_idx = pcvalue_at(
&self.data[self.pctab_off..],
pcdata_inltree_off,
current_pc_off,
);
}
frames.push(physical_frame);
frames
}
fn file_and_line_for_pc(
&self,
func_start: usize,
target_off: u32,
) -> (Option<String>, Option<u32>) {
let Ok(pcfile_off) = read_u32(self.data, func_start + 20, self.little_endian) else {
return (None, None);
};
let Ok(pcln_off) = read_u32(self.data, func_start + 24, self.little_endian) else {
return (None, None);
};
let Ok(cu_offset) = read_u32(self.data, func_start + 32, self.little_endian) else {
return (None, None);
};
let file_idx = pcvalue_at(
&self.data[self.pctab_off..],
pcfile_off as usize,
target_off,
);
let line = pcvalue_at(&self.data[self.pctab_off..], pcln_off as usize, target_off);
let file = if file_idx >= 0 {
self.resolve_file(cu_offset as usize, file_idx as usize)
} else {
None
};
let line = if line > 0 { Some(line as u32) } else { None };
(file, line)
}
fn resolve_file(&self, cu_offset: usize, file_idx: usize) -> Option<String> {
let cutab_entry = cu_offset
.checked_add(file_idx)?
.checked_mul(4)?
.checked_add(self.cu_off)?;
if cutab_entry.checked_add(4)? > self.data.len() {
return None;
}
let raw = read_u32(self.data, cutab_entry, self.little_endian).ok()?;
if raw == u32::MAX {
return None;
}
let abs = self.filetab_off.checked_add(raw as usize)?;
read_cstring(self.data, abs).ok()
}
pub fn text_start(&self) -> u64 {
self.text_start
}
pub fn ptrsize(&self) -> u8 {
self.ptrsize
}
pub fn quantum(&self) -> u8 {
self.quantum
}
pub fn functions(&self) -> Result<Vec<Function>> {
const ENTRY_SIZE: usize = 8;
const MAX_FUNCS: u64 = 5_000_000;
let available_bytes = self.data.len().saturating_sub(self.funcdata_off);
let max_possible = (available_bytes / ENTRY_SIZE).saturating_sub(1) as u64;
let n = self.nfunc.min(MAX_FUNCS).min(max_possible) as usize;
if (self.nfunc > MAX_FUNCS || self.nfunc > max_possible) && self.nfunc != 0 {
return Err(Error::BadPclntab {
offset: 0,
reason: format!(
"nfunc {} exceeds available functab capacity (max {} from byte range, cap {})",
self.nfunc, max_possible, MAX_FUNCS,
),
});
}
let table_bytes = n.saturating_add(1).saturating_mul(ENTRY_SIZE);
if self.funcdata_off + table_bytes > self.data.len() {
return Err(Error::BadPclntab {
offset: 0,
reason: format!("functab ({n} entries x {ENTRY_SIZE}) extends past pclntab end"),
});
}
let mut out = Vec::with_capacity(n);
let entry_size = ENTRY_SIZE;
for i in 0..n {
let entry = self.funcdata_off + i * entry_size;
let text_off = read_u32(self.data, entry, self.little_endian)?;
let func_off = read_u32(self.data, entry + 4, self.little_endian)? as usize;
match self.read_function(text_off, func_off) {
Ok(f) => out.push(f),
Err(_) => continue,
}
}
Ok(out)
}
pub fn functions_with_offsets(&self) -> Result<Vec<FuncEntry>> {
const ENTRY_SIZE: usize = 8;
const MAX_FUNCS: u64 = 5_000_000;
let available_bytes = self.data.len().saturating_sub(self.funcdata_off);
let max_possible = (available_bytes / ENTRY_SIZE).saturating_sub(1) as u64;
let n = self.nfunc.min(MAX_FUNCS).min(max_possible) as usize;
if (self.nfunc > MAX_FUNCS || self.nfunc > max_possible) && self.nfunc != 0 {
return Err(Error::BadPclntab {
offset: 0,
reason: format!(
"nfunc {} exceeds functab capacity (max {}, cap {})",
self.nfunc, max_possible, MAX_FUNCS,
),
});
}
let table_bytes = n.saturating_add(1).saturating_mul(ENTRY_SIZE);
if self.funcdata_off + table_bytes > self.data.len() {
return Err(Error::BadPclntab {
offset: 0,
reason: format!("functab ({n} entries) extends past pclntab end"),
});
}
let mut out = Vec::with_capacity(n);
let read_entry = |i: usize| -> Result<(u32, u32)> {
let entry = self.funcdata_off + i * ENTRY_SIZE;
let t = read_u32(self.data, entry, self.little_endian)?;
let f = read_u32(self.data, entry + 4, self.little_endian)?;
Ok((t, f))
};
for i in 0..n {
let (text_off, func_off) = match read_entry(i) {
Ok(v) => v,
Err(_) => continue,
};
let next_text_off = read_entry(i + 1).map(|(t, _)| t).unwrap_or(u32::MAX);
let size = next_text_off.saturating_sub(text_off) as u64;
let func_start = self.funcdata_off + func_off as usize;
if func_start + 8 > self.data.len() {
continue;
}
let name_off = match read_u32(self.data, func_start + 4, self.little_endian) {
Ok(v) => v as usize,
Err(_) => continue,
};
let name = self.read_name(name_off).unwrap_or_default();
out.push(FuncEntry {
address: self.text_start + text_off as u64,
name,
func_off: func_off as usize,
size,
});
}
Ok(out)
}
fn read_function(&self, text_off: u32, func_off: usize) -> Result<Function> {
let func_start = self.funcdata_off + func_off;
if func_start + 8 > self.data.len() {
return Err(Error::ShortRead {
wanted: 8,
offset: func_start,
available: self.data.len().saturating_sub(func_start),
});
}
let name_off = read_u32(self.data, func_start + 4, self.little_endian)? as usize;
let address = self.text_start + text_off as u64;
let name = self.read_name(name_off)?;
let (file, start_line) = self.read_file_and_line(func_start).unwrap_or((None, None));
Ok(Function {
address,
name,
file,
start_line,
})
}
fn read_name(&self, off: usize) -> Result<String> {
let abs = self.funcname_off + off;
read_cstring(self.data, abs)
}
fn read_file_and_line(&self, func_start: usize) -> Option<(Option<String>, Option<u32>)> {
if func_start + 36 > self.data.len() {
return None;
}
let pcfile_off = read_u32(self.data, func_start + 20, self.little_endian).ok()? as usize;
let pcln_off = read_u32(self.data, func_start + 24, self.little_endian).ok()? as usize;
let cu_offset = read_u32(self.data, func_start + 32, self.little_endian).ok()? as usize;
let file_idx = first_pcvalue(&self.data[self.pctab_off..], pcfile_off)?;
let line = first_pcvalue(&self.data[self.pctab_off..], pcln_off)?;
let file = self.resolve_file(cu_offset, file_idx as usize);
Some((file, if line > 0 { Some(line as u32) } else { None }))
}
}
fn pcvalue_at(table: &[u8], start: usize, target_off: u32) -> i64 {
if start >= table.len() {
return -1;
}
let mut value: i64 = -1;
let mut pc: u32 = 0;
let mut pos = start;
let quantum: u32 = 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 -1,
};
if uval == 0 && pc != 0 {
return -1;
}
pos += n;
let delta_val = zig_zag(uval);
value = value.wrapping_add(delta_val);
let (pc_delta, n2) = match read_varint(&table[pos..]) {
Some(v) => v,
None => return -1,
};
pos += n2;
pc = pc.wrapping_add((pc_delta as u32) * quantum);
if target_off < pc {
return value;
}
}
-1
}
fn first_pcvalue(table: &[u8], start: usize) -> Option<i64> {
if start >= table.len() {
return None;
}
let (uval, n) = read_varint(&table[start..])?;
if uval == 0 {
return None;
}
let delta = zig_zag(uval);
let mut value: i64 = -1;
value = value.wrapping_add(delta);
let _ = n;
Some(value)
}
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
}
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 slice = &buf[off..off + 4];
Ok(if little_endian {
u32::from_le_bytes(slice.try_into().unwrap())
} else {
u32::from_be_bytes(slice.try_into().unwrap())
})
}
fn read_u64(buf: &[u8], off: usize, little_endian: bool) -> Result<u64> {
if off + 8 > buf.len() {
return Err(Error::ShortRead {
wanted: 8,
offset: off,
available: buf.len().saturating_sub(off),
});
}
let slice = &buf[off..off + 8];
Ok(if little_endian {
u64::from_le_bytes(slice.try_into().unwrap())
} else {
u64::from_be_bytes(slice.try_into().unwrap())
})
}
fn read_uintptr(buf: &[u8], off: usize, ptrsize: usize, little_endian: bool) -> Result<u64> {
match ptrsize {
4 => read_u32(buf, off, little_endian).map(|v| v as u64),
8 => read_u64(buf, off, little_endian),
_ => Err(Error::BadPclntab {
offset: off,
reason: format!("ptrsize {ptrsize} is neither 4 nor 8"),
}),
}
}
fn read_cstring(buf: &[u8], off: usize) -> Result<String> {
if off >= buf.len() {
return Err(Error::ShortRead {
wanted: 1,
offset: off,
available: 0,
});
}
let end = buf[off..]
.iter()
.position(|&b| b == 0)
.map(|p| off + p)
.unwrap_or(buf.len());
std::str::from_utf8(&buf[off..end])
.map(|s| s.to_string())
.map_err(|_| Error::BadString { offset: off })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pre_118_magics_are_rejected_by_version() {
assert_eq!(
unsupported_pre118_magic(0xfffffffa),
Some("Go 1.16 or 1.17")
);
assert_eq!(unsupported_pre118_magic(0xfffffffb), Some("Go 1.2 to 1.15"));
assert_eq!(unsupported_pre118_magic(MAGIC_1_18), None);
assert_eq!(unsupported_pre118_magic(MAGIC_1_20), None);
assert_eq!(unsupported_pre118_magic(0x1234_5678), None);
}
#[test]
fn varint_decodes_single_byte() {
assert_eq!(read_varint(&[0x05]), Some((5, 1)));
assert_eq!(read_varint(&[0x7f]), Some((127, 1)));
}
#[test]
fn varint_decodes_multi_byte() {
assert_eq!(read_varint(&[0xac, 0x02]), Some((300, 2)));
}
#[test]
fn varint_unterminated_returns_none() {
assert!(read_varint(&[0x80, 0x80, 0x80]).is_none());
}
#[test]
fn zig_zag_round_trips() {
assert_eq!(zig_zag(0), 0);
assert_eq!(zig_zag(1), -1);
assert_eq!(zig_zag(2), 1);
assert_eq!(zig_zag(3), -2);
}
#[test]
fn cstring_stops_at_nul() {
let buf = b"hello\0world";
assert_eq!(read_cstring(buf, 0).unwrap(), "hello");
assert_eq!(read_cstring(buf, 6).unwrap(), "world");
}
#[test]
fn u32_respects_endianness() {
let buf = [0x01, 0x02, 0x03, 0x04];
assert_eq!(read_u32(&buf, 0, true).unwrap(), 0x04030201);
assert_eq!(read_u32(&buf, 0, false).unwrap(), 0x01020304);
}
#[test]
fn resolve_file_rejects_overflowing_indices() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join("hello.linux-amd64.stripped");
if !path.exists() {
return;
}
let bin = GoBinary::open(&path).expect("open fixture");
let pcln = Pclntab::parse(&bin).expect("parse pclntab");
for (cu, idx) in [(usize::MAX, usize::MAX), (usize::MAX, 0), (0, usize::MAX)] {
assert!(
pcln.resolve_file(cu, idx).is_none(),
"overflowing ({cu}, {idx}) must resolve to no file"
);
}
}
}