use crate::{
formats::{BinaryContext, BinaryFormat, GoSections},
structures::{
Arch, PclntabVersion,
util::{advance, advance_n, read_uintptr, read_uvarint, slice_at},
},
};
const MAGICS: &[([u8; 4], PclntabVersion)] = &[
([0xf1, 0xff, 0xff, 0xff], PclntabVersion::Go120),
([0xf0, 0xff, 0xff, 0xff], PclntabVersion::Go118),
([0xfa, 0xff, 0xff, 0xff], PclntabVersion::Go116),
([0xfb, 0xff, 0xff, 0xff], PclntabVersion::Go12),
];
#[derive(Debug, Clone, Copy)]
pub struct ParsedPclntab<'a> {
pub data: &'a [u8],
pub offset: usize,
pub version: PclntabVersion,
pub min_lc: u8,
pub ptr_size: u8,
pub nfunc: usize,
pub nfiles: usize,
pub funcname_offset: usize,
pub cu_offset: usize,
pub filetab_offset: usize,
pub pctab_offset: usize,
pub functab_offset: usize,
pub text_start: u64,
pub header_text_start: Option<u64>,
}
#[derive(Debug, Clone, Copy)]
pub struct PclntabMeta {
pub offset: usize,
pub version: PclntabVersion,
pub min_lc: u8,
pub ptr_size: u8,
pub nfunc: usize,
pub nfiles: usize,
pub funcname_offset: usize,
pub cu_offset: usize,
pub filetab_offset: usize,
pub pctab_offset: usize,
pub functab_offset: usize,
pub text_start: u64,
pub header_text_start: Option<u64>,
}
impl PclntabMeta {
pub fn attach<'a>(&self, address_data: &'a [u8]) -> Option<ParsedPclntab<'a>> {
let data = address_data.get(self.offset..)?;
Some(ParsedPclntab {
data,
offset: self.offset,
version: self.version,
min_lc: self.min_lc,
ptr_size: self.ptr_size,
nfunc: self.nfunc,
nfiles: self.nfiles,
funcname_offset: self.funcname_offset,
cu_offset: self.cu_offset,
filetab_offset: self.filetab_offset,
pctab_offset: self.pctab_offset,
functab_offset: self.functab_offset,
text_start: self.text_start,
header_text_start: self.header_text_start,
})
}
}
impl<'a> ParsedPclntab<'a> {
pub fn meta(&self) -> PclntabMeta {
PclntabMeta {
offset: self.offset,
version: self.version,
min_lc: self.min_lc,
ptr_size: self.ptr_size,
nfunc: self.nfunc,
nfiles: self.nfiles,
funcname_offset: self.funcname_offset,
cu_offset: self.cu_offset,
filetab_offset: self.filetab_offset,
pctab_offset: self.pctab_offset,
functab_offset: self.functab_offset,
text_start: self.text_start,
header_text_start: self.header_text_start,
}
}
pub fn arch(&self) -> Arch {
match (self.min_lc, self.ptr_size) {
(1, 4) => Arch::X86,
(1, 8) => Arch::X86_64,
(4, 4) => Arch::Arm,
(4, 8) => Arch::Arm64,
(2, 8) => Arch::S390x,
_ => Arch::Unknown,
}
}
pub fn func_name(&self, name_off: u32) -> Option<&'a str> {
let pos = self.funcname_offset.checked_add(name_off as usize)?;
let remaining = self.data.get(pos..)?;
let end = remaining.iter().position(|&b| b == 0)?;
std::str::from_utf8(remaining.get(..end)?).ok()
}
pub fn file_name(&self, file_off: u32) -> Option<&'a str> {
let pos = self.filetab_offset.checked_add(file_off as usize)?;
let remaining = self.data.get(pos..)?;
let end = remaining.iter().position(|&b| b == 0)?;
std::str::from_utf8(remaining.get(..end)?).ok()
}
pub fn func_entries(&self) -> FuncEntryIter<'a> {
FuncEntryIter {
data: self.data,
base: self.functab_offset,
index: 0,
count: self.nfunc,
version: self.version,
ptr_size: self.ptr_size,
text_start: self.text_start,
}
}
fn func_struct_base(&self) -> usize {
match self.version {
PclntabVersion::Go12 => 0,
_ => self.functab_offset,
}
}
pub fn parse_func(&self, func_off: u32) -> Option<FuncData> {
let off = self.func_struct_base().checked_add(func_off as usize)?;
let d = self.data.get(off..)?;
let lay = func_layout(self.version, self.ptr_size);
if d.len() < lay.size {
return None;
}
let entry_off = if lay.entry_is_abs {
let abs = read_uintptr(d, 0, self.ptr_size)?;
u32::try_from(abs.saturating_sub(self.text_start)).unwrap_or(u32::MAX)
} else {
u32::from_le_bytes(slice_at::<4>(d, 0)?)
};
let start_line = match lay.start_line {
Some(o) => i32::from_le_bytes(slice_at::<4>(d, o)?),
None => 0,
};
let flag = match lay.flag {
Some(o) => *d.get(o)?,
None => 0,
};
let cu_offset = match lay.cu_offset {
Some(o) => u32::from_le_bytes(slice_at::<4>(d, o)?),
None => 0,
};
let func_id = match lay.func_id {
Some(o) => *d.get(o)?,
None => 0,
};
let nfuncdata = match lay.nfuncdata {
Some(o) => *d.get(o)?,
None => 0,
};
Some(FuncData {
func_off,
entry_off,
name_off: i32::from_le_bytes(slice_at::<4>(d, lay.name_off)?),
args: i32::from_le_bytes(slice_at::<4>(d, lay.args)?),
deferreturn: u32::from_le_bytes(slice_at::<4>(d, lay.deferreturn)?),
pcsp: u32::from_le_bytes(slice_at::<4>(d, lay.pcsp)?),
pcfile: u32::from_le_bytes(slice_at::<4>(d, lay.pcfile)?),
pcln: u32::from_le_bytes(slice_at::<4>(d, lay.pcln)?),
npcdata: u32::from_le_bytes(slice_at::<4>(d, lay.npcdata)?),
cu_offset,
start_line,
func_id,
flag,
nfuncdata,
})
}
pub fn pcdata_at(&self, func: &FuncData, i: u32) -> Option<u32> {
if i >= func.npcdata {
return None;
}
let base = self
.func_struct_base()
.checked_add(func.func_off as usize)?
.checked_add(func_layout(self.version, self.ptr_size).size)?;
let pos = base.checked_add((i as usize).checked_mul(4)?)?;
Some(u32::from_le_bytes(slice_at::<4>(self.data, pos)?))
}
pub fn funcdata_at(&self, func: &FuncData, i: u8) -> Option<u32> {
if i >= func.nfuncdata {
return None;
}
let base = self
.func_struct_base()
.checked_add(func.func_off as usize)?
.checked_add(func_layout(self.version, self.ptr_size).size)?;
let pcdata_bytes = (func.npcdata as usize).checked_mul(4)?;
let after_pcdata = base.checked_add(pcdata_bytes)?;
let pos = after_pcdata.checked_add((i as usize).checked_mul(4)?)?;
Some(u32::from_le_bytes(slice_at::<4>(self.data, pos)?))
}
pub fn decode_pcvalue(&self, pctab_off: u32) -> PcValueIter<'a> {
let start = self.pctab_offset.saturating_add(pctab_off as usize);
let data = self.data.get(start..).unwrap_or(&[]);
PcValueIter {
data,
pos: 0,
pc: 0,
val: -1,
min_lc: self.min_lc as u32,
done: data.is_empty(),
}
}
pub fn decode_pcln(&self, func: &FuncData) -> PcLineIter<'a> {
PcLineIter {
inner: self.decode_pcvalue(func.pcln),
start_line: func.start_line,
}
}
pub fn decode_pcfile(&self, func: &FuncData) -> PcFileIter<'a> {
PcFileIter {
inner: self.decode_pcvalue(func.pcfile),
}
}
pub fn decode_pcfile_paths<'pcl>(&'pcl self, func: &FuncData) -> PcFilePathIter<'pcl, 'a> {
PcFilePathIter {
inner: self.decode_pcfile(func),
pcl: self,
cu_offset: func.cu_offset,
}
}
pub fn decode_pcln_with_files<'pcl>(&'pcl self, func: &FuncData) -> PcLineFileIter<'pcl, 'a> {
PcLineFileIter {
inner: self.decode_pcln(func),
pcfile: self.decode_pcfile(func).collect(),
cursor: None,
pcl: self,
cu_offset: func.cu_offset,
}
}
pub fn resolve_source_file(&self, func: &FuncData) -> Option<&'a str> {
let (_, idx) = self.decode_pcfile(func).next()?;
self.resolve_file_index(func.cu_offset, idx)
}
pub fn resolve_file_index(&self, cu_offset: u32, file_idx: u32) -> Option<&'a str> {
match self.version {
PclntabVersion::Go12 => self.resolve_file_go12(file_idx),
_ => self.resolve_file_via_cu(cu_offset, file_idx),
}
}
pub fn resolve_file_via_cu(&self, cu_offset: u32, file_idx: u32) -> Option<&'a str> {
let logical = (cu_offset as usize).checked_add(file_idx as usize)?;
let byte_offset = logical.checked_mul(4)?;
let cu_pos = self.cu_offset.checked_add(byte_offset)?;
let file_off = u32::from_le_bytes(slice_at::<4>(self.data, cu_pos)?);
self.file_name(file_off)
}
pub fn resolve_file_go12(&self, file_idx: u32) -> Option<&'a str> {
if file_idx == 0 {
return None;
}
let entry_pos = self
.filetab_offset
.checked_add((file_idx as usize).checked_mul(4)?)?;
let path_off = u32::from_le_bytes(slice_at::<4>(self.data, entry_pos)?) as usize;
let remaining = self.data.get(path_off..)?;
let end = remaining.iter().position(|&b| b == 0)?;
std::str::from_utf8(remaining.get(..end)?).ok()
}
pub fn line_range(&self, func: &FuncData) -> Option<(i32, i32)> {
let mut iter = self.decode_pcln(func);
let (_, first) = iter.next()?;
let (mut min, mut max) = (first, first);
for (_, line) in iter {
if line < min {
min = line;
}
if line > max {
max = line;
}
}
Some((min, max))
}
pub fn max_frame_size(&self, func: &FuncData) -> Option<i32> {
self.decode_pcvalue(func.pcsp).map(|(_, v)| v).max()
}
pub fn file_names(&self) -> FileNameIter<'a> {
let kind = match self.version {
PclntabVersion::Go12 => FileTabKind::U32Array,
_ => FileTabKind::NullTerminated,
};
FileNameIter {
data: self.data,
filetab_offset: self.filetab_offset,
kind,
pos: 0,
index: 1,
count: self.nfiles,
}
}
}
pub struct FuncEntryIter<'a> {
data: &'a [u8],
base: usize,
index: usize,
count: usize,
version: PclntabVersion,
ptr_size: u8,
text_start: u64,
}
impl Iterator for FuncEntryIter<'_> {
type Item = (u32, u32);
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.count {
return None;
}
let (entry_off, func_off) =
if matches!(self.version, PclntabVersion::Go12 | PclntabVersion::Go116) {
let ps = self.ptr_size as usize;
let stride = ps.checked_mul(2)?;
let off = self
.index
.checked_mul(stride)
.and_then(|d| self.base.checked_add(d))?;
let entry_abs = read_uintptr(self.data, off, self.ptr_size)?;
let funcoff = read_uintptr(self.data, off.checked_add(ps)?, self.ptr_size)?;
let eo =
u32::try_from(entry_abs.saturating_sub(self.text_start)).unwrap_or(u32::MAX);
let fo = u32::try_from(funcoff).unwrap_or(u32::MAX);
(eo, fo)
} else {
let off = self
.index
.checked_mul(8)
.and_then(|d| self.base.checked_add(d))?;
let eo = u32::from_le_bytes(slice_at::<4>(self.data, off)?);
let fo = u32::from_le_bytes(slice_at::<4>(self.data, off.checked_add(4)?)?);
(eo, fo)
};
self.index = self.index.checked_add(1)?;
Some((entry_off, func_off))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.count.saturating_sub(self.index);
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for FuncEntryIter<'_> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FileTabKind {
NullTerminated,
U32Array,
}
pub struct FileNameIter<'a> {
data: &'a [u8],
filetab_offset: usize,
kind: FileTabKind,
pos: usize,
index: usize,
count: usize,
}
impl<'a> Iterator for FileNameIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
match self.kind {
FileTabKind::NullTerminated => self.next_null_terminated(),
FileTabKind::U32Array => self.next_u32_array(),
}
}
}
impl<'a> FileNameIter<'a> {
fn next_null_terminated(&mut self) -> Option<&'a str> {
let tab = self.data.get(self.filetab_offset..)?;
while self.count > 0 && self.pos < tab.len() {
let rest = tab.get(self.pos..)?;
let end = rest.iter().position(|&b| b == 0)?;
self.count = self.count.checked_sub(1)?;
self.pos = self.pos.checked_add(end)?.checked_add(1)?;
if end > 0
&& let Some(name_bytes) = rest.get(..end)
&& let Ok(name) = std::str::from_utf8(name_bytes)
{
return Some(name);
}
}
None
}
fn next_u32_array(&mut self) -> Option<&'a str> {
while self.index < self.count {
let slot = self.index;
self.index = self.index.checked_add(1)?;
let entry_pos = self.filetab_offset.checked_add(slot.checked_mul(4)?)?;
let path_off = match slice_at::<4>(self.data, entry_pos) {
Some(b) => u32::from_le_bytes(b) as usize,
None => continue,
};
let rest = match self.data.get(path_off..) {
Some(r) => r,
None => continue,
};
let end = match rest.iter().position(|&b| b == 0) {
Some(e) => e,
None => continue,
};
if end > 0
&& let Some(name_bytes) = rest.get(..end)
&& let Ok(name) = std::str::from_utf8(name_bytes)
{
return Some(name);
}
}
None
}
}
pub struct PcValueIter<'a> {
data: &'a [u8],
pos: usize,
pc: u32,
val: i32,
min_lc: u32,
done: bool,
}
impl Iterator for PcValueIter<'_> {
type Item = (u32, i32);
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let (uvdelta, n1) = match self.data.get(self.pos..).and_then(read_uvarint) {
Some(v) => v,
None => {
self.done = true;
return None;
}
};
self.pos = match self.pos.checked_add(n1) {
Some(p) => p,
None => {
self.done = true;
return None;
}
};
let half = (uvdelta >> 1) as i32;
let vdelta = if uvdelta & 1 != 0 {
half.wrapping_neg().wrapping_sub(1)
} else {
half
};
self.val = self.val.wrapping_add(vdelta);
let (uvpcdelta, n2) = match self.data.get(self.pos..).and_then(read_uvarint) {
Some(v) => v,
None => {
self.done = true;
return None;
}
};
self.pos = match self.pos.checked_add(n2) {
Some(p) => p,
None => {
self.done = true;
return None;
}
};
if uvpcdelta == 0 {
self.done = true;
return None;
}
self.pc = self
.pc
.wrapping_add((uvpcdelta as u32).wrapping_mul(self.min_lc));
Some((self.pc, self.val))
}
}
pub struct PcLineIter<'a> {
inner: PcValueIter<'a>,
start_line: i32,
}
impl Iterator for PcLineIter<'_> {
type Item = (u32, i32);
fn next(&mut self) -> Option<Self::Item> {
let (pc, v) = self.inner.next()?;
Some((pc, v.wrapping_add(self.start_line)))
}
}
pub struct PcFileIter<'a> {
inner: PcValueIter<'a>,
}
impl Iterator for PcFileIter<'_> {
type Item = (u32, u32);
fn next(&mut self) -> Option<Self::Item> {
let (pc, v) = self.inner.next()?;
Some((pc, v as u32))
}
}
pub struct PcFilePathIter<'pcl, 'a> {
inner: PcFileIter<'a>,
pcl: &'pcl ParsedPclntab<'a>,
cu_offset: u32,
}
impl<'a> Iterator for PcFilePathIter<'_, 'a> {
type Item = (u32, &'a str);
fn next(&mut self) -> Option<Self::Item> {
loop {
let (pc, idx) = self.inner.next()?;
if let Some(path) = self.pcl.resolve_file_index(self.cu_offset, idx) {
return Some((pc, path));
}
}
}
}
pub struct PcLineFileIter<'pcl, 'a> {
inner: PcLineIter<'a>,
pcfile: Vec<(u32, u32)>,
cursor: Option<usize>,
pcl: &'pcl ParsedPclntab<'a>,
cu_offset: u32,
}
impl<'a> Iterator for PcLineFileIter<'_, 'a> {
type Item = (u32, i32, &'a str);
fn next(&mut self) -> Option<Self::Item> {
loop {
let (pc, line) = self.inner.next()?;
let mut next_idx = match self.cursor {
Some(c) => c.saturating_add(1),
None => 0,
};
while let Some(&(t_pc, _)) = self.pcfile.get(next_idx) {
if t_pc > pc {
break;
}
self.cursor = Some(next_idx);
next_idx = match next_idx.checked_add(1) {
Some(n) => n,
None => break,
};
}
let cursor = match self.cursor {
Some(c) => c,
None => continue,
};
let (_, file_idx) = match self.pcfile.get(cursor) {
Some(t) => *t,
None => continue,
};
match self.pcl.resolve_file_index(self.cu_offset, file_idx) {
Some(path) => return Some((pc, line, path)),
None => continue,
}
}
}
}
#[derive(Debug)]
pub struct FuncData {
pub func_off: u32,
pub entry_off: u32,
pub name_off: i32,
pub args: i32,
pub deferreturn: u32,
pub pcsp: u32,
pub pcfile: u32,
pub pcln: u32,
pub npcdata: u32,
pub cu_offset: u32,
pub start_line: i32,
pub func_id: u8,
pub flag: u8,
pub nfuncdata: u8,
}
impl FuncData {
pub fn args_size(&self) -> u32 {
if self.args < 0 { 0 } else { self.args as u32 }
}
}
pub fn parse<'a>(ctx: &'a BinaryContext<'a>) -> Option<ParsedPclntab<'a>> {
let data = ctx.data();
let sections = ctx.sections();
if let Some(ref range) = sections.gopclntab
&& let Some(raw_end) = range.offset.checked_add(range.size)
{
let end = raw_end.min(data.len());
if let Some(section_data) = data.get(range.offset..end)
&& let Some(parsed) = try_parse_at(section_data, range.offset)
{
return Some(parsed);
}
}
let (search_data, stride) = if ctx.format() == BinaryFormat::Wasm {
(ctx.structure_search_data(), 1usize)
} else {
(data, 4usize)
};
if let Some(parsed) = scan_for_magic_strided(search_data, stride) {
return Some(parsed);
}
if let Some(parsed) = scan_relaxed(data, sections) {
return Some(parsed);
}
if let Some(parsed) = scan_via_moduledata(ctx) {
return Some(parsed);
}
scan_via_functab(data)
}
fn try_parse_at(data: &[u8], base_offset: usize) -> Option<ParsedPclntab<'_>> {
if data.len() < 8 {
return None;
}
let magic_bytes = slice_at::<4>(data, 0)?;
let version = MAGICS
.iter()
.find(|(m, _)| *m == magic_bytes)
.map(|(_, v)| *v)?;
parse_header(data, base_offset, version)
}
fn scan_for_magic_strided(data: &[u8], stride: usize) -> Option<ParsedPclntab<'_>> {
for offset in (0..data.len().saturating_sub(72)).step_by(stride) {
let magic = match slice_at::<4>(data, offset) {
Some(m) => m,
None => continue,
};
if MAGICS.iter().any(|(m, _)| *m == magic)
&& let Some(rest) = data.get(offset..)
&& let Some(parsed) = try_parse_at(rest, offset)
{
return Some(parsed);
}
}
None
}
fn try_parse_relaxed(data: &[u8], base_offset: usize) -> Option<ParsedPclntab<'_>> {
if data.len() < 8 {
return None;
}
let magic_bytes = slice_at::<4>(data, 0)?;
let version = MAGICS
.iter()
.find(|(m, _)| *m == magic_bytes)
.map(|(_, v)| *v)
.unwrap_or(PclntabVersion::Go120);
if *data.get(4)? != 0 || *data.get(5)? != 0 {
return None;
}
let min_lc = *data.get(6)?;
let ptr_size = *data.get(7)?;
if !matches!(min_lc, 1 | 2 | 4) || !matches!(ptr_size, 4 | 8) {
return None;
}
let parsed = parse_header(data, base_offset, version)?;
if !(parsed.funcname_offset < parsed.cu_offset
&& parsed.cu_offset < parsed.filetab_offset
&& parsed.filetab_offset < parsed.pctab_offset
&& parsed.pctab_offset < parsed.functab_offset)
{
return None;
}
if let Some(fndata) = data.get(parsed.funcname_offset..) {
if fndata.first() != Some(&0) {
return None;
}
if fndata.len() > 2 {
let scan_end = fndata.len().min(32);
if let Some(window) = fndata.get(1..scan_end)
&& !window.iter().any(|&b| b.is_ascii_alphanumeric())
{
return None;
}
}
}
Some(parsed)
}
fn scan_relaxed<'a>(data: &'a [u8], sections: &GoSections) -> Option<ParsedPclntab<'a>> {
if let Some(ref range) = sections.gopclntab {
let raw_end = range.offset.checked_add(range.size)?;
let end = raw_end.min(data.len());
let section_data = data.get(range.offset..end)?;
return try_parse_relaxed(section_data, range.offset);
}
for offset in (0..data.len().saturating_sub(72)).step_by(8) {
if let Some(rest) = data.get(offset..)
&& let Some(parsed) = try_parse_relaxed(rest, offset)
{
return Some(parsed);
}
}
for offset in (4..data.len().saturating_sub(40)).step_by(8) {
if let Some(rest) = data.get(offset..)
&& let Some(parsed) = try_parse_relaxed(rest, offset)
{
return Some(parsed);
}
}
None
}
fn scan_via_moduledata<'a>(ctx: &BinaryContext<'a>) -> Option<ParsedPclntab<'a>> {
let data = ctx.data();
let pclntab_range = ctx.sections().gopclntab.as_ref()?;
let pclntab_va = pclntab_range.va;
if pclntab_va == 0 || !ctx.has_va_mapping() {
return None;
}
for &ptr_size in &[8u8, 4u8] {
let ps = ptr_size as usize;
let pclntab_va_end = pclntab_va.checked_add(pclntab_range.size as u64)?;
let header_window_end = pclntab_va.checked_add(64)?;
for offset in (0..data.len().saturating_sub(ps)).step_by(ps) {
let candidate_va = match ps {
4 => match slice_at::<4>(data, offset) {
Some(b) => u32::from_le_bytes(b) as u64,
None => continue,
},
8 => match slice_at::<8>(data, offset) {
Some(b) => u64::from_le_bytes(b),
None => continue,
},
_ => continue,
};
if candidate_va >= pclntab_va
&& candidate_va < header_window_end
&& candidate_va < pclntab_va_end
{
let target_file_off = match ctx.va_to_file(candidate_va) {
Some(o) => o,
None => continue,
};
let header_end = match target_file_off.checked_add(72) {
Some(e) => e,
None => continue,
};
if header_end <= data.len()
&& let Some(rest) = data.get(target_file_off..)
&& let Some(parsed) = try_parse_relaxed(rest, target_file_off)
{
return Some(parsed);
}
}
}
}
None
}
const FUNCTAB_MIN_RUN: usize = 100;
fn scan_via_functab<'a>(data: &'a [u8]) -> Option<ParsedPclntab<'a>> {
let min_required = FUNCTAB_MIN_RUN.checked_mul(8)?.checked_add(72)?;
if data.len() < min_required {
return None;
}
let mut offset: usize = 0;
let run_bytes = FUNCTAB_MIN_RUN.checked_mul(8)?;
while let Some(end) = offset.checked_add(run_bytes) {
if end > data.len() {
break;
}
let window = match data.get(offset..) {
Some(w) => w,
None => break,
};
let run_len = count_monotonic_run(window);
if run_len >= FUNCTAB_MIN_RUN {
if let Some(parsed) = recover_header_from_functab(data, offset, run_len) {
return Some(parsed);
}
let skip = run_len.checked_mul(8)?;
offset = offset.checked_add(skip)?;
} else {
offset = offset.checked_add(8)?;
}
}
None
}
fn count_monotonic_run(data: &[u8]) -> usize {
let mut count: usize = 0;
let mut prev_entry: u32 = 0;
let mut i: usize = 0;
while let Some(end) = i.checked_add(8) {
if end > data.len() {
break;
}
let entry_off = match slice_at::<4>(data, i) {
Some(b) => u32::from_le_bytes(b),
None => break,
};
if count == 0 {
prev_entry = entry_off;
count = 1;
i = match i.checked_add(8) {
Some(v) => v,
None => break,
};
continue;
}
if entry_off <= prev_entry {
break;
}
prev_entry = entry_off;
count = match count.checked_add(1) {
Some(v) => v,
None => break,
};
i = match i.checked_add(8) {
Some(v) => v,
None => break,
};
}
count
}
fn recover_header_from_functab<'a>(
data: &'a [u8],
functab_file_offset: usize,
run_len: usize,
) -> Option<ParsedPclntab<'a>> {
let max_distance = functab_file_offset.min(16 * 1024 * 1024);
let mut dist: usize = 4;
while dist <= max_distance {
let candidate = match functab_file_offset.checked_sub(dist) {
Some(c) => c,
None => break,
};
let hdr = match data.get(candidate..) {
Some(h) => h,
None => break,
};
if hdr.len() < 8 {
dist = match dist.checked_add(4) {
Some(d) => d,
None => break,
};
continue;
}
let pad1 = match hdr.get(4) {
Some(b) => *b,
None => break,
};
let pad2 = match hdr.get(5) {
Some(b) => *b,
None => break,
};
let min_lc = match hdr.get(6) {
Some(b) => *b,
None => break,
};
let ptr_size = match hdr.get(7) {
Some(b) => *b,
None => break,
};
if pad1 != 0 || pad2 != 0 {
dist = dist.checked_add(4)?;
continue;
}
if !matches!(min_lc, 1 | 2 | 4) || !matches!(ptr_size, 4 | 8) {
dist = dist.checked_add(4)?;
continue;
}
let ps = ptr_size as usize;
let header_size = ps.checked_mul(8).and_then(|x| x.checked_add(8))?;
let header_end = candidate.checked_add(header_size)?;
if header_end > data.len() {
dist = dist.checked_add(4)?;
continue;
}
let pclnoffset_pos = ps.checked_mul(7).and_then(|x| x.checked_add(8))?;
let read_end = candidate
.checked_add(pclnoffset_pos)
.and_then(|x| x.checked_add(ps))?;
if read_end > data.len() {
dist = dist.checked_add(4)?;
continue;
}
let read_pos = candidate.checked_add(pclnoffset_pos)?;
let pln_offset = usize::try_from(read_uintptr(data, read_pos, ptr_size)?).ok()?;
if pln_offset == dist {
let nfunc_pos = candidate.checked_add(8)?;
let nfunc = usize::try_from(read_uintptr(data, nfunc_pos, ptr_size)?).ok()?;
let nfunc_plus_one = nfunc.checked_add(1);
let run_plus_one = run_len.checked_add(1);
if nfunc > 0
&& (nfunc == run_len
|| nfunc_plus_one == Some(run_len)
|| Some(nfunc) == run_plus_one)
&& let Some(rest) = data.get(candidate..)
&& let Some(parsed) = try_parse_relaxed(rest, candidate)
{
return Some(parsed);
}
}
dist = dist.checked_add(4)?;
}
None
}
fn parse_header(
data: &[u8],
base_offset: usize,
version: PclntabVersion,
) -> Option<ParsedPclntab<'_>> {
if version == PclntabVersion::Go12 {
return parse_header_go12(data, base_offset);
}
if *data.get(4)? != 0 || *data.get(5)? != 0 {
return None;
}
let min_lc = *data.get(6)?;
let ptr_size = *data.get(7)?;
if !matches!(min_lc, 1 | 2 | 4) || !matches!(ptr_size, 4 | 8) {
return None;
}
let ps = ptr_size as usize;
let off_base: usize = match version {
PclntabVersion::Go116 => 2,
_ => 3,
};
let header_size = advance_n(8, off_base.saturating_add(5), ps)?;
if data.len() < header_size {
return None;
}
let read_field = |idx: usize| -> Option<usize> {
let off = advance_n(8, idx, ps)?;
usize::try_from(read_uintptr(data, off, ptr_size)?).ok()
};
let nfunc = read_field(0)?;
let nfiles = read_field(1)?;
let funcname_offset = read_field(off_base)?;
let cu_offset = read_field(off_base.saturating_add(1))?;
let filetab_offset = read_field(off_base.saturating_add(2))?;
let pctab_offset = read_field(off_base.saturating_add(3))?;
let functab_offset = read_field(off_base.saturating_add(4))?;
if nfunc > 10_000_000 || nfiles > 10_000_000 {
return None;
}
if funcname_offset > data.len() || filetab_offset > data.len() || functab_offset > data.len() {
return None;
}
let text_start = match version {
PclntabVersion::Go116 => read_uintptr(data, functab_offset, ptr_size).unwrap_or(0),
_ => 0,
};
let header_text_start = if off_base > 2 {
let off = advance_n(8, 2, ps)?;
read_uintptr(data, off, ptr_size).filter(|&va| va != 0)
} else {
None
};
Some(ParsedPclntab {
data,
offset: base_offset,
version,
min_lc,
ptr_size,
nfunc,
nfiles,
funcname_offset,
cu_offset,
filetab_offset,
pctab_offset,
functab_offset,
text_start,
header_text_start,
})
}
fn parse_header_go12(data: &[u8], base_offset: usize) -> Option<ParsedPclntab<'_>> {
if data.len() < 16 || *data.get(4)? != 0 || *data.get(5)? != 0 {
return None;
}
let min_lc = *data.get(6)?;
let ptr_size = *data.get(7)?;
if !matches!(min_lc, 1 | 2 | 4) || !matches!(ptr_size, 4 | 8) {
return None;
}
let ps = ptr_size as usize;
let nfunctab = usize::try_from(read_uintptr(data, 8, ptr_size)?).ok()?;
if nfunctab == 0 || nfunctab > 10_000_000 {
return None;
}
let functab_offset = advance(8, ps)?;
let functab_words = nfunctab.checked_mul(2)?.checked_add(1)?;
let functab_size = functab_words.checked_mul(ps)?;
let fileoff_pos = functab_offset.checked_add(functab_size)?;
let filetab_offset = u32::from_le_bytes(slice_at::<4>(data, fileoff_pos)?) as usize;
if filetab_offset == 0 || filetab_offset >= data.len() {
return None;
}
let nfiletab = u32::from_le_bytes(slice_at::<4>(data, filetab_offset)?) as usize;
if nfiletab > 10_000_000 {
return None;
}
let text_start = read_uintptr(data, functab_offset, ptr_size)?;
Some(ParsedPclntab {
data,
offset: base_offset,
version: PclntabVersion::Go12,
min_lc,
ptr_size,
nfunc: nfunctab,
nfiles: nfiletab,
funcname_offset: 0,
cu_offset: 0,
filetab_offset,
pctab_offset: 0,
functab_offset,
text_start,
header_text_start: Some(text_start),
})
}
#[derive(Clone, Copy)]
struct FuncLayout {
size: usize,
entry_is_abs: bool,
name_off: usize,
args: usize,
deferreturn: usize,
pcsp: usize,
pcfile: usize,
pcln: usize,
npcdata: usize,
cu_offset: Option<usize>,
start_line: Option<usize>,
func_id: Option<usize>,
flag: Option<usize>,
nfuncdata: Option<usize>,
}
fn func_layout(version: PclntabVersion, ptr_size: u8) -> FuncLayout {
let p = ptr_size as usize;
match version {
PclntabVersion::Go12 => FuncLayout {
size: p.saturating_add(32),
entry_is_abs: true,
name_off: p,
args: p.saturating_add(4),
deferreturn: p.saturating_add(8),
pcsp: p.saturating_add(12),
pcfile: p.saturating_add(16),
pcln: p.saturating_add(20),
npcdata: p.saturating_add(24),
cu_offset: None,
start_line: None,
func_id: None,
flag: None,
nfuncdata: None,
},
PclntabVersion::Go116 => FuncLayout {
size: p.saturating_add(36),
entry_is_abs: true,
name_off: p,
args: p.saturating_add(4),
deferreturn: p.saturating_add(8),
pcsp: p.saturating_add(12),
pcfile: p.saturating_add(16),
pcln: p.saturating_add(20),
npcdata: p.saturating_add(24),
cu_offset: Some(p.saturating_add(28)),
start_line: None,
func_id: Some(p.saturating_add(32)),
flag: None,
nfuncdata: Some(p.saturating_add(35)),
},
PclntabVersion::Go118 => FuncLayout {
size: 40,
entry_is_abs: false,
name_off: 4,
args: 8,
deferreturn: 12,
pcsp: 16,
pcfile: 20,
pcln: 24,
npcdata: 28,
cu_offset: Some(32),
start_line: None,
func_id: Some(36),
flag: Some(37),
nfuncdata: Some(39),
},
PclntabVersion::Go120 => FuncLayout {
size: 44,
entry_is_abs: false,
name_off: 4,
args: 8,
deferreturn: 12,
pcsp: 16,
pcfile: 20,
pcln: 24,
npcdata: 28,
cu_offset: Some(32),
start_line: Some(36),
func_id: Some(40),
flag: Some(41),
nfuncdata: Some(43),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::{GoSections, SectionRange};
fn build_synthetic_pclntab(magic: [u8; 4]) -> Vec<u8> {
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&magic);
data[4] = 0; data[5] = 0; data[6] = 4; data[7] = 8;
let nfunc: u64 = 3;
let nfiles: u64 = 2;
data[8..16].copy_from_slice(&nfunc.to_le_bytes());
data[16..24].copy_from_slice(&nfiles.to_le_bytes());
data[24..32].copy_from_slice(&0u64.to_le_bytes());
let offsets: [u64; 5] = [100, 200, 300, 400, 500];
for (i, &off) in offsets.iter().enumerate() {
let pos = 8 + (3 + i) * 8;
data[pos..pos + 8].copy_from_slice(&off.to_le_bytes());
}
data[100] = 0; data[101..111].copy_from_slice(b"runtime.ma");
data[111] = 0;
let functab_base = 500;
for i in 0..4u32 {
let entry_off = (i + 1) * 0x100;
let func_off = i * 44;
let pos = functab_base + (i as usize) * 8;
data[pos..pos + 4].copy_from_slice(&entry_off.to_le_bytes());
data[pos + 4..pos + 8].copy_from_slice(&func_off.to_le_bytes());
}
data
}
#[test]
fn test_magic_detection() {
let magic: [u8; 4] = [0xf1, 0xff, 0xff, 0xff];
let version = MAGICS.iter().find(|(m, _)| *m == magic).map(|(_, v)| *v);
assert_eq!(version, Some(PclntabVersion::Go120));
}
#[test]
fn test_arch_detection() {
let data = build_synthetic_pclntab([0xf1, 0xff, 0xff, 0xff]);
let ctx = BinaryContext::new(&data);
let parsed = parse(&ctx).unwrap();
assert_eq!(parsed.version, PclntabVersion::Go120);
assert_eq!(parsed.min_lc, 4);
assert_eq!(parsed.ptr_size, 8);
assert_eq!(parsed.nfunc, 3);
assert_eq!(parsed.arch(), Arch::Arm64);
}
#[test]
fn test_strategy_a_relaxed_header_zeroed_magic() {
let mut data = build_synthetic_pclntab([0xf1, 0xff, 0xff, 0xff]);
data[0..4].copy_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let ctx = BinaryContext::new(&data);
let parsed = parse(&ctx).unwrap();
assert_eq!(parsed.min_lc, 4);
assert_eq!(parsed.ptr_size, 8);
assert_eq!(parsed.nfunc, 3);
assert_eq!(parsed.version, PclntabVersion::Go120);
}
#[test]
fn test_strategy_a_relaxed_rejects_garbage() {
let data = vec![0x42u8; 4096];
let ctx = BinaryContext::new(&data);
assert!(parse(&ctx).is_none());
}
#[test]
fn test_strategy_a_relaxed_with_section_hint() {
let mut data = vec![0u8; 8192];
let pclntab = build_synthetic_pclntab([0x00, 0x00, 0x00, 0x00]); data[1024..1024 + pclntab.len()].copy_from_slice(&pclntab);
let sections = GoSections {
has_gopclntab: true,
has_go_buildinfo: false,
has_go_buildid_note: false,
gopclntab: Some(SectionRange {
offset: 1024,
size: pclntab.len(),
va: 0x400000,
}),
go_buildinfo: None,
go_module: None,
typelink: None,
itablink: None,
go_type: None,
go_func: None,
fipsinfo: None,
noptrdata: None,
data_section: None,
text_section: None,
};
let parsed = scan_relaxed(&data, §ions).unwrap();
assert_eq!(parsed.offset, 1024);
assert_eq!(parsed.nfunc, 3);
}
#[test]
fn test_strategy_c_functab_monotonicity() {
let data = build_synthetic_pclntab([0xf1, 0xff, 0xff, 0xff]);
let run = count_monotonic_run(&data[500..]);
assert_eq!(run, 4);
}
#[test]
fn test_monotonic_run_rejects_nonmonotonic() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&0x200u32.to_le_bytes());
data[4..8].copy_from_slice(&0u32.to_le_bytes());
data[8..12].copy_from_slice(&0x100u32.to_le_bytes());
data[12..16].copy_from_slice(&44u32.to_le_bytes());
let run = count_monotonic_run(&data);
assert_eq!(run, 1); }
#[test]
fn test_strategy_c_recover_header() {
let mut data = build_synthetic_pclntab([0x00, 0x00, 0x00, 0x00]);
let nfunc = FUNCTAB_MIN_RUN as u64;
data[8..16].copy_from_slice(&nfunc.to_le_bytes());
let functab_offset = 500;
let needed = functab_offset + (FUNCTAB_MIN_RUN + 1) * 8 + 256;
data.resize(needed, 0);
for i in 0..=(FUNCTAB_MIN_RUN as u32) {
let entry_off = (i + 1) * 0x10;
let func_off = i * 44;
let pos = functab_offset + (i as usize) * 8;
data[pos..pos + 4].copy_from_slice(&entry_off.to_le_bytes());
data[pos + 4..pos + 8].copy_from_slice(&func_off.to_le_bytes());
}
let result = recover_header_from_functab(&data, functab_offset, FUNCTAB_MIN_RUN + 1);
assert!(result.is_some(), "should recover pcHeader from functab");
let parsed = result.unwrap();
assert_eq!(parsed.offset, 0);
assert_eq!(parsed.nfunc, FUNCTAB_MIN_RUN);
}
fn build_synthetic_go12_pclntab() -> Vec<u8> {
let mut data = vec![0u8; 4096];
let put_u64 = |d: &mut [u8], off: usize, v: u64| {
d[off..off + 8].copy_from_slice(&v.to_le_bytes());
};
let put_u32 = |d: &mut [u8], off: usize, v: u32| {
d[off..off + 4].copy_from_slice(&v.to_le_bytes());
};
let put_str = |d: &mut [u8], off: usize, s: &str| {
d[off..off + s.len()].copy_from_slice(s.as_bytes());
d[off + s.len()] = 0;
};
data[0..4].copy_from_slice(&[0xfb, 0xff, 0xff, 0xff]);
data[6] = 1;
data[7] = 8;
put_u64(&mut data, 8, 2);
let text = 0x1000u64;
put_u64(&mut data, 16, text); put_u64(&mut data, 24, 100); put_u64(&mut data, 32, 0x1100); put_u64(&mut data, 40, 150); put_u64(&mut data, 48, 0x1200);
put_u32(&mut data, 56, 300);
put_u32(&mut data, 300, 3);
put_u32(&mut data, 304, 400);
put_u32(&mut data, 308, 420);
put_str(&mut data, 200, "main.main");
put_str(&mut data, 230, "main.worker");
put_str(&mut data, 400, "/src/main.go");
put_str(&mut data, 420, "/src/util.go");
put_u64(&mut data, 100, text);
put_u32(&mut data, 108, 200);
put_u64(&mut data, 150, 0x1100);
put_u32(&mut data, 158, 230);
data
}
#[test]
fn go12_header_parses_legacy_layout() {
let data = build_synthetic_go12_pclntab();
let ctx = BinaryContext::new(&data);
let p = parse(&ctx).expect("legacy pclntab should parse");
assert_eq!(p.version, PclntabVersion::Go12);
assert_eq!(p.min_lc, 1);
assert_eq!(p.ptr_size, 8);
assert_eq!(p.arch(), Arch::X86_64);
assert_eq!(p.nfunc, 2);
assert_eq!(p.nfiles, 3); assert_eq!(p.funcname_offset, 0);
assert_eq!(p.pctab_offset, 0);
assert_eq!(p.functab_offset, 16);
assert_eq!(p.filetab_offset, 300);
assert_eq!(p.text_start, 0x1000);
assert_eq!(p.header_text_start, Some(0x1000));
}
#[test]
fn go12_func_entries_and_names() {
let data = build_synthetic_go12_pclntab();
let ctx = BinaryContext::new(&data);
let p = parse(&ctx).unwrap();
let entries: Vec<(u32, u32)> = p.func_entries().collect();
assert_eq!(entries, vec![(0x0, 100), (0x100, 150)]);
let f0 = p.parse_func(100).expect("func0");
assert_eq!(f0.entry_off, 0);
assert_eq!(f0.name_off, 200);
assert_eq!(p.func_name(f0.name_off as u32), Some("main.main"));
let f1 = p.parse_func(150).expect("func1");
assert_eq!(f1.entry_off, 0x100);
assert_eq!(p.func_name(f1.name_off as u32), Some("main.worker"));
}
#[test]
fn go12_filetab_is_u32_array() {
let data = build_synthetic_go12_pclntab();
let ctx = BinaryContext::new(&data);
let p = parse(&ctx).unwrap();
let files: Vec<&str> = p.file_names().collect();
assert_eq!(files, vec!["/src/main.go", "/src/util.go"]);
assert_eq!(p.resolve_file_go12(0), None);
assert_eq!(p.resolve_file_go12(1), Some("/src/main.go"));
assert_eq!(p.resolve_file_go12(2), Some("/src/util.go"));
assert_eq!(p.resolve_file_index(0, 1), Some("/src/main.go"));
}
}