#![deny(clippy::arithmetic_side_effects)]
use crate::elf::read::consts::{
SHF_ALLOC, SHF_EXECINSTR, SHF_TLS, SHF_WRITE, SHT_NOBITS, SHT_NOTE,
};
use crate::script::{Pattern, init_priority};
use std::collections::HashMap;
const KEY: usize = 4;
#[derive(Clone, Copy, Debug)]
struct Candidate {
position: u32,
word: u64,
mask: u64,
}
fn tail_word(name: &[u8]) -> (u64, u64) {
let tail = name.get(KEY..).unwrap_or_default();
let mut bytes = [0u8; 8];
let len = tail.len().min(8);
if let (Some(head), Some(from)) = (bytes.get_mut(..len), tail.get(..len)) {
head.copy_from_slice(from);
}
let mask = match len {
8 => u64::MAX,
_ => (1u64 << (len.wrapping_mul(8))).wrapping_sub(1),
};
(u64::from_le_bytes(bytes), mask)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SortMode {
None,
Name,
InitPriority,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FileFilter {
Any,
NotCrtBeginEnd,
CrtBegin,
}
#[derive(Clone, Copy, Debug)]
pub struct InputRule {
pub patterns: &'static [&'static str],
pub sort: SortMode,
pub files: FileFilter,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Synthetic {
None,
BuildId,
Interp,
Hash,
GnuHash,
DynSym,
DynStr,
VerSym,
VerDef,
VerNeed,
RelaDyn,
RelaPlt,
RelrDyn,
Plt,
PltGot,
PltSec,
EhFrameHdr,
GnuProperty,
DynRelro,
Dynamic,
Got,
GotPlt,
DynBss,
Common,
Comment,
EhFrameEnd,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum OrphanClass {
Note,
Text,
Rodata,
Tdata,
Tbss,
Data,
Bss,
NonAlloc,
}
#[derive(Clone, Copy, Debug)]
pub struct OutputRule {
pub name: &'static str,
pub inputs: &'static [InputRule],
pub keep: bool,
pub synthetic: Synthetic,
pub hold: Option<OrphanClass>,
pub relro: bool,
}
const fn plain(patterns: &'static [&'static str]) -> InputRule {
InputRule {
patterns,
sort: SortMode::None,
files: FileFilter::Any,
}
}
const fn rule(name: &'static str, inputs: &'static [InputRule]) -> OutputRule {
OutputRule {
name,
inputs,
keep: false,
synthetic: Synthetic::None,
hold: None,
relro: false,
}
}
const fn relro(mut rule: OutputRule) -> OutputRule {
rule.relro = true;
rule
}
const fn synthetic_only(name: &'static str, synthetic: Synthetic) -> OutputRule {
synth(rule(name, &[]), synthetic)
}
const fn keep(mut rule: OutputRule) -> OutputRule {
rule.keep = true;
rule
}
const fn synth(mut rule: OutputRule, synthetic: Synthetic) -> OutputRule {
rule.synthetic = synthetic;
rule
}
const fn hold(mut rule: OutputRule, class: OrphanClass) -> OutputRule {
rule.hold = Some(class);
rule
}
macro_rules! default_rules {
($dyn:literal, $plt:literal, $iplt:literal) => {
default_rules!($dyn, $plt, $iplt, [])
};
($dyn:literal, $plt:literal, $iplt:literal, [$($unwind:expr),* $(,)?]) => {
&[
hold(
synth(
rule(".note.gnu.build-id", &[plain(&[".note.gnu.build-id"])]),
Synthetic::BuildId,
),
OrphanClass::Note,
),
synthetic_only(".interp", Synthetic::Interp),
synthetic_only(".hash", Synthetic::Hash),
synthetic_only(".gnu.hash", Synthetic::GnuHash),
synthetic_only(".dynsym", Synthetic::DynSym),
synthetic_only(".dynstr", Synthetic::DynStr),
synthetic_only(".gnu.version", Synthetic::VerSym),
synthetic_only(".gnu.version_d", Synthetic::VerDef),
synthetic_only(".gnu.version_r", Synthetic::VerNeed),
synthetic_only($dyn, Synthetic::RelaDyn),
synth(
rule($plt, &[plain(&[$plt]), plain(&[$iplt])]),
Synthetic::RelaPlt,
),
synthetic_only(".relr.dyn", Synthetic::RelrDyn),
keep(rule(".init", &[plain(&[".init"])])),
synth(rule(".plt", &[plain(&[".plt", ".iplt"])]), Synthetic::Plt),
synth(rule(".plt.got", &[plain(&[".plt.got"])]), Synthetic::PltGot),
synth(rule(".plt.sec", &[plain(&[".plt.sec"])]), Synthetic::PltSec),
hold(
rule(
".text",
&[
plain(&[".text.unlikely", ".text.*_unlikely", ".text.unlikely.*"]),
plain(&[".text.exit", ".text.exit.*"]),
plain(&[".text.startup", ".text.startup.*"]),
plain(&[".text.hot", ".text.hot.*"]),
InputRule {
patterns: &[".text.sorted.*"],
sort: SortMode::Name,
files: FileFilter::Any,
},
plain(&[".text", ".stub", ".text.*", ".gnu.linkonce.t.*"]),
plain(&[".gnu.warning"]),
],
),
OrphanClass::Text,
),
keep(rule(".fini", &[plain(&[".fini"])])),
hold(
rule(
".rodata",
&[plain(&[".rodata", ".rodata.*", ".gnu.linkonce.r.*"])],
),
OrphanClass::Rodata,
),
rule(".rodata1", &[plain(&[".rodata1"])]),
$($unwind,)*
synth(
rule(
".eh_frame_hdr",
&[
plain(&[".eh_frame_hdr"]),
plain(&[".eh_frame_entry", ".eh_frame_entry.*"]),
],
),
Synthetic::EhFrameHdr,
),
synth(
keep(rule(
".eh_frame",
&[plain(&[".eh_frame"]), plain(&[".eh_frame.*"])],
)),
Synthetic::EhFrameEnd,
),
rule(".sframe", &[plain(&[".sframe"]), plain(&[".sframe.*"])]),
rule(
".gcc_except_table",
&[plain(&[".gcc_except_table", ".gcc_except_table.*"])],
),
rule(".gnu_extab", &[plain(&[".gnu_extab*"])]),
rule(".exception_ranges", &[plain(&[".exception_ranges*"])]),
rule(".note.build-id", &[plain(&[".note.build-id"])]),
synth(
rule(".note.gnu.property", &[plain(&[".note.gnu.property"])]),
Synthetic::GnuProperty,
),
rule(".note.ABI-tag", &[plain(&[".note.ABI-tag"])]),
rule(".note.package", &[plain(&[".note.package"])]),
rule(".note.dlopen", &[plain(&[".note.dlopen"])]),
rule(".note.netbsd.ident", &[plain(&[".note.netbsd.ident"])]),
rule(".note.openbsd.ident", &[plain(&[".note.openbsd.ident"])]),
relro(hold(
rule(
".tdata",
&[plain(&[".tdata", ".tdata.*", ".gnu.linkonce.td.*"])],
),
OrphanClass::Tdata,
)),
relro(hold(
rule(
".tbss",
&[
plain(&[".tbss", ".tbss.*", ".gnu.linkonce.tb.*"]),
plain(&[".tcommon"]),
],
),
OrphanClass::Tbss,
)),
relro(keep(rule(".preinit_array", &[plain(&[".preinit_array"])]))),
relro(keep(rule(
".init_array",
&[
InputRule {
patterns: &[".init_array.*", ".ctors.*"],
sort: SortMode::InitPriority,
files: FileFilter::Any,
},
InputRule {
patterns: &[".init_array", ".ctors"],
sort: SortMode::None,
files: FileFilter::NotCrtBeginEnd,
},
],
))),
relro(keep(rule(
".fini_array",
&[
InputRule {
patterns: &[".fini_array.*", ".dtors.*"],
sort: SortMode::InitPriority,
files: FileFilter::Any,
},
InputRule {
patterns: &[".fini_array", ".dtors"],
sort: SortMode::None,
files: FileFilter::NotCrtBeginEnd,
},
],
))),
relro(keep(rule(
".ctors",
&[
InputRule {
patterns: &[".ctors"],
sort: SortMode::None,
files: FileFilter::CrtBegin,
},
plain(&[".ctors"]),
],
))),
relro(keep(rule(
".dtors",
&[
InputRule {
patterns: &[".dtors"],
sort: SortMode::None,
files: FileFilter::CrtBegin,
},
plain(&[".dtors"]),
],
))),
relro(keep(rule(".jcr", &[plain(&[".jcr"])]))),
relro(synth(
rule(
".data.rel.ro",
&[
plain(&[".data.rel.ro.local*", ".gnu.linkonce.d.rel.ro.local.*"]),
plain(&[".data.rel.ro", ".data.rel.ro.*", ".gnu.linkonce.d.rel.ro.*"]),
],
),
Synthetic::DynRelro,
)),
relro(synth(
rule(".dynamic", &[plain(&[".dynamic"])]),
Synthetic::Dynamic,
)),
relro(synth(
rule(".got", &[plain(&[".got", ".toc"]), plain(&[".igot"])]),
Synthetic::Got,
)),
synth(
rule(".got.plt", &[plain(&[".got.plt"]), plain(&[".igot.plt"])]),
Synthetic::GotPlt,
),
hold(
rule(
".data",
&[plain(&[".data", ".data.*", ".gnu.linkonce.d.*"])],
),
OrphanClass::Data,
),
rule(".data1", &[plain(&[".data1"])]),
hold(
synth(
rule(
".bss",
&[
plain(&[".dynbss"]),
plain(&[".bss", ".bss.*", ".gnu.linkonce.b.*"]),
],
),
Synthetic::DynBss,
),
OrphanClass::Bss,
),
rule(
".lbss",
&[
plain(&[".dynlbss"]),
plain(&[".lbss", ".lbss.*", ".gnu.linkonce.lb.*"]),
],
),
rule(
".lrodata",
&[plain(&[".lrodata", ".lrodata.*", ".gnu.linkonce.lr.*"])],
),
rule(
".ldata",
&[plain(&[".ldata", ".ldata.*", ".gnu.linkonce.l.*"])],
),
synth(
rule(".comment", &[plain(&[".comment"])]),
Synthetic::Comment,
),
rule(
".gnu.build.attributes",
&[plain(&[".gnu.build.attributes", ".gnu.build.attributes.*"])],
),
rule(".debug", &[plain(&[".debug"])]),
rule(".line", &[plain(&[".line"])]),
rule(".debug_srcinfo", &[plain(&[".debug_srcinfo"])]),
rule(".debug_sfnames", &[plain(&[".debug_sfnames"])]),
rule(".debug_aranges", &[plain(&[".debug_aranges"])]),
rule(".debug_pubnames", &[plain(&[".debug_pubnames"])]),
rule(
".debug_info",
&[plain(&[".debug_info", ".gnu.linkonce.wi.*"])],
),
rule(".debug_abbrev", &[plain(&[".debug_abbrev"])]),
rule(
".debug_line",
&[plain(&[".debug_line", ".debug_line.*", ".debug_line_end"])],
),
rule(".debug_frame", &[plain(&[".debug_frame"])]),
rule(".debug_str", &[plain(&[".debug_str"])]),
rule(".debug_loc", &[plain(&[".debug_loc"])]),
rule(".debug_macinfo", &[plain(&[".debug_macinfo"])]),
rule(".debug_weaknames", &[plain(&[".debug_weaknames"])]),
rule(".debug_funcnames", &[plain(&[".debug_funcnames"])]),
rule(".debug_typenames", &[plain(&[".debug_typenames"])]),
rule(".debug_varnames", &[plain(&[".debug_varnames"])]),
rule(".debug_pubtypes", &[plain(&[".debug_pubtypes"])]),
rule(".debug_ranges", &[plain(&[".debug_ranges"])]),
rule(".debug_addr", &[plain(&[".debug_addr"])]),
rule(".debug_line_str", &[plain(&[".debug_line_str"])]),
rule(".debug_loclists", &[plain(&[".debug_loclists"])]),
rule(".debug_macro", &[plain(&[".debug_macro"])]),
rule(".debug_names", &[plain(&[".debug_names"])]),
rule(".debug_rnglists", &[plain(&[".debug_rnglists"])]),
rule(".debug_str_offsets", &[plain(&[".debug_str_offsets"])]),
hold(
rule(".debug_sup", &[plain(&[".debug_sup"])]),
OrphanClass::NonAlloc,
),
]
};
}
pub static DEFAULT_RULES: &[OutputRule] = default_rules!(".rela.dyn", ".rela.plt", ".rela.iplt");
pub static REL_RULES: &[OutputRule] = default_rules!(".rel.dyn", ".rel.plt", ".rel.iplt");
pub static ARM_RULES: &[OutputRule] = default_rules!(
".rel.dyn",
".rel.plt",
".rel.iplt",
[
rule(
".ARM.extab",
&[plain(&[".ARM.extab*", ".gnu.linkonce.armextab.*"])],
),
rule(
".ARM.exidx",
&[plain(&[".ARM.exidx*", ".gnu.linkonce.armexidx.*"])],
),
]
);
pub struct RuleSet<'r> {
pub outputs: &'static [OutputRule],
patterns: Vec<(u16, u16, Pattern)>,
index: HashMap<[u8; KEY], Vec<Candidate>, foldhash::fast::FixedState>,
indexed: bool,
holds: Vec<(OrphanClass, u16)>,
pub script: Option<&'r crate::elf::script_layout::LayoutScript>,
pub diagnostics: Option<&'r dyn crate::diag::DiagnosticSink>,
}
impl std::fmt::Debug for RuleSet<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuleSet")
.field("outputs", &self.outputs.len())
.field("script", &self.script.is_some())
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Placement {
pub output: u16,
pub input: u16,
}
impl<'r> RuleSet<'r> {
#[must_use]
pub fn default_rules() -> Self {
Self::new(DEFAULT_RULES)
}
#[must_use]
pub fn default_rules_for(arch: crate::elf::arch::Arch) -> Self {
Self::new(match arch {
crate::elf::arch::Arch::Arm => ARM_RULES,
arch if arch.uses_rel() => REL_RULES,
_ => DEFAULT_RULES,
})
}
#[must_use]
pub fn for_link(
script: Option<&'r crate::elf::script_layout::LayoutScript>,
diagnostics: &'r dyn crate::diag::DiagnosticSink,
arch: crate::elf::arch::Arch,
) -> Self {
let mut rules = Self::default_rules_for(arch);
rules.script = script;
rules.diagnostics = Some(diagnostics);
rules
}
#[must_use]
pub fn new(outputs: &'static [OutputRule]) -> Self {
let mut patterns = Vec::new();
let mut holds = Vec::new();
for (output_index, output) in outputs.iter().enumerate() {
let output_index = u16::try_from(output_index).unwrap_or(u16::MAX);
for (input_index, input) in output.inputs.iter().enumerate() {
let input_index = u16::try_from(input_index).unwrap_or(u16::MAX);
for pattern in input.patterns {
patterns.push((
output_index,
input_index,
Pattern::section(pattern.as_bytes()),
));
}
}
if let Some(class) = output.hold {
holds.push((class, output_index));
}
}
let mut index: HashMap<[u8; KEY], Vec<Candidate>, foldhash::fast::FixedState> =
HashMap::default();
let mut indexed = true;
for (position, (.., pattern)) in patterns.iter().enumerate() {
let prefix = pattern.literal_prefix();
let Some(key) = prefix.first_chunk::<KEY>() else {
indexed = false;
continue;
};
let (word, mask) = tail_word(prefix);
index.entry(*key).or_default().push(Candidate {
position: u32::try_from(position).unwrap_or(u32::MAX),
word,
mask,
});
}
Self {
outputs,
patterns,
index,
indexed,
holds,
script: None,
diagnostics: None,
}
}
#[must_use]
pub fn place(&self, name: &[u8], file_name: &[u8]) -> Option<Placement> {
match name.first_chunk::<KEY>() {
Some(key) if self.indexed => {
let bucket = self.index.get(key)?;
let (tail, _) = tail_word(name);
bucket
.iter()
.filter(|candidate| tail & candidate.mask == candidate.word)
.find_map(|c| self.try_at(c.position as usize, name, file_name))
}
_ => (0..self.patterns.len()).find_map(|p| self.try_at(p, name, file_name)),
}
}
fn try_at(&self, position: usize, name: &[u8], file_name: &[u8]) -> Option<Placement> {
let (output, input, pattern) = self.patterns.get(position)?;
if !pattern.matches(name) {
return None;
}
let rule = self
.outputs
.get(usize::from(*output))
.and_then(|o| o.inputs.get(usize::from(*input)))?;
let applies = match rule.files {
FileFilter::Any => true,
FileFilter::NotCrtBeginEnd => {
!(matches!(name, b".ctors" | b".dtors") && is_crt_begin_end(file_name))
}
FileFilter::CrtBegin => is_crt_begin(file_name),
};
applies.then_some(Placement {
output: *output,
input: *input,
})
}
#[must_use]
pub fn hold(&self, class: OrphanClass) -> u16 {
self.holds
.iter()
.find(|(c, _)| *c == class)
.map_or(u16::MAX, |(_, output)| *output)
}
}
#[must_use]
pub fn orphan_class(flags: u64, sh_type: u32) -> OrphanClass {
if flags & SHF_ALLOC == 0 {
OrphanClass::NonAlloc
} else if sh_type == SHT_NOTE {
OrphanClass::Note
} else if flags & SHF_TLS != 0 {
if sh_type == SHT_NOBITS {
OrphanClass::Tbss
} else {
OrphanClass::Tdata
}
} else if flags & SHF_EXECINSTR != 0 {
OrphanClass::Text
} else if flags & SHF_WRITE == 0 {
OrphanClass::Rodata
} else if sh_type == SHT_NOBITS {
OrphanClass::Bss
} else {
OrphanClass::Data
}
}
#[must_use]
pub fn priority(name: &[u8]) -> u32 {
init_priority(name).unwrap_or(65536)
}
fn base_name(path: &[u8]) -> &[u8] {
match path.iter().rposition(|&b| b == b'/') {
Some(at) => path.get(at.saturating_add(1)..).unwrap_or(path),
None => path,
}
}
#[must_use]
pub fn is_crt_begin(file: &[u8]) -> bool {
let name = base_name(file);
name == b"crtbegin.o"
|| (name.len() == 11 && name.starts_with(b"crtbegin") && name.ends_with(b".o"))
}
#[must_use]
pub fn is_crt_begin_end(file: &[u8]) -> bool {
let name = base_name(file);
is_crt_begin(file)
|| name == b"crtend.o"
|| (name.len() == 9 && name.starts_with(b"crtend") && name.ends_with(b".o"))
}
#[must_use]
pub fn is_c_identifier(name: &[u8]) -> bool {
match name.split_first() {
Some((first, rest)) => {
(first.is_ascii_alphabetic() || *first == b'_')
&& rest.iter().all(|b| b.is_ascii_alphanumeric() || *b == b'_')
}
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn output_of(rules: &RuleSet, name: &str, file: &str) -> Option<&'static str> {
rules
.place(name.as_bytes(), file.as_bytes())
.and_then(|p| rules.outputs.get(usize::from(p.output)))
.map(|o| o.name)
}
#[test]
fn places_the_arm_exception_index() {
let rules = RuleSet::new(ARM_RULES);
assert_eq!(
output_of(&rules, ".ARM.exidx.text.f", "a.o"),
Some(".ARM.exidx")
);
assert_eq!(output_of(&rules, ".ARM.extab", "a.o"), Some(".ARM.extab"));
assert_eq!(output_of(&rules, ".text", "a.o"), Some(".text"));
let position = |name: &str| ARM_RULES.iter().position(|r| r.name == name);
assert!(position(".rodata") < position(".ARM.exidx"));
assert!(position(".ARM.extab") < position(".ARM.exidx"));
assert!(position(".ARM.exidx") < position(".tdata"));
}
#[test]
fn places_like_gnu_ld() {
let rules = RuleSet::default_rules();
assert_eq!(output_of(&rules, ".text", "a.o"), Some(".text"));
assert_eq!(output_of(&rules, ".text.hot.foo", "a.o"), Some(".text"));
assert_eq!(output_of(&rules, ".rodata.str1.1", "a.o"), Some(".rodata"));
assert_eq!(
output_of(&rules, ".init_array.00100", "a.o"),
Some(".init_array")
);
assert_eq!(output_of(&rules, ".ctors", "x/crtbegin.o"), Some(".ctors"));
assert_eq!(output_of(&rules, ".ctors", "main.o"), Some(".init_array"));
assert_eq!(output_of(&rules, ".ctors", "crtend.o"), Some(".ctors"));
assert_eq!(output_of(&rules, ".tbss.x", "a.o"), Some(".tbss"));
assert_eq!(
output_of(&rules, ".data.rel.ro.local", "a.o"),
Some(".data.rel.ro")
);
assert_eq!(output_of(&rules, "rodata.cst32", "a.o"), None);
assert_eq!(output_of(&rules, "qld_items", "a.o"), None);
let unlikely = rules.place(b".text.unlikely.x", b"a.o").unwrap();
let normal = rules.place(b".text.x", b"a.o").unwrap();
assert!(unlikely.input < normal.input);
}
#[test]
fn the_prefix_index_agrees_with_a_full_scan() {
static SHORT: &[OutputRule] = &[
rule(".x", &[plain(&[".x", ".x.*"])]),
rule(".text", &[plain(&["*.text", ".text*"])]),
];
let mut names: Vec<Vec<u8>> = vec![
b"".to_vec(),
b".".to_vec(),
b".x".to_vec(),
b".xy".to_vec(),
b".x.1".to_vec(),
b"a.text".to_vec(),
b"rodata".to_vec(),
b".gnu.linkonce.t.f".to_vec(),
];
for rules in [DEFAULT_RULES, REL_RULES, ARM_RULES, SHORT] {
for output in rules {
for input in output.inputs {
for pattern in input.patterns {
let text = pattern.replace('*', "z");
names.push(text.into_bytes());
names.push(pattern.trim_end_matches('*').as_bytes().to_vec());
}
}
}
}
for rules in [DEFAULT_RULES, REL_RULES, ARM_RULES, SHORT] {
let set = RuleSet::new(rules);
for name in &names {
for file in [&b"a.o"[..], b"crtbegin.o", b"crtend.o"] {
let scan = (0..set.patterns.len()).find_map(|p| set.try_at(p, name, file));
assert_eq!(
set.place(name, file),
scan,
"{}",
String::from_utf8_lossy(name)
);
}
}
}
}
#[test]
fn classes_and_names() {
assert_eq!(orphan_class(SHF_ALLOC, 1), OrphanClass::Rodata);
assert_eq!(orphan_class(SHF_ALLOC | SHF_WRITE, 1), OrphanClass::Data);
assert_eq!(
orphan_class(SHF_ALLOC | SHF_WRITE, SHT_NOBITS),
OrphanClass::Bss
);
assert_eq!(orphan_class(0, 1), OrphanClass::NonAlloc);
assert!(is_c_identifier(b"qld_items"));
assert!(!is_c_identifier(b".text"));
assert!(is_crt_begin(b"/usr/lib/gcc/crtbeginT.o"));
assert!(is_crt_begin_end(b"crtendS.o"));
assert!(!is_crt_begin_end(b"main.o"));
assert_eq!(priority(b".init_array.00150"), 150);
assert_eq!(priority(b".init_array"), 65536);
}
}