Skip to content

Commit 07bee16

Browse files
x86-64: Recover jump tables, TLS SECREL, and RVA relocations
Extend the x86-64 recovery pass beyond REL32 branches/RIP-relative refs: - Track __ImageBase across `lea reg, [rip+imagebase]` to recover image-base-relative (ADDR32NB) accesses, including switch jump tables, which now emit their own bounding data symbol. - Recover TLS variables as SECREL relocations, committing them only when the function actually performs a `gs:[0x58]` TLS access. - Model the REL32 family (REL32_1..REL32_5) via a trailing-immediate count so RIP-relative operands with trailing immediates get the correct reloc type and addend. On the PE side, collect S_GTHREAD32/S_LTHREAD32 TLS variables from the PDB, resolve __ImageBase, and synthesize MSVC `??_C@` decorated names for folded/anonymous string-literal COMDATs that carry no PDB symbol.
1 parent 2f41843 commit 07bee16

7 files changed

Lines changed: 573 additions & 52 deletions

File tree

crates/delink-ida/src/resolver.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,9 @@ impl delink_x86_64::recover::SymbolResolver for IdaSymbols {
172172
fn resolve_data(&self, va: u64) -> Option<(String, i64)> {
173173
IdaSymbols::resolve_data(self, va)
174174
}
175+
// image_base()/in_text() intentionally use the trait defaults: image-base-
176+
// relative recovery (jump tables, ADDR32NB) is PE/COFF-specific and this
177+
// format-generic path (COFF/ELF × x86/x86-64) can't represent it. The
178+
// defaults (u64::MAX / false) keep the recovery pass from emitting relocs
179+
// this emit would drop. See delink-pe for the PE-side implementation.
175180
}

crates/delink-pe/src/cu.rs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type CuIndexResult = (
103103
PeCuIndex,
104104
BTreeMap<u64, PeFunction>,
105105
BTreeMap<u64, PeVariable>,
106+
BTreeMap<u32, String>,
106107
Vec<String>,
107108
);
108109

@@ -158,21 +159,51 @@ pub fn build_cu_index(
158159
// separately so they can fill gaps where no S_GDATA32/S_LDATA32 exists.
159160
let mut mangled_by_va: HashMap<u64, String> = HashMap::new();
160161
let mut public_data_by_va: HashMap<u64, String> = HashMap::new();
162+
// Thread-local variables: `.tls` section-relative offset → mangled name,
163+
// used to recover SECREL relocations on `mov r32, <tls-offset>` loads.
164+
// Global TLS (`S_GTHREAD32`) lives in the global stream; file-static TLS
165+
// (`S_LTHREAD32`) is collected from the module streams below.
166+
let mut tls_variables: BTreeMap<u32, String> = BTreeMap::new();
161167
{
168+
// Public (S_PUB32) names, keyed by section:offset, so a TLS variable's
169+
// ThreadStorage record (which carries the undecorated name) can be paired
170+
// with the decorated/mangled public name that the code actually references.
171+
let mut pub_by_secoff: HashMap<(u16, u32), String> = HashMap::new();
172+
let mut tls_pending: Vec<(u16, u32, String)> = Vec::new();
162173
let global_syms = pdb.global_symbols().context("PDB global symbols")?;
163174
let mut iter = global_syms.iter();
164175
while let Some(sym) = iter.next()? {
165-
if let Ok(pdb::SymbolData::Public(p)) = sym.parse() {
166-
if let Some(rva) = p.offset.to_rva(&address_map) {
167-
let va = image_base + rva.0 as u64;
176+
match sym.parse() {
177+
Ok(pdb::SymbolData::Public(p)) => {
168178
let name = p.name.to_string().into_owned();
169-
mangled_by_va.insert(va, name.clone());
170-
if !p.function && !p.code {
171-
public_data_by_va.insert(va, name);
179+
pub_by_secoff
180+
.entry((p.offset.section, p.offset.offset))
181+
.or_insert_with(|| name.clone());
182+
if let Some(rva) = p.offset.to_rva(&address_map) {
183+
let va = image_base + rva.0 as u64;
184+
mangled_by_va.insert(va, name.clone());
185+
if !p.function && !p.code {
186+
public_data_by_va.insert(va, name);
187+
}
172188
}
173189
}
190+
Ok(pdb::SymbolData::ThreadStorage(t)) => {
191+
tls_pending.push((
192+
t.offset.section,
193+
t.offset.offset,
194+
t.name.to_string().into_owned(),
195+
));
196+
}
197+
_ => {}
174198
}
175199
}
200+
for (section, offset, fallback) in tls_pending {
201+
let name = pub_by_secoff
202+
.get(&(section, offset))
203+
.cloned()
204+
.unwrap_or(fallback);
205+
tls_variables.entry(offset).or_insert(name);
206+
}
176207
}
177208

178209
// --- Collect section contributions per module (0-based module index) ---
@@ -275,6 +306,13 @@ pub fn build_cu_index(
275306
};
276307
all_variables.entry(va).or_insert_with(|| v);
277308
}
309+
Ok(pdb::SymbolData::ThreadStorage(t)) => {
310+
// TLS variables live in `.tls`; key by the section-relative
311+
// offset, which is exactly the SECREL value the linker bakes
312+
// into `mov r32, <tls-offset>`.
313+
let name = t.name.to_string().into_owned();
314+
tls_variables.entry(t.offset.offset).or_insert(name);
315+
}
278316
Ok(pdb::SymbolData::Label(l)) => {
279317
let Some(rva) = l.offset.to_rva(&address_map) else {
280318
continue;
@@ -346,6 +384,7 @@ pub fn build_cu_index(
346384
PeCuIndex { units },
347385
all_functions,
348386
all_variables,
387+
tls_variables,
349388
inlined_functions.into_iter().collect(),
350389
))
351390
}

crates/delink-pe/src/emit.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use crate::{BaseRelocKind, PeArch, PeContext, PeSection};
2222

2323
// AMD64 relocation type constants.
2424
const REL_AMD64_ADDR64: u16 = object::pe::IMAGE_REL_AMD64_ADDR64;
25+
const REL_AMD64_ADDR32NB: u16 = object::pe::IMAGE_REL_AMD64_ADDR32NB;
26+
const REL_AMD64_SECREL: u16 = object::pe::IMAGE_REL_AMD64_SECREL;
2527
const REL_AMD64_REL32: u16 = object::pe::IMAGE_REL_AMD64_REL32;
2628

2729
// I386 relocation type constants.
@@ -138,7 +140,9 @@ pub fn emit_pe_cu(pe: &PeContext, cu: &PeCompilationUnit, out_path: &Path) -> Re
138140
for r in &recovery.relocs {
139141
let off = r.offset as usize;
140142
let zero_len = match r.kind {
141-
delink_x86_64::RelocKind::Rel32 => 4,
143+
delink_x86_64::RelocKind::Rel32 { .. } => 4,
144+
delink_x86_64::RelocKind::Addr32Nb => 4,
145+
delink_x86_64::RelocKind::Secrel => 4,
142146
delink_x86_64::RelocKind::Addr64 => 8,
143147
};
144148
if off + zero_len <= fn_bytes.len() {
@@ -224,20 +228,49 @@ pub fn emit_pe_cu(pe: &PeContext, cu: &PeCompilationUnit, out_path: &Path) -> Re
224228
total_relocs += 1;
225229
}
226230

231+
// Emit a data symbol at each recovered jump table. Besides giving
232+
// the table entries a home, this symbol bounds the function in
233+
// objdiff (which otherwise disassembles the table as trailing code).
234+
for jt in &recovery.jump_tables {
235+
let jt_id = obj.add_symbol(Symbol {
236+
name: sanitize_symbol_name(&jt.name),
237+
value: fn_offset + jt.offset,
238+
size: jt.entry_count * 4,
239+
kind: SymbolKind::Data,
240+
scope: SymbolScope::Compilation,
241+
weak: false,
242+
section: SymbolSection::Section(sid),
243+
flags: SymbolFlags::None,
244+
});
245+
local_syms.insert(jt.name.clone(), jt_id);
246+
}
247+
227248
for r in &recovery.relocs {
228249
let sym_id = resolve_symbol(&mut obj, &local_syms, &mut undef_cache, &r.target);
250+
// The REL32/REL32_1..REL32_5 type constants are consecutive, so
251+
// the trailing-immediate width both selects the type and extends
252+
// the PC-relative bias the object writer folds into the field
253+
// (REL32 → +4, REL32_1 → +5, …); subtract it so the written field
254+
// lands at the intended addend. ADDR32NB (RVA) gets no bias.
255+
let (typ, addend) = match r.kind {
256+
delink_x86_64::RelocKind::Rel32 { trailing } => (
257+
REL_AMD64_REL32 + trailing as u16,
258+
r.addend - REL32_FIELD_BYTES - trailing as i64,
259+
),
260+
delink_x86_64::RelocKind::Addr32Nb => (REL_AMD64_ADDR32NB, r.addend),
261+
delink_x86_64::RelocKind::Secrel => (REL_AMD64_SECREL, r.addend),
262+
delink_x86_64::RelocKind::Addr64 => (REL_AMD64_ADDR64, r.addend),
263+
};
229264
obj.add_relocation(
230265
sid,
231266
Relocation {
232267
offset: fn_offset + r.offset,
233268
symbol: sym_id,
234-
addend: r.addend - REL32_FIELD_BYTES,
235-
flags: RelocationFlags::Coff {
236-
typ: REL_AMD64_REL32,
237-
},
269+
addend,
270+
flags: RelocationFlags::Coff { typ },
238271
},
239272
)
240-
.with_context(|| format!("add rel32 reloc at {:#x}", r.offset))?;
273+
.with_context(|| format!("add reloc at {:#x}", r.offset))?;
241274
total_relocs += 1;
242275
}
243276
}

crates/delink-pe/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::collections::HashMap;
88

99
pub mod cu;
1010
pub mod emit;
11+
pub mod mangle;
1112
pub mod symbols;
1213

1314
pub use cu::{PeCompilationUnit, PeContrib, PeCuIndex, PeFunction, PeVariable};
@@ -122,11 +123,12 @@ pub fn load_pe_and_pdb(exe_data: &[u8], pdb_data: &[u8]) -> Result<PeContext> {
122123
let base_relocations = parse_base_relocations(&sections, image_base);
123124
let imports = parse_imports(exe_data, &sections, image_base, arch);
124125

125-
let (cu_index, all_functions, all_variables, inlined_functions) =
126+
let (cu_index, all_functions, all_variables, tls_variables, inlined_functions) =
126127
cu::build_cu_index(pdb_data, image_base, &sections, arch)?;
127128
let symbols = symbols::PeGlobalSymbols::build(
128129
all_functions,
129130
all_variables,
131+
tls_variables,
130132
&imports,
131133
&sections,
132134
image_base,

crates/delink-pe/src/mangle.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! Synthesis of MSVC string-literal symbol names (`??_C@_0…`).
2+
//!
3+
//! MSVC emits every string literal as a COMDAT with a deterministic decorated
4+
//! name derived purely from the string's bytes. Those COMDATs are frequently
5+
//! folded/anonymous and carry **no** symbol record in the PDB, so a data
6+
//! reference to one falls through the PDB-symbol lookup to a raw
7+
//! `__delink_pe_<section>_start + offset` section-relative fallback — which
8+
//! never matches the `??_C@…` symbol our own recompiled object references.
9+
//!
10+
//! Since the name is a pure function of the bytes, we reproduce MSVC's mangling
11+
//! here and look the literal up **by address** (reading the bytes at the
12+
//! reference target) rather than by a name that isn't in the PDB.
13+
//!
14+
//! Name format: `??_C@_0<len><crc>@<text>@`
15+
//! * `<len>` — byte length *including* the NUL terminator, as a mangled number
16+
//! (`1..=10` → `'0'..'9'` = value-1; otherwise base-16 nibbles `A`(0)..`P`(15)
17+
//! most-significant first, terminated by `@`).
18+
//! * `<crc>` — CRC-32 (poly 0xEDB88320, init 0xFFFFFFFF, **no** final inversion)
19+
//! over the bytes *including* the NUL, as 8 nibbles `A`..`P`, MSB first.
20+
//! * `<text>` — up to the first 32 source bytes, escaped (see [`escape_byte`]),
21+
//! terminated by `@`.
22+
23+
/// Precomputed CRC-32 lookup table (reflected, poly 0xEDB88320).
24+
const fn crc32_table() -> [u32; 256] {
25+
let mut table = [0u32; 256];
26+
let mut n = 0usize;
27+
while n < 256 {
28+
let mut c = n as u32;
29+
let mut k = 0;
30+
while k < 8 {
31+
c = if c & 1 != 0 {
32+
0xEDB8_8320 ^ (c >> 1)
33+
} else {
34+
c >> 1
35+
};
36+
k += 1;
37+
}
38+
table[n] = c;
39+
n += 1;
40+
}
41+
table
42+
}
43+
44+
const CRC32_TABLE: [u32; 256] = crc32_table();
45+
46+
/// MSVC's string-literal checksum: CRC-32 with init `0xFFFFFFFF` and **no**
47+
/// final XOR (unlike zlib, which inverts the result).
48+
fn msvc_string_crc(bytes: &[u8]) -> u32 {
49+
let mut c = 0xFFFF_FFFFu32;
50+
for &b in bytes {
51+
c = CRC32_TABLE[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
52+
}
53+
c
54+
}
55+
56+
/// Append `n` nibbles of `v` (most-significant first) using the `A`(0)..`P`(15)
57+
/// alphabet MSVC uses for mangled numbers.
58+
fn push_nibbles(out: &mut String, v: u32, n: u32) {
59+
for i in (0..n).rev() {
60+
let nib = (v >> (i * 4)) & 0xF;
61+
out.push((b'A' + nib as u8) as char);
62+
}
63+
}
64+
65+
/// Encode a mangled number: `1..=10` as a single digit `'0'..'9'` (value-1),
66+
/// otherwise base-16 nibbles `A`..`P` (MSB first) terminated by `@`.
67+
fn encode_number(out: &mut String, value: u32) {
68+
if (1..=10).contains(&value) {
69+
out.push((b'0' + (value - 1) as u8) as char);
70+
return;
71+
}
72+
// Minimal nibble count.
73+
let mut nibbles = 0u32;
74+
let mut x = value;
75+
while x != 0 {
76+
nibbles += 1;
77+
x >>= 4;
78+
}
79+
if nibbles == 0 {
80+
nibbles = 1;
81+
}
82+
push_nibbles(out, value, nibbles);
83+
out.push('@');
84+
}
85+
86+
/// The ten single-character escapes MSVC uses for common punctuation, indexed
87+
/// `0..=9` → `?0`..`?9`. Order is significant.
88+
const SPECIAL: &[u8] = b",/\\:. \n\t'-";
89+
90+
/// Escape one source byte into the string-literal text field.
91+
fn escape_byte(out: &mut String, b: u8) {
92+
if b.is_ascii_alphanumeric() || b == b'_' {
93+
out.push(b as char);
94+
} else if let Some(idx) = SPECIAL.iter().position(|&c| c == b) {
95+
out.push('?');
96+
out.push((b'0' + idx as u8) as char);
97+
} else {
98+
out.push_str("?$");
99+
out.push((b'A' + (b >> 4)) as char);
100+
out.push((b'A' + (b & 0xF)) as char);
101+
}
102+
}
103+
104+
/// The maximum source-byte prefix MSVC embeds in the decorated name's text field.
105+
const MAX_TEXT_BYTES: usize = 32;
106+
107+
/// Build the MSVC `??_C@` decorated name for a narrow (char) string literal.
108+
///
109+
/// `content` is the string's bytes **without** the terminating NUL; this
110+
/// function appends it (MSVC hashes and counts the NUL).
111+
pub fn narrow_string_symbol(content: &[u8]) -> String {
112+
// Length and CRC are over the bytes including the NUL terminator.
113+
let total_len = content.len() as u32 + 1;
114+
// CRC over content + NUL, without allocating a joined buffer.
115+
let crc = {
116+
let mut c = msvc_string_crc(content);
117+
c = CRC32_TABLE[((c ^ 0) & 0xFF) as usize] ^ (c >> 8);
118+
c
119+
};
120+
121+
let mut out = String::from("??_C@_0");
122+
encode_number(&mut out, total_len);
123+
push_nibbles(&mut out, crc, 8);
124+
out.push('@');
125+
// Text: escape up to MAX_TEXT_BYTES source bytes (NUL only appears if the
126+
// whole string fits, since content excludes it and we cap before it).
127+
let text_bytes = content.len().min(MAX_TEXT_BYTES);
128+
for &b in &content[..text_bytes] {
129+
escape_byte(&mut out, b);
130+
}
131+
if content.len() < MAX_TEXT_BYTES {
132+
// Short enough that the NUL terminator is part of the embedded text.
133+
escape_byte(&mut out, 0);
134+
}
135+
out.push('@');
136+
out
137+
}
138+
139+
#[cfg(test)]
140+
mod tests {
141+
use super::*;
142+
143+
#[test]
144+
fn known_string_literal_names() {
145+
// Ground-truth (content, decorated name) pairs extracted from
146+
// MSVC-compiled objects, spanning the single-digit and multi-nibble
147+
// length forms and the space/`?5` escape.
148+
let cases: &[(&[u8], &str)] = &[
149+
(b"system", "??_C@_06FHFOAHML@system?$AA@"),
150+
(b"generic", "??_C@_07DCLBNMLN@generic?$AA@"),
151+
(b"iostream", "??_C@_08LLGCOLLL@iostream?$AA@"),
152+
(b"tag_resource_lruv", "??_C@_0BC@DKEHMPJE@tag_resource_lruv?$AA@"),
153+
(b"string too long", "??_C@_0BA@JFNIOLAK@string?5too?5long?$AA@"),
154+
];
155+
for (content, expected) in cases {
156+
assert_eq!(&narrow_string_symbol(content), expected, "content = {:?}", content);
157+
}
158+
}
159+
160+
#[test]
161+
fn crc_matches_reference() {
162+
// "system\0" → 0x575E07CB
163+
assert_eq!(msvc_string_crc(b"system\x00"), 0x575E_07CB);
164+
}
165+
}

0 commit comments

Comments
 (0)