use serde::Serialize;
use crate::error::Error;
use crate::gobin::GoBinary;
use crate::itabs::Itab;
use crate::pclntab::{Function, Pclntab};
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum As {
Bytes,
Qwords,
Ptrs,
Ifaces,
SliceHeader,
String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DataRow {
pub addr: u64,
pub bytes: Vec<u8>,
pub rendering: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Symbol {
Function {
name: String,
entry: u64,
offset: u64,
},
Itab {
interface: String,
concrete: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
methods: Vec<ItabMethodEntry>,
},
SectionOffset { section: String, offset: u64 },
Unmapped,
Scalar { value: u64 },
}
#[derive(Debug, Clone, Serialize)]
pub struct ItabMethodEntry {
pub name: String,
pub concrete_fn: u64,
}
impl Symbol {
pub fn render(&self) -> String {
match self {
Symbol::Function {
name,
entry: _,
offset,
} if *offset == 0 => name.clone(),
Symbol::Function { name, offset, .. } => format!("{name} + 0x{offset:x}"),
Symbol::Itab {
interface,
concrete,
methods,
} => {
let mut s = format!("itab({interface} = {concrete})");
if !methods.is_empty() {
s.push_str(" [");
for (i, m) in methods.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
s.push_str(&format!(".{}() -> 0x{:x}", m.name, m.concrete_fn));
}
s.push(']');
}
s
}
Symbol::SectionOffset { section, offset } => format!("{section}+0x{offset:x}"),
Symbol::Unmapped => "(unmapped)".to_string(),
Symbol::Scalar { value } => format!("0x{value:x}"),
}
}
}
pub fn inspect(
bin: &GoBinary,
pcln: &Pclntab<'_>,
itabs_v: &[Itab],
functions: &[Function],
addr: u64,
len: usize,
mode: As,
) -> Result<Vec<DataRow>> {
let (file_off, bounded_len) = bounded_read(bin, addr, len)?;
let bytes = &bin.bytes[file_off..file_off + bounded_len];
let ctx = SymCtx::new(bin, pcln, itabs_v, functions);
match mode {
As::Bytes => Ok(rows_bytes(addr, bytes)),
As::Qwords => Ok(rows_qwords(addr, bytes, None)),
As::Ptrs => Ok(rows_qwords(addr, bytes, Some(&ctx))),
As::Ifaces => Ok(rows_ifaces(addr, bytes, &ctx)),
As::SliceHeader => Ok(rows_slice_headers(addr, bytes, &ctx)),
As::String => Ok(rows_strings(addr, bytes, bin, &ctx)),
}
}
pub fn symbolize(
bin: &GoBinary,
pcln: &Pclntab<'_>,
itabs_v: &[Itab],
functions: &[Function],
value: u64,
) -> Symbol {
let ctx = SymCtx::new(bin, pcln, itabs_v, functions);
ctx.symbolize(value)
}
fn bounded_read(bin: &GoBinary, addr: u64, len: usize) -> Result<(usize, usize)> {
if addr < 0x1000 {
return Err(Error::Xrefs(format!(
"address 0x{addr:x} is below the first plausible runtime page; \
refusing to read (this is almost always a typo or a shell-\
variable that collapsed to 0)"
)));
}
let s = bin
.sections
.iter()
.filter(|s| s.addr != 0)
.find(|s| s.contains_addr(addr))
.ok_or_else(|| {
Error::Xrefs(format!("address 0x{addr:x} is not in any loadable section"))
})?;
let file_off = s.file_offset_of(addr).ok_or_else(|| {
Error::Xrefs(format!(
"address 0x{addr:x} lies past the file-backed portion of {}",
s.name
))
})?;
let max_available = s.file_size - (addr - s.addr) as usize;
let bounded = len.min(max_available);
Ok((file_off, bounded))
}
fn rows_bytes(start: u64, bytes: &[u8]) -> Vec<DataRow> {
let mut out = Vec::new();
for (i, chunk) in bytes.chunks(16).enumerate() {
let mut hex = String::with_capacity(48);
for b in chunk {
hex.push_str(&format!("{b:02x} "));
}
let mut ascii = String::with_capacity(16);
for &b in chunk {
ascii.push(if (0x20..=0x7E).contains(&b) {
b as char
} else {
'.'
});
}
out.push(DataRow {
addr: start + (i as u64) * 16,
bytes: chunk.to_vec(),
rendering: format!("{hex:<48} |{ascii}|"),
});
}
out
}
fn rows_qwords(start: u64, bytes: &[u8], ctx: Option<&SymCtx>) -> Vec<DataRow> {
let mut out = Vec::new();
for (i, chunk) in bytes.chunks(8).enumerate() {
if chunk.len() < 8 {
break;
}
let v = u64::from_le_bytes(chunk.try_into().unwrap());
let rendering = match ctx {
Some(c) => format!("0x{v:016x} {}", c.symbolize(v).render()),
None => format!("0x{v:016x}"),
};
out.push(DataRow {
addr: start + (i as u64) * 8,
bytes: chunk.to_vec(),
rendering,
});
}
out
}
fn rows_ifaces(start: u64, bytes: &[u8], ctx: &SymCtx) -> Vec<DataRow> {
let mut out = Vec::new();
for (i, chunk) in bytes.chunks(16).enumerate() {
if chunk.len() < 16 {
break;
}
let itab_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
let data_ptr = u64::from_le_bytes(chunk[8..16].try_into().unwrap());
let itab_sym = ctx.symbolize(itab_ptr);
let data_sym = ctx.symbolize(data_ptr);
out.push(DataRow {
addr: start + (i as u64) * 16,
bytes: chunk.to_vec(),
rendering: format!(
"iface{{ itab=0x{:016x} ({}), data=0x{:016x} ({}) }}",
itab_ptr,
itab_sym.render(),
data_ptr,
data_sym.render(),
),
});
}
out
}
fn rows_slice_headers(start: u64, bytes: &[u8], ctx: &SymCtx) -> Vec<DataRow> {
let mut out = Vec::new();
for (i, chunk) in bytes.chunks(24).enumerate() {
if chunk.len() < 24 {
break;
}
let data_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
let len = u64::from_le_bytes(chunk[8..16].try_into().unwrap());
let cap = u64::from_le_bytes(chunk[16..24].try_into().unwrap());
out.push(DataRow {
addr: start + (i as u64) * 24,
bytes: chunk.to_vec(),
rendering: format!(
"slice{{ data=0x{:016x} ({}), len={}, cap={} }}",
data_ptr,
ctx.symbolize(data_ptr).render(),
len,
cap,
),
});
}
out
}
fn rows_strings(start: u64, bytes: &[u8], bin: &GoBinary, ctx: &SymCtx) -> Vec<DataRow> {
let mut out = Vec::new();
for (i, chunk) in bytes.chunks(16).enumerate() {
if chunk.len() < 16 {
break;
}
let data_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
let len = u64::from_le_bytes(chunk[8..16].try_into().unwrap());
let preview = read_string_preview(bin, data_ptr, len);
let preview_render = match preview {
Some(s) => format!("{s:?}"),
None => format!("({})", ctx.symbolize(data_ptr).render()),
};
out.push(DataRow {
addr: start + (i as u64) * 16,
bytes: chunk.to_vec(),
rendering: format!(
"string{{ ptr=0x{:016x}, len={} }} {}",
data_ptr, len, preview_render,
),
});
}
out
}
fn read_string_preview(bin: &GoBinary, ptr: u64, len: u64) -> Option<String> {
if len == 0 || len > 4096 {
return None;
}
let s = bin.sections.iter().find(|s| s.contains_addr(ptr))?;
let off = s.file_offset_of(ptr)?;
let end = off + (len as usize);
if end > bin.bytes.len() {
return None;
}
let raw = &bin.bytes[off..end];
if raw
.iter()
.any(|&b| !((0x20..=0x7E).contains(&b) || b == b'\n' || b == b'\t'))
{
return None;
}
Some(String::from_utf8_lossy(raw).into_owned())
}
struct SymCtx<'a> {
bin: &'a GoBinary,
pcln: &'a Pclntab<'a>,
itab_by_addr: std::collections::HashMap<u64, &'a Itab>,
function_by_addr: std::collections::HashMap<u64, &'a Function>,
}
impl<'a> SymCtx<'a> {
fn new(
bin: &'a GoBinary,
pcln: &'a Pclntab<'a>,
itabs_v: &'a [Itab],
functions: &'a [Function],
) -> Self {
SymCtx {
bin,
pcln,
itab_by_addr: itabs_v.iter().map(|it| (it.addr, it)).collect(),
function_by_addr: functions.iter().map(|f| (f.address, f)).collect(),
}
}
fn symbolize(&self, value: u64) -> Symbol {
if value < 0x1000 {
return Symbol::Scalar { value };
}
if let Some(it) = self.itab_by_addr.get(&value) {
return Symbol::Itab {
interface: it.interface_name.clone(),
concrete: it.concrete_name.clone(),
methods: it
.methods
.iter()
.map(|m| ItabMethodEntry {
name: m.interface_method.clone(),
concrete_fn: m.concrete_fn,
})
.collect(),
};
}
if let Some(f) = self.function_by_addr.get(&value) {
return Symbol::Function {
name: f.name.clone(),
entry: f.address,
offset: 0,
};
}
if let Some(f) = self.pcln.lookup(value) {
let entry = f.address;
return Symbol::Function {
name: f.name,
entry,
offset: value - entry,
};
}
for s in &self.bin.sections {
if s.contains_addr(value) && !s.name.is_empty() {
return Symbol::SectionOffset {
section: s.name.clone(),
offset: value - s.addr,
};
}
}
Symbol::Unmapped
}
}