use object::write::{Object as Writer, Relocation, Symbol, SymbolSection};
use object::{
Architecture, BinaryFormat, Endianness, RelocationFlags, SectionFlags, SectionKind,
SymbolFlags, SymbolKind, elf,
};
use rucc_target::{ObjectFormat, TargetInfo};
use rucc_tuple::Arch;
use crate::file::Error;
use crate::section::{Array, Binding, Reloc, Visibility};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Part {
pub name: String,
pub bytes: Vec<u8>,
pub size: u64,
pub align: u64,
pub shape: Shape,
pub relocs: Vec<Reloc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Shape {
pub alloc: bool,
pub write: bool,
pub exec: bool,
pub thread: bool,
pub bits: bool,
pub array: Option<Array>,
}
impl Shape {
#[must_use]
pub fn of(name: &str) -> Shape {
let base = Shape { alloc: true, bits: true, ..Shape::default() };
let head = name.split_once('.').map_or(name, |(_, rest)| rest);
let head = head.split_once('.').map_or(head, |(first, _)| first);
match head {
"text" | "init" | "fini" => Shape { exec: true, ..base },
"rodata" | "eh_frame_hdr" => base,
"bss" => Shape { write: true, bits: false, ..base },
"tbss" => Shape { write: true, thread: true, bits: false, ..base },
"tdata" => Shape { write: true, thread: true, ..base },
_ if Array::of(name).is_some() => Shape { write: true, array: Array::of(name), ..base },
"debug_info" | "debug_abbrev" | "debug_line" | "debug_str" | "comment" => {
Shape { alloc: false, bits: true, ..Shape::default() }
}
_ => Shape { write: true, ..base },
}
}
pub(crate) fn sh_flags(self) -> elf::SectionFlags {
let mut flags = 0;
if self.alloc {
flags |= elf::SHF_ALLOC.0;
}
if self.write {
flags |= elf::SHF_WRITE.0;
}
if self.exec {
flags |= elf::SHF_EXECINSTR.0;
}
if self.thread {
flags |= elf::SHF_TLS.0;
}
elf::SectionFlags(flags)
}
pub(crate) fn sh_type(self) -> elf::SectionType {
match self.array {
_ if !self.bits => elf::SHT_NOBITS,
Some(Array::Init) => elf::SHT_INIT_ARRAY,
Some(Array::Fini) => elf::SHT_FINI_ARRAY,
Some(Array::Preinit) => elf::SHT_PREINIT_ARRAY,
None => elf::SHT_PROGBITS,
}
}
pub(crate) const fn kind(self) -> SectionKind {
match self {
Shape { bits: false, thread: true, .. } => SectionKind::UninitializedTls,
Shape { bits: false, .. } => SectionKind::UninitializedData,
Shape { thread: true, .. } => SectionKind::Tls,
Shape { exec: true, .. } => SectionKind::Text,
Shape { alloc: false, .. } => SectionKind::Other,
Shape { write: false, .. } => SectionKind::ReadOnlyData,
Shape { .. } => SectionKind::Data,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Name {
pub name: String,
pub at: Held,
pub size: u64,
pub sort: Sort,
pub binding: Binding,
pub visibility: Visibility,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Held {
In {
part: usize,
offset: u64,
},
Absolute(u64),
Common {
size: u64,
align: u64,
},
Undefined,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Sort {
Func,
Object,
Thread,
File,
#[default]
Untyped,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Assembled {
pub parts: Vec<Part>,
pub names: Vec<Name>,
}
pub fn assembled(input: &Assembled, target: &TargetInfo) -> Result<Vec<u8>, Error> {
if target.tuple.arch() != Arch::X86_64 || target.object_format != ObjectFormat::Elf {
return Err(Error::Format { triple: target.tuple.to_string() });
}
let mut obj = Writer::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little);
let mut made = Vec::with_capacity(input.parts.len());
for part in &input.parts {
let id = obj.add_section(Vec::new(), part.name.clone().into_bytes(), part.shape.kind());
obj.section_mut(id).flags =
SectionFlags::Elf { sh_type: part.shape.sh_type(), sh_flags: part.shape.sh_flags() };
let align = part.align.max(1);
if part.shape.bits {
obj.append_section_data(id, &part.bytes, align);
} else {
obj.append_section_bss(id, part.size, align);
}
made.push(id);
}
let mut symbols = std::collections::BTreeMap::new();
for name in &input.names {
let (section, value, size) = match name.at {
Held::In { part, offset } => {
let Some(id) = made.get(part) else {
let why = format!(
"'{}' is in section {part} and there is no such section",
name.name
);
return Err(Error::Refused { why });
};
(SymbolSection::Section(*id), offset, name.size)
}
Held::Absolute(value) => (SymbolSection::Absolute, value, name.size),
Held::Common { size, align } => (SymbolSection::Common, align, size),
Held::Undefined => (SymbolSection::Undefined, 0, 0),
};
let id = obj.add_symbol(Symbol {
name: name.name.clone().into_bytes(),
value,
size,
kind: sort_of(name.sort),
scope: crate::file::scope_of(name.binding),
weak: name.binding == Binding::Weak,
section,
flags: SymbolFlags::None,
});
crate::elf::see(&mut obj, id, name.binding, name.visibility);
if matches!(name.at, Held::Common { .. }) {
if let SymbolFlags::Elf { st_info, .. } = obj.symbol_flags_mut(id) {
*st_info = elf::STB_GLOBAL | elf::STT_OBJECT;
}
}
symbols.insert(name.name.clone(), id);
}
for (part, id) in input.parts.iter().zip(&made) {
for reloc in &part.relocs {
let Some(symbol) = symbols.get(&reloc.symbol) else {
let why =
format!("'{}' is named by a relocation and by nothing else", reloc.symbol);
return Err(Error::Refused { why });
};
let r_type = crate::elf::r_type(reloc.kind).ok_or_else(|| Error::Refused {
why: format!("no relocation is {:?}", reloc.kind),
})?;
obj.add_relocation(
*id,
Relocation {
offset: reloc.at as u64,
symbol: *symbol,
addend: reloc.addend,
flags: RelocationFlags::Elf { r_type },
},
)
.map_err(|why| Error::Refused { why: why.to_string() })?;
}
}
if !input.parts.iter().any(|part| part.name == ".note.GNU-stack") {
obj.add_section(Vec::new(), b".note.GNU-stack".to_vec(), SectionKind::Metadata);
}
obj.write().map_err(|why| Error::Refused { why: why.to_string() })
}
#[must_use]
pub fn assembled_defines(input: &Assembled) -> Vec<String> {
input
.names
.iter()
.filter(|name| name.binding != Binding::Local && name.at != Held::Undefined)
.map(|name| name.name.clone())
.collect()
}
fn sort_of(sort: Sort) -> SymbolKind {
match sort {
Sort::Func => SymbolKind::Text,
Sort::Object => SymbolKind::Data,
Sort::Thread => SymbolKind::Tls,
Sort::File => SymbolKind::File,
Sort::Untyped => SymbolKind::Label,
}
}
#[cfg(test)]
mod tests {
use super::*;
use object::read::elf::{FileHeader as _, Sym as _};
use object::read::{Object as _, ObjectSection as _, ObjectSymbol as _};
use rucc_target::{Arch as TargetArch, Env, Os, Triple};
use crate::section::Reference;
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(TargetArch::X86_64, Os::Linux, Env::Gnu))
}
fn part(name: &str, bytes: Vec<u8>) -> Part {
Part {
name: name.to_owned(),
size: bytes.len() as u64,
bytes,
align: 1,
shape: Shape::of(name),
relocs: Vec::new(),
}
}
fn at(name: &str, offset: u64, sort: Sort, binding: Binding) -> Name {
Name {
name: name.to_owned(),
at: Held::In { part: 0, offset },
size: 0,
sort,
binding,
visibility: Visibility::Default,
}
}
fn raw(bytes: &[u8], want: &str) -> (u8, u64) {
let header = elf::FileHeader64::<Endianness>::parse(bytes).expect("a header");
let endian = header.endian().expect("an endianness");
let table = header.sections(endian, bytes).expect("the sections");
let symbols = table.symbols(endian, bytes, elf::SHT_SYMTAB).expect("a symbol table");
for symbol in symbols.iter() {
if symbols.symbol_name(endian, symbol).expect("a name") == want.as_bytes() {
return (symbol.st_info().0, symbol.st_value(endian));
}
}
panic!("there is no symbol called '{want}'");
}
fn st_info(bytes: &[u8], want: &str) -> u8 {
raw(bytes, want).0
}
#[test]
fn a_section_carries_the_flags_the_source_said_and_not_the_ones_its_name_suggests() {
let mut odd = part(".init.text", vec![0x90]);
odd.shape = Shape { alloc: true, exec: true, bits: true, ..Shape::default() };
let input = Assembled { parts: vec![odd], names: Vec::new() };
let bytes = assembled(&input, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let section = file.section_by_name(".init.text").expect("the section");
assert_eq!(section.data().expect("the bytes"), &[0x90]);
let SectionFlags::Elf { sh_flags, sh_type } = section.flags() else {
panic!("this is an ELF file");
};
assert_eq!(sh_flags.0, elf::SHF_ALLOC.0 | elf::SHF_EXECINSTR.0);
assert_eq!(sh_flags.0 & elf::SHF_WRITE.0, 0, "nothing said it was writable");
assert_eq!(sh_type, elf::SHT_PROGBITS);
}
#[test]
fn a_section_that_holds_no_bytes_still_says_how_long_it_is() {
let mut room = part(".bss", Vec::new());
room.size = 4096;
room.align = 16;
let input = Assembled { parts: vec![room], names: Vec::new() };
let bytes = assembled(&input, &target()).expect("an object");
assert!(bytes.len() < 4096, "the empty space was written out: {} bytes", bytes.len());
let file = object::File::parse(&bytes[..]).expect("a readable object");
let section = file.section_by_name(".bss").expect("the section");
assert_eq!(section.size(), 4096);
assert_eq!(section.align(), 16);
let SectionFlags::Elf { sh_type, .. } = section.flags() else { panic!("an ELF file") };
assert_eq!(sh_type, elf::SHT_NOBITS);
}
#[test]
fn a_label_nobody_stated_a_type_for_is_a_symbol_with_no_type() {
let input = Assembled {
parts: vec![part(".text", vec![0; 8])],
names: vec![at("plain", 4, Sort::Untyped, Binding::Global)],
};
let bytes = assembled(&input, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let plain = file.symbols().find(|s| s.name() == Ok("plain")).expect("the label");
assert_eq!(plain.address(), 4);
assert_eq!(st_info(&bytes, "plain") & 0xf, elf::STT_NOTYPE.0);
}
#[test]
fn what_type_said_is_what_the_symbol_gets() {
let input = Assembled {
parts: vec![part(".text", vec![0; 8])],
names: vec![
at("run", 0, Sort::Func, Binding::Global),
at("held", 4, Sort::Object, Binding::Local),
],
};
let bytes = assembled(&input, &target()).expect("an object");
assert_eq!(st_info(&bytes, "run") & 0xf, elf::STT_FUNC.0);
assert_eq!(st_info(&bytes, "held") & 0xf, elf::STT_OBJECT.0);
assert_eq!(st_info(&bytes, "run") >> 4, elf::STB_GLOBAL.0);
assert_eq!(st_info(&bytes, "held") >> 4, elf::STB_LOCAL.0);
}
#[test]
fn a_common_symbol_is_written_the_way_gas_writes_one() {
let input = Assembled {
parts: Vec::new(),
names: vec![Name {
name: "shared".to_owned(),
at: Held::Common { size: 8, align: 8 },
size: 0,
sort: Sort::Object,
binding: Binding::Global,
visibility: Visibility::Default,
}],
};
let bytes = assembled(&input, &target()).expect("an object");
assert_eq!(st_info(&bytes, "shared"), elf::STB_GLOBAL.0 << 4 | elf::STT_OBJECT.0);
let file = object::File::parse(&bytes[..]).expect("a readable object");
let shared = file.symbols().find(|s| s.name() == Ok("shared")).expect("the symbol");
assert!(shared.is_common(), "the linker has to be asked for the space");
assert_eq!(shared.size(), 8);
assert_eq!(raw(&bytes, "shared").1, 8, "the boundary it has to start on");
}
#[test]
fn a_set_is_a_number_rather_than_a_place() {
let input = Assembled {
parts: vec![part(".text", vec![0; 8])],
names: vec![Name {
name: "size_of_it".to_owned(),
at: Held::Absolute(25),
size: 0,
sort: Sort::Untyped,
binding: Binding::Global,
visibility: Visibility::Default,
}],
};
let bytes = assembled(&input, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let sym = file.symbols().find(|s| s.name() == Ok("size_of_it")).expect("the symbol");
assert_eq!(sym.address(), 25);
assert_eq!(sym.section(), object::SymbolSection::Absolute, "it is not in any section");
}
#[test]
fn a_relocation_names_a_symbol_and_lands_where_the_bytes_are() {
let mut data = part(".data", vec![0; 8]);
data.relocs.push(Reloc {
at: 0,
symbol: "message".to_owned(),
kind: Reference::Address { bytes: 8 },
addend: 0,
after: 0,
});
let input = Assembled {
parts: vec![data],
names: vec![Name {
name: "message".to_owned(),
at: Held::Undefined,
size: 0,
sort: Sort::Untyped,
binding: Binding::Global,
visibility: Visibility::Default,
}],
};
let bytes = assembled(&input, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let section = file.section_by_name(".data").expect("the section");
let (at, reloc) = section.relocations().next().expect("one relocation");
assert_eq!(at, 0);
assert_eq!(reloc.addend(), 0);
let RelocationFlags::Elf { r_type } = reloc.flags() else { panic!("an ELF file") };
assert_eq!(r_type, elf::R_X86_64_64);
}
#[test]
fn a_relocation_against_a_name_the_file_never_mentions_is_refused() {
let mut data = part(".data", vec![0; 8]);
data.relocs.push(Reloc {
at: 0,
symbol: "nowhere".to_owned(),
kind: Reference::Address { bytes: 8 },
addend: 0,
after: 0,
});
let input = Assembled { parts: vec![data], names: Vec::new() };
let why = assembled(&input, &target()).expect_err("this cannot be written");
assert!(format!("{why}").contains("nowhere"), "{why}");
}
#[test]
fn the_stack_is_marked_once_whoever_asked_for_it() {
let bare = Assembled { parts: vec![part(".text", vec![0x90])], names: Vec::new() };
let bytes = assembled(&bare, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert!(file.section_by_name(".note.GNU-stack").is_some(), "the marker was left out");
let said = Assembled {
parts: vec![part(".text", vec![0x90]), part(".note.GNU-stack", Vec::new())],
names: Vec::new(),
};
let bytes = assembled(&said, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let marks = file.sections().filter(|s| s.name() == Ok(".note.GNU-stack")).count();
assert_eq!(marks, 1, "the file said it and it was said again");
}
#[test]
fn only_the_names_a_linker_could_find_are_offered_to_an_archive() {
let input = Assembled {
parts: vec![part(".text", vec![0; 8])],
names: vec![
at("reachable", 0, Sort::Func, Binding::Global),
at("mine", 4, Sort::Func, Binding::Local),
Name {
name: "elsewhere".to_owned(),
at: Held::Undefined,
size: 0,
sort: Sort::Untyped,
binding: Binding::Global,
visibility: Visibility::Default,
},
],
};
assert_eq!(assembled_defines(&input), vec!["reachable".to_owned()]);
}
#[test]
fn a_machine_this_does_not_write_is_refused_rather_than_written_wrong() {
let input = Assembled { parts: vec![part(".text", vec![0x90])], names: Vec::new() };
let elsewhere = TargetInfo::new(Triple::new(TargetArch::Aarch64, Os::Linux, Env::Gnu));
let why = assembled(&input, &elsewhere).expect_err("this cannot be written");
assert!(format!("{why}").contains("aarch64"), "{why}");
}
}