From 3d2cea7256d4a419a5850057b0eb8f293bf38760 Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Fri, 7 Aug 2026 17:01:51 +0800 Subject: [PATCH] Add COFF support to -Zstaticlib-rename-internal-symbols --- compiler/rustc_codegen_ssa/src/back/link.rs | 34 ++-- .../rustc_codegen_ssa/src/back/symbol_edit.rs | 157 +++++++++++++++- compiler/rustc_codegen_ssa/src/diagnostics.rs | 2 +- .../rmake.rs | 168 ++++++++++++++++++ 4 files changed, 346 insertions(+), 15 deletions(-) create mode 100644 tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index 47462ca2cac91..8e2a6f5f85b1a 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -837,23 +837,31 @@ fn link_staticlib( let rename = sess.opts.unstable_opts.staticlib_rename_internal_symbols; let exported_symbols = if hide || rename { - if !matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO) { - if hide { - sess.dcx().emit_warn(diagnostics::StaticlibHideInternalSymbolsUnsupported { - binary_format: sess.target.archive_format.to_string(), - }); - } - if rename { - sess.dcx().emit_warn(diagnostics::StaticlibRenameInternalSymbolsUnsupported { - binary_format: sess.target.archive_format.to_string(), - }); - } - None - } else { + let hide_supported = + matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO); + // Rename only rewrites symbol names, so it also works on COFF; hide + // needs a visibility concept COFF lacks. + let rename_supported = matches!( + sess.target.binary_format, + BinaryFormat::Elf | BinaryFormat::MachO | BinaryFormat::Coff + ); + if hide && !hide_supported { + sess.dcx().emit_warn(diagnostics::StaticlibHideInternalSymbolsUnsupported { + binary_format: sess.target.archive_format.to_string(), + }); + } + if rename && !rename_supported { + sess.dcx().emit_warn(diagnostics::StaticlibRenameInternalSymbolsUnsupported { + binary_format: sess.target.archive_format.to_string(), + }); + } + if (hide && hide_supported) || (rename && rename_supported) { crate_info .exported_symbols .get(&CrateType::StaticLib) .map(|symbols| symbols.iter().map(|symbol| symbol.name.clone()).collect()) + } else { + None } } else { None diff --git a/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs b/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs index ef8008216187a..adc152bc4a6e9 100644 --- a/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs +++ b/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs @@ -7,9 +7,10 @@ use std::borrow::Cow; use std::mem; +use object::read::coff::{CoffHeader, ImageSymbol as _}; use object::read::elf::{SectionHeader as _, Sym as _}; use object::read::macho::Nlist; -use object::{Endianness, elf, macho}; +use object::{Endianness, elf, macho, pe}; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; struct Patch { @@ -57,6 +58,22 @@ pub(super) fn apply_edits<'a>( rename, mem::offset_of!(macho::Nlist32, n_type), ), + Some(object::File::Coff(f)) => coff_edit_impl( + data, + f.coff_header(), + exported, + hide, + rename, + coff_strip_underscore(f.coff_header()), + ), + Some(object::File::CoffBig(f)) => coff_edit_impl( + data, + f.coff_header(), + exported, + hide, + rename, + coff_strip_underscore(f.coff_header()), + ), _ => None, }; match result { @@ -84,10 +101,30 @@ pub(super) fn collect_internal_names( object::File::MachO32(_) => { macho_collect_impl::>(data, exported, out) } + object::File::Coff(f) => coff_collect_impl( + data, + f.coff_header(), + exported, + out, + coff_strip_underscore(f.coff_header()), + ), + object::File::CoffBig(f) => coff_collect_impl( + data, + f.coff_header(), + exported, + out, + coff_strip_underscore(f.coff_header()), + ), _ => {} } } +/// Whether this machine's COFF ABI decorates external symbols with a leading `_` +/// (i686 only). +fn coff_strip_underscore(header: &impl CoffHeader) -> bool { + header.machine() == pe::IMAGE_FILE_MACHINE_I386 +} + fn elf_collect_impl>( data: &[u8], exported: &FxHashSet, @@ -441,6 +478,124 @@ fn macho_rebuild_strtab( Some(result) } +// --------------------------------------------------------------------------- +// COFF: single-pass collection + apply +// --------------------------------------------------------------------------- + +fn coff_collect_impl<'data, Coff: CoffHeader>( + data: &'data [u8], + header: &'data Coff, + exported: &FxHashSet, + out: &mut FxHashSet, + strip_underscore: bool, +) { + let Ok(symbols) = header.symbols(data) else { return }; + let strings = symbols.strings(); + + for (_index, sym) in symbols.iter() { + let sclass = sym.storage_class(); + if sclass != pe::IMAGE_SYM_CLASS_EXTERNAL && sclass != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL { + continue; + } + if sym.section_number() <= 0 { + continue; + } + let Ok(name_bytes) = sym.name(strings) else { continue }; + let Ok(mut name) = str::from_utf8(name_bytes).map(String::from) else { continue }; + if strip_underscore { + name = name.strip_prefix('_').unwrap_or(&name).to_string(); + } + if !exported.contains(&name) { + out.insert(name); + } + } +} + +fn coff_edit_impl<'data, Coff: CoffHeader>( + data: &'data [u8], + header: &'data Coff, + exported: &FxHashSet, + hide: bool, + rename: Option<&(FxHashSet, &str)>, + strip_underscore: bool, +) -> Option> { + // COFF has no visibility concept, so hide is a no-op. + let _ = (hide, exported); + + let pointer_to_symbol_table = header.pointer_to_symbol_table() as usize; + let number_of_symbols = header.number_of_symbols() as usize; + // ImageSymbol is 18 bytes; ImageSymbolEx (bigobj) is 20. + let sym_size = mem::size_of::(); + + let symbol_bytes_offset = pointer_to_symbol_table; + let strtab_base = pointer_to_symbol_table + number_of_symbols * sym_size; + if strtab_base > data.len() { + return None; + } + + if data.len() < strtab_base + 4 { + return None; + } + let old_len = + u32::from_le_bytes(data[strtab_base..strtab_base + 4].try_into().unwrap()) as usize; + if strtab_base + old_len > data.len() { + return None; + } + + let Ok(symbols) = header.symbols(data) else { return None }; + let strings = symbols.strings(); + + let mut renames = Vec::new(); + for (index, sym) in symbols.iter() { + let sclass = sym.storage_class(); + if sclass != pe::IMAGE_SYM_CLASS_EXTERNAL && sclass != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL { + continue; + } + let Ok(name_bytes) = sym.name(strings) else { continue }; + let Ok(name) = str::from_utf8(name_bytes) else { continue }; + let check_name = + if strip_underscore { name.strip_prefix('_').unwrap_or(name) } else { name }; + if rename.is_some_and(|(rename_set, _)| rename_set.contains(check_name)) { + renames.push(RenameEntry { + name_field_offset: symbol_bytes_offset + index.0 * sym_size, + name: name.to_string(), + }); + } + } + if renames.is_empty() { + return None; + } + let suffix = rename.unwrap().1; + + let mut new_strtab = Vec::new(); + let mut map: FxHashMap = FxHashMap::default(); + let mut sorted_names: Vec<&str> = renames.iter().map(|r| r.name.as_str()).collect(); + sorted_names.sort(); + sorted_names.dedup(); + for name in &sorted_names { + let rel_offset = (old_len + new_strtab.len()) as u32; + new_strtab.extend_from_slice(name.as_bytes()); + new_strtab.extend_from_slice(suffix.as_bytes()); + new_strtab.push(0); + map.insert(name.to_string(), rel_offset); + } + + let mut result = data.to_vec(); + let new_len = (old_len + new_strtab.len()) as u32; + result[strtab_base..strtab_base + 4].copy_from_slice(&new_len.to_le_bytes()); + result.extend_from_slice(&new_strtab); + + // Long names are stored as name[0] = 0 and name[4..8] = string table offset. + for r in &renames { + result[r.name_field_offset] = 0; + let rel = map[&r.name]; + result[r.name_field_offset + 4..r.name_field_offset + 8] + .copy_from_slice(&rel.to_le_bytes()); + } + + Some(result) +} + // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- diff --git a/compiler/rustc_codegen_ssa/src/diagnostics.rs b/compiler/rustc_codegen_ssa/src/diagnostics.rs index e6aab553072f2..7d12a4c6db000 100644 --- a/compiler/rustc_codegen_ssa/src/diagnostics.rs +++ b/compiler/rustc_codegen_ssa/src/diagnostics.rs @@ -700,7 +700,7 @@ pub(crate) struct StaticlibHideInternalSymbolsUnsupported { #[derive(Diagnostic)] #[diag( - "-Zstaticlib-rename-internal-symbols only supports ELF and Mach-O targets, but the target uses `{$binary_format}`" + "-Zstaticlib-rename-internal-symbols only supports ELF, Mach-O, and COFF targets, but the target uses `{$binary_format}`" )] pub(crate) struct StaticlibRenameInternalSymbolsUnsupported { pub binary_format: String, diff --git a/tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs new file mode 100644 index 0000000000000..49234bbf80833 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs @@ -0,0 +1,168 @@ +//@ only-windows +//@ ignore-cross-compile + +use std::collections::HashSet; + +use run_make_support::object::read::archive::ArchiveFile; +use run_make_support::object::read::coff::ImageSymbol as _; +use run_make_support::object::{File, pe}; +use run_make_support::path_helpers::source_root; +use run_make_support::{cc, extra_c_flags, rfs, run, rustc, static_lib_name}; + +const EXPORTED: &[&str] = &["my_add", "my_hash_lookup", "call_internal", "my_safe_div"]; + +fn main() { + let hide_sibling = source_root().join("tests/run-make/staticlib-hide-internal-symbols"); + let rename_sibling = source_root().join("tests/run-make/staticlib-rename-internal-symbols"); + rfs::copy(hide_sibling.join("lib.rs"), "lib.rs"); + rfs::copy(hide_sibling.join("main.c"), "main.c"); + rfs::copy(rename_sibling.join("liba.rs"), "liba.rs"); + rfs::copy(rename_sibling.join("libb.rs"), "libb.rs"); + rfs::copy(rename_sibling.join("dual_main.c"), "dual_main.c"); + + test_basic_functionality(); + test_rs_suffix_present(); + test_dual_staticlib_linking(); +} + +fn test_basic_functionality() { + let lib_name = static_lib_name("lib"); + + rustc() + .input("lib.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + cc().input("main.c").input(&lib_name).out_exe("main").args(extra_c_flags()).run(); + run("main"); + + rfs::remove_file(&lib_name); +} + +fn test_rs_suffix_present() { + let lib_name = static_lib_name("lib"); + + rustc() + .input("lib.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + let data = rfs::read(&lib_name); + check_rename_symbols(&data); + + rfs::remove_file(&lib_name); +} + +fn test_dual_staticlib_linking() { + let liba_name = static_lib_name("liba"); + let libb_name = static_lib_name("libb"); + + rustc() + .input("liba.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + rustc() + .input("libb.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + cc().input("dual_main.c") + .input(&liba_name) + .input(&libb_name) + .out_exe("dual_main") + .args(extra_c_flags()) + .run(); + run("dual_main"); +} + +fn check_rename_symbols(archive_data: &[u8]) { + let archive = ArchiveFile::parse(archive_data).unwrap(); + let mut found_exported = HashSet::new(); + let mut found_rs_suffix = false; + + for member in archive.members() { + let member = member.unwrap(); + if !member.name().ends_with(b".rcgu.o") { + continue; + } + // Copy to an aligned buffer: COFF headers are parsed with an aligned + // `read`, which fails on archive members that sit at an odd offset. + let data = member.data(archive_data).unwrap().to_vec(); + match File::parse(&*data) { + Ok(File::Coff(f)) => check_coff_symbols( + f.coff_header(), + &data, + &mut found_exported, + &mut found_rs_suffix, + ), + Ok(File::CoffBig(f)) => check_coff_symbols( + f.coff_header(), + &data, + &mut found_exported, + &mut found_rs_suffix, + ), + Ok(_) => panic!("unexpected object file format in archive member"), + Err(e) => panic!("failed to parse archive member: {e}"), + } + } + + assert!(found_rs_suffix, "expected to find at least one renamed symbol with .rs suffix"); + for expected in EXPORTED { + assert!( + found_exported.contains(*expected), + "expected to find exported symbol `{expected}` in archive" + ); + } +} + +fn check_coff_symbols( + header: &Coff, + data: &[u8], + found_exported: &mut HashSet, + found_rs_suffix: &mut bool, +) { + // i686 decorates symbol names with a leading underscore. + let strip_underscore = header.machine() == pe::IMAGE_FILE_MACHINE_I386; + let Ok(symbols) = header.symbols(data) else { return }; + let strings = symbols.strings(); + + for (_index, symbol) in symbols.iter() { + let storage_class = symbol.storage_class(); + if storage_class != pe::IMAGE_SYM_CLASS_EXTERNAL + && storage_class != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL + { + continue; + } + if symbol.section_number() <= 0 { + continue; + } + let Ok(name_bytes) = symbol.name(strings) else { continue }; + let Ok(mut name) = str::from_utf8(name_bytes).map(String::from) else { continue }; + if strip_underscore { + name = name.strip_prefix('_').unwrap_or(&name).to_string(); + } + + if EXPORTED.contains(&name.as_str()) { + assert!( + !name.contains(".rs"), + "exported symbol `{name}` should not contain .rs suffix" + ); + found_exported.insert(name); + } else { + assert!( + name.contains(".rs"), + "internal symbol `{name}` should contain .rs suffix after rename" + ); + *found_rs_suffix = true; + } + } +}