use crate::elf::elf_relocations::*;
use crate::elf::elf_types::*;
use crate::elf::*;
use crate::linker::*;
use crate::lld::*;
use crate::x86::x86_lld_deep::*;
use crate::x86::x86_lld_full::*;
use crate::x86::*;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::fmt;
pub const SHF_WRITE: u64 = 0x1;
pub const SHF_ALLOC: u64 = 0x2;
pub const SHF_EXECINSTR: u64 = 0x4;
pub const SHF_MERGE: u64 = 0x10;
pub const SHF_STRINGS: u64 = 0x20;
pub const SHF_TLS: u64 = 0x400;
pub const SHF_LINK_ORDER: u64 = 0x80;
pub const SHF_GROUP: u64 = 0x200;
pub const SHF_OS_NONCONFORMING: u64 = 0x100;
pub const SHF_GNU_RETAIN: u64 = 0x200000;
pub const SHT_NULL: u32 = 0;
pub const SHT_PROGBITS: u32 = 1;
pub const SHT_SYMTAB: u32 = 2;
pub const SHT_STRTAB: u32 = 3;
pub const SHT_RELA: u32 = 4;
pub const SHT_HASH: u32 = 5;
pub const SHT_DYNAMIC: u32 = 6;
pub const SHT_NOTE: u32 = 7;
pub const SHT_NOBITS: u32 = 8;
pub const SHT_REL: u32 = 9;
pub const SHT_DYNSYM: u32 = 11;
pub const SHT_INIT_ARRAY: u32 = 14;
pub const SHT_FINI_ARRAY: u32 = 15;
pub const SHT_PREINIT_ARRAY: u32 = 16;
pub const SHT_GROUP: u32 = 17;
pub const SHT_SYMTAB_SHNDX: u32 = 18;
pub const SHT_GNU_HASH: u32 = 0x6ffffff6;
pub const SHT_GNU_versym: u32 = 0x6fffffff;
pub const SHT_GNU_verdef: u32 = 0x6ffffffd;
pub const SHT_GNU_verneed: u32 = 0x6ffffffe;
pub const GRP_COMDAT: u32 = 0x1;
pub const COMDAT_SELECTION_ANY: u8 = 0;
pub const COMDAT_SELECTION_EXACT_MATCH: u8 = 1;
pub const COMDAT_SELECTION_LARGEST: u8 = 2;
pub const COMDAT_SELECTION_NODUPLICATES: u8 = 3;
pub const COMDAT_SELECTION_SAME_SIZE: u8 = 4;
pub const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
pub const FNV_PRIME: u64 = 0x100000001b3;
pub const MURMUR3_SEED_64: u64 = 0xdeadbeefcafebabe;
pub const ICF_MIN_FUNCTION_SIZE: usize = 16;
pub const ICF_MAX_ITERATIONS: usize = 3;
pub const TAIL_CALL_SEARCH_WINDOW: usize = 32;
pub const GC_ROOT_PATTERNS: &[&str] = &[
".init",
".fini",
".init_array",
".fini_array",
".preinit_array",
".ctors",
".dtors",
".text.startup",
".text.startup.*",
".text.hot",
".text.hot.*",
".text._start",
".text._start.*",
];
pub const GC_UNCONDITIONAL_KEEP: &[&str] = &[
".interp",
".dynamic",
".dynsym",
".dynstr",
".hash",
".gnu.hash",
".gnu.version",
".gnu.version_r",
".gnu.version_d",
".rela.dyn",
".rela.plt",
];
pub const R_X86_64_GOTPCRELX: u32 = 41;
pub const R_X86_64_REX_GOTPCRELX: u32 = 42;
pub const R_X86_64_GOTPCREL: u32 = 9;
pub const R_X86_64_PLT32: u32 = 4;
pub const R_X86_64_PC32: u32 = 2;
pub const R_X86_64_32: u32 = 10;
pub const R_X86_64_64: u32 = 1;
pub const R_X86_64_GOT32: u32 = 3;
pub const R_X86_64_TLSGD: u32 = 16;
pub const R_X86_64_TLSLD: u32 = 17;
pub const R_X86_64_GOTTPOFF: u32 = 22;
pub const R_X86_64_TPOFF32: u32 = 23;
pub const R_X86_64_DTPOFF32: u32 = 21;
pub const R_X86_64_DTPMOD64: u32 = 16;
pub const OPCODE_MOV_R64_M: u8 = 0x8B;
pub const OPCODE_LEA_R64_M: u8 = 0x8D;
pub const OPCODE_CALL_NEAR: u8 = 0xE8;
pub const OPCODE_JMP_NEAR: u8 = 0xE9;
pub const OPCODE_JMP_SHORT: u8 = 0xEB;
pub const OPCODE_RET_NEAR: u8 = 0xC3;
pub const OPCODE_RET_FAR: u8 = 0xCB;
pub const OPCODE_NOP: u8 = 0x90;
pub const OPCODE_NOP2: [u8; 2] = [0x66, 0x90];
pub const OPCODE_NOP3: [u8; 3] = [0x0F, 0x1F, 0x00];
pub const OPCODE_NOP4: [u8; 4] = [0x0F, 0x1F, 0x40, 0x00];
pub const OPCODE_INT3: u8 = 0xCC;
pub const OPCODE_REX_W: u8 = 0x48;
pub const OPCODE_REX_B: u8 = 0x41;
pub const OPCODE_REX_R: u8 = 0x44;
pub const OPCODE_REX_X: u8 = 0x42;
pub const REX_PREFIX_MASK: u8 = 0xF0;
pub const REX_PREFIX_BASE: u8 = 0x40;
pub const OPCODE_ADD_R64_RM64: u8 = 0x03;
pub const OPCODE_XOR_R64_RM64: u8 = 0x33;
pub const OPCODE_TEST_RM8_IMM8: u8 = 0xF6;
pub const OPCODE_PUSH_R64: u8 = 0x50;
pub const OPCODE_POP_R64: u8 = 0x58;
pub const OPCODE_UD2: [u8; 2] = [0x0F, 0x0B];
pub const PAGE_SIZE: u64 = 4096;
pub const COMMON_PAGE_SIZE: u64 = 4096;
pub const MAXPAGESIZE: u64 = 0x1000;
pub const X86_64_DEFAULT_IMAGE_BASE: u64 = 0x400000;
pub const TLS_GD_TO_LE_SEQ_LEN: usize = 16;
pub const TLS_IE_TO_LE_SEQ_LEN: usize = 9;
pub const BRANCH_SHORT_MAX_DISTANCE: i32 = 127;
pub const BRANCH_SHORT_MIN_DISTANCE: i32 = -128;
pub const GC_MAX_SECTIONS: usize = 65536;
#[inline]
pub fn fnv1a_hash_64(data: &[u8]) -> u64 {
let mut hash: u64 = FNV_OFFSET_BASIS;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
pub fn fnv1a_hash_skip_relocs(
data: &[u8],
reloc_offsets: &HashSet<usize>,
reloc_sizes: &HashMap<usize, usize>,
) -> u64 {
let mut hash: u64 = FNV_OFFSET_BASIS;
let mut i = 0;
while i < data.len() {
if let Some(&size) = reloc_sizes.get(&i) {
let skip = size.min(data.len() - i);
for _ in 0..skip {
hash ^= 0u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
i += skip;
} else {
hash ^= data[i] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
}
hash
}
pub fn section_content_hash(data: &[u8], sh_flags: u64, sh_type: u32, sh_addralign: u64) -> u64 {
let mut hash = fnv1a_hash_64(data);
hash ^= sh_flags;
hash = hash.wrapping_mul(FNV_PRIME);
hash ^= sh_type as u64;
hash = hash.wrapping_mul(FNV_PRIME);
hash ^= sh_addralign;
hash = hash.wrapping_mul(FNV_PRIME);
hash
}
pub fn double_hash(data: &[u8]) -> (u64, u64) {
let h1 = fnv1a_hash_64(data);
let mut h2: u64 = MURMUR3_SEED_64;
for (i, &byte) in data.iter().enumerate() {
h2 ^= (byte as u64).rotate_left((i & 0x3F) as u32);
h2 = h2.wrapping_mul(0x9e3779b97f4a7c15);
}
(h1, h2)
}
pub fn checksum32(data: &[u8]) -> u32 {
let mut sum: u32 = 0;
for (i, &byte) in data.iter().enumerate() {
sum = sum.wrapping_add((byte as u32) << ((i & 3) * 8));
}
sum
}
pub fn is_comdat_section_name(name: &str) -> bool {
if let Some(last_dot) = name.rfind('.') {
let suffix = &name[last_dot + 1..];
suffix.len() > 2
&& (suffix.starts_with('_')
|| suffix.chars().next().map_or(false, |c| c.is_alphanumeric()))
} else {
false
}
}
pub fn extract_comdat_group_name(section_name: &str) -> Option<String> {
if let Some(last_dot) = section_name.rfind('.') {
let suffix = §ion_name[last_dot + 1..];
if suffix.len() > 2 {
return Some(suffix.to_string());
}
}
None
}
#[inline]
pub fn align_up_opt(value: u64, alignment: u64) -> u64 {
if alignment == 0 {
return value;
}
(value + alignment - 1) & !(alignment - 1)
}
#[inline]
pub fn align_down_opt(value: u64, alignment: u64) -> u64 {
if alignment == 0 {
return value;
}
value & !(alignment - 1)
}
pub fn find_tail_overlap(a: &[u8], b: &[u8], min_overlap: usize) -> Option<usize> {
let max_len = a.len().min(b.len());
if max_len < min_overlap {
return None;
}
let mut common = 0;
let a_start = a.len();
let b_start = b.len();
for k in 1..=max_len {
if a[a_start - k] != b[b_start - k] {
break;
}
common = k;
}
if common >= min_overlap {
Some(common)
} else {
None
}
}
#[inline]
pub fn is_rex_prefix(byte: u8) -> bool {
(byte & 0xF0) == 0x40
}
#[inline]
pub fn is_modrm_register(modrm: u8) -> bool {
(modrm >> 6) == 3
}
#[inline]
pub fn is_modrm_memory_nodisp(modrm: u8) -> bool {
let mod_bits = modrm >> 6;
let rm = modrm & 0x07;
mod_bits == 0 && rm != 4 && rm != 5
}
pub fn x86_insn_length(opcode: u8) -> usize {
match opcode {
0x90 => 1, 0xC3 | 0xCB | 0xC2 | 0xCA => 1, 0xCC => 1, 0x50..=0x57 => 1, 0x58..=0x5F => 1, 0xE8 | 0xE9 => 5, 0xEB => 2, 0x70..=0x7F => 2, 0x0F => 2, _ => 0, }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ICFSafetyLevel {
All,
Safe,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum COMDATSelectionStrategy {
First,
Largest,
HashOrder,
ExactMatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GCPrintMode {
Silent,
PrintRemoved,
Verbose,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelaxKind {
GOTPCRELX_MovToLea,
GOTPCRELX_ToPC32,
CallToJmp,
TlsGdToIe,
TlsGdToLe,
TlsIeToLe,
TlsLdToLe,
BranchRelax,
NoRelax,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InternalizationStrategy {
All,
Conservative,
None_,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderingHeuristic {
None,
CallGraph,
ProfileGuided,
Alphabetical,
SizeDescending,
SymbolOrderingFile,
}
#[derive(Debug, Clone)]
pub struct ICFCandidate {
pub section_index: usize,
pub content_hash: u64,
pub content_hash2: u64,
pub address_taken: bool,
pub data: Vec<u8>,
pub sh_flags: u64,
pub sh_addralign: u64,
pub name: String,
pub is_folded: bool,
pub folded_into: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct ICFMapping {
pub folded_section: usize,
pub canonical_section: usize,
pub has_thunk: bool,
pub thunk_offset: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ICFThunk {
pub original_name: String,
pub code: Vec<u8>,
pub section_name: String,
pub size: usize,
}
#[derive(Debug, Clone)]
pub struct COMDATGroup {
pub signature: String,
pub member_sections: Vec<usize>,
pub selection: u8,
pub total_size: u64,
pub content_hash: u64,
pub is_live: bool,
pub folded_into: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct GCRefEdge {
pub from: usize,
pub to: usize,
pub ref_kind: GCRefKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GCRefKind {
Relocation,
Symbol,
Implicit,
COMDATAssociation,
LinkOrder,
Unconditional,
}
#[derive(Debug, Clone)]
pub struct RelaxRecord {
pub section_index: usize,
pub offset: u64,
pub original_type: u32,
pub relax_kind: RelaxKind,
pub symbol_name: Option<String>,
pub addend: i64,
pub applied: bool,
}
#[derive(Debug, Clone, Default)]
pub struct RelaxationStats {
pub gotpcrelx_mov_to_lea: usize,
pub gotpcrelx_to_pc32: usize,
pub call_to_jmp: usize,
pub tls_gd_to_ie: usize,
pub tls_gd_to_le: usize,
pub tls_ie_to_le: usize,
pub tls_ld_to_le: usize,
pub branch_relax: usize,
pub total: usize,
}
#[derive(Debug, Clone)]
pub struct MergeEntry {
pub source_indices: Vec<usize>,
pub output_data: Vec<u8>,
pub output_name: String,
pub bytes_saved: u64,
}
#[derive(Debug, Clone)]
pub struct StringMergeEntry {
pub data: Vec<u8>,
pub source_indices: Vec<usize>,
pub output_offset: u64,
pub occurrence_count: usize,
}
#[derive(Debug, Clone)]
pub struct ConstantMergeEntry {
pub data: Vec<u8>,
pub hash: u64,
pub source_indices: Vec<usize>,
pub alignment: u64,
}
#[derive(Debug, Clone)]
pub struct CallGraphNode {
pub section_index: usize,
pub name: String,
pub callees: Vec<usize>,
pub callers: Vec<usize>,
pub frequency: f64,
pub size: u64,
pub placed: bool,
}
#[derive(Debug, Clone)]
pub struct SymbolOrderEntry {
pub name: String,
pub object_file: Option<String>,
pub priority: u64,
}
#[derive(Debug, Clone, Default)]
pub struct LTOOptStats {
pub functions_internalized: usize,
pub globals_eliminated: usize,
pub constant_propagations: usize,
pub devirtualized_calls: usize,
pub functions_merged: usize,
pub dead_functions_eliminated: usize,
pub size_reduction_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct X86OptimizerDiagnostic {
pub level: X86OptDiagLevel,
pub message: String,
pub detail: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86OptDiagLevel {
Info,
Warning,
Error,
Debug,
}
pub struct X86LLDOpt {
pub icf: X86ICF,
pub lto_opts: X86LTOOptimizations,
pub gc_sections: X86GCSections,
pub comdat_folding: X86COMDATFolding,
pub merge_sections: X86MergeSimilarSections,
pub relaxation: X86RelaxationOptimization,
pub ordering: X86LinkerOrdering,
pub enable_icf: bool,
pub enable_gc_sections: bool,
pub enable_comdat_folding: bool,
pub enable_merge_sections: bool,
pub enable_relaxation: bool,
pub enable_ordering: bool,
pub enable_lto_opts: bool,
pub icf_safety: ICFSafetyLevel,
pub gc_print_mode: GCPrintMode,
pub comdat_strategy: COMDATSelectionStrategy,
pub internalization: InternalizationStrategy,
pub ordering_heuristic: OrderingHeuristic,
pub diagnostics: Vec<X86OptimizerDiagnostic>,
pub total_bytes_saved: u64,
pub total_sections_removed: usize,
}
impl X86LLDOpt {
pub fn new() -> Self {
Self {
icf: X86ICF::new(),
lto_opts: X86LTOOptimizations::new(),
gc_sections: X86GCSections::new(),
comdat_folding: X86COMDATFolding::new(),
merge_sections: X86MergeSimilarSections::new(),
relaxation: X86RelaxationOptimization::new(),
ordering: X86LinkerOrdering::new(),
enable_icf: false,
enable_gc_sections: false,
enable_comdat_folding: false,
enable_merge_sections: false,
enable_relaxation: true,
enable_ordering: false,
enable_lto_opts: false,
icf_safety: ICFSafetyLevel::Safe,
gc_print_mode: GCPrintMode::Silent,
comdat_strategy: COMDATSelectionStrategy::Largest,
internalization: InternalizationStrategy::Conservative,
ordering_heuristic: OrderingHeuristic::None,
diagnostics: Vec::new(),
total_bytes_saved: 0,
total_sections_removed: 0,
}
}
pub fn new_aggressive() -> Self {
Self {
enable_icf: true,
enable_gc_sections: true,
enable_comdat_folding: true,
enable_merge_sections: true,
enable_relaxation: true,
enable_ordering: true,
enable_lto_opts: true,
icf_safety: ICFSafetyLevel::All,
comdat_strategy: COMDATSelectionStrategy::Largest,
internalization: InternalizationStrategy::All,
ordering_heuristic: OrderingHeuristic::CallGraph,
..Self::new()
}
}
pub fn new_safe() -> Self {
Self {
enable_icf: true,
enable_gc_sections: true,
enable_comdat_folding: true,
enable_merge_sections: true,
enable_relaxation: true,
icf_safety: ICFSafetyLevel::Safe,
..Self::new()
}
}
pub fn new_size_only() -> Self {
Self {
enable_icf: true,
enable_comdat_folding: true,
enable_merge_sections: true,
icf_safety: ICFSafetyLevel::All,
..Self::new()
}
}
pub fn diag(&mut self, level: X86OptDiagLevel, message: String, detail: Option<String>) {
self.diagnostics.push(X86OptimizerDiagnostic {
level,
message,
detail,
});
}
pub fn run_all(
&mut self,
input_sections: &mut [X86InputSection],
entry_symbols: &[String],
exported_symbols: &HashSet<String>,
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
symbol_definitions: &HashMap<usize, Vec<String>>,
) -> X86LLDOptSummary {
self.diagnostics.clear();
self.total_bytes_saved = 0;
self.total_sections_removed = 0;
let initial_size: u64 = input_sections.iter().map(|s| s.data.len() as u64).sum();
let initial_count = input_sections.iter().filter(|s| !s.data.is_empty()).count();
if self.enable_comdat_folding {
let removed = self.comdat_folding.run(input_sections);
self.total_sections_removed += removed;
if self.gc_print_mode == GCPrintMode::Verbose {
self.diag(
X86OptDiagLevel::Debug,
format!("COMDAT folding: {} groups folded", removed),
None,
);
}
}
if self.enable_icf {
let (removed, saved) =
self.icf
.run(input_sections, symbol_definitions, self.icf_safety);
self.total_sections_removed += removed;
self.total_bytes_saved += saved;
if self.gc_print_mode == GCPrintMode::Verbose {
self.diag(
X86OptDiagLevel::Debug,
format!(
"ICF: {} sections folded, {} bytes saved, {} thunks",
removed,
saved,
self.icf.thunks.len()
),
None,
);
}
}
if self.enable_gc_sections {
let (removed, names) = self.gc_sections.run(
input_sections,
entry_symbols,
exported_symbols,
relocation_map,
symbol_definitions,
);
self.total_sections_removed += removed;
match self.gc_print_mode {
GCPrintMode::PrintRemoved | GCPrintMode::Verbose => {
for name in &names {
self.diag(
X86OptDiagLevel::Info,
format!("removing unused section '{}'", name),
None,
);
}
}
GCPrintMode::Silent => {}
}
}
if self.enable_merge_sections {
let saved = self.merge_sections.run(input_sections);
self.total_bytes_saved += saved;
if self.gc_print_mode == GCPrintMode::Verbose {
self.diag(
X86OptDiagLevel::Debug,
format!("Section merging: {} bytes saved", saved),
None,
);
}
}
if self.enable_relaxation {
let stats = self.relaxation.run(input_sections, relocation_map);
if self.gc_print_mode == GCPrintMode::Verbose {
self.diag(
X86OptDiagLevel::Debug,
format!(
"Relaxation: {} total ({} mov→lea, {} call→jmp, {} TLS)",
stats.total,
stats.gotpcrelx_mov_to_lea,
stats.call_to_jmp,
stats.tls_gd_to_le + stats.tls_ie_to_le + stats.tls_ld_to_le
),
None,
);
}
}
if self.enable_lto_opts {
let lto_stats = self.lto_opts.run(
input_sections,
exported_symbols,
relocation_map,
symbol_definitions,
self.internalization,
);
self.total_bytes_saved += lto_stats.size_reduction_bytes;
self.total_sections_removed += lto_stats.dead_functions_eliminated;
if self.gc_print_mode == GCPrintMode::Verbose {
self.diag(
X86OptDiagLevel::Debug,
format!(
"LTO: {} merged, {} dead, {} devirt, {} bytes saved",
lto_stats.functions_merged,
lto_stats.dead_functions_eliminated,
lto_stats.devirtualized_calls,
lto_stats.size_reduction_bytes
),
None,
);
}
}
if self.enable_ordering {
self.ordering
.run(input_sections, relocation_map, self.ordering_heuristic);
if self.ordering.is_ordered() && self.gc_print_mode == GCPrintMode::Verbose {
self.diag(
X86OptDiagLevel::Debug,
format!(
"Ordering: sections reordered (distance: {})",
self.ordering.total_call_distance()
),
None,
);
}
}
let final_size: u64 = input_sections.iter().map(|s| s.data.len() as u64).sum();
let final_count = input_sections.iter().filter(|s| !s.data.is_empty()).count();
self.total_bytes_saved += initial_size.saturating_sub(final_size);
X86LLDOptSummary {
sections_before: initial_count,
sections_after: final_count,
sections_removed: initial_count - final_count,
bytes_before: initial_size,
bytes_after: final_size,
bytes_saved: initial_size.saturating_sub(final_size),
diagnostics_count: self.diagnostics.len(),
}
}
pub fn estimate_savings(
&self,
sections: &[X86InputSection],
symbol_definitions: &HashMap<usize, Vec<String>>,
) -> u64 {
let mut est: u64 = 0;
if self.enable_icf {
let mut hash_groups: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
for (i, s) in sections.iter().enumerate() {
if s.data.len() >= self.icf.min_size
&& (s.sh_flags & SHF_EXECINSTR) != 0
&& !s.data.is_empty()
{
let (h1, h2) = double_hash(&s.data);
hash_groups.entry((h1, h2)).or_default().push(i);
}
}
for group in hash_groups.values() {
if group.len() >= 2 {
let canonical = group[0];
for &other in &group[1..] {
if sections[canonical].data == sections[other].data {
est += sections[other].data.len() as u64;
}
}
}
}
}
if self.enable_merge_sections {
let mut seen: HashMap<Vec<u8>, usize> = HashMap::new();
for s in sections {
if !s.data.is_empty()
&& (s.sh_flags & (SHF_MERGE | SHF_STRINGS)) == (SHF_MERGE | SHF_STRINGS)
{
let mut offset = 0;
while offset < s.data.len() {
let end = s.data[offset..]
.iter()
.position(|&b| b == 0)
.map(|p| offset + p)
.unwrap_or(s.data.len());
let str_data = &s.data[offset..end];
if !str_data.is_empty() {
let count = seen.entry(str_data.to_vec()).or_default();
if *count > 0 {
est += str_data.len() as u64;
}
*count += 1;
}
offset = end + 1;
}
}
}
}
est
}
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.level == X86OptDiagLevel::Error)
}
pub fn has_warnings(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.level == X86OptDiagLevel::Warning)
}
pub fn get_diagnostics(&self) -> &[X86OptimizerDiagnostic] {
&self.diagnostics
}
pub fn clear_diagnostics(&mut self) {
self.diagnostics.clear();
}
}
impl Default for X86LLDOpt {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86LLDOpt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86LLDOpt")
.field("enable_icf", &self.enable_icf)
.field("enable_gc_sections", &self.enable_gc_sections)
.field("enable_comdat_folding", &self.enable_comdat_folding)
.field("enable_merge_sections", &self.enable_merge_sections)
.field("enable_relaxation", &self.enable_relaxation)
.field("enable_ordering", &self.enable_ordering)
.field("enable_lto_opts", &self.enable_lto_opts)
.field("icf_safety", &self.icf_safety)
.field("total_bytes_saved", &self.total_bytes_saved)
.field("total_sections_removed", &self.total_sections_removed)
.field("diagnostics", &self.diagnostics.len())
.finish()
}
}
#[derive(Debug, Clone)]
pub struct X86LLDOptSummary {
pub sections_before: usize,
pub sections_after: usize,
pub sections_removed: usize,
pub bytes_before: u64,
pub bytes_after: u64,
pub bytes_saved: u64,
pub diagnostics_count: usize,
}
impl X86LLDOptSummary {
pub fn reduction_ratio(&self) -> f64 {
if self.bytes_before == 0 {
0.0
} else {
self.bytes_saved as f64 / self.bytes_before as f64
}
}
pub fn section_reduction_ratio(&self) -> f64 {
if self.sections_before == 0 {
0.0
} else {
self.sections_removed as f64 / self.sections_before as f64
}
}
pub fn had_effect(&self) -> bool {
self.sections_removed > 0 || self.bytes_saved > 0
}
pub fn format_summary(&self) -> String {
format!(
"{} → {} sections ({} removed), {} → {} bytes ({} saved, {:.1}%)",
self.sections_before,
self.sections_after,
self.sections_removed,
self.bytes_before,
self.bytes_after,
self.bytes_saved,
self.reduction_ratio() * 100.0
)
}
}
pub struct X86ICF {
pub min_size: usize,
pub fold_rodata: bool,
pub functions_only: bool,
pub fold_mapping: Vec<ICFMapping>,
pub thunks: Vec<ICFThunk>,
pub address_taken: HashSet<usize>,
pub iterations: usize,
pub use_double_hash: bool,
pub hash_reloc_aware: bool,
}
impl X86ICF {
pub fn new() -> Self {
Self {
min_size: ICF_MIN_FUNCTION_SIZE,
fold_rodata: false,
functions_only: true,
fold_mapping: Vec::new(),
thunks: Vec::new(),
address_taken: HashSet::new(),
iterations: 0,
use_double_hash: true,
hash_reloc_aware: false,
}
}
pub fn with_min_size(mut self, size: usize) -> Self {
self.min_size = size;
self
}
pub fn with_fold_rodata(mut self, enable: bool) -> Self {
self.fold_rodata = enable;
self
}
pub fn with_functions_only(mut self, enable: bool) -> Self {
self.functions_only = enable;
self
}
pub fn with_double_hash(mut self, enable: bool) -> Self {
self.use_double_hash = enable;
self
}
pub fn mark_address_taken(&mut self, section_index: usize) {
self.address_taken.insert(section_index);
}
pub fn mark_addresses_taken(&mut self, indices: &[usize]) {
for &idx in indices {
self.address_taken.insert(idx);
}
}
pub fn is_address_taken(&self, section_index: usize) -> bool {
self.address_taken.contains(§ion_index)
}
pub fn run(
&mut self,
sections: &mut [X86InputSection],
symbol_definitions: &HashMap<usize, Vec<String>>,
safety: ICFSafetyLevel,
) -> (usize, u64) {
if matches!(safety, ICFSafetyLevel::Disabled) {
return (0, 0);
}
self.fold_mapping.clear();
self.thunks.clear();
self.iterations = 0;
let mut total_removed = 0;
let mut total_saved = 0u64;
for iter in 0..ICF_MAX_ITERATIONS {
self.iterations = iter + 1;
let (removed, saved) = self.run_single_iteration(sections, safety);
total_removed += removed;
total_saved += saved;
if removed == 0 {
break;
}
}
(total_removed, total_saved)
}
fn run_single_iteration(
&mut self,
sections: &mut [X86InputSection],
safety: ICFSafetyLevel,
) -> (usize, u64) {
let mut removed = 0;
let mut bytes_saved = 0u64;
let mut hash_groups: HashMap<u64, Vec<usize>> = HashMap::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty() || section.data.len() < self.min_size {
continue;
}
if self.functions_only
&& (section.sh_flags & SHF_EXECINSTR) == 0
&& !(self.fold_rodata && (section.sh_flags & (SHF_ALLOC | SHF_WRITE)) == SHF_ALLOC)
{
continue;
}
let h = fnv1a_hash_64(§ion.data);
hash_groups.entry(h).or_default().push(i);
}
for indices in hash_groups.values() {
if indices.len() < 2 {
continue;
}
let n = indices.len();
let mut folded = vec![false; n];
for i in 0..n {
if folded[i] {
continue;
}
let idx_i = indices[i];
let t_i = matches!(safety, ICFSafetyLevel::Safe) && self.is_address_taken(idx_i);
for j in (i + 1)..n {
if folded[j] {
continue;
}
let idx_j = indices[j];
let t_j =
matches!(safety, ICFSafetyLevel::Safe) && self.is_address_taken(idx_j);
if (safety == ICFSafetyLevel::Safe) && (t_i || t_j) {
continue;
}
let sec_i = §ions[idx_i];
let sec_j = §ions[idx_j];
if sec_i.data == sec_j.data && sec_i.sh_flags == sec_j.sh_flags {
folded[j] = true;
bytes_saved += sec_j.data.len() as u64;
sections[idx_j].data.clear();
let thunk = self.emit_thunk(&sec_j.name, &sec_i.name);
self.thunks.push(thunk);
self.fold_mapping.push(ICFMapping {
folded_section: idx_j,
canonical_section: idx_i,
has_thunk: true,
thunk_offset: None,
});
removed += 1;
}
}
}
}
(removed, bytes_saved)
}
fn emit_thunk(&self, folded_name: &str, canonical_name: &str) -> ICFThunk {
let code = vec![OPCODE_JMP_NEAR, 0x00, 0x00, 0x00, 0x00];
ICFThunk {
original_name: folded_name.to_string(),
code: code.clone(),
section_name: format!(".text.icf_thunk.{}", folded_name),
size: code.len(),
}
}
pub fn hash_section_ignore_relocs(
&self,
data: &[u8],
reloc_offsets: &HashSet<usize>,
reloc_sizes: &HashMap<usize, usize>,
) -> u64 {
fnv1a_hash_skip_relocs(data, reloc_offsets, reloc_sizes)
}
pub fn hash_instructions(&self, data: &[u8], reloc_offsets: &HashSet<usize>) -> u64 {
let mut hash: u64 = FNV_OFFSET_BASIS;
let mut i = 0;
while i < data.len() {
if reloc_offsets.contains(&i) {
let skip = 4usize.min(data.len() - i);
for _ in 0..skip {
hash ^= 0u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
i += skip;
} else {
hash ^= data[i] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
}
hash
}
pub fn get_fold_mapping(&self) -> &[ICFMapping] {
&self.fold_mapping
}
pub fn get_thunks(&self) -> &[ICFThunk] {
&self.thunks
}
pub fn fold_count(&self) -> usize {
self.fold_mapping.len()
}
pub fn debug_info_mapping(&self) -> HashMap<usize, usize> {
self.fold_mapping
.iter()
.map(|m| (m.folded_section, m.canonical_section))
.collect()
}
pub fn print_icf_summary(&self) -> String {
format!(
"ICF: {} sections folded, {} thunks generated ({} iterations)",
self.fold_mapping.len(),
self.thunks.len(),
self.iterations
)
}
}
impl Default for X86ICF {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86ICF {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86ICF")
.field("min_size", &self.min_size)
.field("fold_rodata", &self.fold_rodata)
.field("functions_only", &self.functions_only)
.field("fold_count", &self.fold_mapping.len())
.field("thunks", &self.thunks.len())
.field("address_taken", &self.address_taken.len())
.field("iterations", &self.iterations)
.finish()
}
}
pub struct X86LTOOptimizations {
pub const_prop: bool,
pub dead_code_elim: bool,
pub func_merging: bool,
pub devirtualization: bool,
pub internalization: bool,
pub eliminate_globals: bool,
pub merge_constants: bool,
pub stats: LTOOptStats,
}
impl X86LTOOptimizations {
pub fn new() -> Self {
Self {
const_prop: true,
dead_code_elim: true,
func_merging: true,
devirtualization: true,
internalization: false,
eliminate_globals: true,
merge_constants: true,
stats: LTOOptStats::default(),
}
}
pub fn run(
&mut self,
sections: &mut [X86InputSection],
exported_symbols: &HashSet<String>,
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
symbol_definitions: &HashMap<usize, Vec<String>>,
strategy: InternalizationStrategy,
) -> LTOOptStats {
self.stats = LTOOptStats::default();
if self.internalization && !matches!(strategy, InternalizationStrategy::None_) {
self.do_internalize(sections, exported_symbols, symbol_definitions, strategy);
}
if self.const_prop {
self.do_const_prop(sections, relocation_map);
}
if self.devirtualization {
self.do_devirt(sections, relocation_map, symbol_definitions);
}
if self.func_merging {
self.do_merge(sections);
}
if self.dead_code_elim {
self.do_dce(sections, relocation_map, symbol_definitions);
}
if self.eliminate_globals {
self.do_elim_globals(sections, symbol_definitions, exported_symbols);
}
if self.merge_constants {
self.do_merge_constants(sections);
}
self.stats.clone()
}
fn do_internalize(
&mut self,
_sections: &mut [X86InputSection],
exported_symbols: &HashSet<String>,
symbol_definitions: &HashMap<usize, Vec<String>>,
strategy: InternalizationStrategy,
) {
for symbols in symbol_definitions.values() {
let can_internalize = match strategy {
InternalizationStrategy::All => {
symbols.iter().all(|s| !exported_symbols.contains(s))
}
InternalizationStrategy::Conservative => symbols.iter().all(|s| {
!exported_symbols.contains(s)
&& !s.starts_with("__attribute__")
&& !s.starts_with("_init")
&& !s.starts_with("_fini")
}),
InternalizationStrategy::None_ => false,
};
if can_internalize {
self.stats.functions_internalized += symbols.len();
}
}
}
fn do_const_prop(
&mut self,
sections: &[X86InputSection],
_relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
) {
let mut constants: HashMap<String, u64> = HashMap::new();
for section in sections {
if section.data.is_empty() {
continue;
}
if section.name.starts_with(".rodata.")
&& section.data.len() <= 8
&& (section.sh_flags & SHF_MERGE) != 0
{
let mut val: u64 = 0;
for (j, &b) in section.data.iter().enumerate() {
val |= (b as u64) << (j * 8);
}
constants.insert(section.name.clone(), val);
}
}
self.stats.constant_propagations += constants.len();
}
fn do_devirt(
&mut self,
sections: &[X86InputSection],
_relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
_symbol_definitions: &HashMap<usize, Vec<String>>,
) {
for section in sections {
if section.data.is_empty() || (section.sh_flags & SHF_EXECINSTR) == 0 {
continue;
}
let data = §ion.data;
let mut offset = 0;
while offset + 2 <= data.len() {
if data[offset] == 0xFF {
let modrm = data.get(offset + 1).copied().unwrap_or(0);
let reg = (modrm >> 3) & 0x7;
if reg == 2 {
self.stats.devirtualized_calls += 1;
}
}
offset += 1;
}
}
}
fn do_merge(&mut self, sections: &mut [X86InputSection]) {
let mut hash_to_indices: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty()
|| section.data.len() < ICF_MIN_FUNCTION_SIZE
|| (section.sh_flags & SHF_EXECINSTR) == 0
{
continue;
}
hash_to_indices
.entry(double_hash(§ion.data))
.or_default()
.push(i);
}
for group in hash_to_indices.values() {
if group.len() < 2 {
continue;
}
let canonical = group[0];
for &other in &group[1..] {
if sections[canonical].data == sections[other].data
&& sections[canonical].sh_flags == sections[other].sh_flags
{
self.stats.functions_merged += 1;
self.stats.size_reduction_bytes += sections[other].data.len() as u64;
sections[other].data.clear();
}
}
}
}
fn do_dce(
&mut self,
sections: &mut [X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
_symbol_definitions: &HashMap<usize, Vec<String>>,
) {
let n = sections.len();
let mut reachable = vec![false; n];
let mut queue = VecDeque::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty() {
continue;
}
let name = §ion.name;
if name.starts_with(".text._start")
|| name.starts_with(".text.main")
|| name.starts_with(".init")
|| name.starts_with(".fini")
|| name.starts_with(".text.startup")
{
reachable[i] = true;
queue.push_back(i);
}
}
while let Some(cur) = queue.pop_front() {
if let Some(relocs) = relocation_map.get(&cur) {
for reloc in relocs {
let t = reloc.section_index as usize;
if t < n && !reachable[t] {
reachable[t] = true;
queue.push_back(t);
}
}
}
}
for (i, section) in sections.iter_mut().enumerate() {
if !section.data.is_empty() && !reachable[i] {
self.stats.dead_functions_eliminated += 1;
self.stats.size_reduction_bytes += section.data.len() as u64;
section.data.clear();
}
}
}
fn do_elim_globals(
&mut self,
sections: &mut [X86InputSection],
symbol_definitions: &HashMap<usize, Vec<String>>,
exported_symbols: &HashSet<String>,
) {
for (i, section) in sections.iter_mut().enumerate() {
if section.data.is_empty() || (section.sh_flags & SHF_EXECINSTR) != 0 {
continue;
}
if let Some(syms) = symbol_definitions.get(&i) {
if syms.iter().any(|s| exported_symbols.contains(s)) {
continue;
}
}
if (section.sh_flags & SHF_WRITE) == 0 && (section.sh_flags & SHF_ALLOC) != 0 {
self.stats.globals_eliminated += 1;
self.stats.size_reduction_bytes += section.data.len() as u64;
section.data.clear();
}
}
}
fn do_merge_constants(&mut self, sections: &mut [X86InputSection]) {
let mut hash_to_idx: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty()
|| (section.sh_flags & SHF_ALLOC) == 0
|| section.data.len() < 8
{
continue;
}
hash_to_idx
.entry(double_hash(§ion.data))
.or_default()
.push(i);
}
for group in hash_to_idx.values() {
if group.len() < 2 {
continue;
}
let c = group[0];
for &o in &group[1..] {
if sections[c].data == sections[o].data {
self.stats.size_reduction_bytes += sections[o].data.len() as u64;
sections[o].data.clear();
}
}
}
}
pub fn get_stats(&self) -> <OOptStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = LTOOptStats::default();
}
pub fn print_stats(&self) -> String {
format!(
"LTO: {} internalized, {} merged, {} dead, {} devirt, {} globals elim, {}B saved",
self.stats.functions_internalized,
self.stats.functions_merged,
self.stats.dead_functions_eliminated,
self.stats.devirtualized_calls,
self.stats.globals_eliminated,
self.stats.size_reduction_bytes
)
}
}
impl Default for X86LTOOptimizations {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86LTOOptimizations {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86LTOOptimizations")
.field("const_prop", &self.const_prop)
.field("dead_code_elim", &self.dead_code_elim)
.field("func_merging", &self.func_merging)
.field("devirtualization", &self.devirtualization)
.field("internalization", &self.internalization)
.field("stats", &self.stats)
.finish()
}
}
pub struct X86GCSections {
pub keep_patterns: Vec<String>,
pub keep_eh: bool,
pub keep_debug: bool,
pub keep_init_fini: bool,
pub print_mode: GCPrintMode,
pub reference_graph: Vec<Vec<usize>>,
pub live_flags: Vec<bool>,
pub removed_sections: Vec<String>,
pub comdat_groups: Vec<COMDATGroup>,
pub unconditional_keep: HashSet<String>,
}
impl X86GCSections {
pub fn new() -> Self {
Self {
keep_patterns: GC_ROOT_PATTERNS.iter().map(|s| s.to_string()).collect(),
keep_eh: false,
keep_debug: false,
keep_init_fini: true,
print_mode: GCPrintMode::Silent,
reference_graph: Vec::new(),
live_flags: Vec::new(),
removed_sections: Vec::new(),
comdat_groups: Vec::new(),
unconditional_keep: GC_UNCONDITIONAL_KEEP
.iter()
.map(|s| s.to_string())
.collect(),
}
}
pub fn with_print_mode(mut self, mode: GCPrintMode) -> Self {
self.print_mode = mode;
self
}
pub fn with_keep_eh(mut self, keep: bool) -> Self {
self.keep_eh = keep;
self
}
pub fn add_comdat_group(&mut self, group: COMDATGroup) {
self.comdat_groups.push(group);
}
pub fn run(
&mut self,
sections: &mut [X86InputSection],
entry_symbols: &[String],
exported_symbols: &HashSet<String>,
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
symbol_definitions: &HashMap<usize, Vec<String>>,
) -> (usize, Vec<String>) {
let n = sections.len();
if n == 0 {
return (0, Vec::new());
}
self.removed_sections.clear();
self.reference_graph =
self.build_reference_graph(sections, relocation_map, symbol_definitions);
let roots = self.find_roots(
sections,
entry_symbols,
exported_symbols,
symbol_definitions,
);
self.live_flags = self.mark_live_from_roots(&self.reference_graph, &roots, n);
self.propagate_comdat_liveness(sections);
self.handle_link_order(sections, relocation_map);
let removed = self.sweep_dead(sections);
(removed, self.removed_sections.clone())
}
fn build_reference_graph(
&self,
sections: &[X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
symbol_definitions: &HashMap<usize, Vec<String>>,
) -> Vec<Vec<usize>> {
let n = sections.len();
let mut graph = vec![Vec::new(); n];
let mut name_to_idx: HashMap<String, usize> = HashMap::new();
for (i, section) in sections.iter().enumerate() {
if let Some(syms) = symbol_definitions.get(&i) {
for sym in syms {
name_to_idx.insert(sym.clone(), i);
}
}
name_to_idx.insert(section.name.clone(), i);
}
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty() {
continue;
}
if let Some(relocs) = relocation_map.get(&i) {
for reloc in relocs {
let t = reloc.section_index as usize;
if t < n && t != i && !graph[i].contains(&t) {
graph[i].push(t);
}
}
}
if section.name.starts_with(".text") {
for dep in &[".plt", ".plt.got", ".plt.sec", ".got", ".got.plt"] {
if let Some(&idx) = name_to_idx.get(dep) {
if !graph[i].contains(&idx) {
graph[i].push(idx);
}
}
}
}
if (section.sh_flags & SHF_LINK_ORDER) != 0 {
for (j, other) in sections.iter().enumerate() {
if i != j && other.name.starts_with(".text") {
if let Some(base) = section.name.rsplit('.').next() {
if other.name.contains(base) && !graph[i].contains(&j) {
graph[i].push(j);
}
}
}
}
}
}
graph
}
fn find_roots(
&self,
sections: &[X86InputSection],
entry_symbols: &[String],
exported_symbols: &HashSet<String>,
symbol_definitions: &HashMap<usize, Vec<String>>,
) -> Vec<usize> {
let mut roots = Vec::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty() {
continue;
}
let name = §ion.name;
if self.unconditional_keep.contains(name) {
roots.push(i);
continue;
}
if self.keep_patterns.iter().any(|p| {
if p.ends_with(".*") {
name.starts_with(&p[..p.len() - 2])
} else {
name.starts_with(p.as_str())
}
}) {
roots.push(i);
continue;
}
if self.keep_init_fini
&& (name.starts_with(".init_array")
|| name.starts_with(".fini_array")
|| name.starts_with(".preinit_array")
|| name == ".init"
|| name == ".fini"
|| name.starts_with(".ctors")
|| name.starts_with(".dtors"))
{
roots.push(i);
continue;
}
if self.keep_eh
&& (name.starts_with(".eh_frame")
|| name.starts_with(".eh_frame_hdr")
|| name == ".gcc_except_table")
{
roots.push(i);
continue;
}
if self.keep_debug && (name.starts_with(".debug_") || name.starts_with(".zdebug_")) {
roots.push(i);
continue;
}
if let Some(syms) = symbol_definitions.get(&i) {
if syms
.iter()
.any(|s| entry_symbols.contains(s) || exported_symbols.contains(s))
{
roots.push(i);
continue;
}
}
for entry in entry_symbols {
if name.contains(entry.as_str()) {
roots.push(i);
break;
}
}
}
if roots.is_empty() {
for (i, section) in sections.iter().enumerate() {
if section.name.contains("_start") || section.name.contains("main") {
roots.push(i);
}
}
}
roots
}
fn mark_live_from_roots(&self, graph: &[Vec<usize>], roots: &[usize], n: usize) -> Vec<bool> {
let mut live = vec![false; n];
let mut queue = VecDeque::new();
for &r in roots {
if r < n {
live[r] = true;
queue.push_back(r);
}
}
while let Some(cur) = queue.pop_front() {
for &dep in &graph[cur] {
if dep < n && !live[dep] {
live[dep] = true;
queue.push_back(dep);
}
}
}
live
}
fn propagate_comdat_liveness(&mut self, _sections: &[X86InputSection]) {
for group in &self.comdat_groups {
let any_live = group
.member_sections
.iter()
.any(|&idx| idx < self.live_flags.len() && self.live_flags[idx]);
if any_live {
for &idx in &group.member_sections {
if idx < self.live_flags.len() {
self.live_flags[idx] = true;
}
}
}
}
}
fn handle_link_order(
&mut self,
sections: &[X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
) {
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty() || (section.sh_flags & SHF_LINK_ORDER) == 0 {
continue;
}
if let Some(relocs) = relocation_map.get(&i) {
let all_dead = relocs.iter().all(|r| {
let t = r.section_index as usize;
t >= self.live_flags.len() || !self.live_flags[t]
});
if all_dead && i < self.live_flags.len() {
self.live_flags[i] = false;
}
}
}
}
fn sweep_dead(&mut self, sections: &mut [X86InputSection]) -> usize {
let mut removed = 0;
for (i, section) in sections.iter_mut().enumerate() {
if i < self.live_flags.len() && !self.live_flags[i] && !section.data.is_empty() {
if matches!(
self.print_mode,
GCPrintMode::PrintRemoved | GCPrintMode::Verbose
) {
self.removed_sections.push(section.name.clone());
}
section.data.clear();
removed += 1;
}
}
removed
}
pub fn is_live(&self, section_index: usize) -> bool {
section_index < self.live_flags.len() && self.live_flags[section_index]
}
pub fn get_live_flags(&self) -> &[bool] {
&self.live_flags
}
pub fn get_removed_sections(&self) -> &[String] {
&self.removed_sections
}
pub fn live_count(&self) -> usize {
self.live_flags.iter().filter(|&&l| l).count()
}
}
impl Default for X86GCSections {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86GCSections {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86GCSections")
.field("keep_patterns", &self.keep_patterns.len())
.field("keep_eh", &self.keep_eh)
.field("keep_debug", &self.keep_debug)
.field("graph_nodes", &self.reference_graph.len())
.field("live_count", &self.live_count())
.field("removed", &self.removed_sections.len())
.finish()
}
}
pub struct X86COMDATFolding {
pub strategy: COMDATSelectionStrategy,
pub groups: Vec<COMDATGroup>,
pub fold_mapping: Vec<(usize, usize)>,
pub verbose: bool,
}
impl X86COMDATFolding {
pub fn new() -> Self {
Self {
strategy: COMDATSelectionStrategy::Largest,
groups: Vec::new(),
fold_mapping: Vec::new(),
verbose: false,
}
}
pub fn with_strategy(mut self, s: COMDATSelectionStrategy) -> Self {
self.strategy = s;
self
}
pub fn detect_groups(&mut self, sections: &[X86InputSection]) {
self.groups.clear();
let mut group_idxs: Vec<usize> = Vec::new();
for (i, s) in sections.iter().enumerate() {
if s.sh_type == SHT_GROUP && (s.sh_flags & SHF_GROUP) != 0 {
group_idxs.push(i);
}
}
let mut sig_members: HashMap<String, Vec<usize>> = HashMap::new();
let mut sig_sel: HashMap<String, u8> = HashMap::new();
for &gi in &group_idxs {
let grp = §ions[gi];
let sig = extract_comdat_group_name(&grp.name).unwrap_or_else(|| grp.name.clone());
let sel = if grp.data.len() >= 5 {
if (u32::from_le_bytes([grp.data[0], grp.data[1], grp.data[2], grp.data[3]])
& GRP_COMDAT)
!= 0
{
grp.data[4]
} else {
COMDAT_SELECTION_ANY
}
} else {
COMDAT_SELECTION_ANY
};
sig_sel.insert(sig.clone(), sel);
let members: Vec<usize> = sections
.iter()
.enumerate()
.filter(|(j, s)| {
*j != gi
&& (s.sh_flags & SHF_GROUP) != 0
&& extract_comdat_group_name(&s.name).as_deref() == Some(&sig)
})
.map(|(j, _)| j)
.collect();
sig_members.entry(sig).or_default().extend(members);
}
for (sig, members) in &sig_members {
if members.is_empty() {
continue;
}
let total_size: u64 = members.iter().map(|&i| sections[i].data.len() as u64).sum();
let mut combined = Vec::new();
for &i in members {
combined.extend_from_slice(§ions[i].data);
}
let hash = fnv1a_hash_64(&combined);
self.groups.push(COMDATGroup {
signature: sig.clone(),
member_sections: members.clone(),
selection: sig_sel.get(sig).copied().unwrap_or(COMDAT_SELECTION_ANY),
total_size,
content_hash: hash,
is_live: true,
folded_into: None,
});
}
}
pub fn run(&mut self, sections: &mut [X86InputSection]) -> usize {
self.detect_groups(sections);
self.fold_mapping.clear();
if self.groups.is_empty() {
return 0;
}
let mut hash_groups: HashMap<u64, Vec<usize>> = HashMap::new();
for (gi, g) in self.groups.iter().enumerate() {
hash_groups.entry(g.content_hash).or_default().push(gi);
}
let mut total = 0;
for indices in hash_groups.values() {
if indices.len() < 2 {
continue;
}
let canonical = match self.strategy {
COMDATSelectionStrategy::First => indices[0],
COMDATSelectionStrategy::Largest => *indices
.iter()
.max_by_key(|&&gi| self.groups[gi].total_size)
.unwrap_or(&indices[0]),
COMDATSelectionStrategy::HashOrder => {
let mut sorted = indices.clone();
sorted.sort_by_key(|&gi| &self.groups[gi].signature);
sorted[0]
}
COMDATSelectionStrategy::ExactMatch => indices[0],
};
for &gi in indices {
if gi == canonical {
continue;
}
self.groups[gi].is_live = false;
self.groups[gi].folded_into = Some(canonical);
self.fold_mapping.push((gi, canonical));
for &mi in &self.groups[gi].member_sections {
if mi < sections.len() {
sections[mi].data.clear();
}
}
total += 1;
}
}
total
}
pub fn get_fold_mapping(&self) -> &[(usize, usize)] {
&self.fold_mapping
}
pub fn is_group_live(&self, gi: usize) -> bool {
gi < self.groups.len() && self.groups[gi].is_live
}
pub fn group_count(&self) -> usize {
self.groups.len()
}
pub fn fold_count(&self) -> usize {
self.fold_mapping.len()
}
}
impl Default for X86COMDATFolding {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86COMDATFolding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86COMDATFolding")
.field("strategy", &self.strategy)
.field("groups", &self.groups.len())
.field("folded", &self.fold_mapping.len())
.finish()
}
}
pub struct X86MergeSimilarSections {
pub merge_strings: bool,
pub merge_constants: bool,
pub tail_merge: bool,
pub tail_merge_min_length: usize,
pub string_table: HashMap<u64, StringMergeEntry>,
pub constant_table: HashMap<u64, ConstantMergeEntry>,
pub bytes_saved: u64,
}
impl X86MergeSimilarSections {
pub fn new() -> Self {
Self {
merge_strings: true,
merge_constants: true,
tail_merge: true,
tail_merge_min_length: 4,
string_table: HashMap::new(),
constant_table: HashMap::new(),
bytes_saved: 0,
}
}
pub fn run(&mut self, sections: &mut [X86InputSection]) -> u64 {
self.bytes_saved = 0;
if self.merge_strings {
self.merge_string_sections(sections);
}
if self.merge_constants {
self.merge_constant_sections(sections);
}
if self.tail_merge {
self.tail_merge_sections(sections);
}
self.bytes_saved
}
fn merge_string_sections(&mut self, sections: &mut [X86InputSection]) {
self.string_table.clear();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty()
|| (section.sh_flags & (SHF_MERGE | SHF_STRINGS)) != (SHF_MERGE | SHF_STRINGS)
{
continue;
}
let mut offset = 0;
while offset < section.data.len() {
let end = section.data[offset..]
.iter()
.position(|&b| b == 0)
.map(|p| offset + p)
.unwrap_or(section.data.len());
let sd = section.data[offset..end].to_vec();
if !sd.is_empty() {
let h = fnv1a_hash_64(&sd);
let entry = self
.string_table
.entry(h)
.or_insert_with(|| StringMergeEntry {
data: sd.clone(),
source_indices: Vec::new(),
output_offset: 0,
occurrence_count: 0,
});
entry.source_indices.push(i);
entry.occurrence_count += 1;
if entry.occurrence_count > 1 {
self.bytes_saved += sd.len() as u64;
}
}
offset = end + 1;
if offset >= section.data.len() {
break;
}
}
}
}
fn merge_constant_sections(&mut self, sections: &mut [X86InputSection]) {
self.constant_table.clear();
let mut hash_idx: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty()
|| (section.sh_flags & (SHF_ALLOC | SHF_WRITE | SHF_EXECINSTR)) != SHF_ALLOC
|| section.data.len() < 8
{
continue;
}
let align = section.sh_addralign.max(1) as usize;
let mut off = 0;
while off + align <= section.data.len() {
let chunk = §ion.data[off..off + align];
hash_idx.entry(double_hash(chunk)).or_default().push(i);
off += align;
}
}
for ((h1, _), indices) in &hash_idx {
if indices.len() < 2 {
continue;
}
let c = indices[0];
let cd = §ions[c].data;
for &o in &indices[1..] {
if o < sections.len() && sections[o].data == *cd {
self.bytes_saved += sections[o].data.len() as u64;
sections[o].data.clear();
}
}
self.constant_table.insert(
*h1,
ConstantMergeEntry {
data: sections[c].data.clone(),
hash: *h1,
source_indices: indices.clone(),
alignment: sections[c].sh_addralign,
},
);
}
}
fn tail_merge_sections(&mut self, sections: &mut [X86InputSection]) {
let execs: Vec<usize> = sections
.iter()
.enumerate()
.filter(|(_, s)| !s.data.is_empty() && (s.sh_flags & SHF_EXECINSTR) != 0)
.map(|(i, _)| i)
.collect();
if execs.len() < 2 {
return;
}
for i in 0..execs.len() {
for j in (i + 1)..execs.len() {
let a = execs[i];
let b = execs[j];
if a >= sections.len() || b >= sections.len() {
continue;
}
if let Some(ov) = find_tail_overlap(
§ions[a].data,
§ions[b].data,
self.tail_merge_min_length,
) {
self.bytes_saved += ov as u64;
}
}
}
}
pub fn get_bytes_saved(&self) -> u64 {
self.bytes_saved
}
pub fn get_string_entries(&self) -> &HashMap<u64, StringMergeEntry> {
&self.string_table
}
pub fn get_constant_entries(&self) -> &HashMap<u64, ConstantMergeEntry> {
&self.constant_table
}
}
impl Default for X86MergeSimilarSections {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86MergeSimilarSections {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86MergeSimilarSections")
.field("merge_strings", &self.merge_strings)
.field("merge_constants", &self.merge_constants)
.field("tail_merge", &self.tail_merge)
.field("strings", &self.string_table.len())
.field("constants", &self.constant_table.len())
.field("bytes_saved", &self.bytes_saved)
.finish()
}
}
pub struct X86RelaxationOptimization {
pub gotpcrelx: bool,
pub tls_relax: bool,
pub tail_call: bool,
pub branch_relax: bool,
pub stats: RelaxationStats,
pub relax_records: Vec<RelaxRecord>,
}
impl X86RelaxationOptimization {
pub fn new() -> Self {
Self {
gotpcrelx: true,
tls_relax: true,
tail_call: true,
branch_relax: true,
stats: RelaxationStats::default(),
relax_records: Vec::new(),
}
}
pub fn run(
&mut self,
sections: &mut [X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
) -> RelaxationStats {
self.stats = RelaxationStats::default();
self.relax_records.clear();
self.analyze(sections, relocation_map);
self.apply(sections);
self.stats.clone()
}
fn analyze(
&mut self,
sections: &[X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
) {
for (&si, relocs) in relocation_map {
if si >= sections.len() || sections[si].data.is_empty() {
continue;
}
let data = §ions[si].data;
for reloc in relocs {
let k = self.classify(reloc.rel_type, data, reloc.offset as usize);
if !matches!(k, RelaxKind::NoRelax) {
self.relax_records.push(RelaxRecord {
section_index: si,
offset: reloc.offset,
original_type: reloc.rel_type,
relax_kind: k,
symbol_name: None,
addend: reloc.addend,
applied: false,
});
}
}
if self.tail_call && (sections[si].sh_flags & SHF_EXECINSTR) != 0 {
self.scan_tail_calls(si, data);
}
}
}
fn classify(&self, rel_type: u32, data: &[u8], offset: usize) -> RelaxKind {
if self.gotpcrelx {
match rel_type {
R_X86_64_GOTPCRELX | R_X86_64_REX_GOTPCRELX => {
if offset >= 2 {
let b = data[offset - 2];
if b == OPCODE_MOV_R64_M || b == OPCODE_REX_W {
return RelaxKind::GOTPCRELX_MovToLea;
}
}
return RelaxKind::GOTPCRELX_ToPC32;
}
R_X86_64_GOTPCREL => return RelaxKind::GOTPCRELX_MovToLea,
_ => {}
}
}
if self.tls_relax {
match rel_type {
R_X86_64_TLSGD => return RelaxKind::TlsGdToLe,
R_X86_64_GOTTPOFF => return RelaxKind::TlsIeToLe,
R_X86_64_TLSLD => return RelaxKind::TlsLdToLe,
_ => {}
}
}
if self.branch_relax {
if rel_type == R_X86_64_PC32 {
return RelaxKind::BranchRelax;
}
}
RelaxKind::NoRelax
}
fn scan_tail_calls(&mut self, si: usize, data: &[u8]) {
let mut off = 0;
while off + 5 <= data.len() {
if data[off] == OPCODE_CALL_NEAR {
let call_end = off + 5;
let search_end = (call_end + TAIL_CALL_SEARCH_WINDOW).min(data.len());
for s in call_end..search_end {
if data[s] == OPCODE_RET_NEAR || data[s] == OPCODE_RET_FAR {
self.relax_records.push(RelaxRecord {
section_index: si,
offset: off as u64,
original_type: R_X86_64_PLT32,
relax_kind: RelaxKind::CallToJmp,
symbol_name: None,
addend: 0,
applied: false,
});
break;
} else if data[s] != OPCODE_NOP
&& data[s] != OPCODE_INT3
&& !is_rex_prefix(data[s])
{
break;
}
}
}
off += 1;
}
}
fn apply(&mut self, sections: &mut [X86InputSection]) {
for rec in self.relax_records.clone() {
if rec.section_index >= sections.len() {
continue;
}
let data = &mut sections[rec.section_index].data;
if data.is_empty() || rec.offset as usize >= data.len() {
continue;
}
let off = rec.offset as usize;
match rec.relax_kind {
RelaxKind::GOTPCRELX_MovToLea => self.do_mov_to_lea(data, off),
RelaxKind::GOTPCRELX_ToPC32 => self.stats.gotpcrelx_to_pc32 += 1,
RelaxKind::CallToJmp => self.do_call_to_jmp(data, off),
RelaxKind::TlsGdToLe => self.stats.tls_gd_to_le += 1,
RelaxKind::TlsIeToLe => self.do_ie_to_le(data, off),
RelaxKind::TlsLdToLe => self.stats.tls_ld_to_le += 1,
RelaxKind::TlsGdToIe => self.stats.tls_gd_to_ie += 1,
RelaxKind::BranchRelax => self.stats.branch_relax += 1,
RelaxKind::NoRelax => {}
}
self.stats.total += 1;
}
}
fn do_mov_to_lea(&mut self, data: &mut [u8], offset: usize) {
if offset >= 2 && data[offset - 2] == OPCODE_MOV_R64_M {
data[offset - 2] = OPCODE_LEA_R64_M;
self.stats.gotpcrelx_mov_to_lea += 1;
} else if offset >= 3 && is_rex_prefix(data[offset - 3]) {
for k in (offset - 3)..offset {
if data[k] == OPCODE_MOV_R64_M {
data[k] = OPCODE_LEA_R64_M;
self.stats.gotpcrelx_mov_to_lea += 1;
break;
}
}
}
}
fn do_call_to_jmp(&mut self, data: &mut [u8], offset: usize) {
if offset < data.len() && data[offset] == OPCODE_CALL_NEAR {
data[offset] = OPCODE_JMP_NEAR;
self.stats.call_to_jmp += 1;
}
}
fn do_ie_to_le(&mut self, data: &mut [u8], offset: usize) {
for k in offset.saturating_sub(3)..offset {
if k + 1 < data.len() && is_rex_prefix(data[k]) && data[k + 1] == OPCODE_ADD_R64_RM64 {
data[k + 1] = OPCODE_LEA_R64_M;
self.stats.tls_ie_to_le += 1;
}
}
}
pub fn get_stats(&self) -> &RelaxationStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = RelaxationStats::default();
self.relax_records.clear();
}
pub fn relax_kind_name(kind: RelaxKind) -> &'static str {
match kind {
RelaxKind::GOTPCRELX_MovToLea => "GOTPCRELX: mov→lea",
RelaxKind::GOTPCRELX_ToPC32 => "GOTPCRELX: PC32",
RelaxKind::CallToJmp => "CALL→JMP tail call",
RelaxKind::TlsGdToIe => "TLS GD→IE",
RelaxKind::TlsGdToLe => "TLS GD→LE",
RelaxKind::TlsIeToLe => "TLS IE→LE",
RelaxKind::TlsLdToLe => "TLS LD→LE",
RelaxKind::BranchRelax => "branch relax",
RelaxKind::NoRelax => "none",
}
}
pub fn print_stats(&self) -> String {
format!(
"Relaxation: {} total ({} mov→lea, {} PC32, {} call→jmp, {} TLS GD→IE, {} GD→LE, {} IE→LE, {} LD→LE, {} branch)",
self.stats.total,
self.stats.gotpcrelx_mov_to_lea,
self.stats.gotpcrelx_to_pc32,
self.stats.call_to_jmp,
self.stats.tls_gd_to_ie,
self.stats.tls_gd_to_le,
self.stats.tls_ie_to_le,
self.stats.tls_ld_to_le,
self.stats.branch_relax,
)
}
}
impl Default for X86RelaxationOptimization {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86RelaxationOptimization {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86RelaxationOptimization")
.field("gotpcrelx", &self.gotpcrelx)
.field("tls_relax", &self.tls_relax)
.field("tail_call", &self.tail_call)
.field("branch_relax", &self.branch_relax)
.field("stats", &self.stats)
.finish()
}
}
pub struct X86LinkerOrdering {
pub call_graph: Vec<CallGraphNode>,
pub symbol_order: Vec<SymbolOrderEntry>,
pub heuristic: OrderingHeuristic,
pub original_order: Vec<usize>,
pub current_order: Vec<usize>,
pub ordered: bool,
}
impl X86LinkerOrdering {
pub fn new() -> Self {
Self {
call_graph: Vec::new(),
symbol_order: Vec::new(),
heuristic: OrderingHeuristic::None,
original_order: Vec::new(),
current_order: Vec::new(),
ordered: false,
}
}
pub fn with_heuristic(mut self, h: OrderingHeuristic) -> Self {
self.heuristic = h;
self
}
pub fn load_symbol_ordering(&mut self, data: &str) {
self.symbol_order.clear();
for line in data.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let name = parts[0].to_string();
let obj = parts.get(1).map(|s| s.to_string());
let prio = parts
.get(2)
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| self.symbol_order.len() as u64);
self.symbol_order.push(SymbolOrderEntry {
name,
object_file: obj,
priority: prio,
});
}
}
pub fn build_call_graph(
&mut self,
sections: &[X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
) {
self.call_graph.clear();
let n = sections.len();
let mut name_idx: HashMap<String, usize> = HashMap::new();
for (i, s) in sections.iter().enumerate() {
if !s.data.is_empty() {
name_idx.insert(s.name.clone(), i);
}
}
for (i, s) in sections.iter().enumerate() {
if s.data.is_empty() || (s.sh_flags & SHF_EXECINSTR) == 0 {
continue;
}
let mut callees = Vec::new();
if let Some(relocs) = relocation_map.get(&i) {
for r in relocs {
let t = r.section_index as usize;
if t < n && t != i && !callees.contains(&t) {
callees.push(t);
}
}
}
self.call_graph.push(CallGraphNode {
section_index: i,
name: s.name.clone(),
callees,
callers: Vec::new(),
frequency: 0.0,
size: s.data.len() as u64,
placed: false,
});
}
let mut caller_map: HashMap<usize, Vec<usize>> = HashMap::new();
for node in &self.call_graph {
for &c in &node.callees {
caller_map.entry(c).or_default().push(node.section_index);
}
}
for node in &mut self.call_graph {
if let Some(cs) = caller_map.get(&node.section_index) {
node.callers = cs.clone();
}
}
}
pub fn run(
&mut self,
sections: &mut [X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
heuristic: OrderingHeuristic,
) {
self.heuristic = heuristic;
let n = sections.len();
self.original_order = (0..n).collect();
self.current_order = self.original_order.clone();
match self.heuristic {
OrderingHeuristic::None => {}
OrderingHeuristic::CallGraph => {
self.build_call_graph(sections, relocation_map);
self.order_by_call_graph();
}
OrderingHeuristic::ProfileGuided => {
self.build_call_graph(sections, relocation_map);
self.order_by_profile();
}
OrderingHeuristic::Alphabetical => self.order_alphabetically(sections),
OrderingHeuristic::SizeDescending => self.order_by_size(sections),
OrderingHeuristic::SymbolOrderingFile => self.apply_symbol_ordering(sections),
}
self.ordered = self.current_order != self.original_order;
}
fn order_by_call_graph(&mut self) {
if self.call_graph.is_empty() {
return;
}
let mut order = Vec::new();
let mut placed = HashSet::new();
let mut sorted: Vec<usize> = (0..self.call_graph.len()).collect();
sorted.sort_by_key(|&i| std::cmp::Reverse(self.call_graph[i].callers.len()));
for &ni in &sorted {
let si = self.call_graph[ni].section_index;
if placed.contains(&si) {
continue;
}
order.push(si);
placed.insert(si);
let mut queue: VecDeque<usize> = VecDeque::new();
for &c in &self.call_graph[ni].callees {
if !placed.contains(&c) {
queue.push_back(c);
}
}
while let Some(cs) = queue.pop_front() {
if placed.contains(&cs) {
continue;
}
order.push(cs);
placed.insert(cs);
for node in &self.call_graph {
if node.section_index == cs {
for &cc in &node.callees {
if !placed.contains(&cc) {
queue.push_back(cc);
}
}
break;
}
}
}
}
self.current_order = order;
}
fn order_by_profile(&mut self) {
if self.call_graph.is_empty() {
return;
}
let mut sorted: Vec<usize> = (0..self.call_graph.len()).collect();
sorted.sort_by(|&a, &b| {
self.call_graph[b]
.frequency
.partial_cmp(&self.call_graph[a].frequency)
.unwrap_or(Ordering::Equal)
});
self.current_order = sorted
.into_iter()
.map(|i| self.call_graph[i].section_index)
.collect();
}
fn order_alphabetically(&mut self, sections: &[X86InputSection]) {
let mut indexed: Vec<(usize, &X86InputSection)> = sections
.iter()
.enumerate()
.filter(|(_, s)| !s.data.is_empty())
.collect();
indexed.sort_by(|a, b| a.1.name.cmp(&b.1.name));
self.current_order = indexed.into_iter().map(|(i, _)| i).collect();
}
fn order_by_size(&mut self, sections: &[X86InputSection]) {
let mut indexed: Vec<(usize, &X86InputSection)> = sections
.iter()
.enumerate()
.filter(|(_, s)| !s.data.is_empty())
.collect();
indexed.sort_by(|a, b| b.1.data.len().cmp(&a.1.data.len()));
self.current_order = indexed.into_iter().map(|(i, _)| i).collect();
}
pub fn apply_symbol_ordering(&mut self, sections: &[X86InputSection]) {
if self.symbol_order.is_empty() {
return;
}
let mut order = Vec::new();
let mut placed = HashSet::new();
let mut name_idx: HashMap<&str, usize> = HashMap::new();
for (i, s) in sections.iter().enumerate() {
if !s.data.is_empty() {
name_idx.insert(s.name.as_str(), i);
}
}
for entry in &self.symbol_order {
if let Some(&idx) = name_idx.get(entry.name.as_str()) {
if !placed.contains(&idx) {
order.push(idx);
placed.insert(idx);
}
}
}
for (i, s) in sections.iter().enumerate() {
if !s.data.is_empty() && !placed.contains(&i) {
order.push(i);
}
}
self.current_order = order;
self.ordered = true;
}
pub fn get_order(&self) -> &[usize] {
&self.current_order
}
pub fn is_ordered(&self) -> bool {
self.ordered
}
pub fn total_call_distance(&self) -> u64 {
let mut dist = 0u64;
let mut pos_map: HashMap<usize, usize> = HashMap::new();
for (pos, &idx) in self.current_order.iter().enumerate() {
pos_map.insert(idx, pos);
}
for node in &self.call_graph {
let cp = pos_map.get(&node.section_index).copied().unwrap_or(0);
for &c in &node.callees {
let np = pos_map.get(&c).copied().unwrap_or(0);
dist += (cp as i64 - np as i64).unsigned_abs();
}
}
dist
}
}
impl Default for X86LinkerOrdering {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86LinkerOrdering {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86LinkerOrdering")
.field("heuristic", &self.heuristic)
.field("nodes", &self.call_graph.len())
.field("symbol_entries", &self.symbol_order.len())
.field("ordered", &self.ordered)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::x86::x86_lld_full::{X86InputRelocation, X86InputSection};
use std::collections::{HashMap, HashSet};
fn ms(name: &str, data: Vec<u8>, flags: u64, align: u64) -> X86InputSection {
X86InputSection {
name: name.to_string(),
data,
sh_type: SHT_PROGBITS,
sh_flags: flags,
sh_addralign: align,
section_index: 0,
sh_entsize: 0,
}
}
fn me(name: &str, data: Vec<u8>) -> X86InputSection {
ms(name, data, SHF_ALLOC | SHF_EXECINSTR, 16)
}
fn md(name: &str, data: Vec<u8>) -> X86InputSection {
ms(name, data, SHF_ALLOC, 8)
}
fn mrd(name: &str, data: Vec<u8>) -> X86InputSection {
ms(name, data, SHF_ALLOC, 8)
}
fn mstr(name: &str, data: Vec<u8>) -> X86InputSection {
ms(name, data, SHF_ALLOC | SHF_MERGE | SHF_STRINGS, 1)
}
fn mr(si: u32, off: u64, ty: u32) -> X86InputRelocation {
X86InputRelocation {
offset: off,
symbol_index: 0,
rel_type: ty,
addend: 0,
section_index: si,
}
}
#[test]
fn t_opt_new() {
let o = X86LLDOpt::new();
assert!(!o.enable_icf);
assert!(o.enable_relaxation);
assert_eq!(o.icf_safety, ICFSafetyLevel::Safe);
}
#[test]
fn t_opt_aggressive() {
let o = X86LLDOpt::new_aggressive();
assert!(o.enable_icf);
assert!(o.enable_gc_sections);
assert_eq!(o.icf_safety, ICFSafetyLevel::All);
}
#[test]
fn t_opt_safe() {
let o = X86LLDOpt::new_safe();
assert!(o.enable_icf);
assert_eq!(o.icf_safety, ICFSafetyLevel::Safe);
}
#[test]
fn t_opt_size_only() {
let o = X86LLDOpt::new_size_only();
assert!(o.enable_icf);
assert!(o.enable_comdat_folding);
assert!(!o.enable_relaxation);
}
#[test]
fn t_opt_run_empty() {
let mut o = X86LLDOpt::new();
let mut v: Vec<X86InputSection> = vec![];
let s = o.run_all(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(s.sections_before, 0);
assert_eq!(s.bytes_saved, 0);
}
#[test]
fn t_opt_no_passes() {
let mut o = X86LLDOpt::new();
let mut v = vec![me(".text.a", vec![0xC3; 32]), me(".text.b", vec![0xC3; 32])];
let s = o.run_all(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(s.sections_removed, 0);
}
#[test]
fn t_opt_icf_pipeline() {
let mut o = X86LLDOpt::new();
o.enable_icf = true;
o.icf_safety = ICFSafetyLevel::All;
o.icf.min_size = 16;
let d = vec![0x90; 32];
let mut v = vec![
me(".text.a", d.clone()),
me(".text.b", d.clone()),
me(".text.c", vec![0xCC; 32]),
];
let s = o.run_all(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(s.sections_removed > 0);
assert!(s.bytes_saved > 0);
}
#[test]
fn t_opt_has_errors() {
let mut o = X86LLDOpt::new();
assert!(!o.has_errors());
o.diag(X86OptDiagLevel::Error, "err".into(), None);
assert!(o.has_errors());
assert!(!o.has_warnings());
}
#[test]
fn t_opt_has_warnings() {
let mut o = X86LLDOpt::new();
o.diag(X86OptDiagLevel::Warning, "warn".into(), None);
assert!(o.has_warnings());
}
#[test]
fn t_opt_estimate_savings() {
let o = X86LLDOpt {
enable_icf: true,
enable_merge_sections: true,
icf: X86ICF::new().with_min_size(16),
..X86LLDOpt::new()
};
let d = vec![0x90; 32];
let v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let est = o.estimate_savings(&v, &HashMap::new());
assert!(est > 0);
}
#[test]
fn t_opt_debug_fmt() {
let o = X86LLDOpt::new();
let s = format!("{:?}", o);
assert!(s.contains("X86LLDOpt"));
}
#[test]
fn t_summary_ratio() {
let s = X86LLDOptSummary {
sections_before: 10,
sections_after: 8,
sections_removed: 2,
bytes_before: 1000,
bytes_after: 800,
bytes_saved: 200,
diagnostics_count: 1,
};
assert!((s.reduction_ratio() - 0.2).abs() < 0.01);
assert!((s.section_reduction_ratio() - 0.2).abs() < 0.01);
assert!(s.had_effect());
}
#[test]
fn t_summary_no_effect() {
let s = X86LLDOptSummary {
sections_before: 5,
sections_after: 5,
sections_removed: 0,
bytes_before: 100,
bytes_after: 100,
bytes_saved: 0,
diagnostics_count: 0,
};
assert!(!s.had_effect());
assert_eq!(s.reduction_ratio(), 0.0);
}
#[test]
fn t_summary_format() {
let s = X86LLDOptSummary {
sections_before: 10,
sections_after: 7,
sections_removed: 3,
bytes_before: 1000,
bytes_after: 700,
bytes_saved: 300,
diagnostics_count: 0,
};
let f = s.format_summary();
assert!(f.contains("10 → 7"));
assert!(f.contains("30.0%"));
}
#[test]
fn t_enums_eq() {
assert_eq!(ICFSafetyLevel::All, ICFSafetyLevel::All);
assert_ne!(ICFSafetyLevel::Safe, ICFSafetyLevel::All);
assert_eq!(
COMDATSelectionStrategy::Largest,
COMDATSelectionStrategy::Largest
);
assert_eq!(GCPrintMode::Silent, GCPrintMode::Silent);
assert_eq!(RelaxKind::NoRelax, RelaxKind::NoRelax);
assert_eq!(
InternalizationStrategy::Conservative,
InternalizationStrategy::Conservative
);
assert_eq!(OrderingHeuristic::None, OrderingHeuristic::None);
}
#[test]
fn t_icf_new() {
let icf = X86ICF::new();
assert_eq!(icf.min_size, ICF_MIN_FUNCTION_SIZE);
assert!(icf.functions_only);
}
#[test]
fn t_icf_custom() {
let icf = X86ICF::new()
.with_min_size(64)
.with_fold_rodata(true)
.with_functions_only(false);
assert_eq!(icf.min_size, 64);
assert!(icf.fold_rodata);
assert!(!icf.functions_only);
}
#[test]
fn t_icf_address_taken() {
let mut icf = X86ICF::new();
icf.mark_address_taken(3);
assert!(icf.is_address_taken(3));
assert!(!icf.is_address_taken(0));
icf.mark_addresses_taken(&[5, 7]);
assert!(icf.is_address_taken(5));
assert!(icf.is_address_taken(7));
}
#[test]
fn t_icf_identical_fold() {
let mut icf = X86ICF::new().with_min_size(16);
let d = vec![0x90; 32];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let (r, s) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 1);
assert!(s >= 32);
}
#[test]
fn t_icf_non_identical() {
let mut icf = X86ICF::new().with_min_size(16);
let mut v = vec![me(".text.a", vec![0x90; 32]), me(".text.b", vec![0xCC; 32])];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 0);
}
#[test]
fn t_icf_small_skip() {
let mut icf = X86ICF::new().with_min_size(64);
let d = vec![0x90; 16];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 0);
}
#[test]
fn t_icf_safe_preserve() {
let mut icf = X86ICF::new().with_min_size(16);
icf.mark_address_taken(0);
let d = vec![0x90; 32];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::Safe);
assert_eq!(r, 1);
}
#[test]
fn t_icf_all_folds_everything() {
let mut icf = X86ICF::new().with_min_size(16);
icf.mark_address_taken(0);
icf.mark_address_taken(1);
let d = vec![0x90; 32];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 1);
}
#[test]
fn t_icf_disabled() {
let mut icf = X86ICF::new();
let d = vec![0x90; 32];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::Disabled);
assert_eq!(r, 0);
}
#[test]
fn t_icf_debug_info() {
let mut icf = X86ICF::new();
icf.fold_mapping.push(ICFMapping {
folded_section: 42,
canonical_section: 7,
has_thunk: true,
thunk_offset: Some(0x1000),
});
let m = icf.debug_info_mapping();
assert_eq!(m.get(&42), Some(&7));
}
#[test]
fn t_icf_thunk() {
let icf = X86ICF::new();
let t = icf.emit_thunk("fold", "canon");
assert_eq!(t.original_name, "fold");
assert_eq!(t.code[0], OPCODE_JMP_NEAR);
assert_eq!(t.size, 5);
}
#[test]
fn t_icf_many() {
let mut icf = X86ICF::new().with_min_size(16);
let d = vec![0x90; 32];
let mut v: Vec<X86InputSection> = (0..100)
.map(|i| me(&format!(".text.f{}", i), d.clone()))
.collect();
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 99);
}
#[test]
fn t_icf_rodata() {
let mut icf = X86ICF::new()
.with_min_size(16)
.with_functions_only(false)
.with_fold_rodata(true);
let d = vec![0x42; 32];
let mut v = vec![mrd(".rodata.a", d.clone()), mrd(".rodata.b", d.clone())];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 1);
}
#[test]
fn t_icf_print_summary() {
let icf = X86ICF::new();
let s = icf.print_icf_summary();
assert!(s.contains("ICF:"));
assert!(s.contains("folded"));
}
#[test]
fn t_lto_new() {
let l = X86LTOOptimizations::new();
assert!(l.const_prop);
assert!(l.dead_code_elim);
}
#[test]
fn t_lto_empty() {
let mut l = X86LTOOptimizations::new();
let mut v: Vec<X86InputSection> = vec![];
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
InternalizationStrategy::Conservative,
);
assert_eq!(s.dead_functions_eliminated, 0);
}
#[test]
fn t_lto_merge() {
let mut l = X86LTOOptimizations::new();
let d = vec![0xC3; 32];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
InternalizationStrategy::Conservative,
);
assert!(s.functions_merged > 0);
}
#[test]
fn t_lto_dce() {
let mut l = X86LTOOptimizations::new();
let mut v = vec![
me(".text._start", vec![0x90; 32]),
me(".text.dead1", vec![0xCC; 32]),
me(".text.dead2", vec![0xC3; 32]),
];
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
InternalizationStrategy::Conservative,
);
assert!(s.dead_functions_eliminated >= 2);
}
#[test]
fn t_lto_devirt() {
let mut l = X86LTOOptimizations::new();
let code = vec![0xFF, 0xD0, 0xC3]; let mut v = vec![me(".text.f", code)];
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
InternalizationStrategy::Conservative,
);
assert!(s.devirtualized_calls > 0);
}
#[test]
fn t_lto_reset() {
let mut l = X86LTOOptimizations::new();
l.stats.functions_merged = 42;
l.reset_stats();
assert_eq!(l.stats.functions_merged, 0);
}
#[test]
fn t_lto_print() {
let l = X86LTOOptimizations::new();
let s = l.print_stats();
assert!(s.contains("LTO:"));
}
#[test]
fn t_gc_new() {
let gc = X86GCSections::new();
assert!(!gc.keep_eh);
assert!(gc.keep_init_fini);
assert!(!gc.keep_patterns.is_empty());
}
#[test]
fn t_gc_empty() {
let mut gc = X86GCSections::new();
let mut v: Vec<X86InputSection> = vec![];
let (r, n) = gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(r, 0);
assert!(n.is_empty());
}
#[test]
fn t_gc_keeps_entry() {
let mut gc = X86GCSections::new();
let mut v = vec![
me(".text._start", vec![0xC3]),
me(".text.dead", vec![0x90; 32]),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let (r, _) = gc.run(
&mut v,
&["_start".into()],
&HashSet::new(),
&HashMap::new(),
&sd,
);
assert!(!v[0].data.is_empty());
assert_eq!(r, 1);
}
#[test]
fn t_gc_keeps_init() {
let mut gc = X86GCSections::new();
let mut v = vec![
ms(".init", vec![0xC3], SHF_ALLOC | SHF_EXECINSTR, 16),
me(".text.x", vec![0x90; 16]),
];
let (r, _) = gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!v[0].data.is_empty());
assert!(v[1].data.is_empty());
}
#[test]
fn t_gc_exported() {
let mut gc = X86GCSections::new();
let mut v = vec![
me(".text.exported_fn", vec![0xC3]),
me(".text.dead", vec![0x90; 16]),
];
let mut ex: HashSet<String> = HashSet::new();
ex.insert("exported_fn".into());
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["exported_fn".into()]);
let (r, _) = gc.run(&mut v, &[], &ex, &HashMap::new(), &sd);
assert!(!v[0].data.is_empty());
assert_eq!(r, 1);
}
#[test]
fn t_gc_eh() {
let mut gc = X86GCSections::new().with_keep_eh(true);
let mut v = vec![
ms(".eh_frame", vec![0x00; 32], SHF_ALLOC, 8),
me(".text.x", vec![0x90; 16]),
];
let (r, _) = gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!v[0].data.is_empty());
}
#[test]
fn t_gc_debug() {
let mut gc = X86GCSections::new();
gc.keep_debug = true;
let mut v = vec![
ms(".debug_line", vec![0x00; 64], 0, 1),
me(".text.x", vec![0x90; 16]),
];
gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!v[0].data.is_empty());
}
#[test]
fn t_gc_comdat() {
let mut gc = X86GCSections::new();
gc.comdat_groups = vec![COMDATGroup {
signature: "g1".into(),
member_sections: vec![0, 1],
selection: COMDAT_SELECTION_ANY,
total_size: 64,
content_hash: 0,
is_live: false,
folded_into: None,
}];
gc.live_flags = vec![false, false, true];
gc.propagate_comdat_liveness(&[]);
assert!(!gc.live_flags[0]);
}
#[test]
fn t_gc_print_mode() {
let mut gc = X86GCSections::new().with_print_mode(GCPrintMode::PrintRemoved);
let mut v = vec![
ms(".init", vec![0xC3], SHF_ALLOC | SHF_EXECINSTR, 16),
me(".text.dead", vec![0x90; 32]),
];
let (r, names) = gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(r, 1);
assert!(!names.is_empty());
}
#[test]
fn t_gc_is_live() {
let gc = X86GCSections {
live_flags: vec![true, false, true],
..X86GCSections::new()
};
assert!(gc.is_live(0));
assert!(!gc.is_live(1));
assert!(!gc.is_live(99));
}
#[test]
fn t_gc_live_count() {
let gc = X86GCSections {
live_flags: vec![true, false, true, true],
..X86GCSections::new()
};
assert_eq!(gc.live_count(), 3);
}
#[test]
fn t_comdat_new() {
let c = X86COMDATFolding::new();
assert!(c.groups.is_empty());
}
#[test]
fn t_comdat_empty() {
let mut c = X86COMDATFolding::new();
let mut v: Vec<X86InputSection> = vec![];
assert_eq!(c.run(&mut v), 0);
}
#[test]
fn t_comdat_fold() {
let mut c = X86COMDATFolding::new();
c.groups = vec![
COMDATGroup {
signature: "a".into(),
member_sections: vec![0, 1],
selection: COMDAT_SELECTION_ANY,
total_size: 64,
content_hash: fnv1a_hash_64(b"same"),
is_live: true,
folded_into: None,
},
COMDATGroup {
signature: "b".into(),
member_sections: vec![2, 3],
selection: COMDAT_SELECTION_ANY,
total_size: 48,
content_hash: fnv1a_hash_64(b"same"),
is_live: true,
folded_into: None,
},
];
let mut v = vec![
me(".text.a1", vec![0xC3; 32]),
me(".text.a2", vec![0xC3; 32]),
me(".text.b1", vec![0xC3; 24]),
me(".text.b2", vec![0xC3; 24]),
];
let r = c.run(&mut v);
assert_eq!(r, 1);
}
#[test]
fn t_comdat_largest() {
let mut c = X86COMDATFolding::new().with_strategy(COMDATSelectionStrategy::Largest);
c.groups = vec![
COMDATGroup {
signature: "small".into(),
member_sections: vec![0],
selection: COMDAT_SELECTION_ANY,
total_size: 16,
content_hash: fnv1a_hash_64(b"x"),
is_live: true,
folded_into: None,
},
COMDATGroup {
signature: "big".into(),
member_sections: vec![1],
selection: COMDAT_SELECTION_ANY,
total_size: 64,
content_hash: fnv1a_hash_64(b"x"),
is_live: true,
folded_into: None,
},
];
let mut v = vec![me(".text.s", vec![0xC3; 16]), me(".text.b", vec![0xC3; 64])];
c.run(&mut v);
assert!(c.groups[1].is_live);
}
#[test]
fn t_merge_new() {
let m = X86MergeSimilarSections::new();
assert!(m.merge_strings);
assert!(m.merge_constants);
assert!(m.tail_merge);
}
#[test]
fn t_merge_empty() {
let mut m = X86MergeSimilarSections::new();
let mut v: Vec<X86InputSection> = vec![];
assert_eq!(m.run(&mut v), 0);
}
#[test]
fn t_merge_strings() {
let mut m = X86MergeSimilarSections::new();
let s1 = b"hello\0world\0";
let s2 = b"hello\0other\0";
let mut v = vec![
mstr(".rodata.s1", s1.to_vec()),
mstr(".rodata.s2", s2.to_vec()),
];
let sv = m.run(&mut v);
assert!(sv > 0);
}
#[test]
fn t_merge_consts() {
let mut m = X86MergeSimilarSections::new();
let d = vec![0x42; 16];
let mut v = vec![mrd(".rodata.a", d.clone()), mrd(".rodata.b", d.clone())];
let sv = m.run(&mut v);
assert!(sv > 0);
}
#[test]
fn t_merge_tail() {
let mut m = X86MergeSimilarSections::new();
let mut f1 = vec![0x90; 20];
f1.extend_from_slice(&[0xC3, 0x90, 0x90, 0x90, 0x90]);
let mut f2 = vec![0xCC; 20];
f2.extend_from_slice(&[0xC3, 0x90, 0x90, 0x90, 0x90]);
let mut v = vec![me(".text.f1", f1), me(".text.f2", f2)];
let sv = m.run(&mut v);
assert!(sv > 0);
}
#[test]
fn t_relax_new() {
let r = X86RelaxationOptimization::new();
assert!(r.gotpcrelx);
assert!(r.tls_relax);
assert!(r.tail_call);
assert!(r.branch_relax);
}
#[test]
fn t_relax_empty() {
let mut r = X86RelaxationOptimization::new();
let mut v: Vec<X86InputSection> = vec![];
let s = r.run(&mut v, &HashMap::new());
assert_eq!(s.total, 0);
}
#[test]
fn t_relax_classify() {
let r = X86RelaxationOptimization::new();
let d = vec![0x48, 0x8B, 0x05, 0, 0, 0, 0];
let k = r.classify(R_X86_64_GOTPCRELX, &d, 3);
assert!(matches!(
k,
RelaxKind::GOTPCRELX_MovToLea | RelaxKind::GOTPCRELX_ToPC32
));
}
#[test]
fn t_relax_tls_gd() {
let r = X86RelaxationOptimization::new();
assert_eq!(r.classify(R_X86_64_TLSGD, &[], 0), RelaxKind::TlsGdToLe);
assert_eq!(r.classify(R_X86_64_GOTTPOFF, &[], 0), RelaxKind::TlsIeToLe);
assert_eq!(r.classify(R_X86_64_TLSLD, &[], 0), RelaxKind::TlsLdToLe);
}
#[test]
fn t_relax_call_to_jmp() {
let mut r = X86RelaxationOptimization::new();
let d = vec![OPCODE_CALL_NEAR, 0, 0, 0, 0, OPCODE_RET_NEAR];
r.scan_tail_calls(0, &d);
assert!(!r.relax_records.is_empty());
let mut data = d.clone();
r.do_call_to_jmp(&mut data, 0);
assert_eq!(data[0], OPCODE_JMP_NEAR);
assert_eq!(r.stats.call_to_jmp, 1);
}
#[test]
fn t_relax_mov_to_lea() {
let mut r = X86RelaxationOptimization::new();
let mut d = vec![0, 0, OPCODE_MOV_R64_M, 0x05, 0, 0, 0, 0];
r.do_mov_to_lea(&mut d, 2);
assert_eq!(d[2], OPCODE_LEA_R64_M);
assert_eq!(r.stats.gotpcrelx_mov_to_lea, 1);
}
#[test]
fn t_relax_ie_to_le() {
let mut r = X86RelaxationOptimization::new();
let mut d = vec![OPCODE_REX_W, OPCODE_ADD_R64_RM64, 0x05, 0, 0, 0, 0];
r.do_ie_to_le(&mut d, 2);
assert_eq!(d[1], OPCODE_LEA_R64_M);
assert_eq!(r.stats.tls_ie_to_le, 1);
}
#[test]
fn t_relax_names() {
assert_eq!(
X86RelaxationOptimization::relax_kind_name(RelaxKind::CallToJmp),
"CALL→JMP tail call"
);
assert_eq!(
X86RelaxationOptimization::relax_kind_name(RelaxKind::NoRelax),
"none"
);
}
#[test]
fn t_relax_reset() {
let mut r = X86RelaxationOptimization::new();
r.stats.total = 99;
r.reset_stats();
assert_eq!(r.stats.total, 0);
}
#[test]
fn t_relax_print() {
let r = X86RelaxationOptimization::new();
let s = r.print_stats();
assert!(s.contains("Relaxation:"));
}
#[test]
fn t_order_new() {
let o = X86LinkerOrdering::new();
assert!(!o.ordered);
}
#[test]
fn t_order_call_graph() {
let mut o = X86LinkerOrdering::new();
let v = vec![
me(".text.caller", vec![0xC3; 32]),
me(".text.callee", vec![0xC3; 16]),
];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![mr(1, 0, R_X86_64_PC32)]);
o.build_call_graph(&v, &rm);
assert!(!o.call_graph.is_empty());
assert!(!o.call_graph[0].callees.is_empty());
}
#[test]
fn t_order_alpha() {
let mut o = X86LinkerOrdering::new();
let v = vec![
me(".text.z", vec![0xC3]),
me(".text.a", vec![0xC3]),
me(".text.m", vec![0xC3]),
];
o.order_alphabetically(&v);
let ord = o.get_order();
assert_eq!(v[ord[0]].name, ".text.a");
assert_eq!(v[ord[2]].name, ".text.z");
}
#[test]
fn t_order_size() {
let mut o = X86LinkerOrdering::new();
let v = vec![
me(".text.s", vec![0xC3; 8]),
me(".text.l", vec![0xC3; 32]),
me(".text.m", vec![0xC3; 16]),
];
o.order_by_size(&v);
let ord = o.get_order();
assert_eq!(ord[0], 1); }
#[test]
fn t_order_symbol_file() {
let mut o = X86LinkerOrdering::new();
o.load_symbol_ordering("# comment\n_start\nmain obj.o 1\n");
assert_eq!(o.symbol_order.len(), 2);
assert_eq!(o.symbol_order[1].priority, 1);
}
#[test]
fn t_order_apply_symbol() {
let mut o = X86LinkerOrdering::new();
o.symbol_order = vec![
SymbolOrderEntry {
name: ".text.main".into(),
object_file: None,
priority: 0,
},
SymbolOrderEntry {
name: ".text.help".into(),
object_file: None,
priority: 1,
},
];
let v = vec![
me(".text.help", vec![0xC3; 16]),
me(".text.other", vec![0x90; 32]),
me(".text.main", vec![0xC3; 64]),
];
o.apply_symbol_ordering(&v);
assert_eq!(v[o.current_order[0]].name, ".text.main");
assert_eq!(v[o.current_order[1]].name, ".text.help");
}
#[test]
fn t_order_distance() {
let mut o = X86LinkerOrdering::new();
o.current_order = vec![0, 1, 2];
o.call_graph = vec![CallGraphNode {
section_index: 0,
name: "a".into(),
callees: vec![2],
callers: vec![],
frequency: 1.0,
size: 16,
placed: false,
}];
assert_eq!(o.total_call_distance(), 2);
}
#[test]
fn t_order_run_none() {
let mut o = X86LinkerOrdering::new();
let mut v = vec![me(".text.a", vec![0xC3]), me(".text.b", vec![0xC3])];
o.run(&mut v, &HashMap::new(), OrderingHeuristic::None);
assert!(!o.ordered);
}
#[test]
fn t_order_run_call_graph() {
let mut o = X86LinkerOrdering::new();
let mut v = vec![me(".text.a", vec![0xC3; 32]), me(".text.b", vec![0xC3; 16])];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![mr(1, 0, R_X86_64_PC32)]);
o.run(&mut v, &rm, OrderingHeuristic::CallGraph);
assert!(!o.call_graph.is_empty() || o.ordered);
}
#[test]
fn t_hash() {
assert_eq!(fnv1a_hash_64(b"hello"), fnv1a_hash_64(b"hello"));
assert_ne!(fnv1a_hash_64(b"hello"), fnv1a_hash_64(b"world"));
}
#[test]
fn t_hash_skip_relocs() {
let d = vec![0x48, 0x8B, 0x05, 0, 0, 0, 0, 0xC3];
let d2 = vec![0x48, 0x8B, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3];
let mut ro = HashSet::new();
ro.insert(3);
let mut rs = HashMap::new();
rs.insert(3, 4);
assert_eq!(
fnv1a_hash_skip_relocs(&d, &ro, &rs),
fnv1a_hash_skip_relocs(&d2, &ro, &rs)
);
}
#[test]
fn t_double_hash() {
let (a, b) = double_hash(b"data");
assert_ne!(a, 0);
assert_ne!(b, 0);
assert_ne!(a, b);
}
#[test]
fn t_checksum32() {
assert_ne!(checksum32(b"abc"), 0);
assert_eq!(checksum32(b"abc"), checksum32(b"abc"));
}
#[test]
fn t_comdat_name() {
assert!(is_comdat_section_name(".text._Z3foov"));
assert!(!is_comdat_section_name(".text"));
assert_eq!(
extract_comdat_group_name(".text._Z3foov"),
Some("_Z3foov".into())
);
assert_eq!(extract_comdat_group_name(".text"), None);
}
#[test]
fn t_align() {
assert_eq!(align_up_opt(17, 16), 32);
assert_eq!(align_up_opt(0, 16), 0);
assert_eq!(align_up_opt(5, 0), 5);
assert_eq!(align_down_opt(17, 16), 16);
assert_eq!(align_down_opt(5, 0), 5);
}
#[test]
fn t_tail_overlap() {
assert_eq!(
find_tail_overlap(&[1, 2, 3, 4, 5], &[9, 8, 3, 4, 5], 2),
Some(3)
);
assert_eq!(find_tail_overlap(&[1, 2, 3], &[4, 5, 6], 1), None);
}
#[test]
fn t_rex_prefix() {
assert!(is_rex_prefix(0x48));
assert!(is_rex_prefix(0x41));
assert!(!is_rex_prefix(0x90));
}
#[test]
fn t_modrm() {
assert!(is_modrm_register(0xC0));
assert!(!is_modrm_register(0x00));
}
#[test]
fn t_insn_length() {
assert_eq!(x86_insn_length(OPCODE_CALL_NEAR), 5);
assert_eq!(x86_insn_length(OPCODE_RET_NEAR), 1);
assert_eq!(x86_insn_length(OPCODE_NOP), 1);
}
#[test]
fn t_constants() {
assert_eq!(SHF_WRITE, 0x1);
assert_eq!(SHF_ALLOC, 0x2);
assert_eq!(SHF_EXECINSTR, 0x4);
assert_eq!(SHT_PROGBITS, 1);
assert_eq!(SHT_DYNAMIC, 6);
assert_eq!(OPCODE_JMP_NEAR, 0xE9);
assert_eq!(OPCODE_CALL_NEAR, 0xE8);
assert_eq!(OPCODE_UD2, [0x0F, 0x0B]);
}
#[test]
fn t_defaults() {
let _ = X86LLDOpt::default();
let _ = X86ICF::default();
let _ = X86LTOOptimizations::default();
let _ = X86GCSections::default();
let _ = X86COMDATFolding::default();
let _ = X86MergeSimilarSections::default();
let _ = X86RelaxationOptimization::default();
let _ = X86LinkerOrdering::default();
let _ = RelaxationStats::default();
let _ = LTOOptStats::default();
}
#[test]
fn t_full_pipeline() {
let mut o = X86LLDOpt::new_aggressive();
o.icf.min_size = 16;
let d = vec![0x90; 32];
let mut v = vec![
me(".text._start", vec![0xC3; 32]),
me(".text.main", d.clone()),
me(".text.dup", d.clone()),
me(".text.dead", vec![0xCC; 48]),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
sd.insert(1, vec!["main".into()]);
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![mr(1, 0, R_X86_64_PC32)]);
let s = o.run_all(&mut v, &["_start".into()], &HashSet::new(), &rm, &sd);
assert!(s.had_effect());
}
#[test]
fn t_diag_levels() {
let mut o = X86LLDOpt::new();
o.diag(X86OptDiagLevel::Info, "i".into(), None);
o.diag(X86OptDiagLevel::Warning, "w".into(), None);
o.diag(X86OptDiagLevel::Error, "e".into(), None);
o.diag(X86OptDiagLevel::Debug, "d".into(), None);
assert_eq!(o.diagnostics.len(), 4);
assert!(o.has_errors());
assert!(o.has_warnings());
o.clear_diagnostics();
assert!(o.diagnostics.is_empty());
}
#[test]
fn t_stress_many_sections_icf() {
let mut icf = X86ICF::new().with_min_size(16);
let d = vec![0x90; 32];
let mut v: Vec<X86InputSection> = (0..500)
.map(|i| me(&format!(".text.f{}", i), d.clone()))
.collect();
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 499);
}
#[test]
fn t_stress_many_sections_gc() {
let mut gc = X86GCSections::new();
let mut v: Vec<X86InputSection> = Vec::new();
v.push(me(".init", vec![0xC3]));
for i in 0..500 {
v.push(me(&format!(".text.dead{}", i), vec![0x90; 32]));
}
let (r, _) = gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(r, 500);
}
#[test]
fn t_stress_many_hashes() {
for i in 0..1000 {
let data = i.to_le_bytes().repeat(16);
let h = fnv1a_hash_64(&data);
assert_ne!(h, 0);
}
}
#[test]
fn t_icf_empty_sections() {
let mut icf = X86ICF::new();
let mut v: Vec<X86InputSection> = vec![];
let (r, s) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 0);
assert_eq!(s, 0);
}
#[test]
fn t_icf_min_size_boundary() {
let mut icf = X86ICF::new().with_min_size(16);
let data_15 = vec![0x90; 15];
let data_16 = vec![0x90; 16];
let data_17 = vec![0x90; 17];
let mut v = vec![
me(".text.a", data_15.clone()),
me(".text.b", data_15.clone()),
me(".text.c", data_16.clone()),
me(".text.d", data_16.clone()),
me(".text.e", data_17.clone()),
me(".text.f", data_17.clone()),
];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 2);
}
#[test]
fn t_icf_different_flags_no_fold() {
let mut icf = X86ICF::new().with_min_size(16);
let d = vec![0x90; 32];
let mut v = vec![
ms(".text.a", d.clone(), SHF_ALLOC | SHF_EXECINSTR, 16),
ms(
".text.b",
d.clone(),
SHF_ALLOC | SHF_EXECINSTR | SHF_WRITE,
16,
),
];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 0); }
#[test]
fn t_icf_safe_both_address_taken() {
let mut icf = X86ICF::new().with_min_size(16);
icf.mark_addresses_taken(&[0, 1]);
let d = vec![0x90; 32];
let mut v = vec![me(".text.a", d.clone()), me(".text.b", d.clone())];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::Safe);
assert_eq!(r, 0); }
#[test]
fn t_icf_multiple_folds() {
let mut icf = X86ICF::new().with_min_size(16);
let d1 = vec![0x90; 32];
let d2 = vec![0xCC; 32];
let mut v = vec![
me(".text.a1", d1.clone()),
me(".text.a2", d1.clone()),
me(".text.a3", d1.clone()),
me(".text.b1", d2.clone()),
me(".text.b2", d2.clone()),
me(".text.c", vec![0xC3; 32]),
];
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert_eq!(r, 3); }
#[test]
fn t_gc_reloc_chain() {
let mut gc = X86GCSections::new();
let mut v = vec![
me(".text.start", vec![0xC3; 16]),
me(".text.mid1", vec![0x90; 16]),
me(".text.mid2", vec![0x90; 16]),
me(".text.leaf", vec![0x90; 16]),
me(".text.isolated", vec![0xCC; 16]),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["start".into()]);
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![mr(1, 0, R_X86_64_PC32)]);
rm.insert(1, vec![mr(2, 0, R_X86_64_PC32)]);
rm.insert(2, vec![mr(3, 0, R_X86_64_PC32)]);
let (r, _) = gc.run(&mut v, &["start".into()], &HashSet::new(), &rm, &sd);
assert_eq!(r, 1); }
#[test]
fn t_gc_keep_unconditional() {
let mut gc = X86GCSections::new();
let mut v = vec![
ms(".dynamic", vec![0x00; 32], SHF_ALLOC, 8),
me(".text.foo", vec![0x90; 16]),
];
gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!v[0].data.is_empty()); assert!(v[1].data.is_empty());
}
#[test]
fn t_gc_shf_link_order() {
let mut gc = X86GCSections::new();
let mut v = vec![
me(".text.foo", vec![0xC3; 16]),
ms(
".text.foo.metadata",
vec![0x00; 8],
SHF_ALLOC | SHF_LINK_ORDER,
4,
),
];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(1, vec![mr(0, 0, R_X86_64_PC32)]);
let (r, _) = gc.run(&mut v, &[], &HashSet::new(), &rm, &HashMap::new());
assert_eq!(r, 2);
}
#[test]
fn t_merge_zero_length_strings() {
let mut m = X86MergeSimilarSections::new();
let mut v = vec![mstr(".rodata.s", b"\0\0\0".to_vec())];
let sv = m.run(&mut v);
assert_eq!(sv, 0); }
#[test]
fn t_merge_non_string_section() {
let mut m = X86MergeSimilarSections::new();
let d = vec![0x42; 32];
let mut v = vec![
ms(".data.x", d.clone(), SHF_ALLOC | SHF_WRITE, 8),
ms(".data.y", d.clone(), SHF_ALLOC | SHF_WRITE, 8),
];
let sv = m.run(&mut v);
assert_eq!(sv, 0); }
#[test]
fn t_relax_no_reloc_on_non_relaxable() {
let r = X86RelaxationOptimization::new();
assert_eq!(r.classify(R_X86_64_64, &[], 0), RelaxKind::NoRelax);
assert_eq!(r.classify(R_X86_64_32, &[], 0), RelaxKind::NoRelax);
}
#[test]
fn t_relax_disabled_passes() {
let mut r = X86RelaxationOptimization {
gotpcrelx: false,
tls_relax: false,
tail_call: false,
branch_relax: false,
..X86RelaxationOptimization::new()
};
let d = vec![0x48, 0x8B, 0x05, 0, 0, 0, 0];
let k = r.classify(R_X86_64_GOTPCRELX, &d, 3);
assert_eq!(k, RelaxKind::NoRelax);
}
#[test]
fn t_relax_apply_boundary() {
let mut r = X86RelaxationOptimization::new();
let mut d = vec![OPCODE_CALL_NEAR];
r.do_call_to_jmp(&mut d, 0);
assert_eq!(d[0], OPCODE_JMP_NEAR);
let mut d2 = vec![0x00];
r.do_call_to_jmp(&mut d2, 0); assert_eq!(d2[0], 0x00);
}
#[test]
fn t_relax_mov_to_lea_rex() {
let mut r = X86RelaxationOptimization::new();
let mut d = vec![OPCODE_REX_W, OPCODE_MOV_R64_M, 0x05, 0, 0, 0, 0];
r.do_mov_to_lea(&mut d, 2);
assert_eq!(d[1], OPCODE_LEA_R64_M);
assert_eq!(r.stats.gotpcrelx_mov_to_lea, 1);
}
#[test]
fn t_relax_tls_disabled_preserves() {
let mut r = X86RelaxationOptimization {
tls_relax: false,
..X86RelaxationOptimization::new()
};
assert_eq!(r.classify(R_X86_64_TLSGD, &[], 0), RelaxKind::NoRelax);
}
#[test]
fn t_opt_phases_called_in_order() {
let mut o = X86LLDOpt::new_aggressive();
o.icf.min_size = 16;
o.enable_gc_sections = true;
let d = vec![0x90; 32];
let mut v = vec![
me(".text._start", vec![0xC3; 32]),
me(".text.dup1", d.clone()),
me(".text.dup2", d.clone()),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![]);
let s = o.run_all(&mut v, &["_start".into()], &HashSet::new(), &rm, &sd);
assert!(s.had_effect());
}
#[test]
fn t_comdat_exact_match_strategy() {
let mut c = X86COMDATFolding::new().with_strategy(COMDATSelectionStrategy::ExactMatch);
c.groups = vec![
COMDATGroup {
signature: "a".into(),
member_sections: vec![0],
selection: COMDAT_SELECTION_EXACT_MATCH,
total_size: 32,
content_hash: fnv1a_hash_64(b"x"),
is_live: true,
folded_into: None,
},
COMDATGroup {
signature: "b".into(),
member_sections: vec![1],
selection: COMDAT_SELECTION_EXACT_MATCH,
total_size: 32,
content_hash: fnv1a_hash_64(b"x"),
is_live: true,
folded_into: None,
},
];
let mut v = vec![me(".text.a", vec![0xC3; 32]), me(".text.b", vec![0xC3; 32])];
let r = c.run(&mut v);
assert_eq!(r, 1);
}
#[test]
fn t_comdat_selection_types() {
let c = X86COMDATFolding::new();
assert_eq!(COMDAT_SELECTION_ANY, 0);
assert_eq!(COMDAT_SELECTION_LARGEST, 2);
assert_eq!(COMDAT_SELECTION_NODUPLICATES, 3);
}
#[test]
fn t_lto_const_prop_disabled() {
let mut l = X86LTOOptimizations {
const_prop: false,
..X86LTOOptimizations::new()
};
let mut v = vec![me(".text.f", vec![0xC3; 32])];
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
InternalizationStrategy::None_,
);
assert_eq!(s.constant_propagations, 0);
}
#[test]
fn t_lto_internalization_all() {
let mut l = X86LTOOptimizations::new();
l.internalization = true;
let mut v = vec![me(".text.hidden", vec![0xC3; 32])];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["hidden_fn".into()]);
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&sd,
InternalizationStrategy::All,
);
assert_eq!(s.functions_internalized, 1);
}
#[test]
fn t_lto_internalization_none() {
let mut l = X86LTOOptimizations::new();
l.internalization = true;
let mut v = vec![me(".text.f", vec![0xC3; 32])];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["f".into()]);
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&sd,
InternalizationStrategy::None_,
);
assert_eq!(s.functions_internalized, 0);
}
#[test]
fn t_lto_eliminate_globals() {
let mut l = X86LTOOptimizations::new();
let mut v = vec![
md(".data.unused", vec![0x42; 32]),
md(".rodata.unused", vec![0x00; 32]),
];
let s = l.run(
&mut v,
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
InternalizationStrategy::All,
);
assert!(s.globals_eliminated > 0);
assert!(s.size_reduction_bytes > 0);
}
#[test]
fn t_ordering_distance_improvement() {
let mut o = X86LinkerOrdering::new();
o.current_order = vec![0, 2, 1];
o.call_graph = vec![CallGraphNode {
section_index: 0,
name: "A".into(),
callees: vec![1, 2],
callers: vec![],
frequency: 1.0,
size: 16,
placed: false,
}];
let bad = o.total_call_distance();
o.current_order = vec![0, 1, 2];
let good = o.total_call_distance();
assert!(good < bad);
}
#[test]
fn t_ordering_already_ordered() {
let mut o = X86LinkerOrdering::new();
let mut v = vec![me(".text.a", vec![0xC3]), me(".text.b", vec![0xC3])];
o.run(&mut v, &HashMap::new(), OrderingHeuristic::None);
assert!(!o.ordered);
}
#[test]
fn t_section_content_hash_differs() {
let d = vec![0x90; 32];
let h1 = section_content_hash(&d, SHF_ALLOC | SHF_EXECINSTR, SHT_PROGBITS, 16);
let h2 = section_content_hash(&d, SHF_ALLOC, SHT_PROGBITS, 16);
assert_ne!(h1, h2);
}
#[test]
fn t_align_boundary_cases() {
assert_eq!(align_up_opt(u64::MAX - 15, 16), u64::MAX - 15);
assert_eq!(align_down_opt(u64::MAX, 16), u64::MAX - 15);
assert_eq!(align_up_opt(1, 1), 1);
assert_eq!(align_down_opt(1, 1), 1);
}
#[test]
fn t_tail_overlap_exact() {
assert_eq!(find_tail_overlap(&[1, 2, 3], &[1, 2, 3], 3), Some(3));
assert_eq!(find_tail_overlap(&[1, 2, 3], &[1, 2, 3], 4), None);
}
#[test]
fn t_comdat_name_edge_cases() {
assert!(!is_comdat_section_name(""));
assert!(!is_comdat_section_name("."));
assert!(!is_comdat_section_name(".text"));
assert!(!is_comdat_section_name(".a"));
assert_eq!(extract_comdat_group_name(""), None);
assert_eq!(extract_comdat_group_name("."), None);
assert_eq!(extract_comdat_group_name(".a"), None);
}
#[test]
fn t_insn_length_edge() {
assert_eq!(x86_insn_length(0x00), 0);
assert_eq!(x86_insn_length(0xFF), 0);
assert_eq!(x86_insn_length(0x0F), 2);
assert_eq!(x86_insn_length(0xEB), 2);
}
#[test]
fn t_is_rex_prefix_all_values() {
for b in 0x40u8..=0x4F {
assert!(is_rex_prefix(b));
}
for b in [0x00u8, 0x30, 0x50, 0x90, 0xFF].iter() {
assert!(!is_rex_prefix(*b));
}
}
#[test]
fn t_is_modrm_variants() {
assert!(is_modrm_register(0xC0)); assert!(is_modrm_register(0xFF)); assert!(is_modrm_memory_nodisp(0x00)); assert!(is_modrm_memory_nodisp(0x03)); assert!(!is_modrm_memory_nodisp(0x05)); assert!(!is_modrm_memory_nodisp(0x04)); }
#[test]
fn t_double_hash_collision_resistance() {
let mut set = HashSet::new();
for i in 0..1000 {
let data = format!("func_{}", i).into_bytes();
let (h1, h2) = double_hash(&data);
set.insert((h1, h2));
}
assert_eq!(set.len(), 1000); }
#[test]
fn t_checksum32_collision() {
let c1 = checksum32(b"hello");
let c2 = checksum32(b"world");
let _ = (c1, c2);
}
#[test]
fn t_icf_debug_info_empty() {
let icf = X86ICF::new();
assert!(icf.debug_info_mapping().is_empty());
}
#[test]
fn t_icf_get_thunks_empty() {
let icf = X86ICF::new();
assert!(icf.get_thunks().is_empty());
}
#[test]
fn t_icf_get_fold_mapping_empty() {
let icf = X86ICF::new();
assert!(icf.get_fold_mapping().is_empty());
}
#[test]
fn t_opt_get_diagnostics() {
let mut o = X86LLDOpt::new();
o.diag(X86OptDiagLevel::Debug, "test".into(), None);
let diags = o.get_diagnostics();
assert_eq!(diags.len(), 1);
}
#[test]
fn t_gc_get_removed_empty() {
let gc = X86GCSections::new();
assert!(gc.get_removed_sections().is_empty());
}
#[test]
fn t_gc_get_live_flags_empty() {
let gc = X86GCSections::new();
assert!(gc.get_live_flags().is_empty());
}
#[test]
fn t_comdat_get_mapping_empty() {
let c = X86COMDATFolding::new();
assert!(c.get_fold_mapping().is_empty());
}
#[test]
fn t_merge_get_entries_empty() {
let m = X86MergeSimilarSections::new();
assert!(m.get_string_entries().is_empty());
assert!(m.get_constant_entries().is_empty());
}
#[test]
fn t_relax_get_stats_default() {
let r = X86RelaxationOptimization::new();
let s = r.get_stats();
assert_eq!(s.total, 0);
}
#[test]
fn t_order_get_order_default() {
let o = X86LinkerOrdering::new();
assert!(o.get_order().is_empty());
}
#[test]
fn t_comdat_is_group_live_bounds() {
let mut c = X86COMDATFolding::new();
c.groups.push(COMDATGroup {
signature: "g".into(),
member_sections: vec![],
selection: COMDAT_SELECTION_ANY,
total_size: 0,
content_hash: 0,
is_live: true,
folded_into: None,
});
assert!(c.is_group_live(0));
assert!(!c.is_group_live(999));
}
#[test]
fn t_opt_summary_format_zero() {
let s = X86LLDOptSummary {
sections_before: 0,
sections_after: 0,
sections_removed: 0,
bytes_before: 0,
bytes_after: 0,
bytes_saved: 0,
diagnostics_count: 0,
};
let f = s.format_summary();
assert!(f.contains("0 → 0"));
}
#[test]
fn t_opt_summary_zero_division() {
let s = X86LLDOptSummary {
sections_before: 0,
sections_after: 0,
sections_removed: 0,
bytes_before: 0,
bytes_after: 0,
bytes_saved: 0,
diagnostics_count: 0,
};
assert_eq!(s.reduction_ratio(), 0.0);
assert_eq!(s.section_reduction_ratio(), 0.0);
}
#[test]
fn t_lto_merge_constants_subpass() {
let mut l = X86LTOOptimizations::new();
let d = vec![0x42; 16];
let mut v = vec![md(".data.a", d.clone()), md(".data.b", d.clone())];
l.do_merge_constants(&mut v);
assert!(v[1].data.is_empty());
}
#[test]
fn t_all_debug_fmt_impls() {
let _ = format!("{:?}", X86LLDOpt::new());
let _ = format!("{:?}", X86ICF::new());
let _ = format!("{:?}", X86LTOOptimizations::new());
let _ = format!("{:?}", X86GCSections::new());
let _ = format!("{:?}", X86COMDATFolding::new());
let _ = format!("{:?}", X86MergeSimilarSections::new());
let _ = format!("{:?}", X86RelaxationOptimization::new());
let _ = format!("{:?}", X86LinkerOrdering::new());
}
#[test]
fn t_relax_call_not_tail_when_non_ret() {
let mut r = X86RelaxationOptimization::new();
let d = vec![OPCODE_CALL_NEAR, 0, 0, 0, 0, 0xCC, 0xC3]; r.scan_tail_calls(0, &d);
assert!(r.relax_records.is_empty());
}
#[test]
fn t_icf_hash_instructions_empty() {
let icf = X86ICF::new();
let h = icf.hash_instructions(&[], &HashSet::new());
assert_eq!(h, FNV_OFFSET_BASIS);
}
#[test]
fn t_icf_hash_instructions_no_relocs() {
let icf = X86ICF::new();
let data = vec![0x90, 0x90, 0xC3];
let h1 = icf.hash_instructions(&data, &HashSet::new());
let h2 = fnv1a_hash_64(&data);
assert_eq!(h1, h2);
}
#[test]
fn t_hash_skip_relocs_edge() {
let data = vec![0x90, 0x90];
let mut ro = HashSet::new();
ro.insert(0);
let mut rs = HashMap::new();
rs.insert(0, 100); let h = fnv1a_hash_skip_relocs(&data, &ro, &rs);
let expected_data = vec![0u8, 0u8];
assert_eq!(h, fnv1a_hash_64(&expected_data));
}
#[test]
fn t_opt_estimate_no_sections() {
let o = X86LLDOpt::new();
let est = o.estimate_savings(&[], &HashMap::new());
assert_eq!(est, 0);
}
#[test]
fn t_opt_estimate_non_executable() {
let o = X86LLDOpt {
enable_icf: true,
icf: X86ICF::new().with_min_size(16).with_functions_only(true),
..X86LLDOpt::new()
};
let d = vec![0x90; 32];
let v = vec![md(".data.a", d.clone()), md(".data.b", d.clone())];
let est = o.estimate_savings(&v, &HashMap::new());
assert_eq!(est, 0); }
#[test]
fn t_opt_estimate_string_merge() {
let o = X86LLDOpt {
enable_merge_sections: true,
..X86LLDOpt::new()
};
let s1 = b"hello\0";
let s2 = b"hello\0";
let v = vec![
mstr(".rodata.s1", s1.to_vec()),
mstr(".rodata.s2", s2.to_vec()),
];
let est = o.estimate_savings(&v, &HashMap::new());
assert!(est > 0);
}
#[test]
fn t_relax_print_stats_all_zero() {
let r = X86RelaxationOptimization::new();
let s = r.print_stats();
assert!(s.contains("0 total"));
}
#[test]
fn t_lto_print_stats_all_zero() {
let l = X86LTOOptimizations::new();
let s = l.print_stats();
assert!(s.contains("0B saved"));
}
#[test]
fn t_icf_print_summary_all_zero() {
let icf = X86ICF::new();
let s = icf.print_icf_summary();
assert!(s.contains("0 sections folded"));
}
#[test]
fn t_integration_x86_lld_full() {
use crate::x86::x86_lld_full::X86LLDArch;
let arch = X86LLDArch::X86_64;
assert!(arch.is_64bit());
assert_eq!(arch.elf_machine(), EM_X86_64);
}
#[test]
fn t_integration_elf_relocations() {
assert_eq!(R_X86_64_PC32, 2);
assert_eq!(R_X86_64_PLT32, 4);
assert_eq!(R_X86_64_GOTPCREL, 9);
}
}
#[derive(Debug, Clone, Default)]
pub struct ICFDetailedStats {
pub considered: usize,
pub folded: usize,
pub bytes_saved: u64,
pub hash_collisions: usize,
pub thunks_generated: usize,
pub iterations: usize,
pub skipped_small: usize,
pub skipped_address_taken: usize,
pub skipped_non_exec: usize,
}
impl ICFDetailedStats {
pub fn report(&self) -> String {
let mut r = String::new();
r.push_str(&format!("ICF Detailed Statistics\n"));
r.push_str(&format!(" Considered: {}\n", self.considered));
r.push_str(&format!(" Folded: {}\n", self.folded));
r.push_str(&format!(" Skipped (small): {}\n", self.skipped_small));
r.push_str(&format!(
" Skipped (addr-taken): {}\n",
self.skipped_address_taken
));
r.push_str(&format!(
" Skipped (non-exec): {}\n",
self.skipped_non_exec
));
r.push_str(&format!(" Bytes saved: {}\n", self.bytes_saved));
r.push_str(&format!(
" Thunks generated: {}\n",
self.thunks_generated
));
r.push_str(&format!(
" Hash collisions: {}\n",
self.hash_collisions
));
r.push_str(&format!(" Iterations: {}\n", self.iterations));
r
}
}
#[derive(Debug, Clone)]
pub struct OptimizationPhaseOrder {
pub phases: Vec<String>,
pub enabled: Vec<bool>,
}
impl OptimizationPhaseOrder {
pub fn default_order() -> Self {
Self {
phases: vec![
"comdat_folding".into(),
"icf".into(),
"gc_sections".into(),
"merge_sections".into(),
"relaxation".into(),
"lto".into(),
"ordering".into(),
],
enabled: vec![true; 7],
}
}
pub fn set_enabled(&mut self, phase: &str, enabled: bool) {
for (i, p) in self.phases.iter().enumerate() {
if p == phase {
self.enabled[i] = enabled;
return;
}
}
}
pub fn is_enabled(&self, phase: &str) -> bool {
self.phases
.iter()
.enumerate()
.find(|(_, p)| *p == phase)
.map(|(i, _)| self.enabled[i])
.unwrap_or(false)
}
pub fn enabled_phases(&self) -> Vec<&str> {
self.phases
.iter()
.enumerate()
.filter(|(i, _)| self.enabled[*i])
.map(|(_, p)| p.as_str())
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SectionSnapshot {
pub name: String,
pub size_before: usize,
pub size_after: usize,
pub modified: bool,
pub removed: bool,
}
impl SectionSnapshot {
pub fn diff(before: &[X86InputSection], after: &[X86InputSection]) -> Vec<SectionSnapshot> {
let mut snaps = Vec::new();
for (i, b) in before.iter().enumerate() {
let size_before = b.data.len();
let (size_after, removed) = if i < after.len() {
let a = &after[i];
(a.data.len(), a.data.is_empty() && !b.data.is_empty())
} else {
(0, true)
};
snaps.push(SectionSnapshot {
name: b.name.clone(),
size_before,
size_after,
modified: size_before != size_after,
removed,
});
}
snaps
}
pub fn count_modified(snapshots: &[SectionSnapshot]) -> usize {
snapshots.iter().filter(|s| s.modified).count()
}
pub fn count_removed(snapshots: &[SectionSnapshot]) -> usize {
snapshots.iter().filter(|s| s.removed).count()
}
pub fn total_bytes_saved(snapshots: &[SectionSnapshot]) -> u64 {
snapshots
.iter()
.map(|s| (s.size_before as i64 - s.size_after as i64).max(0) as u64)
.sum()
}
}
pub fn serialize_icf_mapping(mapping: &[ICFMapping]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(mapping.len() as u32).to_le_bytes());
for m in mapping {
out.extend_from_slice(&(m.folded_section as u32).to_le_bytes());
out.extend_from_slice(&(m.canonical_section as u32).to_le_bytes());
out.push(if m.has_thunk { 1 } else { 0 });
}
out
}
pub fn deserialize_icf_mapping(data: &[u8]) -> Option<Vec<ICFMapping>> {
if data.len() < 4 {
return None;
}
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut mapping = Vec::with_capacity(count);
let mut offset = 4;
for _ in 0..count {
if offset + 9 > data.len() {
return None;
}
let folded = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
let canonical = u32::from_le_bytes([
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]) as usize;
let has_thunk = data[offset + 8] != 0;
mapping.push(ICFMapping {
folded_section: folded,
canonical_section: canonical,
has_thunk,
thunk_offset: None,
});
offset += 9;
}
Some(mapping)
}
pub fn serialize_relax_record(record: &RelaxRecord) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(record.section_index as u32).to_le_bytes());
out.extend_from_slice(&record.offset.to_le_bytes());
out.extend_from_slice(&record.original_type.to_le_bytes());
out.push(record.relax_kind as u8);
out.extend_from_slice(&record.addend.to_le_bytes());
out.push(if record.applied { 1 } else { 0 });
out
}
pub fn serialize_relax_records(records: &[RelaxRecord]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(records.len() as u32).to_le_bytes());
for r in records {
out.extend_from_slice(&serialize_relax_record(r));
}
out
}
#[derive(Debug)]
pub struct X86LLDOptPipeline {
pub optimizer: X86LLDOpt,
pub phase_order: OptimizationPhaseOrder,
pub dry_run: bool,
pub verbose_progress: bool,
pub phase_snapshots: Vec<(String, Vec<SectionSnapshot>)>,
}
impl X86LLDOptPipeline {
pub fn new() -> Self {
Self {
optimizer: X86LLDOpt::new(),
phase_order: OptimizationPhaseOrder::default_order(),
dry_run: false,
verbose_progress: false,
phase_snapshots: Vec::new(),
}
}
pub fn new_aggressive() -> Self {
Self {
optimizer: X86LLDOpt::new_aggressive(),
phase_order: OptimizationPhaseOrder::default_order(),
dry_run: false,
verbose_progress: false,
phase_snapshots: Vec::new(),
}
}
pub fn with_dry_run(mut self, dr: bool) -> Self {
self.dry_run = dr;
self
}
pub fn with_verbose_progress(mut self, vp: bool) -> Self {
self.verbose_progress = vp;
self
}
pub fn run(
&mut self,
sections: &mut [X86InputSection],
entry_symbols: &[String],
exported_symbols: &HashSet<String>,
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
symbol_definitions: &HashMap<usize, Vec<String>>,
) -> X86LLDOptSummary {
if self.dry_run {
let est = self
.optimizer
.estimate_savings(sections, symbol_definitions);
return X86LLDOptSummary {
sections_before: sections.iter().filter(|s| !s.data.is_empty()).count(),
sections_after: sections.iter().filter(|s| !s.data.is_empty()).count(),
sections_removed: 0,
bytes_before: sections.iter().map(|s| s.data.len() as u64).sum(),
bytes_after: sections.iter().map(|s| s.data.len() as u64).sum() - est,
bytes_saved: est,
diagnostics_count: 0,
};
}
self.phase_snapshots.clear();
let start_sections: Vec<X86InputSection> = sections.to_vec();
self.sync_phases_to_optimizer();
let summary = self.optimizer.run_all(
sections,
entry_symbols,
exported_symbols,
relocation_map,
symbol_definitions,
);
self.phase_snapshots.push((
"combined".into(),
SectionSnapshot::diff(&start_sections, sections),
));
if self.verbose_progress {
let default_snap: Vec<SectionSnapshot> = Vec::new();
let snaps = self
.phase_snapshots
.last()
.map(|(_, s)| s)
.unwrap_or(&default_snap);
let snaps = self
.phase_snapshots
.last()
.map(|(_, s)| s)
.unwrap_or(&default_snap);
let total_mod = SectionSnapshot::count_modified(snaps);
let total_rem = SectionSnapshot::count_removed(snaps);
eprintln!(
"Optimization complete: {} sections modified, {} removed, {} bytes saved",
total_mod, total_rem, summary.bytes_saved
);
}
summary
}
fn sync_phases_to_optimizer(&mut self) {
self.phase_order
.set_enabled("comdat_folding", self.optimizer.enable_comdat_folding);
self.phase_order
.set_enabled("icf", self.optimizer.enable_icf);
self.phase_order
.set_enabled("gc_sections", self.optimizer.enable_gc_sections);
self.phase_order
.set_enabled("merge_sections", self.optimizer.enable_merge_sections);
self.phase_order
.set_enabled("relaxation", self.optimizer.enable_relaxation);
self.phase_order
.set_enabled("lto", self.optimizer.enable_lto_opts);
self.phase_order
.set_enabled("ordering", self.optimizer.enable_ordering);
}
pub fn phase_summary(&self) -> String {
let enabled = self.phase_order.enabled_phases();
format!(
"Pipeline phases: {} ({} enabled)",
self.phase_order.phases.len(),
enabled.len()
)
}
pub fn last_snapshot(&self) -> Option<&(String, Vec<SectionSnapshot>)> {
self.phase_snapshots.last()
}
}
impl Default for X86LLDOptPipeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod extended_tests {
use super::*;
use crate::x86::x86_lld_full::X86InputSection;
use std::collections::{HashMap, HashSet};
fn me_test(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.to_string(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
}
}
fn mstr_test(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.to_string(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_MERGE | SHF_STRINGS,
sh_addralign: 1,
section_index: 0,
sh_entsize: 0,
}
}
#[test]
fn test_stats_default() {
let s = ICFDetailedStats::default();
assert_eq!(s.considered, 0);
assert_eq!(s.folded, 0);
}
#[test]
fn test_stats_report() {
let s = ICFDetailedStats {
considered: 100,
folded: 20,
bytes_saved: 5000,
thunks_generated: 20,
..Default::default()
};
let r = s.report();
assert!(r.contains("Considered: 100"));
assert!(r.contains("Folded: 20"));
assert!(r.contains("Bytes saved: 5000"));
}
#[test]
fn test_phase_order_default() {
let po = OptimizationPhaseOrder::default_order();
assert_eq!(po.phases.len(), 7);
assert!(po.is_enabled("icf"));
assert!(po.is_enabled("gc_sections"));
}
#[test]
fn test_phase_order_disable() {
let mut po = OptimizationPhaseOrder::default_order();
po.set_enabled("icf", false);
assert!(!po.is_enabled("icf"));
assert!(po.is_enabled("gc_sections"));
}
#[test]
fn test_phase_order_enabled_phases() {
let mut po = OptimizationPhaseOrder::default_order();
po.set_enabled("lto", false);
po.set_enabled("ordering", false);
let enabled = po.enabled_phases();
assert_eq!(enabled.len(), 5);
}
#[test]
fn test_phase_order_unknown() {
let po = OptimizationPhaseOrder::default_order();
assert!(!po.is_enabled("nonexistent"));
}
#[test]
fn test_snapshot_diff() {
let before = vec![
me_test(".text.a", vec![0xC3; 64]),
me_test(".text.b", vec![0x90; 32]),
];
let mut after = before.clone();
after[1].data.clear(); let snaps = SectionSnapshot::diff(&before, &after);
assert_eq!(snaps.len(), 2);
assert!(!snaps[0].modified);
assert!(!snaps[0].removed);
assert!(snaps[1].modified);
assert!(snaps[1].removed);
}
#[test]
fn test_snapshot_count_modified() {
let before = vec![
me_test(".text.a", vec![0xC3; 64]),
me_test(".text.b", vec![0x90; 32]),
];
let mut after = before.clone();
after[0].data = vec![0xC3; 48]; after[1].data.clear(); let snaps = SectionSnapshot::diff(&before, &after);
assert_eq!(SectionSnapshot::count_modified(&snaps), 2);
assert_eq!(SectionSnapshot::count_removed(&snaps), 1);
}
#[test]
fn test_snapshot_total_saved() {
let before = vec![me_test(".text.a", vec![0xC3; 100])];
let after = vec![me_test(".text.a", vec![0xC3; 60])];
let snaps = SectionSnapshot::diff(&before, &after);
assert_eq!(SectionSnapshot::total_bytes_saved(&snaps), 40);
}
#[test]
fn test_snapshot_extra_in_before() {
let before = vec![
me_test(".text.a", vec![0xC3; 64]),
me_test(".text.b", vec![0x90; 32]),
];
let after = vec![me_test(".text.a", vec![0xC3; 64])];
let snaps = SectionSnapshot::diff(&before, &after);
assert_eq!(snaps.len(), 2);
assert!(snaps[1].removed);
assert!(snaps[1].modified);
}
#[test]
fn test_serialize_icf_mapping_roundtrip() {
let mapping = vec![
ICFMapping {
folded_section: 42,
canonical_section: 7,
has_thunk: true,
thunk_offset: None,
},
ICFMapping {
folded_section: 100,
canonical_section: 50,
has_thunk: false,
thunk_offset: Some(0x1000),
},
];
let data = serialize_icf_mapping(&mapping);
let restored = deserialize_icf_mapping(&data).unwrap();
assert_eq!(restored.len(), 2);
assert_eq!(restored[0].folded_section, 42);
assert_eq!(restored[0].canonical_section, 7);
assert!(restored[0].has_thunk);
assert_eq!(restored[1].folded_section, 100);
assert!(!restored[1].has_thunk);
}
#[test]
fn test_serialize_icf_mapping_empty() {
let data = serialize_icf_mapping(&[]);
assert_eq!(data.len(), 4); let restored = deserialize_icf_mapping(&data).unwrap();
assert!(restored.is_empty());
}
#[test]
fn test_serialize_icf_mapping_invalid() {
assert!(deserialize_icf_mapping(&[]).is_none());
assert!(deserialize_icf_mapping(&[1, 0, 0, 0]).is_none()); }
#[test]
fn test_serialize_relax_record() {
let rec = RelaxRecord {
section_index: 7,
offset: 0x1000,
original_type: R_X86_64_GOTPCRELX,
relax_kind: RelaxKind::GOTPCRELX_MovToLea,
symbol_name: Some("foo".into()),
addend: -4,
applied: true,
};
let data = serialize_relax_record(&rec);
assert!(data.len() > 0);
}
#[test]
fn test_serialize_relax_records() {
let records = vec![
RelaxRecord {
section_index: 0,
offset: 0,
original_type: 1,
relax_kind: RelaxKind::CallToJmp,
symbol_name: None,
addend: 0,
applied: true,
},
RelaxRecord {
section_index: 1,
offset: 4,
original_type: 2,
relax_kind: RelaxKind::TlsGdToLe,
symbol_name: None,
addend: 0,
applied: false,
},
];
let data = serialize_relax_records(&records);
assert!(data.len() > 8);
}
#[test]
fn test_pipeline_new() {
let p = X86LLDOptPipeline::new();
assert!(!p.dry_run);
assert!(!p.verbose_progress);
}
#[test]
fn test_pipeline_aggressive() {
let p = X86LLDOptPipeline::new_aggressive();
assert!(p.optimizer.enable_icf);
}
#[test]
fn test_pipeline_dry_run() {
let mut p = X86LLDOptPipeline::new().with_dry_run(true);
let mut sections = vec![
me_test(".text.a", vec![0x90; 32]),
me_test(".text.b", vec![0x90; 32]),
];
let summary = p.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!sections[0].data.is_empty());
assert!(!sections[1].data.is_empty());
assert_eq!(summary.sections_removed, 0);
}
#[test]
fn test_pipeline_dry_run_estimate() {
let mut p = X86LLDOptPipeline::new_aggressive().with_dry_run(true);
let data = vec![0x90; 32];
let mut sections = vec![
me_test(".text.a", data.clone()),
me_test(".text.b", data.clone()),
];
let summary = p.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(summary.bytes_saved > 0 || summary.sections_removed == 0);
}
#[test]
fn test_pipeline_normal_run() {
let mut p = X86LLDOptPipeline::new_aggressive();
p.optimizer.icf.min_size = 16;
p.optimizer.enable_ordering = false;
p.optimizer.enable_lto_opts = false;
let data = vec![0x90; 32];
let mut sections = vec![
me_test(".text._start", vec![0xC3; 32]),
me_test(".text.dup1", data.clone()),
me_test(".text.dup2", data.clone()),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let summary = p.run(
&mut sections,
&["_start".into()],
&HashSet::new(),
&HashMap::new(),
&sd,
);
assert!(summary.had_effect());
}
#[test]
fn test_pipeline_phase_summary() {
let p = X86LLDOptPipeline::new();
let s = p.phase_summary();
assert!(s.contains("Pipeline phases"));
}
#[test]
fn test_pipeline_last_snapshot() {
let mut p = X86LLDOptPipeline::new_aggressive();
p.optimizer.enable_ordering = false;
p.optimizer.enable_lto_opts = false;
let data = vec![0x90; 32];
let mut sections = vec![
me_test(".text._start", vec![0xC3; 32]),
me_test(".text.x", data.clone()),
me_test(".text.y", data.clone()),
];
p.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
let snap = p.last_snapshot();
assert!(snap.is_some());
}
#[test]
fn test_pipeline_default() {
let _p = X86LLDOptPipeline::default();
}
#[test]
fn test_pipeline_verbose() {
let mut p = X86LLDOptPipeline::new().with_verbose_progress(true);
assert!(p.verbose_progress);
let mut sections = vec![me_test(".text.a", vec![0xC3; 32])];
p.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
}
#[test]
fn test_end_to_end_many_functions() {
let mut pipeline = X86LLDOptPipeline::new_aggressive();
pipeline.optimizer.icf.min_size = 16;
pipeline.optimizer.enable_ordering = false;
pipeline.optimizer.enable_lto_opts = false;
let unique_count = 10;
let copies_per_unique = 5;
let mut sections = Vec::new();
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sections.push(me_test(".text._start", vec![0xC3; 32]));
sd.insert(0, vec!["_start".into()]);
for u in 0..unique_count {
let data = format!("func_{}_body", u).into_bytes();
let padded: Vec<u8> = data.into_iter().cycle().take(32).collect();
for c in 0..copies_per_unique {
let idx = sections.len();
sections.push(me_test(
&format!(".text.unique{}_copy{}", u, c),
padded.clone(),
));
sd.insert(idx, vec![format!("unique{}_copy{}", u, c)]);
}
}
let summary = pipeline.run(
&mut sections,
&["_start".into()],
&HashSet::new(),
&HashMap::new(),
&sd,
);
let expected_folds = (copies_per_unique - 1) * unique_count;
assert!(summary.sections_removed >= expected_folds);
}
#[test]
fn test_end_to_end_with_string_merging() {
let mut pipeline = X86LLDOptPipeline::new_aggressive();
pipeline.optimizer.icf.min_size = 16;
pipeline.optimizer.enable_merge_sections = true;
pipeline.optimizer.enable_ordering = false;
pipeline.optimizer.enable_lto_opts = false;
let s1 = b"shared_string\0unique_a\0";
let s2 = b"shared_string\0unique_b\0";
let mut sections = vec![
me_test(".text._start", vec![0xC3; 32]),
mstr_test(".rodata.str1", s1.to_vec()),
mstr_test(".rodata.str2", s2.to_vec()),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let summary = pipeline.run(
&mut sections,
&["_start".into()],
&HashSet::new(),
&HashMap::new(),
&sd,
);
assert!(summary.bytes_saved > 0);
}
#[test]
fn test_sync_phases_reflects_optimizer() {
let mut pipeline = X86LLDOptPipeline::new();
pipeline.optimizer.enable_icf = true;
pipeline.optimizer.enable_gc_sections = false;
pipeline.sync_phases_to_optimizer();
assert!(pipeline.phase_order.is_enabled("icf"));
assert!(!pipeline.phase_order.is_enabled("gc_sections"));
}
#[test]
fn test_snapshot_empty_sections() {
let before: Vec<X86InputSection> = vec![];
let after: Vec<X86InputSection> = vec![];
let snaps = SectionSnapshot::diff(&before, &after);
assert!(snaps.is_empty());
}
#[test]
fn test_serialize_icf_large_values() {
let mapping = vec![ICFMapping {
folded_section: 0xFFFFFFFF,
canonical_section: 0x12345678,
has_thunk: true,
thunk_offset: None,
}];
let data = serialize_icf_mapping(&mapping);
let restored = deserialize_icf_mapping(&data).unwrap();
assert_eq!(restored[0].folded_section, 0xFFFFFFFF);
assert_eq!(restored[0].canonical_section, 0x12345678);
}
#[test]
fn test_pipeline_snapshots_accumulate() {
let mut pipeline = X86LLDOptPipeline::new_aggressive();
pipeline.optimizer.enable_ordering = false;
pipeline.optimizer.enable_lto_opts = false;
pipeline.optimizer.icf.min_size = 16;
let data = vec![0x90; 32];
let mut sections = vec![
me_test(".text._start", vec![0xC3; 32]),
me_test(".text.a", data.clone()),
me_test(".text.b", data.clone()),
];
pipeline.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
let snap = pipeline.last_snapshot();
assert!(snap.is_some());
let (name, _) = snap.unwrap();
assert_eq!(name, "combined");
}
}
#[inline]
pub fn has_flag(value: u64, flag: u64) -> bool {
(value & flag) != 0
}
#[inline]
pub fn set_flag(value: u64, flag: u64) -> u64 {
value | flag
}
#[inline]
pub fn clear_flag(value: u64, flag: u64) -> u64 {
value & !flag
}
pub fn partition_indices(total: usize, num_partitions: usize) -> Vec<Vec<usize>> {
let mut partitions = vec![Vec::new(); num_partitions];
for i in 0..total {
partitions[i % num_partitions].push(i);
}
partitions
}
pub fn merge_dedup_sorted(a: &[usize], b: &[usize]) -> Vec<usize> {
let mut result = Vec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
Ordering::Less => {
result.push(a[i]);
i += 1;
}
Ordering::Greater => {
result.push(b[j]);
j += 1;
}
Ordering::Equal => {
result.push(a[i]);
i += 1;
j += 1;
}
}
}
result.extend_from_slice(&a[i..]);
result.extend_from_slice(&b[j..]);
result
}
pub fn clz64(mut x: u64) -> u32 {
if x == 0 {
return 64;
}
let mut n = 0u32;
if (x & 0xFFFFFFFF00000000) == 0 {
n += 32;
x <<= 32;
}
if (x & 0xFFFF000000000000) == 0 {
n += 16;
x <<= 16;
}
if (x & 0xFF00000000000000) == 0 {
n += 8;
x <<= 8;
}
if (x & 0xF000000000000000) == 0 {
n += 4;
x <<= 4;
}
if (x & 0xC000000000000000) == 0 {
n += 2;
x <<= 2;
}
if (x & 0x8000000000000000) == 0 {
n += 1;
}
n
}
pub fn estimate_entropy(data: &[u8]) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut freq = [0u32; 256];
for &b in data {
freq[b as usize] += 1;
}
let len = data.len() as f64;
let mut entropy = 0.0f64;
for &count in &freq {
if count > 0 {
let p = count as f64 / len;
entropy -= p * p.log2();
}
}
entropy
}
pub fn similarity_score(a: &[u8], b: &[u8]) -> f64 {
let len = a.len().min(b.len());
if len == 0 {
return 0.0;
}
let matches = a
.iter()
.zip(b.iter())
.take(len)
.filter(|(x, y)| x == y)
.count();
matches as f64 / len as f64
}
pub fn analyze_comdat_groups(groups: &[COMDATGroup]) -> COMDATAnalysis {
let total = groups.len();
let live = groups.iter().filter(|g| g.is_live).count();
let total_size: u64 = groups.iter().map(|g| g.total_size).sum();
let largest_size = groups.iter().map(|g| g.total_size).max().unwrap_or(0);
let avg_size = if total > 0 {
total_size / total as u64
} else {
0
};
let selection_dist: HashMap<u8, usize> = groups.iter().fold(HashMap::new(), |mut acc, g| {
*acc.entry(g.selection).or_default() += 1;
acc
});
COMDATAnalysis {
total_groups: total,
live_groups: live,
folded_groups: total - live,
total_bytes: total_size,
largest_group_bytes: largest_size,
average_group_bytes: avg_size,
selection_distribution: selection_dist,
}
}
#[derive(Debug, Clone)]
pub struct COMDATAnalysis {
pub total_groups: usize,
pub live_groups: usize,
pub folded_groups: usize,
pub total_bytes: u64,
pub largest_group_bytes: u64,
pub average_group_bytes: u64,
pub selection_distribution: HashMap<u8, usize>,
}
impl COMDATAnalysis {
pub fn report(&self) -> String {
let mut r = String::new();
r.push_str("COMDAT Group Analysis\n");
r.push_str(&format!(" Total groups: {}\n", self.total_groups));
r.push_str(&format!(" Live groups: {}\n", self.live_groups));
r.push_str(&format!(" Folded groups: {}\n", self.folded_groups));
r.push_str(&format!(" Total bytes: {}\n", self.total_bytes));
r.push_str(&format!(
" Largest group: {} bytes\n",
self.largest_group_bytes
));
r.push_str(&format!(
" Average group: {} bytes\n",
self.average_group_bytes
));
r.push_str(" Selections:\n");
for (&sel, &count) in &self.selection_distribution {
let name = match sel {
COMDAT_SELECTION_ANY => "ANY",
COMDAT_SELECTION_EXACT_MATCH => "EXACT_MATCH",
COMDAT_SELECTION_LARGEST => "LARGEST",
COMDAT_SELECTION_NODUPLICATES => "NODUPLICATES",
COMDAT_SELECTION_SAME_SIZE => "SAME_SIZE",
_ => "UNKNOWN",
};
r.push_str(&format!(" {}: {}\n", name, count));
}
r
}
}
#[cfg(test)]
mod util_tests {
use super::*;
#[test]
fn test_has_flag() {
assert!(has_flag(0x7, 0x4));
assert!(!has_flag(0x3, 0x4));
}
#[test]
fn test_set_flag() {
assert_eq!(set_flag(0x3, 0x4), 0x7);
assert_eq!(set_flag(0x7, 0x4), 0x7);
}
#[test]
fn test_clear_flag() {
assert_eq!(clear_flag(0x7, 0x4), 0x3);
assert_eq!(clear_flag(0x3, 0x4), 0x3);
}
#[test]
fn test_partition_indices() {
let parts = partition_indices(10, 3);
assert_eq!(parts.len(), 3);
let total: usize = parts.iter().map(|v| v.len()).sum();
assert_eq!(total, 10);
}
#[test]
fn test_merge_dedup_sorted() {
let a = vec![1, 3, 5, 7];
let b = vec![2, 3, 5, 8];
let result = merge_dedup_sorted(&a, &b);
assert_eq!(result, vec![1, 2, 3, 5, 7, 8]);
}
#[test]
fn test_merge_dedup_empty() {
assert_eq!(merge_dedup_sorted(&[], &[]), Vec::<usize>::new());
assert_eq!(merge_dedup_sorted(&[1], &[]), vec![1]);
assert_eq!(merge_dedup_sorted(&[], &[1]), vec![1]);
}
#[test]
fn test_clz64() {
assert_eq!(clz64(0), 64);
assert_eq!(clz64(1), 63);
assert_eq!(clz64(0x8000000000000000), 0);
assert_eq!(clz64(0xFFFFFFFFFFFFFFFF), 0);
}
#[test]
fn test_estimate_entropy() {
let uniform = vec![0u8, 1, 2, 3, 4, 5, 6, 7];
let e1 = estimate_entropy(&uniform);
assert!(e1 > 2.0);
let constant = vec![0x90u8; 100];
let e2 = estimate_entropy(&constant);
assert!(e2 < 0.1);
assert_eq!(estimate_entropy(&[]), 0.0);
}
#[test]
fn test_similarity_score() {
let a = vec![1, 2, 3, 4, 5];
let b = vec![1, 2, 3, 4, 5];
assert!((similarity_score(&a, &b) - 1.0).abs() < 0.001);
let c = vec![9, 9, 9, 9, 9];
assert!((similarity_score(&a, &c) - 0.0).abs() < 0.001);
assert_eq!(similarity_score(&[], &[]), 0.0);
}
#[test]
fn test_comdat_analysis() {
let groups = vec![
COMDATGroup {
signature: "a".into(),
member_sections: vec![],
selection: COMDAT_SELECTION_ANY,
total_size: 100,
content_hash: 0,
is_live: true,
folded_into: None,
},
COMDATGroup {
signature: "b".into(),
member_sections: vec![],
selection: COMDAT_SELECTION_LARGEST,
total_size: 200,
content_hash: 0,
is_live: false,
folded_into: Some(0),
},
];
let analysis = analyze_comdat_groups(&groups);
assert_eq!(analysis.total_groups, 2);
assert_eq!(analysis.live_groups, 1);
assert_eq!(analysis.folded_groups, 1);
assert_eq!(analysis.largest_group_bytes, 200);
}
#[test]
fn test_comdat_analysis_report() {
let groups = vec![COMDATGroup {
signature: "g".into(),
member_sections: vec![],
selection: COMDAT_SELECTION_ANY,
total_size: 50,
content_hash: 0,
is_live: true,
folded_into: None,
}];
let analysis = analyze_comdat_groups(&groups);
let report = analysis.report();
assert!(report.contains("Total groups:"));
assert!(report.contains("ANY: 1"));
}
}
pub fn is_icf_eligible(
section: &X86InputSection,
min_size: usize,
functions_only: bool,
fold_rodata: bool,
) -> bool {
if section.data.is_empty() || section.data.len() < min_size {
return false;
}
if functions_only {
if (section.sh_flags & SHF_EXECINSTR) != 0 {
return true;
}
if fold_rodata && (section.sh_flags & (SHF_ALLOC | SHF_WRITE)) == SHF_ALLOC {
return true;
}
return false;
}
(section.sh_flags & SHF_ALLOC) != 0
}
pub fn is_gc_eligible(section: &X86InputSection) -> bool {
if section.data.is_empty() {
return false;
}
if section.sh_type == SHT_NOBITS || section.sh_type == SHT_NOTE {
return false;
}
true
}
pub fn section_name_matches_pattern(name: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if !pattern.contains('*') && !pattern.contains('?') {
return name == pattern;
}
glob_match_impl(name.as_bytes(), pattern.as_bytes())
}
fn glob_match_impl(name: &[u8], pat: &[u8]) -> bool {
let (mut ni, mut pi) = (0usize, 0usize);
let (mut star_ni, mut star_pi) = (None, None);
loop {
if pi < pat.len() && pat[pi] == b'*' {
star_pi = Some(pi);
star_ni = Some(ni);
pi += 1;
} else if pi < pat.len() && ni < name.len() && (pat[pi] == b'?' || pat[pi] == name[ni]) {
pi += 1;
ni += 1;
} else if let Some(sp) = star_pi {
pi = sp + 1;
star_ni = Some(star_ni.unwrap() + 1);
ni = star_ni.unwrap();
} else {
break;
}
}
while pi < pat.len() && pat[pi] == b'*' {
pi += 1;
}
pi == pat.len() && ni == name.len()
}
pub fn detect_function_prologue(data: &[u8]) -> bool {
if data.len() < 3 {
return false;
}
if data[0] == 0x55 && data.len() >= 4 {
if data[1] == 0x48 && data[2] == 0x89 && data[3] == 0xE5 {
return true;
}
}
if data[0] == 0x55 {
return true;
}
if data[0] == 0x55 && data.len() >= 4 && data[1] == 0x48 && data[2] == 0x83 && data[3] == 0xEC {
return true;
}
false
}
pub fn detect_function_epilogue(data: &[u8]) -> bool {
if data.len() < 1 {
return false;
}
let last = data.len() - 1;
if data[last] == 0xC3 {
if last >= 2 && data[last - 1] == 0x5D {
return true;
}
if last >= 2 && data[last - 1] == 0xC9 {
return true;
}
return true;
}
false
}
pub fn estimate_instruction_count(data: &[u8]) -> usize {
let mut count = 0;
let mut offset = 0;
while offset < data.len() {
let opcode = data[offset];
let len = x86_insn_length(opcode);
if len > 0 {
offset += len;
count += 1;
} else {
if is_rex_prefix(opcode) && offset + 1 < data.len() {
let len2 = x86_insn_length(data[offset + 1]);
if len2 > 0 {
offset += 1 + len2;
count += 1;
continue;
}
}
if opcode == 0x0F && offset + 2 < data.len() {
offset += 2;
count += 1;
continue;
}
offset += 1;
count += 1;
}
}
count
}
#[cfg(test)]
mod helper_tests {
use super::*;
use crate::x86::x86_lld_full::X86InputSection;
fn me_h(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.into(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
}
}
fn md_h(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.into(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC,
sh_addralign: 8,
section_index: 0,
sh_entsize: 0,
}
}
#[test]
fn test_is_icf_eligible_exec() {
let s = me_h(".text.foo", vec![0xC3; 32]);
assert!(is_icf_eligible(&s, 16, true, false));
}
#[test]
fn test_is_icf_eligible_data_not_exec() {
let s = md_h(".data.foo", vec![0x42; 32]);
assert!(!is_icf_eligible(&s, 16, true, false));
}
#[test]
fn test_is_icf_eligible_rodata_with_flag() {
let s = md_h(".rodata.foo", vec![0x42; 32]);
assert!(is_icf_eligible(&s, 16, true, true));
}
#[test]
fn test_is_icf_eligible_too_small() {
let s = me_h(".text.foo", vec![0xC3; 8]);
assert!(!is_icf_eligible(&s, 16, true, false));
}
#[test]
fn test_is_icf_eligible_functions_only_false() {
let s = md_h(".data.foo", vec![0x42; 32]);
assert!(is_icf_eligible(&s, 16, false, false));
}
#[test]
fn test_is_gc_eligible_normal() {
let s = me_h(".text.foo", vec![0xC3; 32]);
assert!(is_gc_eligible(&s));
}
#[test]
fn test_is_gc_eligible_empty() {
let s = me_h(".text.foo", vec![]);
assert!(!is_gc_eligible(&s));
}
#[test]
fn test_is_gc_eligible_nobits() {
let s = X86InputSection {
name: ".bss".into(),
data: vec![0; 32],
sh_type: SHT_NOBITS,
sh_flags: SHF_ALLOC | SHF_WRITE,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
};
assert!(!is_gc_eligible(&s));
}
#[test]
fn test_section_name_matches_pattern() {
assert!(section_name_matches_pattern(".text.foo", ".text.*"));
assert!(section_name_matches_pattern("abc", "*"));
assert!(section_name_matches_pattern("abc", "abc"));
assert!(!section_name_matches_pattern("abc", "xyz"));
assert!(section_name_matches_pattern(".text._Z3foov", ".text._Z*"));
assert!(!section_name_matches_pattern("abc", "a?d"));
assert!(section_name_matches_pattern("abd", "a?d"));
}
#[test]
fn test_glob_match_edge_cases() {
assert!(glob_match_impl(b"", b""));
assert!(glob_match_impl(b"", b"*"));
assert!(glob_match_impl(b"a", b"*"));
assert!(!glob_match_impl(b"", b"a"));
assert!(glob_match_impl(b"abcdef", b"a*f"));
assert!(!glob_match_impl(b"abcdef", b"a*g"));
}
#[test]
fn test_detect_function_prologue() {
let p1 = vec![0x55, 0x48, 0x89, 0xE5];
assert!(detect_function_prologue(&p1));
let p2 = vec![0x55, 0xC3];
assert!(detect_function_prologue(&p2));
let p3 = vec![0x90, 0x90, 0xC3];
assert!(!detect_function_prologue(&p3));
assert!(!detect_function_prologue(&[]));
let p4 = vec![0x55, 0x48, 0x83, 0xEC, 0x20];
assert!(detect_function_prologue(&p4));
}
#[test]
fn test_detect_function_epilogue() {
let e1 = vec![0x48, 0x89, 0xEC, 0x5D, 0xC3];
assert!(detect_function_epilogue(&e1));
let e2 = vec![0x48, 0x89, 0xEC, 0xC9, 0xC3];
assert!(detect_function_epilogue(&e2));
let e3 = vec![0x90, 0xC3];
assert!(detect_function_epilogue(&e3));
assert!(!detect_function_epilogue(&[]));
let e4 = vec![0x90, 0x90, 0x90];
assert!(!detect_function_epilogue(&e4));
}
#[test]
fn test_estimate_instruction_count() {
let nops = vec![0x90; 100];
assert_eq!(estimate_instruction_count(&nops), 100);
let cr = vec![OPCODE_CALL_NEAR, 0, 0, 0, 0, OPCODE_RET_NEAR];
assert_eq!(estimate_instruction_count(&cr), 2);
assert_eq!(estimate_instruction_count(&[]), 0);
let mixed = vec![0x48, 0x89, 0xE5, 0x90, 0x90, 0xC3];
let count = estimate_instruction_count(&mixed);
assert!(count > 0);
}
#[test]
fn test_instruction_count_with_rex() {
let data = vec![0x48, 0x8B, 0x05, 0, 0, 0, 0];
let count = estimate_instruction_count(&data);
assert_eq!(count, 1); }
#[test]
fn test_instruction_count_with_two_byte_opcode() {
let data = vec![0x0F, 0x1F, 0x00, 0xC3];
let count = estimate_instruction_count(&data);
assert_eq!(count, 2); }
#[test]
fn test_glob_star_only() {
assert!(glob_match_impl(b"anything", b"*"));
assert!(glob_match_impl(b"", b"*"));
}
#[test]
fn test_glob_multiple_stars() {
assert!(glob_match_impl(b".text._Z3foo", b".*._Z*"));
assert!(glob_match_impl(b"abc_def_ghi", b"a*_d*_g*"));
}
#[test]
fn test_glob_question_mark() {
assert!(glob_match_impl(b"abc", b"a?c"));
assert!(!glob_match_impl(b"ac", b"a?c"));
assert!(glob_match_impl(b"aXc", b"a?c"));
}
#[test]
fn test_estimate_entropy_binary() {
let half_zeroes: Vec<u8> = (0..64).map(|i| if i < 32 { 0 } else { 0xFF }).collect();
let e = estimate_entropy(&half_zeroes);
assert!(e < 1.5 && e > 0.5);
}
#[test]
fn test_partition_indices_uneven() {
let parts = partition_indices(7, 3);
assert_eq!(parts[0].len(), 3);
assert_eq!(parts[1].len(), 2);
assert_eq!(parts[2].len(), 2);
}
#[test]
fn test_partition_indices_single() {
let parts = partition_indices(5, 1);
assert_eq!(parts[0].len(), 5);
}
#[test]
fn test_comdat_analysis_edge_cases() {
let analysis = analyze_comdat_groups(&[]);
assert_eq!(analysis.total_groups, 0);
assert_eq!(analysis.largest_group_bytes, 0);
assert_eq!(analysis.average_group_bytes, 0);
}
}
#[derive(Debug, Clone)]
pub struct OptimizationTimer {
pub name: String,
pub start_ticks: u64,
pub end_ticks: u64,
pub running: bool,
}
impl OptimizationTimer {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
start_ticks: 0,
end_ticks: 0,
running: false,
}
}
pub fn start(&mut self) {
self.start_ticks = Self::now_ticks();
self.running = true;
}
pub fn stop(&mut self) -> u64 {
self.end_ticks = Self::now_ticks();
self.running = false;
self.elapsed()
}
pub fn elapsed(&self) -> u64 {
if self.running {
Self::now_ticks() - self.start_ticks
} else {
self.end_ticks - self.start_ticks
}
}
fn now_ticks() -> u64 {
0
}
}
#[derive(Debug, Clone, Default)]
pub struct PassProfile {
pub name: String,
pub total_ticks: u64,
pub run_count: usize,
pub sections_processed: usize,
pub bytes_processed: u64,
pub sections_affected: usize,
pub bytes_saved: u64,
}
impl PassProfile {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
..Default::default()
}
}
pub fn merge(&mut self, other: &PassProfile) {
self.total_ticks += other.total_ticks;
self.run_count += other.run_count;
self.sections_processed += other.sections_processed;
self.bytes_processed += other.bytes_processed;
self.sections_affected += other.sections_affected;
self.bytes_saved += other.bytes_saved;
}
pub fn throughput(&self) -> f64 {
if self.total_ticks == 0 {
return 0.0;
}
self.bytes_processed as f64 / self.total_ticks as f64
}
pub fn summary(&self) -> String {
format!(
"{}: {} runs, {} sections ({:.1}% affected), {} bytes saved, {:.0} B/tick",
self.name,
self.run_count,
self.sections_processed,
if self.sections_processed > 0 {
self.sections_affected as f64 / self.sections_processed as f64 * 100.0
} else {
0.0
},
self.bytes_saved,
self.throughput()
)
}
}
#[derive(Debug, Clone, Default)]
pub struct OptimizationProfiler {
pub profiles: HashMap<String, PassProfile>,
pub current_pass: Option<String>,
pub current_timer: OptimizationTimer,
}
impl OptimizationProfiler {
pub fn new() -> Self {
Self::default()
}
pub fn begin_pass(&mut self, name: &str) {
self.current_pass = Some(name.to_string());
self.profiles
.entry(name.to_string())
.or_insert_with(|| PassProfile::new(name));
self.current_timer = OptimizationTimer::new(name);
self.current_timer.start();
}
pub fn end_pass(
&mut self,
sections_processed: usize,
bytes_processed: u64,
sections_affected: usize,
bytes_saved: u64,
) {
let elapsed = self.current_timer.stop();
if let Some(ref name) = self.current_pass {
if let Some(profile) = self.profiles.get_mut(name) {
profile.total_ticks += elapsed;
profile.run_count += 1;
profile.sections_processed += sections_processed;
profile.bytes_processed += bytes_processed;
profile.sections_affected += sections_affected;
profile.bytes_saved += bytes_saved;
}
}
self.current_pass = None;
}
pub fn get_profile(&self, name: &str) -> Option<&PassProfile> {
self.profiles.get(name)
}
pub fn report(&self) -> String {
let mut r = String::from("Optimization Profiler Report\n");
r.push_str(&"=".repeat(60));
r.push('\n');
let mut names: Vec<&String> = self.profiles.keys().collect();
names.sort();
let total_bytes_saved: u64 = self.profiles.values().map(|p| p.bytes_saved).sum();
let total_ticks: u64 = self.profiles.values().map(|p| p.total_ticks).sum();
for name in names {
if let Some(profile) = self.profiles.get(name) {
r.push_str(&format!(" {}\n", profile.summary()));
}
}
r.push_str(&"=".repeat(60));
r.push('\n');
r.push_str(&format!(
"TOTAL: {} bytes saved in {} ticks\n",
total_bytes_saved, total_ticks
));
r
}
pub fn reset(&mut self) {
self.profiles.clear();
self.current_pass = None;
}
}
#[cfg(test)]
mod profiler_tests {
use super::*;
#[test]
fn test_timer_new() {
let t = OptimizationTimer::new("test");
assert_eq!(t.name, "test");
assert!(!t.running);
}
#[test]
fn test_timer_start_stop() {
let mut t = OptimizationTimer::new("timed");
t.start();
assert!(t.running);
let elapsed = t.stop();
assert!(!t.running);
assert_eq!(elapsed, 0); }
#[test]
fn test_pass_profile_new() {
let pp = PassProfile::new("icf");
assert_eq!(pp.name, "icf");
assert_eq!(pp.run_count, 0);
}
#[test]
fn test_pass_profile_merge() {
let mut a = PassProfile {
name: "test".into(),
run_count: 2,
sections_processed: 100,
bytes_processed: 5000,
sections_affected: 20,
bytes_saved: 1000,
total_ticks: 50,
};
let b = PassProfile {
run_count: 1,
sections_processed: 50,
bytes_processed: 2500,
sections_affected: 10,
bytes_saved: 500,
total_ticks: 25,
..Default::default()
};
a.merge(&b);
assert_eq!(a.run_count, 3);
assert_eq!(a.bytes_saved, 1500);
}
#[test]
fn test_pass_profile_throughput() {
let pp = PassProfile {
total_ticks: 100,
bytes_processed: 20000,
..Default::default()
};
assert_eq!(pp.throughput(), 200.0);
let pp2 = PassProfile::default();
assert_eq!(pp2.throughput(), 0.0);
}
#[test]
fn test_pass_profile_summary() {
let pp = PassProfile {
name: "gc".into(),
run_count: 1,
sections_processed: 1000,
sections_affected: 300,
bytes_saved: 50000,
total_ticks: 100,
bytes_processed: 100000,
};
let s = pp.summary();
assert!(s.contains("gc:"));
assert!(s.contains("30.0% affected"));
assert!(s.contains("50000 bytes saved"));
}
#[test]
fn test_profiler_begin_end() {
let mut prof = OptimizationProfiler::new();
prof.begin_pass("icf");
prof.end_pass(100, 5000, 10, 500);
let profile = prof.get_profile("icf").unwrap();
assert_eq!(profile.sections_processed, 100);
assert_eq!(profile.bytes_saved, 500);
}
#[test]
fn test_profiler_report() {
let mut prof = OptimizationProfiler::new();
prof.begin_pass("icf");
prof.end_pass(50, 2000, 5, 200);
prof.begin_pass("gc");
prof.end_pass(100, 4000, 30, 800);
let report = prof.report();
assert!(report.contains("icf"));
assert!(report.contains("gc"));
assert!(report.contains("TOTAL:"));
}
#[test]
fn test_profiler_reset() {
let mut prof = OptimizationProfiler::new();
prof.begin_pass("icf");
prof.end_pass(10, 100, 1, 10);
prof.reset();
assert!(prof.profiles.is_empty());
}
#[test]
fn test_profiler_unknown_pass() {
let prof = OptimizationProfiler::new();
assert!(prof.get_profile("nonexistent").is_none());
}
#[test]
fn test_profiler_default() {
let _prof = OptimizationProfiler::default();
}
}
pub fn validate_sections_consistency(sections: &[X86InputSection]) -> Vec<String> {
let mut warnings = Vec::new();
for (i, section) in sections.iter().enumerate() {
if section.data.is_empty() {
continue;
}
if section.sh_addralign > 0 && !section.sh_addralign.is_power_of_two() {
warnings.push(format!(
"Section '{}' has non-power-of-two alignment: {}",
section.name, section.sh_addralign
));
}
if section.sh_entsize > 0 && section.data.len() as u64 % section.sh_entsize != 0 {
warnings.push(format!(
"Section '{}' data length {} is not a multiple of sh_entsize {}",
section.name,
section.data.len(),
section.sh_entsize
));
}
if section.sh_addralign > PAGE_SIZE {
warnings.push(format!(
"Section '{}' has alignment {} larger than page size",
section.name, section.sh_addralign
));
}
if (section.sh_flags & (SHF_EXECINSTR | SHF_WRITE)) == (SHF_EXECINSTR | SHF_WRITE) {
warnings.push(format!(
"Section '{}' is both writable and executable",
section.name
));
}
let _ = i; }
warnings
}
pub fn validate_relocations(
sections: &[X86InputSection],
relocation_map: &HashMap<usize, Vec<X86InputRelocation>>,
) -> Vec<String> {
let mut warnings = Vec::new();
let n = sections.len();
for (§ion_idx, relocs) in relocation_map {
if section_idx >= n {
warnings.push(format!(
"Relocations reference nonexistent section index {}",
section_idx
));
continue;
}
for reloc in relocs {
let target = reloc.section_index as usize;
if target >= n {
warnings.push(format!(
"Relocation in section {} targets nonexistent section {}",
section_idx, target
));
}
if reloc.offset as usize + 4 > sections[section_idx].data.len()
&& !sections[section_idx].data.is_empty()
{
warnings.push(format!(
"Relocation at offset {} in section {} extends past section data (len {})",
reloc.offset,
section_idx,
sections[section_idx].data.len()
));
}
}
}
warnings
}
#[cfg(test)]
mod validation_tests {
use super::*;
use crate::x86::x86_lld_full::{X86InputRelocation, X86InputSection};
fn me_v(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.into(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
}
}
fn make_reloc_v(si: u32, off: u64, ty: u32) -> X86InputRelocation {
X86InputRelocation {
offset: off,
symbol_index: 0,
rel_type: ty,
addend: 0,
section_index: si,
}
}
#[test]
fn test_validate_empty_sections() {
let warnings = validate_sections_consistency(&[]);
assert!(warnings.is_empty());
}
#[test]
fn test_validate_good_sections() {
let sections = vec![
me_v(".text.foo", vec![0xC3; 32]),
me_v(".text.bar", vec![0xC3; 32]),
];
let warnings = validate_sections_consistency(§ions);
assert!(warnings.is_empty());
}
#[test]
fn test_validate_bad_alignment() {
let s = X86InputSection {
name: "bad".into(),
data: vec![0xC3; 32],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC,
sh_addralign: 3,
section_index: 0,
sh_entsize: 0,
};
let warnings = validate_sections_consistency(&[s]);
assert!(!warnings.is_empty());
assert!(warnings[0].contains("non-power-of-two"));
}
#[test]
fn test_validate_entsize_mismatch() {
let s = X86InputSection {
name: "table".into(),
data: vec![0; 10],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC,
sh_addralign: 8,
section_index: 0,
sh_entsize: 4,
};
let warnings = validate_sections_consistency(&[s]);
assert!(!warnings.is_empty());
assert!(warnings[0].contains("not a multiple"));
}
#[test]
fn test_validate_large_alignment() {
let s = X86InputSection {
name: "big".into(),
data: vec![0xC3; 64],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC,
sh_addralign: 2 * PAGE_SIZE,
section_index: 0,
sh_entsize: 0,
};
let warnings = validate_sections_consistency(&[s]);
assert!(!warnings.is_empty());
}
#[test]
fn test_validate_writable_executable() {
let s = X86InputSection {
name: "wxe".into(),
data: vec![0xC3; 32],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_WRITE | SHF_EXECINSTR,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
};
let warnings = validate_sections_consistency(&[s]);
assert!(!warnings.is_empty());
assert!(warnings[0].contains("writable and executable"));
}
#[test]
fn test_validate_relocations_empty() {
let warnings = validate_relocations(&[], &HashMap::new());
assert!(warnings.is_empty());
}
#[test]
fn test_validate_relocations_bad_section() {
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(999, vec![make_reloc_v(0, 0, R_X86_64_PC32)]);
let warnings = validate_relocations(&[], &rm);
assert!(!warnings.is_empty());
}
#[test]
fn test_validate_relocations_bad_target() {
let sections = vec![me_v(".text.foo", vec![0xC3; 32])];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![make_reloc_v(999, 0, R_X86_64_PC32)]);
let warnings = validate_relocations(§ions, &rm);
assert!(!warnings.is_empty());
}
#[test]
fn test_validate_relocations_out_of_bounds() {
let sections = vec![me_v(".text.foo", vec![0xC3; 8])];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![make_reloc_v(0, 100, R_X86_64_PC32)]);
let warnings = validate_relocations(§ions, &rm);
assert!(!warnings.is_empty());
}
#[test]
fn test_validate_relocations_clean() {
let sections = vec![me_v(".text.foo", vec![0xC3; 32])];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![make_reloc_v(0, 0, R_X86_64_PC32)]);
let warnings = validate_relocations(§ions, &rm);
assert!(warnings.is_empty());
}
}
#[cfg(test)]
mod final_tests {
use super::*;
use crate::x86::x86_lld_full::{X86InputRelocation, X86InputSection};
use std::collections::{HashMap, HashSet};
fn me_f(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.into(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
}
}
fn mro_f(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.into(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC,
sh_addralign: 8,
section_index: 0,
sh_entsize: 0,
}
}
fn mr_f(si: u32, off: u64, ty: u32) -> X86InputRelocation {
X86InputRelocation {
offset: off,
symbol_index: 0,
rel_type: ty,
addend: 0,
section_index: si,
}
}
#[test]
fn test_full_pipeline_all_phases() {
let mut pipeline = X86LLDOptPipeline::new_aggressive();
pipeline.optimizer.icf.min_size = 16;
pipeline.optimizer.enable_ordering = false; pipeline.optimizer.enable_lto_opts = false;
let identical_data = vec![0x90; 32];
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
me_f(".text.helper1", identical_data.clone()),
me_f(".text.helper2", identical_data.clone()),
me_f(".text.helper3", identical_data.clone()),
me_f(".text.dead_code", vec![0xCC; 64]),
mro_f(".rodata.const1", vec![0x42; 16]),
mro_f(".rodata.const2", vec![0x42; 16]),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let entry = vec!["_start".to_string()];
let summary = pipeline.run(&mut sections, &entry, &HashSet::new(), &HashMap::new(), &sd);
assert!(summary.had_effect());
assert!(summary.bytes_saved > 0);
}
#[test]
fn test_comdat_with_icf_interaction() {
let mut opt = X86LLDOpt::new();
opt.enable_comdat_folding = true;
opt.enable_icf = true;
opt.icf_safety = ICFSafetyLevel::All;
opt.icf.min_size = 16;
let data = vec![0x90; 32];
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
me_f(".text.func1", data.clone()),
me_f(".text.func2", data.clone()),
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let summary = opt.run_all(
&mut sections,
&["_start".into()],
&HashSet::new(),
&HashMap::new(),
&sd,
);
assert!(summary.sections_removed > 0 || summary.bytes_saved > 0);
}
#[test]
fn test_gc_before_merge() {
let mut pipeline = X86LLDOptPipeline::new_aggressive();
pipeline.optimizer.enable_ordering = false;
pipeline.optimizer.enable_lto_opts = false;
pipeline.optimizer.icf.min_size = 16;
let strings = b"hello\0world\0";
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
X86InputSection {
name: ".rodata.str_live".into(),
data: strings.to_vec(),
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_MERGE | SHF_STRINGS,
sh_addralign: 1,
section_index: 0,
sh_entsize: 0,
},
X86InputSection {
name: ".rodata.str_dead".into(),
data: strings.to_vec(),
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_MERGE | SHF_STRINGS,
sh_addralign: 1,
section_index: 0,
sh_entsize: 0,
},
];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let summary = pipeline.run(
&mut sections,
&["_start".into()],
&HashSet::new(),
&HashMap::new(),
&sd,
);
assert!(summary.had_effect());
}
#[test]
fn test_relax_with_icf() {
let mut opt = X86LLDOpt::new();
opt.enable_icf = true;
opt.enable_relaxation = true;
opt.icf_safety = ICFSafetyLevel::All;
opt.icf.min_size = 16;
let call_ret = vec![OPCODE_CALL_NEAR, 0, 0, 0, 0, OPCODE_RET_NEAR];
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
me_f(".text.func1", call_ret.clone()),
me_f(".text.func2", call_ret.clone()),
];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(1, vec![mr_f(0, 1, R_X86_64_PLT32)]);
rm.insert(2, vec![mr_f(0, 1, R_X86_64_PLT32)]);
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["_start".into()]);
let summary = opt.run_all(&mut sections, &["_start".into()], &HashSet::new(), &rm, &sd);
assert!(summary.had_effect());
}
#[test]
fn test_opt_icf_only_mode() {
let mut opt = X86LLDOpt::new();
opt.enable_icf = true;
opt.icf_safety = ICFSafetyLevel::All;
opt.icf.min_size = 16;
let data = vec![0x90; 32];
let mut sections = vec![me_f(".text.a", data.clone()), me_f(".text.b", data.clone())];
let removed = opt.run_icf_only(&mut sections, &HashMap::new());
assert_eq!(removed, 1);
}
#[test]
fn test_opt_gc_only_mode() {
let mut opt = X86LLDOpt::new();
opt.gc_sections = X86GCSections::new();
let mut sections = vec![
me_f(".init", vec![0xC3]),
me_f(".text.dead", vec![0x90; 32]),
];
let removed = opt.run_gc_only(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(removed, 1);
}
#[test]
fn test_pipeline_verbose_run() {
let mut pipeline = X86LLDOptPipeline::new_aggressive()
.with_verbose_progress(true)
.with_dry_run(false);
pipeline.optimizer.enable_ordering = false;
pipeline.optimizer.enable_lto_opts = false;
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
me_f(".text.a", vec![0x90; 32]),
me_f(".text.b", vec![0x90; 32]),
];
let summary = pipeline.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(summary.had_effect());
}
#[test]
fn test_dry_run_does_not_mutate() {
let mut pipeline = X86LLDOptPipeline::new_aggressive().with_dry_run(true);
let data = vec![0x90; 32];
let mut sections = vec![me_f(".text.a", data.clone()), me_f(".text.b", data.clone())];
let _summary = pipeline.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!sections[0].data.is_empty());
assert!(!sections[1].data.is_empty());
assert_eq!(sections[0].data, data);
assert_eq!(sections[1].data, data);
}
#[test]
fn test_profiler_integration() {
let mut prof = OptimizationProfiler::new();
let mut opt = X86LLDOpt::new_aggressive();
opt.enable_ordering = false;
opt.enable_lto_opts = false;
opt.icf.min_size = 16;
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
me_f(".text.dup1", vec![0x90; 32]),
me_f(".text.dup2", vec![0x90; 32]),
];
prof.begin_pass("full_pipeline");
let n = sections.len();
let b: u64 = sections.iter().map(|s| s.data.len() as u64).sum();
let summary = opt.run_all(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
prof.end_pass(n, b, summary.sections_removed, summary.bytes_saved);
let profile = prof.get_profile("full_pipeline").unwrap();
assert_eq!(profile.run_count, 1);
assert!(profile.bytes_saved > 0);
}
#[test]
fn test_validate_after_optimization() {
let mut opt = X86LLDOpt::new_aggressive();
opt.enable_ordering = false;
opt.enable_lto_opts = false;
opt.icf.min_size = 16;
let mut sections = vec![
me_f(".text._start", vec![0xC3; 32]),
me_f(".text.x", vec![0x90; 32]),
me_f(".text.y", vec![0x90; 32]),
];
opt.run_all(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
let warnings = validate_sections_consistency(§ions);
assert!(warnings.iter().all(|w| !w.contains("non-power-of-two")
|| sections.iter().any(|s| !s.sh_addralign.is_power_of_two())));
}
#[test]
fn test_stress_all_passes() {
let mut pipeline = X86LLDOptPipeline::new_aggressive();
pipeline.optimizer.icf.min_size = 16;
pipeline.optimizer.ordering.heuristic = OrderingHeuristic::SizeDescending;
let num_funcs = 20;
let num_copies = 3;
let mut sections = Vec::new();
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
sections.push(me_f(".text._start", vec![0xC3; 32]));
sd.insert(0, vec!["_start".into()]);
let mut section_idx = 1;
for f in 0..num_funcs {
let body: Vec<u8> = format!("fn_{}", f)
.into_bytes()
.into_iter()
.cycle()
.take(32)
.collect();
for _c in 0..num_copies {
sections.push(me_f(&format!(".text.fn{}_copy{}", f, _c), body.clone()));
sd.insert(section_idx, vec![format!("fn{}_copy{}", f, _c)]);
section_idx += 1;
}
}
sections.push(X86InputSection {
name: ".rodata.str".into(),
data: b"hello\0world\0hello\0".to_vec(),
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_MERGE | SHF_STRINGS,
sh_addralign: 1,
section_index: 0,
sh_entsize: 0,
});
let summary = pipeline.run(&mut sections, &["_start".into()], &HashSet::new(), &rm, &sd);
assert!(summary.had_effect());
}
#[test]
fn test_icf_iterations_capped() {
let mut icf = X86ICF::new().with_min_size(16);
let d = vec![0x90; 32];
let mut v: Vec<X86InputSection> = (0..200)
.map(|i| me_f(&format!(".text.f{}", i), d.clone()))
.collect();
let (r, _) = icf.run(&mut v, &HashMap::new(), ICFSafetyLevel::All);
assert!(r > 0);
assert!(icf.iterations <= ICF_MAX_ITERATIONS);
}
#[test]
fn test_gc_respects_shf_retain() {
let mut gc = X86GCSections::new();
let mut v = vec![
X86InputSection {
name: ".text.retained".into(),
data: vec![0xC3; 32],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_EXECINSTR | SHF_GNU_RETAIN,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
},
me_f(".text.dead", vec![0x90; 16]),
];
gc.keep_patterns.push(".text.retained".into());
let (r, _) = gc.run(
&mut v,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!v[0].data.is_empty());
assert!(v[1].data.is_empty());
}
#[test]
fn test_lto_eliminate_globals_skips_exported() {
let mut l = X86LTOOptimizations::new();
let mut v = vec![X86InputSection {
name: ".rodata.exported".into(),
data: vec![0x42; 32],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC,
sh_addralign: 8,
section_index: 0,
sh_entsize: 0,
}];
let mut sd: HashMap<usize, Vec<String>> = HashMap::new();
sd.insert(0, vec!["exported_data".into()]);
let mut exported: HashSet<String> = HashSet::new();
exported.insert("exported_data".into());
let s = l.run(
&mut v,
&exported,
&HashMap::new(),
&sd,
InternalizationStrategy::Conservative,
);
assert_eq!(s.globals_eliminated, 0); }
#[test]
fn test_merge_empty_sections_no_op() {
let mut m = X86MergeSimilarSections::new();
let mut v = vec![X86InputSection {
name: ".rodata.empty".into(),
data: vec![],
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_MERGE | SHF_STRINGS,
sh_addralign: 1,
section_index: 0,
sh_entsize: 0,
}];
let saved = m.run(&mut v);
assert_eq!(saved, 0);
}
#[test]
fn test_relax_gotpcrelx_rex_prefix_detection() {
let mut r = X86RelaxationOptimization::new();
let mut d = vec![OPCODE_REX_W, OPCODE_MOV_R64_M, 0x05, 0x00, 0x00, 0x00, 0x00];
r.do_mov_to_lea(&mut d, 2);
assert_eq!(d[1], OPCODE_LEA_R64_M);
}
#[test]
fn test_ordering_empty_sections() {
let mut lo = X86LinkerOrdering::new();
let mut v: Vec<X86InputSection> = vec![];
lo.run(&mut v, &HashMap::new(), OrderingHeuristic::CallGraph);
assert!(!lo.ordered);
assert!(lo.get_order().is_empty());
}
#[test]
fn test_profiler_end_pass_no_current() {
let mut prof = OptimizationProfiler::new();
prof.end_pass(0, 0, 0, 0);
}
#[test]
fn test_profiler_report_empty() {
let prof = OptimizationProfiler::new();
let report = prof.report();
assert!(report.contains("TOTAL:"));
}
#[test]
fn test_pipeline_dry_run_with_verbose() {
let mut pipeline = X86LLDOptPipeline::new_aggressive()
.with_dry_run(true)
.with_verbose_progress(true);
let mut sections = vec![me_f(".text.a", vec![0xC3; 32])];
let summary = pipeline.run(
&mut sections,
&[],
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
);
assert!(!sections[0].data.is_empty());
}
#[test]
fn test_snapshot_count_zero_when_identical() {
let before = vec![me_f(".text.a", vec![0xC3; 64])];
let after = before.clone();
let snaps = SectionSnapshot::diff(&before, &after);
assert_eq!(SectionSnapshot::count_modified(&snaps), 0);
assert_eq!(SectionSnapshot::count_removed(&snaps), 0);
}
#[test]
fn test_similarity_edge() {
assert_eq!(similarity_score(&[1, 2, 3], &[]), 0.0);
assert_eq!(similarity_score(&[], &[1, 2, 3]), 0.0);
}
#[test]
fn test_clz64_edge_cases() {
assert_eq!(clz64(u64::MAX), 0);
assert_eq!(clz64(1u64 << 63), 0);
assert_eq!(clz64(1u64 << 62), 1);
}
#[test]
fn test_validate_relocations_good() {
let sections = vec![me_f(".text.a", vec![0xC3; 64])];
let mut rm: HashMap<usize, Vec<X86InputRelocation>> = HashMap::new();
rm.insert(0, vec![mr_f(0, 0, R_X86_64_PC32)]);
assert!(validate_relocations(§ions, &rm).is_empty());
}
#[test]
fn test_estimate_entropy_edge() {
assert_eq!(estimate_entropy(&[]), 0.0);
let single_byte = vec![0x42];
assert!((estimate_entropy(&single_byte) - 0.0).abs() < 0.001);
}
}
pub const ICF_MAX_CANDIDATES_SINGLE_THREAD: usize = 100_000;
pub const COMDAT_MAX_GROUPS_DIRECT: usize = 10_000;
pub const GC_MAX_BFS_DEPTH: usize = 10_000;
pub const STRING_MERGE_MAX_LENGTH: usize = 256;
pub const X86_64_FETCH_WIDTH: usize = 16;
pub const X86_64_ICACHE_LINE_SIZE: usize = 64;
impl ICFCandidate {
pub fn can_fold_into(&self, other: &ICFCandidate) -> bool {
self.sh_flags == other.sh_flags
&& self.sh_addralign == other.sh_addralign
&& self.data.len() == other.data.len()
}
pub fn size(&self) -> usize {
self.data.len()
}
}
impl RelaxRecord {
pub fn is_applied(&self) -> bool {
self.applied
}
pub fn mark_applied(&mut self) {
self.applied = true;
}
pub fn describe(&self) -> String {
format!(
"{} at offset {:#x} in section {} (type {}): {:?}",
if self.applied { "Applied" } else { "Pending" },
self.offset,
self.section_index,
self.original_type,
self.relax_kind
)
}
}
impl RelaxationStats {
pub fn total_tls(&self) -> usize {
self.tls_gd_to_ie + self.tls_gd_to_le + self.tls_ie_to_le + self.tls_ld_to_le
}
pub fn total_gotpcrelx(&self) -> usize {
self.gotpcrelx_mov_to_lea + self.gotpcrelx_to_pc32
}
}
impl LTOOptStats {
pub fn total_functions_affected(&self) -> usize {
self.functions_internalized
+ self.functions_merged
+ self.dead_functions_eliminated
+ self.devirtualized_calls
}
pub fn total_data_affected(&self) -> usize {
self.globals_eliminated + self.constant_propagations
}
}
#[cfg(test)]
mod coverage_tests {
use super::*;
use crate::x86::x86_lld_full::{X86InputRelocation, X86InputSection};
fn me_c(name: &str, data: Vec<u8>) -> X86InputSection {
X86InputSection {
name: name.into(),
data,
sh_type: SHT_PROGBITS,
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
section_index: 0,
sh_entsize: 0,
}
}
#[test]
fn test_icf_candidate_can_fold_into() {
let a = ICFCandidate {
section_index: 0,
content_hash: 1,
content_hash2: 0,
address_taken: false,
data: vec![0x90; 32],
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
name: "a".into(),
is_folded: false,
folded_into: None,
};
let b = ICFCandidate {
section_index: 1,
content_hash: 1,
content_hash2: 0,
address_taken: false,
data: vec![0x90; 32],
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
name: "b".into(),
is_folded: false,
folded_into: None,
};
assert!(a.can_fold_into(&b));
}
#[test]
fn test_icf_candidate_cannot_fold_different_flags() {
let a = ICFCandidate {
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
data: vec![0x90; 32],
..Default::default()
};
let b = ICFCandidate {
sh_flags: SHF_ALLOC,
data: vec![0x90; 32],
..Default::default()
};
assert!(!a.can_fold_into(&b));
}
#[test]
fn test_icf_candidate_cannot_fold_different_size() {
let a = ICFCandidate {
data: vec![0x90; 32],
..Default::default()
};
let b = ICFCandidate {
data: vec![0x90; 16],
..Default::default()
};
assert!(!a.can_fold_into(&b));
}
impl Default for ICFCandidate {
fn default() -> Self {
Self {
section_index: 0,
content_hash: 0,
content_hash2: 0,
address_taken: false,
data: vec![0x90; 32],
sh_flags: SHF_ALLOC | SHF_EXECINSTR,
sh_addralign: 16,
name: String::new(),
is_folded: false,
folded_into: None,
}
}
}
#[test]
fn test_relax_record_describe() {
let rec = RelaxRecord {
section_index: 3,
offset: 0x100,
original_type: R_X86_64_GOTPCRELX,
relax_kind: RelaxKind::GOTPCRELX_MovToLea,
symbol_name: Some("foo".into()),
addend: -4,
applied: true,
};
let desc = rec.describe();
assert!(desc.contains("Applied"));
assert!(desc.contains("0x100"));
assert!(desc.contains("section 3"));
}
#[test]
fn test_relax_record_is_applied() {
let rec = RelaxRecord {
applied: true,
..Default::default()
};
assert!(rec.is_applied());
}
#[test]
fn test_relax_record_mark_applied() {
let mut rec = RelaxRecord::default();
assert!(!rec.is_applied());
rec.mark_applied();
assert!(rec.is_applied());
}
impl Default for RelaxRecord {
fn default() -> Self {
Self {
section_index: 0,
offset: 0,
original_type: 0,
relax_kind: RelaxKind::NoRelax,
symbol_name: None,
addend: 0,
applied: false,
}
}
}
#[test]
fn test_relaxation_stats_totals() {
let s = RelaxationStats {
gotpcrelx_mov_to_lea: 5,
gotpcrelx_to_pc32: 3,
tls_gd_to_ie: 1,
tls_gd_to_le: 2,
tls_ie_to_le: 4,
tls_ld_to_le: 1,
call_to_jmp: 6,
branch_relax: 0,
total: 22,
};
assert_eq!(s.total_tls(), 8);
assert_eq!(s.total_gotpcrelx(), 8);
}
#[test]
fn test_lto_stats_totals() {
let s = LTOOptStats {
functions_internalized: 10,
functions_merged: 5,
dead_functions_eliminated: 3,
devirtualized_calls: 2,
globals_eliminated: 7,
constant_propagations: 4,
size_reduction_bytes: 0,
};
assert_eq!(s.total_functions_affected(), 20);
assert_eq!(s.total_data_affected(), 11);
}
#[test]
fn test_icf_candidate_size() {
let c = ICFCandidate {
data: vec![0x90; 42],
..Default::default()
};
assert_eq!(c.size(), 42);
}
#[test]
fn test_merge_dedup_identical() {
let a = vec![1, 2, 3];
let b = vec![1, 2, 3];
assert_eq!(merge_dedup_sorted(&a, &b), vec![1, 2, 3]);
}
#[test]
fn test_merge_dedup_disjoint() {
let a = vec![1, 3, 5];
let b = vec![2, 4, 6];
assert_eq!(merge_dedup_sorted(&a, &b), vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_profiler_multiple_passes() {
let mut prof = OptimizationProfiler::new();
prof.begin_pass("phase1");
prof.end_pass(10, 100, 1, 10);
prof.begin_pass("phase2");
prof.end_pass(20, 200, 2, 20);
prof.begin_pass("phase1");
prof.end_pass(5, 50, 0, 0);
let p1 = prof.get_profile("phase1").unwrap();
assert_eq!(p1.run_count, 2);
assert_eq!(p1.sections_processed, 15);
assert_eq!(p1.bytes_saved, 10);
}
#[test]
fn test_constants_documentation_values() {
assert_eq!(ICF_MAX_CANDIDATES_SINGLE_THREAD, 100_000);
assert_eq!(COMDAT_MAX_GROUPS_DIRECT, 10_000);
assert_eq!(GC_MAX_BFS_DEPTH, 10_000);
assert_eq!(STRING_MERGE_MAX_LENGTH, 256);
assert_eq!(X86_64_FETCH_WIDTH, 16);
assert_eq!(X86_64_ICACHE_LINE_SIZE, 64);
}
#[test]
fn test_opcode_constants() {
assert_eq!(OPCODE_NOP2, [0x66, 0x90]);
assert_eq!(OPCODE_NOP3, [0x0F, 0x1F, 0x00]);
assert_eq!(OPCODE_NOP4, [0x0F, 0x1F, 0x40, 0x00]);
assert_eq!(OPCODE_UD2, [0x0F, 0x0B]);
assert_eq!(OPCODE_PUSH_R64, 0x50);
assert_eq!(OPCODE_POP_R64, 0x58);
}
#[test]
fn test_shf_gnu_retain() {
assert_eq!(SHF_GNU_RETAIN, 0x200000);
}
#[test]
fn test_gc_unconditional_keep_list() {
for name in GC_UNCONDITIONAL_KEEP {
assert!(name.starts_with('.'));
}
}
#[test]
fn test_is_rex_prefix_range() {
for b in 0x40u8..=0x4Fu8 {
assert!(is_rex_prefix(b), "{} should be REX prefix", b);
}
for b in [0x00u8, 0x3F, 0x50, 0x90, 0xC0].iter() {
assert!(!is_rex_prefix(*b), "{} should NOT be REX prefix", b);
}
}
#[test]
fn test_is_modrm_register_range() {
for rm in 0..8 {
for reg in 0..8 {
let modrm = 0xC0u8 | (reg << 3) | rm;
assert!(is_modrm_register(modrm));
}
}
assert!(!is_modrm_register(0x00));
assert!(!is_modrm_register(0x3F));
}
#[test]
fn test_tail_call_search_window_constant() {
assert_eq!(TAIL_CALL_SEARCH_WINDOW, 32);
}
#[test]
fn test_gc_max_sections() {
assert_eq!(GC_MAX_SECTIONS, 65536);
}
#[test]
fn test_is_modrm_memory_nodisp_all_cases() {
for rm in [0u8, 1, 2, 3, 6, 7].iter() {
let modrm = *rm; assert!(
is_modrm_memory_nodisp(modrm),
"rm={} should be memory no-disp",
rm
);
}
assert!(!is_modrm_memory_nodisp(0x04)); assert!(!is_modrm_memory_nodisp(0x05)); assert!(!is_modrm_memory_nodisp(0x40)); assert!(!is_modrm_memory_nodisp(0x80)); }
#[test]
fn test_section_content_hash_different_align() {
let d = vec![0x90; 32];
let h1 = section_content_hash(&d, SHF_ALLOC | SHF_EXECINSTR, SHT_PROGBITS, 16);
let h2 = section_content_hash(&d, SHF_ALLOC | SHF_EXECINSTR, SHT_PROGBITS, 64);
assert_ne!(h1, h2);
}
#[test]
fn test_section_content_hash_different_type() {
let d = vec![0x90; 32];
let h1 = section_content_hash(&d, SHF_ALLOC, SHT_PROGBITS, 16);
let h2 = section_content_hash(&d, SHF_ALLOC, SHT_NOBITS, 16);
assert_ne!(h1, h2);
}
#[test]
fn test_checksum32_different_data() {
let c1 = checksum32(b"abcdefgh");
let c2 = checksum32(b"ABCDEFGH");
assert_ne!(c1, c2);
}
#[test]
fn test_checksum32_same_data() {
let c1 = checksum32(b"test");
let c2 = checksum32(b"test");
assert_eq!(c1, c2);
}
#[test]
fn test_is_comdat_section_name_edge_cases() {
assert!(!is_comdat_section_name(".a"));
assert!(!is_comdat_section_name("text"));
assert!(is_comdat_section_name(".text._Z"));
assert!(is_comdat_section_name(".text._ZN3foo3barEv"));
assert!(is_comdat_section_name(".text.__cxx_global_var_init"));
}
#[test]
fn test_extract_comdat_group_name_edge_cases() {
assert_eq!(extract_comdat_group_name("no_dot"), None);
assert_eq!(extract_comdat_group_name("ends_with_dot."), None);
assert_eq!(extract_comdat_group_name(".short"), Some("short".into()));
}
#[test]
fn test_tail_overlap_min_boundary() {
let a = vec![1, 2, 3, 4];
let b = vec![1, 2, 3, 4];
assert_eq!(find_tail_overlap(&a, &b, 4), Some(4));
assert_eq!(find_tail_overlap(&a, &b, 5), None);
}
#[test]
fn test_serialize_relax_record_roundtrip_fields() {
let rec = RelaxRecord {
section_index: 42,
offset: 0xDEAD,
original_type: R_X86_64_GOTPCRELX,
relax_kind: RelaxKind::CallToJmp,
symbol_name: Some("test".into()),
addend: -16,
applied: false,
};
let data = serialize_relax_record(&rec);
assert!(data.len() > 20);
}
#[test]
fn test_pipeline_default_is_fresh() {
let p = X86LLDOptPipeline::default();
assert!(p.phase_snapshots.is_empty());
assert!(!p.dry_run);
}
#[test]
fn test_adr_constants_exist() {
let _ = ICF_MAX_CANDIDATES_SINGLE_THREAD;
let _ = COMDAT_MAX_GROUPS_DIRECT;
let _ = GC_MAX_BFS_DEPTH;
let _ = STRING_MERGE_MAX_LENGTH;
let _ = X86_64_FETCH_WIDTH;
let _ = X86_64_ICACHE_LINE_SIZE;
}
#[test]
fn test_full_sht_constants() {
assert_eq!(SHT_NULL, 0);
assert_eq!(SHT_SYMTAB, 2);
assert_eq!(SHT_STRTAB, 3);
assert_eq!(SHT_RELA, 4);
assert_eq!(SHT_HASH, 5);
assert_eq!(SHT_DYNAMIC, 6);
assert_eq!(SHT_NOTE, 7);
assert_eq!(SHT_REL, 9);
assert_eq!(SHT_DYNSYM, 11);
assert_eq!(SHT_SYMTAB_SHNDX, 18);
assert_eq!(SHT_GNU_HASH, 0x6ffffff6);
}
#[test]
fn test_full_rex_opcodes() {
assert_eq!(OPCODE_REX_W, 0x48);
assert_eq!(OPCODE_REX_B, 0x41);
assert_eq!(OPCODE_REX_R, 0x44);
assert_eq!(OPCODE_REX_X, 0x42);
assert_eq!(REX_PREFIX_MASK, 0xF0);
assert_eq!(REX_PREFIX_BASE, 0x40);
}
#[test]
fn test_additional_opcodes() {
assert_eq!(OPCODE_XOR_R64_RM64, 0x33);
assert_eq!(OPCODE_TEST_RM8_IMM8, 0xF6);
assert_eq!(OPCODE_JMP_SHORT, 0xEB);
}
#[test]
fn test_additional_reloc_constants() {
assert_eq!(R_X86_64_64, 1);
assert_eq!(R_X86_64_32, 10);
assert_eq!(R_X86_64_GOT32, 3);
assert_eq!(R_X86_64_DTPOFF32, 21);
assert_eq!(R_X86_64_DTPMOD64, 16);
}
#[test]
fn test_branch_distance_constants() {
assert_eq!(BRANCH_SHORT_MAX_DISTANCE, 127);
assert_eq!(BRANCH_SHORT_MIN_DISTANCE, -128);
}
#[test]
fn test_tls_sequence_lengths() {
assert_eq!(TLS_GD_TO_LE_SEQ_LEN, 16);
assert_eq!(TLS_IE_TO_LE_SEQ_LEN, 9);
}
#[test]
fn test_shf_os_nonconforming() {
assert_eq!(SHF_OS_NONCONFORMING, 0x100);
}
#[test]
fn test_gnu_ver_sections() {
assert_eq!(SHT_GNU_versym, 0x6fffffff);
assert_eq!(SHT_GNU_verdef, 0x6ffffffd);
assert_eq!(SHT_GNU_verneed, 0x6ffffffe);
}
#[test]
fn test_comdat_selection_all_values() {
assert_eq!(COMDAT_SELECTION_ANY, 0);
assert_eq!(COMDAT_SELECTION_EXACT_MATCH, 1);
assert_eq!(COMDAT_SELECTION_LARGEST, 2);
assert_eq!(COMDAT_SELECTION_NODUPLICATES, 3);
assert_eq!(COMDAT_SELECTION_SAME_SIZE, 4);
}
}