1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//! Go `.gopclntab` parser for function-name recovery on stripped Go binaries.
//!
//! Supports Go 1.20+ (pcHeader magic 0xfffffff1). Earlier layouts fall
//! through — Go 1.18 used 0xfffffff0 with split tables; Go 1.16 and below
//! packed everything differently.
//!
//! Produces (pc → function_name) pairs suitable for merging into the
//! CLI's symbol list.
use std::collections::HashMap;
const MAGIC_GO120: u32 = 0xfffffff1;
/// Walks ELF/Mach-O/PE sections looking for `.gopclntab`, decodes the
/// Go 1.20+ header, and yields (function_pc, name) pairs. Returns an
/// empty map when the section is absent or unsupported.
pub fn parse(binary: &[u8]) -> HashMap<u64, String> {
let Ok(obj) = goblin::Object::parse(binary) else {
return HashMap::new();
};
let (section_data, section_va) = match find_gopclntab(&obj, binary) {
Some(v) => v,
None => return HashMap::new(),
};
parse_table(section_data, section_va).unwrap_or_default()
}
fn find_gopclntab<'a>(obj: &goblin::Object<'a>, binary: &'a [u8]) -> Option<(&'a [u8], u64)> {
match obj {
goblin::Object::Elf(elf) => {
for sh in &elf.section_headers {
let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
if name == ".gopclntab" {
let start = sh.sh_offset as usize;
let end = start + sh.sh_size as usize;
if end <= binary.len() {
return Some((&binary[start..end], sh.sh_addr));
}
}
}
None
}
goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
for seg in m.segments.iter() {
if let Ok(sects) = seg.sections() {
for (sect, sdata) in sects {
if sect.name().unwrap_or("") == "__gopclntab" {
// sdata is a &[u8] reference; compute its VA.
return Some((sdata, sect.addr));
}
}
}
}
None
}
goblin::Object::PE(pe) => {
// Go Windows binaries place the table in the .text section's
// .gopclntab sub-range; goblin exposes sections by name.
for s in &pe.sections {
if s.name().unwrap_or("") == ".gopclntab" {
let start = s.pointer_to_raw_data as usize;
let end = start + s.size_of_raw_data as usize;
if end <= binary.len() {
let va = pe.image_base as u64 + s.virtual_address as u64;
return Some((&binary[start..end], va));
}
}
}
None
}
_ => None,
}
}
fn parse_table(data: &[u8], _section_va: u64) -> Option<HashMap<u64, String>> {
if data.len() < 64 {
return None;
}
let magic = u32::from_le_bytes(data[0..4].try_into().ok()?);
if magic != MAGIC_GO120 {
// TODO: 0xfffffff0 (Go 1.18/1.19) + 0xfffffffa (Go 1.16) layouts.
return None;
}
// Fixed-layout header for Go 1.20+ (matches src/runtime/symtab.go pcHeader).
// u32 magic
// u16 pad
// u8 minLC
// u8 ptrSize
// uN nfunc
// uN nfiles
// uN textStart
// uN funcnameOffset
// uN cuOffset
// uN filetabOffset
// uN pctabOffset
// uN pclnOffset
let minlc = data[6];
let ptrsize = data[7] as usize;
if ptrsize != 8 && ptrsize != 4 {
return None;
}
let _ = minlc;
let mut p = 8usize;
let read_word = |p: usize| -> Option<u64> {
if p + ptrsize > data.len() {
return None;
}
Some(match ptrsize {
8 => u64::from_le_bytes(data[p..p + 8].try_into().ok()?),
4 => u32::from_le_bytes(data[p..p + 4].try_into().ok()?) as u64,
_ => unreachable!(),
})
};
let nfunc = read_word(p)?;
p += ptrsize;
let _nfiles = read_word(p)?;
p += ptrsize;
let text_start = read_word(p)?;
p += ptrsize;
let funcname_off = read_word(p)? as usize;
p += ptrsize;
let _cu_off = read_word(p)?;
p += ptrsize;
let _filetab_off = read_word(p)?;
p += ptrsize;
let _pctab_off = read_word(p)?;
p += ptrsize;
let pcln_off = read_word(p)? as usize;
if funcname_off >= data.len() || pcln_off >= data.len() {
return None;
}
let pcln = &data[pcln_off..];
// Go 1.20+ functab: array of {u32 entryoff, u32 funcoff} — both
// u32 regardless of ptrSize. Entry size = 8 bytes.
let entry_size = 8usize;
let mut out = HashMap::new();
let max = (nfunc as usize).min(65536);
for i in 0..max {
let base = i * entry_size;
if base + entry_size > pcln.len() {
break;
}
let entry_off = u32::from_le_bytes(pcln[base..base + 4].try_into().ok()?) as u64;
let func_off = u32::from_le_bytes(pcln[base + 4..base + 8].try_into().ok()?) as u64;
let pc = text_start.wrapping_add(entry_off);
// _func struct at pcln_off + func_off.
let fpos = pcln_off + func_off as usize;
if fpos + 8 > data.len() {
continue;
}
// Go 1.20+ _func layout starts with:
// u32 entryoff (pc offset from textStart — duplicate)
// u32 nameoff (offset into funcnametab)
let name_off = u32::from_le_bytes(data[fpos + 4..fpos + 8].try_into().ok()?) as usize;
let name_abs = funcname_off + name_off;
if name_abs >= data.len() {
continue;
}
// Name is null-terminated ASCII.
let end = data[name_abs..]
.iter()
.position(|&b| b == 0)
.map(|p| name_abs + p)
.unwrap_or(data.len());
if end - name_abs == 0 || end - name_abs > 512 {
continue;
}
let name = match std::str::from_utf8(&data[name_abs..end]) {
Ok(s) => s,
Err(_) => continue,
};
if name.is_empty() {
continue;
}
out.insert(pc, sanitize(name));
}
Some(out)
}
/// Convert a Go mangled symbol name into something safe to emit inside C
/// pseudocode. Go names embed `.`, `/`, `(`, `)`, `*`, `[`, `]`, `,`, `:`,
/// spaces — all invalid in C identifiers.
fn sanitize(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut last_underscore = false;
for c in name.chars() {
if c.is_ascii_alphanumeric() {
out.push(c);
last_underscore = false;
} else if !last_underscore {
out.push('_');
last_underscore = true;
}
}
out.trim_end_matches('_').to_string()
}