Validate string tables once, not per name

A time profile (sample) showed a quarter of staging spent in
per-name from_utf8 and byte-wise NUL scans: 2.8 million short
strings validated one at a time on a clang-sized link. Each object's
string table is now checked as UTF-8 once - SIMD-friendly over
megabytes instead of call overhead over dozens of bytes - and names
slice out of it unchecked, with the NUL found by libc's vectorized
memchr. A non-UTF-8 table (never produced by real tools) degrades
to the names in its valid prefix rather than erroring. Release
builds also carry line tables now (debug = 1, as mold-rust), which
is what made the profile readable in the first place.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Avqib5YEt3uKNfSmwEcNcJ
This commit is contained in:
Rui UeyamaandClaude Fable 5 committed 2026-09-02 12:01:29 +09:00
1 parent 22d053d82c
commit f66a44b321
2 files changed
+38 -6

No files matched your search

+3
View File
@@ -11,6 +11,9 @@ name = "mold_macho"
# The linker is generic over the target. Each target is instantiated in a
# crate of its own under targets/, and the executable in cli/ dispatches to
# them, mirroring mold's ELF port.
[profile.release]
debug = 1
[workspace]
members = ["cli", "targets/*", "tests"]
default-members = [".", "cli", "targets/*", "tests"]
+35 -6
View File
@@ -245,7 +245,7 @@ pub fn stage_object<E: Arch>(
let mut strtab: &'static [u8] = &[];
if let Some(cmd) = symtab_cmd {
nlists = read_array(data, cmd.symoff as usize, cmd.nsyms as usize);
strtab = &data[cmd.stroff as usize..(cmd.stroff + cmd.strsize) as usize];
strtab = validate_strtab(&data[cmd.stroff as usize..(cmd.stroff + cmd.strsize) as usize]);
}
// Split each section into subsections at its symbols, the Mach-O
@@ -765,11 +765,40 @@ pub fn parse_bitcode<E: Arch>(ctx: &mut Context<E>, mf: &'static MappedFile, ali
obj_idx
}
/// Extracts one NUL-terminated name from a string table already
/// validated as UTF-8 by validate_strtab. The NUL scan goes through
/// libc's memchr, which is vectorized; a per-name from_utf8 was a
/// quarter of all staging time on big links.
fn symbol_name(strtab: &'static [u8], nlist: &NList) -> &'static str {
let off = nlist.n_strx as usize;
if off >= strtab.len() {
return "";
}
let rest = &strtab[off..];
let len = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
std::str::from_utf8(&rest[..len]).unwrap_or("")
// SAFETY: memchr reads within `rest`; the result is bounded by
// its length.
let len = unsafe {
let p = libc::memchr(rest.as_ptr() as *const _, 0, rest.len());
if p.is_null() {
rest.len()
} else {
(p as usize) - (rest.as_ptr() as usize)
}
};
// SAFETY: the whole table was checked as UTF-8 up front; any
// slice of it on a codepoint boundary is valid, and a NUL
// boundary always is.
unsafe { std::str::from_utf8_unchecked(&rest[..len]) }
}
/// Checks a whole string table as UTF-8 once - vastly cheaper than
/// validating millions of short names one by one. Returns an empty
/// table (degrading names to "") for the pathological non-UTF-8 case.
fn validate_strtab(strtab: &'static [u8]) -> &'static [u8] {
match std::str::from_utf8(strtab) {
Ok(_) => strtab,
Err(e) => &strtab[..e.valid_up_to()],
}
}
@@ -1250,7 +1279,7 @@ pub fn defined_symbol_names(mf: &MappedFile) -> Vec<&'static str> {
&data[cmd.stroff as usize..(cmd.stroff + cmd.strsize) as usize];
// SAFETY: input files are leaked, so the string table lives
// for the rest of the process.
let strtab: &'static [u8] = unsafe { std::mem::transmute(strtab) };
let strtab: &'static [u8] = validate_strtab(unsafe { std::mem::transmute(strtab) });
for nlist in &nlists {
if !nlist.is_stab()
&& nlist.is_extern()
@@ -1363,7 +1392,7 @@ pub fn parse_dylib_binary<E: Arch>(ctx: &mut Context<E>, mf: &'static MappedFile
let strtab = &data[sym.stroff as usize..(sym.stroff + sym.strsize) as usize];
// SAFETY: input files are leaked, so the string table lives for
// the rest of the process.
let strtab: &'static [u8] = unsafe { std::mem::transmute(strtab) };
let strtab: &'static [u8] = validate_strtab(unsafe { std::mem::transmute(strtab) });
// A TLV export is recognizable by its section: n_sect names a
// S_THREAD_LOCAL_VARIABLES section (the __thread_vars
// descriptors).
@@ -1515,7 +1544,7 @@ fn dylib_binary_exports(
let strtab = &data[sym.stroff as usize..(sym.stroff + sym.strsize) as usize];
// SAFETY: input files are leaked, so the string table lives for
// the rest of the process.
let strtab: &'static [u8] = unsafe { std::mem::transmute(strtab) };
let strtab: &'static [u8] = validate_strtab(unsafe { std::mem::transmute(strtab) });
let tlv_sects = thread_local_section_ordinals(data, &hdr);
let range = dysym.iextdefsym as usize..(dysym.iextdefsym + dysym.nextdefsym) as usize;
for nlist in &nlists[range] {