use object::write::{Object as Writer, Relocation, SectionId, Symbol, SymbolSection};
use object::{
Architecture, Endianness, SectionFlags, SectionKind, SymbolFlags, SymbolKind, SymbolScope, elf,
pe,
};
use rucc_base::hash::{Map, Set};
use rucc_target::aarch64::Fixup;
use rucc_target::{ObjectFormat, TargetInfo};
use rucc_tuple::Arch;
use crate::file::{Error, Flavour};
use crate::section::{Array, Binding, Compress, Info, 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>,
pub group: Option<Group>,
pub link: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Group {
pub symbol: String,
pub keep: Keep,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Keep {
One,
Any,
SameSize,
SameContents,
Largest,
Newest,
Together,
}
impl Keep {
#[must_use]
pub fn of(word: &str) -> Option<Keep> {
Some(match word {
"one_only" => Keep::One,
"discard" => Keep::Any,
"same_size" => Keep::SameSize,
"same_contents" => Keep::SameContents,
"largest" => Keep::Largest,
"newest" => Keep::Newest,
_ => return None,
})
}
pub(crate) const fn kind(self) -> object::ComdatKind {
match self {
Keep::One => object::ComdatKind::NoDuplicates,
Keep::Any => object::ComdatKind::Any,
Keep::SameSize => object::ComdatKind::SameSize,
Keep::SameContents => object::ComdatKind::ExactMatch,
Keep::Largest => object::ComdatKind::Largest,
Keep::Newest => object::ComdatKind::Newest,
Keep::Together => object::ComdatKind::Any,
}
}
}
#[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>,
pub merge: u64,
pub strings: bool,
pub note: bool,
pub retain: bool,
pub mach: u32,
pub coff: u32,
}
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 },
}
}
#[must_use]
pub fn implied(name: &str) -> Shape {
let base = Shape { alloc: 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" => Shape { exec: true, ..base },
"rodata" => base,
"data" | "bss" => Shape { write: true, ..base },
"tdata" | "tbss" => Shape { write: true, thread: true, ..base },
_ if Array::of(name).is_some() => Shape { write: true, ..base },
_ => Shape::default(),
}
}
pub fn mach(
segment: &str,
section: &str,
kind: Option<&str>,
attributes: &[&str],
) -> Result<Shape, String> {
let mach = crate::macho::section_flags(segment, section, kind, attributes)?;
let exec = mach & object::macho::S_ATTR_PURE_INSTRUCTIONS.0 != 0;
let typ = object::macho::SectionFlags(mach).typ();
let thread = matches!(
typ,
object::macho::S_THREAD_LOCAL_REGULAR | object::macho::S_THREAD_LOCAL_ZEROFILL
);
Ok(Shape {
alloc: true,
write: segment != "__TEXT",
exec,
thread,
bits: !crate::macho::zero_filled(mach),
mach,
..Shape::default()
})
}
pub fn coff(letters: &str) -> Result<Shape, char> {
let (mut code, mut data, mut zero, mut drop, mut info) =
(false, false, false, false, false);
let (mut read, mut write, mut shared, mut discard) = (true, true, false, false);
let mut writable = false;
for letter in letters.chars() {
match letter {
'a' => {}
'b' => zero = true,
'd' => {
data = true;
write = true;
}
'n' => drop = true,
'D' => discard = true,
'r' => {
writable = false;
write = false;
data |= !code;
}
's' => {
shared = true;
data = true;
write = true;
}
'w' => {
write = true;
writable = true;
}
'x' => {
code = true;
write &= writable;
}
'y' => {
read = false;
write = false;
}
'i' => info = true,
other => return Err(other),
}
}
let mut flags = 0;
if code {
flags |= pe::IMAGE_SCN_CNT_CODE.0 | pe::IMAGE_SCN_MEM_EXECUTE.0;
}
if data {
flags |= pe::IMAGE_SCN_CNT_INITIALIZED_DATA.0;
}
if zero && !data {
flags |= pe::IMAGE_SCN_CNT_UNINITIALIZED_DATA.0;
}
if drop {
flags |= pe::IMAGE_SCN_LNK_REMOVE.0;
}
if read {
flags |= pe::IMAGE_SCN_MEM_READ.0;
}
if write {
flags |= pe::IMAGE_SCN_MEM_WRITE.0;
}
if discard {
flags |= pe::IMAGE_SCN_MEM_DISCARDABLE.0;
}
if shared {
flags |= pe::IMAGE_SCN_MEM_SHARED.0;
}
if info {
flags |= pe::IMAGE_SCN_LNK_INFO.0;
}
Ok(Shape {
alloc: !drop && !info,
write,
exec: code,
bits: !(zero && !data),
coff: flags,
..Shape::default()
})
}
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;
}
if self.retain {
flags |= elf::SHF_GNU_RETAIN.0;
}
if self.merge != 0 {
flags |= elf::SHF_MERGE.0;
if self.strings {
flags |= elf::SHF_STRINGS.0;
}
}
elf::SectionFlags(flags)
}
pub(crate) fn sh_type(self) -> elf::SectionType {
match self.array {
_ if !self.bits => elf::SHT_NOBITS,
_ if self.note => elf::SHT_NOTE,
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,
Ifunc,
File,
#[default]
Untyped,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Assembled {
pub parts: Vec<Part>,
pub names: Vec<Name>,
pub subsections: bool,
}
pub fn assembled(input: &Assembled, target: &TargetInfo) -> Result<Vec<u8>, Error> {
assembled_described(input, target, &Info::default())
}
pub fn assembled_described(
input: &Assembled,
target: &TargetInfo,
info: &Info,
) -> Result<Vec<u8>, Error> {
let (flavour, machine) = match Flavour::of(target) {
Some(flavour) => match flavour.machine(target.tuple.arch()) {
Some(machine) => (flavour, machine),
None => return Err(Error::Format { triple: target.tuple.to_string() }),
},
None if target.tuple.arch() == Arch::Aarch64
&& target.object_format == ObjectFormat::MachO =>
{
return crate::macho::write(input, target, info);
}
None => return Err(Error::Format { triple: target.tuple.to_string() }),
};
let flags_of = |kind, after| flavour.reloc(machine, kind, after);
let carried = machine == Architecture::Aarch64 && flavour == Flavour::Coff;
let mut obj = Writer::new(flavour.binary(), machine, Endianness::Little);
obj.set_mangling(object::write::Mangling::None);
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());
if let Some(mut flags) = flavour.stated(part.shape) {
if let SectionFlags::Elf { sh_flags, .. } = &mut flags {
if part.group.is_some() {
sh_flags.0 |= elf::SHF_GROUP.0;
}
if part.link.is_some() {
sh_flags.0 |= elf::SHF_LINK_ORDER.0;
}
}
obj.section_mut(id).flags = 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);
}
if part.group.is_some() && flavour == Flavour::Coff {
obj.section_symbol(id);
}
made.push(id);
}
let defined: Map<&str, &Name> =
input.names.iter().map(|name| (name.name.as_str(), name)).collect();
let pe32 = flavour == Flavour::Coff && machine == Architecture::I386;
let onto = |reloc: &Reloc| moved(flavour, pe32, input, &defined, reloc);
let wanted: Set<&str> = input
.parts
.iter()
.flat_map(|part| &part.relocs)
.filter(|reloc| onto(reloc).is_none())
.map(|reloc| reloc.symbol.as_str())
.chain(
input
.parts
.iter()
.filter_map(|part| part.group.as_ref())
.map(|group| group.symbol.as_str()),
)
.collect();
let mut symbols = std::collections::BTreeMap::new();
for name in &input.names {
let dropped = flavour == Flavour::Elf || pe32;
if dropped && unseen(name, pe32) && !wanted.contains(name.name.as_str()) {
continue;
}
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 kind = match (flavour, name.at, name.sort) {
(Flavour::Coff, Held::Absolute(_), Sort::Untyped) => SymbolKind::Data,
_ => flavour.sort(name.sort, name.binding),
};
let id = obj.add_symbol(Symbol {
name: name.name.clone().into_bytes(),
value,
size,
kind,
scope: crate::file::scope_of(name.binding),
weak: name.binding == Binding::Weak,
section,
flags: SymbolFlags::None,
});
flavour.see(&mut obj, id, name.binding, name.visibility);
if name.sort == Sort::Ifunc {
if flavour != Flavour::Elf {
let why = format!("'{}' is an indirect function, which only ELF has", name.name);
return Err(Error::Refused { why });
}
crate::elf::indirect(&mut obj, id);
}
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);
}
let mut groups: Vec<(&Group, Vec<SectionId>)> = Vec::new();
let mut grouped: Map<&str, usize> = Map::default();
for (part, id) in input.parts.iter().zip(&made) {
let Some(group) = &part.group else { continue };
match grouped.get(group.symbol.as_str()) {
Some(&at) if flavour == Flavour::Elf => groups[at].1.push(*id),
_ => {
grouped.insert(group.symbol.as_str(), groups.len());
groups.push((group, vec![*id]));
}
}
}
let mut together = Vec::new();
for (group, sections) in groups {
let symbol = match symbols.get(&group.symbol) {
Some(&symbol) => symbol,
None if flavour == Flavour::Elf => obj.add_symbol(Symbol {
name: group.symbol.clone().into_bytes(),
value: 0,
size: 0,
kind: SymbolKind::Label,
scope: SymbolScope::Compilation,
weak: false,
section: SymbolSection::Section(sections[0]),
flags: SymbolFlags::None,
}),
None => {
let why = format!(
"section '{}' is a COMDAT about '{}', which the file does not define",
obj.section(sections[0]).name().unwrap_or_default(),
group.symbol
);
return Err(Error::Refused { why });
}
};
together.push(group.keep == Keep::Together);
obj.add_comdat(object::write::Comdat { kind: group.keep.kind(), symbol, sections });
}
for (part, id) in input.parts.iter().zip(&made) {
for reloc in &part.relocs {
let (symbol, addend) = match onto(reloc) {
Some((part, offset)) => {
(obj.section_symbol(made[part]), reloc.addend + offset as i64)
}
None => {
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 });
};
(symbol, reloc.addend)
}
};
let flags = flags_of(reloc.kind, reloc.after).ok_or_else(|| Error::Refused {
why: format!("no relocation is {:?}", reloc.kind),
})?;
let addend = match reloc.kind {
crate::section::Reference::Field(fixup) if carried => {
let data = obj.section_mut(*id).data_mut();
let Some(bytes) = data.get_mut(reloc.at..reloc.at + 4) else {
let why = format!("a field at {} is past the end of its section", reloc.at);
return Err(Error::Refused { why });
};
let word = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let word = crate::coff::carry(fixup, word, addend)
.map_err(|why| Error::Refused { why })?;
bytes.copy_from_slice(&word.to_le_bytes());
0
}
_ => addend,
};
let record = Relocation { offset: reloc.at as u64, symbol, addend, flags };
crate::file::relocate(&mut obj, *id, record)?;
}
}
let mut named = Map::default();
for chunk in &info.chunks {
let how = if flavour == Flavour::Elf && obj.architecture() != Architecture::I386 {
info.compress
} else {
Compress::None
};
let id = crate::zlib::debug_section(&mut obj, chunk, how);
named.insert(chunk.name.as_str(), id);
}
for chunk in &info.chunks {
let section = named[chunk.name.as_str()];
for reloc in &chunk.relocs {
let spelled =
if pe32 { crate::coff::decorate(&reloc.symbol) } else { reloc.symbol.clone() };
let (symbol, addend) = match named.get(reloc.symbol.as_str()) {
Some(&id) => (obj.section_symbol(id), reloc.addend),
None => match defined.get(spelled.as_str()).map(|name| name.at) {
Some(Held::In { part, offset }) => {
(obj.section_symbol(made[part]), reloc.addend + offset as i64)
}
_ => match symbols.get(&spelled) {
Some(&symbol) => (symbol, reloc.addend),
None => {
let why = format!(
"'{}' is named by the debug information and is not defined here",
reloc.symbol
);
return Err(Error::Refused { why });
}
},
},
};
let kind = flavour.debug(reloc.kind, named.contains_key(reloc.symbol.as_str()));
let flags = flags_of(kind, reloc.after)
.ok_or_else(|| Error::Refused { why: format!("no relocation is {kind:?}") })?;
let record = Relocation { offset: reloc.at as u64, symbol, addend, flags };
crate::file::relocate(&mut obj, section, record)?;
}
}
let mut bytes = obj.write().map_err(|why| Error::Refused { why: why.to_string() })?;
if flavour == Flavour::Elf {
for part in input.parts.iter().filter(|part| part.shape.merge != 0) {
entry_size(&mut bytes, &part.name, part.shape.merge);
}
together_groups(&mut bytes, &together);
linked(&mut bytes, input, &defined)?;
}
Ok(bytes)
}
fn linked(bytes: &mut [u8], input: &Assembled, defined: &Map<&str, &Name>) -> Result<(), Error> {
if input.parts.iter().all(|part| part.link.is_none()) {
return Ok(());
}
let headers = crate::elf::Headers::read(bytes);
let index = |part: usize| {
let name = &input.parts[part].name;
let nth = input.parts[..part].iter().filter(|other| other.name == *name).count();
headers.list.iter().enumerate().filter(|(_, header)| header.name == *name).nth(nth)
};
let mut writes = Vec::new();
for (at, part) in input.parts.iter().enumerate() {
let Some(link) = &part.link else { continue };
let target = match defined.get(link.as_str()).map(|name| name.at) {
Some(Held::In { part, .. }) => Some(part),
_ => input.parts.iter().position(|other| other.name == *link),
};
let Some(target) = target.and_then(index) else {
let why = format!("section '{}' goes with '{link}', which is in no section", part.name);
return Err(Error::Refused { why });
};
let (_, header) = index(at).expect("a header for every section");
let target = u32::try_from(target.0).expect("a file with this many sections in it");
writes.push((header.at + headers.link(), target));
}
for (at, target) in writes {
bytes[at..at + 4].copy_from_slice(&target.to_le_bytes());
}
Ok(())
}
fn entry_size(bytes: &mut [u8], name: &str, size: u64) {
let headers = crate::elf::Headers::read(bytes);
let (field, width) = headers.entry_size();
for header in headers.list.iter().filter(|header| header.name == name) {
let at = header.at + field;
bytes[at..at + width].copy_from_slice(&size.to_le_bytes()[..width]);
}
}
fn together_groups(bytes: &mut [u8], together: &[bool]) {
if !together.contains(&true) {
return;
}
let headers = crate::elf::Headers::read(bytes);
let groups = headers.list.iter().filter(|header| header.sh_type == elf::SHT_GROUP.0);
for (header, _) in groups.zip(together).filter(|(_, together)| **together) {
bytes[header.offset..header.offset + 4].copy_from_slice(&0u32.to_le_bytes());
}
}
fn moved(
flavour: Flavour,
pe32: bool,
input: &Assembled,
defined: &Map<&str, &Name>,
reloc: &Reloc,
) -> Option<(usize, u64)> {
use crate::section::Reference;
let name = defined.get(reloc.symbol.as_str())?;
let Held::In { part, offset } = name.at else { return None };
if name.sort == Sort::Ifunc {
return None;
}
if pe32 && unseen(name, pe32) {
return Some((part, offset));
}
if flavour != Flavour::Elf || name.binding != Binding::Local {
return None;
}
let near = matches!(
reloc.kind,
Reference::Data | Reference::Away | Reference::AwayWide | Reference::Short
);
let fixed = match reloc.kind {
Reference::Call
| Reference::Got
| Reference::GotBare
| Reference::GotKept
| Reference::Slot
| Reference::SlotKept
| Reference::GotFront
| Reference::Thread
| Reference::Tls(_) => false,
Reference::Field(
Fixup::Call26
| Fixup::Jump26
| Fixup::GotPage21
| Fixup::GotLo12
| Fixup::GotTprelPage21
| Fixup::GotTprelLo12Nc
| Fixup::TprelHi12
| Fixup::TprelLo12Nc,
) => false,
_ if input.parts.get(part)?.shape.merge != 0 => !near && reloc.addend == 0,
_ => true,
};
fixed.then_some((part, offset))
}
fn unseen(name: &Name, pe32: bool) -> bool {
name.binding == Binding::Local
&& (name.name.starts_with(".L")
|| name.name.starts_with("..")
|| name.name.contains('\u{1}')
|| (pe32 && name.name.starts_with('L')))
}
#[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()
}
#[cfg(test)]
mod tests {
use super::*;
use object::read::elf::{FileHeader as _, SectionHeader as _, Sym as _};
use object::read::{Object as _, ObjectComdat as _, ObjectSection as _, ObjectSymbol as _};
use object::{RelocationFlags, SectionFlags};
use rucc_target::{Arch as TargetArch, Env, Os, Triple};
use crate::section::{Reference, Tls};
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(TargetArch::X86_64, Os::Linux, Env::Gnu))
}
fn windows() -> TargetInfo {
TargetInfo::new(Triple::new(TargetArch::X86_64, Os::Windows, 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(),
group: None,
link: None,
}
}
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(), subsections: false };
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(), subsections: false };
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)],
subsections: false,
};
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),
],
subsections: false,
};
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,
}],
subsections: false,
};
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,
}],
subsections: false,
};
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_local_set_on_coff_is_a_static_number() {
let input = Assembled {
parts: vec![part(".text", vec![0xc3])],
names: vec![Name {
name: "@feat.00".to_owned(),
at: Held::Absolute(1),
size: 0,
sort: Sort::Untyped,
binding: Binding::Local,
visibility: Visibility::Default,
}],
subsections: false,
};
let bytes = assembled(&input, &windows()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let sym = file.symbols().find(|s| s.name() == Ok("@feat.00")).expect("the symbol");
let coff = object::read::coff::CoffFile::<&[u8]>::parse(&bytes[..]).expect("COFF");
let raw = coff.symbol_by_name("@feat.00").expect("the symbol");
assert_eq!(object::read::coff::Symbol::value(raw.coff_symbol()), 1);
assert_eq!(sym.section(), object::SymbolSection::Absolute);
assert!(sym.is_local());
assert!(
matches!(
sym.flags(),
SymbolFlags::Coff { storage_class: pe::IMAGE_SYM_CLASS_STATIC, .. }
),
"{:?}",
sym.flags()
);
}
#[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,
}],
subsections: false,
};
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_place_only_this_file_sees_is_reached_through_its_section_as_gas_does() {
let mut text = part(".text", vec![0; 32]);
for (at, symbol, kind) in [
(0, ".L3", Reference::Data),
(4, "helper", Reference::Data),
(8, "helper", Reference::Call),
(12, "shared", Reference::Data),
] {
let symbol = symbol.to_owned();
text.relocs.push(Reloc { at, symbol, kind, addend: -4, after: 0 });
}
let input = Assembled {
parts: vec![text],
names: vec![
at(".L3", 20, Sort::Untyped, Binding::Local),
at("helper", 24, Sort::Func, Binding::Local),
at("shared", 28, Sort::Func, Binding::Global),
],
subsections: false,
};
let bytes = assembled(&input, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let names: Vec<_> = file.symbols().filter_map(|sym| sym.name().ok()).collect();
assert!(!names.contains(&".L3") && names.contains(&"helper"), "{names:?}");
let section = file.section_by_name(".text").expect("the section");
let reached: Vec<_> = section
.relocations()
.map(|(at, reloc)| {
let object::RelocationTarget::Symbol(index) = reloc.target() else {
panic!("a symbol")
};
let symbol = file.symbol_by_index(index).expect("the symbol");
let name = if symbol.kind() == SymbolKind::Section {
".text"
} else {
symbol.name().expect("a name")
};
(at, name, reloc.addend())
})
.collect();
assert_eq!(
reached,
[(0, ".text", 16), (4, ".text", 20), (8, "helper", -4), (12, "shared", -4)]
);
}
#[test]
fn a_section_of_constants_may_be_merged_and_a_distance_into_it_keeps_its_name() {
let mut text = part(".text", vec![0; 8]);
text.relocs.push(Reloc {
at: 0,
symbol: ".LC0".to_owned(),
kind: Reference::Data,
addend: -4,
after: 0,
});
let strings = Part {
shape: Shape { merge: 1, strings: true, ..Shape::of(".rodata") },
..part(".rodata.str1.1", b"hi\0".to_vec())
};
let mut name = at(".LC0", 0, Sort::Untyped, Binding::Local);
name.at = Held::In { part: 1, offset: 0 };
let input = Assembled { parts: vec![text, strings], names: vec![name], subsections: false };
let bytes = assembled(&input, &target()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let section = file.section_by_name(".rodata.str1.1").expect("the section");
let SectionFlags::Elf { sh_flags, .. } = section.flags() else { panic!("an ELF file") };
assert_eq!(sh_flags.0, elf::SHF_ALLOC.0 | elf::SHF_MERGE.0 | elf::SHF_STRINGS.0);
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 (_, found) = table.section_by_name(endian, b".rodata.str1.1").expect("the section");
assert_eq!(found.sh_entsize.get(endian), 1);
let text = file.section_by_name(".text").expect("the section");
let (_, reloc) = text.relocations().next().expect("one relocation");
let object::RelocationTarget::Symbol(index) = reloc.target() else { panic!("a symbol") };
assert_eq!(file.symbol_by_index(index).and_then(|sym| sym.name()), Ok(".LC0"));
}
#[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(), subsections: false };
let why = assembled(&input, &target()).expect_err("this cannot be written");
assert!(format!("{why}").contains("nowhere"), "{why}");
}
#[test]
fn the_stack_is_marked_only_by_the_file_and_only_once() {
let bare = Assembled {
parts: vec![part(".text", vec![0x90])],
names: Vec::new(),
subsections: false,
};
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_none(), "a marker nobody wrote");
let said = Assembled {
parts: vec![part(".text", vec![0x90]), part(".note.GNU-stack", Vec::new())],
names: Vec::new(),
subsections: false,
};
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,
},
],
subsections: false,
};
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(),
subsections: false,
};
let elsewhere = TargetInfo::new(Triple::new(TargetArch::Riscv64, Os::Linux, Env::Gnu));
let why = assembled(&input, &elsewhere).expect_err("this cannot be written");
assert!(format!("{why}").contains("riscv64"), "{why}");
}
#[test]
fn a_file_of_assembly_for_aarch64_is_written_with_that_machine_s_relocations() {
let mut text = part(".text", vec![0; 12]);
let field = |at, symbol: &str, fixup, addend| Reloc {
at,
symbol: symbol.to_owned(),
kind: Reference::Field(fixup),
addend,
after: 0,
};
text.relocs = vec![
field(0, ".Ltable", Fixup::AdrPage21, 8),
field(4, ".Ltable", Fixup::AddLo12, 8),
field(8, "g", Fixup::Call26, 0),
];
let mut data = part(".data", vec![0; 16]);
data.relocs = vec![Reloc {
at: 8,
symbol: ".Ltable".to_owned(),
kind: Reference::Address { bytes: 8 },
addend: 0,
after: 0,
}];
let mut table = at(".Ltable", 0, Sort::Object, Binding::Local);
table.at = Held::In { part: 1, offset: 0 };
let input = Assembled {
parts: vec![text, data],
names: vec![
table,
Name { at: Held::Undefined, ..at("g", 0, Sort::Untyped, Binding::Global) },
],
subsections: false,
};
let target = TargetInfo::new(Triple::new(TargetArch::Aarch64, Os::Linux, Env::Gnu));
let bytes = assembled(&input, &target).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert_eq!(file.architecture(), Architecture::Aarch64);
let relocs = |name: &str| -> Vec<(u64, elf::RelocationType, i64)> {
let section = file.section_by_name(name).expect("the section");
section
.relocations()
.map(|(at, reloc)| {
let RelocationFlags::Elf { r_type } = reloc.flags() else { panic!("ELF") };
(at, r_type, reloc.addend())
})
.collect()
};
assert_eq!(
relocs(".text"),
[
(0, elf::R_AARCH64_ADR_PREL_PG_HI21, 8),
(4, elf::R_AARCH64_ADD_ABS_LO12_NC, 8),
(8, elf::R_AARCH64_CALL26, 0)
]
);
assert_eq!(relocs(".data"), [(8, elf::R_AARCH64_ABS64, 0)]);
assert!(file.symbols().all(|s| s.name() != Ok(".Ltable")), "a label only this file sees");
}
#[test]
fn a_file_of_assembly_for_aarch64_windows_carries_its_addends_in_the_instructions() {
let adrp = 0x9000_0000u32;
let add = 0x9100_0000u32;
let ldr = 0xf940_0000u32;
let bl = 0x9400_0000u32;
let words = [adrp, add, ldr, bl, add | 1 << 22, add];
let mut text = part(".text", words.iter().flat_map(|word| word.to_le_bytes()).collect());
let field = |at, symbol: &str, fixup, addend| Reloc {
at,
symbol: symbol.to_owned(),
kind: Reference::Field(fixup),
addend,
after: 0,
};
text.relocs = vec![
field(0, "table", Fixup::AdrPage21, 8),
field(4, "table", Fixup::AddLo12, 8),
field(8, "table", Fixup::Ldst64Lo12, 8),
field(12, "g", Fixup::Call26, 0),
field(16, "counter", Fixup::SecrelHigh12A, 0),
field(20, "counter", Fixup::SecrelLow12A, 0),
];
let mut data = part(".data", vec![0; 16]);
data.relocs = vec![
Reloc {
at: 0,
symbol: "table".to_owned(),
kind: Reference::Address { bytes: 8 },
addend: 8,
after: 0,
},
Reloc { at: 8, symbol: "g".to_owned(), kind: Reference::Data, addend: 0, after: 0 },
Reloc {
at: 12,
symbol: "table".to_owned(),
kind: Reference::Image,
addend: 0,
after: 0,
},
];
let mut table = at("table", 0, Sort::Object, Binding::Global);
table.at = Held::In { part: 1, offset: 0 };
let undefined =
|name| Name { at: Held::Undefined, ..at(name, 0, Sort::Untyped, Binding::Global) };
let input = Assembled {
parts: vec![text, data],
names: vec![table, undefined("g"), undefined("counter")],
subsections: false,
};
let target = TargetInfo::new(Triple::new(TargetArch::Aarch64, Os::Windows, Env::Gnu));
let bytes = assembled(&input, &target).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert_eq!(file.format(), object::BinaryFormat::Coff);
assert_eq!(file.architecture(), Architecture::Aarch64);
let relocs = |name: &str| -> Vec<(u64, u16, String)> {
let section = file.section_by_name(name).expect("the section");
section
.relocations()
.map(|(at, reloc)| {
let RelocationFlags::Coff { typ } = reloc.flags() else { panic!("COFF") };
let object::RelocationTarget::Symbol(symbol) = reloc.target() else {
panic!("a symbol")
};
let symbol = file.symbol_by_index(symbol).expect("the symbol");
(at, typ.0, symbol.name().expect("a name").to_owned())
})
.collect()
};
let named = |at, typ: pe::RelocationType, name: &str| (at, typ.0, name.to_owned());
assert_eq!(
relocs(".text"),
[
named(0, pe::IMAGE_REL_ARM64_PAGEBASE_REL21, "table"),
named(4, pe::IMAGE_REL_ARM64_PAGEOFFSET_12A, "table"),
named(8, pe::IMAGE_REL_ARM64_PAGEOFFSET_12L, "table"),
named(12, pe::IMAGE_REL_ARM64_BRANCH26, "g"),
named(16, pe::IMAGE_REL_ARM64_SECREL_HIGH12A, "counter"),
named(20, pe::IMAGE_REL_ARM64_SECREL_LOW12A, "counter"),
]
);
assert_eq!(
relocs(".data"),
[
named(0, pe::IMAGE_REL_ARM64_ADDR64, "table"),
named(8, pe::IMAGE_REL_ARM64_REL32, "g"),
named(12, pe::IMAGE_REL_ARM64_ADDR32NB, "table"),
]
);
let text = file.section_by_name(".text").expect("the section");
let text = text.data().expect("the bytes");
let word = |nth: usize| u32::from_le_bytes(text[nth * 4..nth * 4 + 4].try_into().unwrap());
assert_eq!(word(0), adrp | 2 << 5, "eight bytes is immhi two and immlo nothing");
assert_eq!(word(1), add | 8 << 10);
assert_eq!(word(2), ldr | 1 << 10, "eight bytes is one doubleword");
assert_eq!([word(3), word(4), word(5)], [bl, add | 1 << 22, add]);
let data = file.section_by_name(".data").expect("the section");
let data = data.data().expect("the bytes");
assert_eq!(data[..8], 8u64.to_le_bytes());
assert_eq!(data[8..12], 4u32.to_le_bytes());
}
#[test]
fn an_addend_an_aarch64_coff_field_cannot_carry_is_refused() {
for (word, fixup, addend) in
[(0xb940_0000u32, Fixup::Ldst32Lo12, 2), (0x9400_0000, Fixup::Call26, 4)]
{
let mut text = part(".text", word.to_le_bytes().to_vec());
text.relocs = vec![Reloc {
at: 0,
symbol: "g".to_owned(),
kind: Reference::Field(fixup),
addend,
after: 0,
}];
let input = Assembled {
parts: vec![text],
names: vec![Name {
at: Held::Undefined,
..at("g", 0, Sort::Untyped, Binding::Global)
}],
subsections: false,
};
let target = TargetInfo::new(Triple::new(TargetArch::Aarch64, Os::Windows, Env::Gnu));
let why = assembled(&input, &target).expect_err("a field that cannot say it");
assert!(format!("{why}").contains(fixup.name()), "{why}");
}
}
#[test]
fn a_file_of_assembly_for_windows_is_written_as_coff() {
let input = Assembled {
parts: vec![part(".text", vec![0xc3])],
names: Vec::new(),
subsections: false,
};
let bytes = assembled(&input, &windows()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert_eq!(file.format(), object::BinaryFormat::Coff);
let section = file.section_by_name(".text").expect("the section");
assert_eq!(section.data().expect("the bytes"), &[0xc3]);
assert_eq!(section.kind(), SectionKind::Text);
assert!(
file.section_by_name(".note.GNU-stack").is_none(),
"a format with no marker got one anyway"
);
}
#[test]
fn a_coff_section_keeps_the_letters_it_was_given_and_its_comdat() {
let drectve = Part {
shape: Shape::coff("yni").expect("the letters"),
..part(".drectve", b" -exclude-symbols:f".to_vec())
};
let refptr = Part {
shape: Shape::coff("dr").expect("the letters"),
group: Some(Group { symbol: ".refptr.x".to_owned(), keep: Keep::Any }),
..part(".rdata$.refptr.x", vec![0; 8])
};
let input = Assembled {
parts: vec![refptr, drectve],
names: vec![at(".refptr.x", 0, Sort::Object, Binding::Global)],
subsections: false,
};
let bytes = assembled(&input, &windows()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let flags = |name: &str| match file.section_by_name(name).expect("the section").flags() {
SectionFlags::Coff { characteristics } => characteristics.0,
other => panic!("{other:?}"),
};
let removed = pe::IMAGE_SCN_LNK_REMOVE.0 | pe::IMAGE_SCN_LNK_INFO.0;
assert_eq!(flags(".drectve") & removed, removed);
assert_eq!(flags(".drectve") & (pe::IMAGE_SCN_MEM_READ.0 | pe::IMAGE_SCN_MEM_WRITE.0), 0);
let refptr = flags(".rdata$.refptr.x");
assert_ne!(refptr & pe::IMAGE_SCN_LNK_COMDAT.0, 0, "not a COMDAT");
assert_eq!(refptr & pe::IMAGE_SCN_MEM_WRITE.0, 0, "`r` did not take writing away");
let comdat = file.comdats().next().expect("the COMDAT");
assert_eq!(comdat.kind(), object::ComdatKind::Any);
assert_eq!(file.symbol_by_index(comdat.symbol()).unwrap().name(), Ok(".refptr.x"));
let input = Assembled { names: Vec::new(), ..input };
assert!(assembled(&input, &windows()).is_err());
}
#[test]
fn a_global_label_with_no_type_under_it_is_still_offered_on_coff() {
let input = Assembled {
parts: vec![part(".text", vec![0; 8])],
names: vec![
at("offered", 0, Sort::Untyped, Binding::Global),
at("ours", 4, Sort::Untyped, Binding::Local),
],
subsections: false,
};
let bytes = assembled(&input, &windows()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let offered = file.symbols().find(|s| s.name() == Ok("offered")).expect("the label");
assert!(offered.is_global(), "a `.globl` label came out local");
let ours = file.symbols().find(|s| s.name() == Ok("ours")).expect("the other label");
assert!(!ours.is_global(), "a label nothing offered came out global");
let bytes = assembled(&input, &target()).expect("an object");
assert_eq!(st_info(&bytes, "offered") & 0xf, elf::STT_NOTYPE.0);
}
#[test]
fn a_relocation_on_coff_says_how_much_of_the_instruction_comes_after_it() {
let mut text = part(".text", vec![0; 16]);
text.relocs.push(Reloc {
at: 2,
symbol: "elsewhere".to_owned(),
kind: Reference::Data,
addend: -8,
after: 4,
});
let input = Assembled {
parts: vec![text],
names: vec![Name {
name: "elsewhere".to_owned(),
at: Held::Undefined,
size: 0,
sort: Sort::Untyped,
binding: Binding::Global,
visibility: Visibility::Default,
}],
subsections: false,
};
let bytes = assembled(&input, &windows()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let section = file.section_by_name(".text").expect("the section");
let (at, reloc) = section.relocations().next().expect("the relocation");
assert_eq!(at, 2);
assert_eq!(
reloc.flags(),
RelocationFlags::Coff { typ: pe::RelocationType(pe::IMAGE_REL_AMD64_REL32.0 + 4) }
);
}
fn i386() -> TargetInfo {
TargetInfo::new(Triple::new(TargetArch::X86, Os::Linux, Env::Gnu))
}
fn implicit(file: &object::File<'_>, section: &str) -> Vec<(u64, u32, String, i64)> {
let section = file.section_by_name(section).expect("the section");
let data = section.data().expect("the bytes");
section
.relocations()
.map(|(at, reloc)| {
assert!(reloc.has_implicit_addend(), "a REL file keeps the addend in the bytes");
assert_eq!(reloc.addend(), 0);
let RelocationFlags::Elf { r_type } = reloc.flags() else { panic!("ELF") };
let width = crate::elf::width_i386(r_type).expect("a width");
let at_ = at as usize;
let mut word = [0u8; 8];
word[..width].copy_from_slice(&data[at_..at_ + width]);
let shift = 64 - 8 * width as u32;
let addend = (i64::from_le_bytes(word) << shift) >> shift;
let object::RelocationTarget::Symbol(index) = reloc.target() else {
panic!("a relocation against a symbol")
};
let symbol = file.symbol_by_index(index).expect("the symbol");
let name = match symbol.kind() {
SymbolKind::Section => {
let section = symbol.section_index().expect("a section symbol's section");
file.section_by_index(section)
.expect("it")
.name()
.expect("a name")
.to_owned()
}
_ => symbol.name().expect("a name").to_owned(),
};
(at, r_type.0, name, addend)
})
.collect()
}
#[test]
fn an_i386_object_is_32_bit_elf_with_the_addends_in_the_bytes() {
let reloc = |at, symbol: &str, kind, addend| Reloc {
at,
symbol: symbol.to_owned(),
kind,
addend,
after: 0,
};
let code = vec![
0xe8, 0, 0, 0, 0, 0xe8, 0, 0, 0, 0, 0x81, 0xc3, 0, 0, 0, 0, 0x8d, 0x83, 0, 0, 0, 0, 0x8b, 0x83, 0, 0, 0, 0, 0x89, 0x83, 0, 0, 0, 0, 0xa1, 0, 0, 0, 0, ];
let mut text = part(".text", code);
text.relocs = vec![
reloc(1, "foo", Reference::Call, -4),
reloc(6, "bar", Reference::Data, -4),
reloc(12, "_GLOBAL_OFFSET_TABLE_", Reference::GotFront, 2),
reloc(18, ".LC0", Reference::GotOffset, 0),
reloc(24, "foo", Reference::Slot, 0),
reloc(30, "foo", Reference::SlotKept, 0),
reloc(35, "counter", Reference::Signed, 8),
];
let rodata = part(".rodata", b"abc\0hi\0\0".to_vec());
let mut data = part(".data", vec![0; 11]);
data.relocs = vec![
reloc(0, "foo", Reference::Address { bytes: 4 }, 16),
reloc(4, "bar", Reference::Away, 0),
reloc(8, "foo", Reference::Address { bytes: 2 }, 0),
reloc(10, "foo", Reference::Address { bytes: 1 }, 0),
];
let undefined = |name: &str| Name {
at: Held::Undefined,
..at(name, 0, Sort::Untyped, Binding::Global)
};
let names = vec![
at("f", 0, Sort::Func, Binding::Global),
Name {
at: Held::In { part: 1, offset: 4 },
..at(".LC0", 0, Sort::Untyped, Binding::Local)
},
undefined("foo"),
undefined("bar"),
undefined("counter"),
undefined("_GLOBAL_OFFSET_TABLE_"),
];
let input = Assembled { parts: vec![text, rodata, data], names, subsections: false };
let bytes = assembled(&input, &i386()).expect("an object");
let header = elf::FileHeader32::<Endianness>::parse(&bytes[..]).expect("a 32 bit header");
let endian = header.endian().expect("an endianness");
assert_eq!(bytes[4], elf::ELFCLASS32.0);
assert_eq!(header.e_machine(endian), elf::EM_386);
assert_eq!(header.e_type(endian), elf::ET_REL);
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert!(!file.is_64());
assert_eq!(file.architecture(), Architecture::I386);
for name in [".rel.text", ".rel.data"] {
let section = file.section_by_name(name).expect("a REL section");
let SectionFlags::Elf { sh_type, .. } = section.flags() else { panic!("ELF") };
assert_eq!(sh_type, elf::SHT_REL, "{name}");
}
assert!(file.section_by_name(".rela.text").is_none(), "i386 has no addend field");
let text = implicit(&file, ".text");
let want = [
(1, elf::R_386_PLT32, "foo", -4),
(6, elf::R_386_PC32, "bar", -4),
(12, elf::R_386_GOTPC, "_GLOBAL_OFFSET_TABLE_", 2),
(18, elf::R_386_GOTOFF, ".rodata", 4),
(24, elf::R_386_GOT32X, "foo", 0),
(30, elf::R_386_GOT32, "foo", 0),
(35, elf::R_386_32, "counter", 8),
];
let want: Vec<_> = want
.into_iter()
.map(|(at, r_type, name, addend)| (at, r_type.0, name.to_owned(), addend))
.collect();
assert_eq!(text, want);
let data = implicit(&file, ".data");
let want = [
(0, elf::R_386_32, "foo", 16),
(4, elf::R_386_PC32, "bar", 0),
(8, elf::R_386_16, "foo", 0),
(10, elf::R_386_8, "foo", 0),
];
let want: Vec<_> = want
.into_iter()
.map(|(at, r_type, name, addend)| (at, r_type.0, name.to_owned(), addend))
.collect();
assert_eq!(data, want);
}
#[test]
fn the_i386_thread_local_references_are_the_relocations_gas_writes() {
let kinds = [
(Tls::General, elf::R_386_TLS_GD),
(Tls::Module, elf::R_386_TLS_LDM),
(Tls::InModule, elf::R_386_TLS_LDO_32),
(Tls::Slot, elf::R_386_TLS_GOTIE),
(Tls::SlotAddress, elf::R_386_TLS_IE),
(Tls::SlotNegated, elf::R_386_TLS_IE_32),
(Tls::Offset, elf::R_386_TLS_LE),
(Tls::Negated, elf::R_386_TLS_LE_32),
];
let mut text = part(".text", vec![0; 4 * kinds.len()]);
text.relocs = kinds
.iter()
.enumerate()
.map(|(n, &(tls, _))| Reloc {
at: 4 * n,
symbol: "x".to_owned(),
kind: Reference::Tls(tls),
addend: n as i64,
after: 0,
})
.collect();
let tdata = part(".tdata", vec![0; 8]);
let names = vec![
at("f", 0, Sort::Func, Binding::Global),
Name {
at: Held::In { part: 1, offset: 4 },
..at("x", 0, Sort::Thread, Binding::Local)
},
];
let input = Assembled { parts: vec![text, tdata], names, subsections: false };
let bytes = assembled(&input, &i386()).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
let want: Vec<_> = kinds
.iter()
.enumerate()
.map(|(n, &(_, r_type))| (4 * n as u64, r_type.0, "x".to_owned(), n as i64))
.collect();
assert_eq!(implicit(&file, ".text"), want);
}
#[test]
fn an_i386_windows_file_of_assembly_keeps_its_names_as_written() {
use object::pe::{
IMAGE_REL_I386_DIR32, IMAGE_REL_I386_DIR32NB, IMAGE_REL_I386_REL32,
IMAGE_REL_I386_SECREL,
};
let reloc = |at, symbol: &str, kind, addend| Reloc {
at,
symbol: symbol.to_owned(),
kind,
addend,
after: 0,
};
let code = vec![
0xe8, 0, 0, 0, 0, 0xe8, 0, 0, 0, 0, 0xa1, 0, 0, 0, 0, 0xc3,
];
let mut text = part(".text", code);
text.relocs = vec![
reloc(1, "_puts", Reference::Call, -4),
reloc(6, "@f@8", Reference::Call, -4),
reloc(11, "_counter", Reference::Address { bytes: 4 }, 8),
];
let mut data = part(".data", vec![0; 12]);
data.relocs = vec![
reloc(0, "_main", Reference::Image, 0),
reloc(4, "_main", Reference::Section, 0),
reloc(8, "_puts", Reference::Away, 0),
];
let undefined = |name: &str| Name {
at: Held::Undefined,
..at(name, 0, Sort::Untyped, Binding::Global)
};
let names = vec![
at("_main", 0, Sort::Func, Binding::Global),
undefined("_puts"),
undefined("@f@8"),
undefined("_counter"),
];
let input = Assembled { parts: vec![text, data], names, subsections: false };
let target = TargetInfo::new(Triple::new(TargetArch::X86, Os::Windows, Env::Gnu));
let bytes = assembled(&input, &target).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert_eq!(file.format(), object::BinaryFormat::Coff);
assert_eq!(file.architecture(), Architecture::I386);
for name in ["_main", "_puts", "@f@8", "_counter"] {
assert!(file.symbol_by_name(name).is_some(), "{name}");
}
assert!(file.symbol_by_name("__main").is_none());
let types = |name: &str| -> Vec<(u64, u16, String)> {
let section = file.section_by_name(name).expect("a section");
section
.relocations()
.map(|(at, reloc)| {
let RelocationFlags::Coff { typ } = reloc.flags() else { panic!("COFF") };
let object::RelocationTarget::Symbol(symbol) = reloc.target() else {
panic!("a symbol")
};
let symbol = file.symbol_by_index(symbol).expect("a symbol");
(at, typ.0, symbol.name().expect("a name").to_owned())
})
.collect()
};
let want = |list: &[(u64, pe::RelocationType, &str)]| -> Vec<(u64, u16, String)> {
list.iter().map(|&(at, typ, name)| (at, typ.0, name.to_owned())).collect()
};
assert_eq!(
types(".text"),
want(&[
(1, IMAGE_REL_I386_REL32, "_puts"),
(6, IMAGE_REL_I386_REL32, "@f@8"),
(11, IMAGE_REL_I386_DIR32, "_counter"),
])
);
assert_eq!(
types(".data"),
want(&[
(0, IMAGE_REL_I386_DIR32NB, "_main"),
(4, IMAGE_REL_I386_SECREL, "_main"),
(8, IMAGE_REL_I386_REL32, "_puts"),
])
);
let code = file.section_by_name(".text").expect("a text section");
let code = code.data().expect("the bytes");
assert_eq!(&code[1..5], &0i32.to_le_bytes(), "a call counts from the end of its bytes");
assert_eq!(&code[11..15], &8i32.to_le_bytes());
let image = file.section_by_name(".data").expect("a data section");
assert_eq!(&image.data().expect("the bytes")[8..12], &4i32.to_le_bytes());
}
#[test]
fn an_i386_windows_label_of_gccs_own_is_its_section_and_not_a_name() {
use object::pe::{IMAGE_REL_I386_DIR32, IMAGE_REL_I386_REL32};
let mut text = part(".text", vec![0xc7, 0x04, 0x24, 0, 0, 0, 0, 0xe9, 0, 0, 0, 0, 0xc3]);
let reloc = |at, symbol: &str, kind, addend| Reloc {
at,
symbol: symbol.to_owned(),
kind,
addend,
after: 0,
};
text.relocs = vec![
reloc(3, "LC1", Reference::Address { bytes: 4 }, 0),
reloc(8, "Lfar", Reference::Data, -4),
];
let rdata = part(".rdata", vec![0; 8]);
let names = vec![
at("_f", 0, Sort::Func, Binding::Global),
at("L3", 12, Sort::Untyped, Binding::Local),
Name {
at: Held::In { part: 1, offset: 4 },
..at("LC1", 0, Sort::Untyped, Binding::Local)
},
Name {
at: Held::In { part: 1, offset: 0 },
..at("Lfar", 0, Sort::Untyped, Binding::Local)
},
];
let input = Assembled { parts: vec![text, rdata], names, subsections: false };
let target = TargetInfo::new(Triple::new(TargetArch::X86, Os::Windows, Env::Gnu));
let bytes = assembled(&input, &target).expect("an object");
let file = object::File::parse(&bytes[..]).expect("a readable object");
assert!(file.symbol_by_name("_f").is_some());
for name in ["L3", "LC1", "Lfar"] {
assert!(file.symbol_by_name(name).is_none(), "{name}");
}
let section = file.section_by_name(".text").expect("a section");
let relocs: Vec<_> = section
.relocations()
.map(|(at, reloc)| {
let RelocationFlags::Coff { typ } = reloc.flags() else { panic!("COFF") };
let object::RelocationTarget::Symbol(symbol) = reloc.target() else {
panic!("a symbol")
};
let symbol = file.symbol_by_index(symbol).expect("a symbol");
(at, typ.0, symbol.name().expect("a name").to_owned())
})
.collect();
assert_eq!(
relocs,
vec![
(3, IMAGE_REL_I386_DIR32.0, ".rdata".to_owned()),
(8, IMAGE_REL_I386_REL32.0, ".rdata".to_owned())
]
);
let code = section.data().expect("the bytes");
assert_eq!(&code[3..7], &4i32.to_le_bytes(), "the distance into .rdata");
assert_eq!(&code[8..12], &0i32.to_le_bytes(), "the start of .rdata, counted from the end");
}
#[test]
fn a_reference_i386_has_no_relocation_for_is_refused() {
for kind in [
Reference::Address { bytes: 8 },
Reference::AwayWide,
Reference::Got,
Reference::GotBare,
Reference::GotKept,
Reference::Thread,
Reference::Image,
Reference::Section,
] {
let mut text = part(".text", vec![0; 8]);
text.relocs =
vec![Reloc { at: 0, symbol: "foo".to_owned(), kind, addend: 0, after: 0 }];
let names =
vec![Name { at: Held::Undefined, ..at("foo", 0, Sort::Untyped, Binding::Global) }];
let input = Assembled { parts: vec![text], names, subsections: false };
let Err(Error::Refused { .. }) = assembled(&input, &i386()) else {
panic!("{kind:?} was written for i386");
};
}
}
#[test]
fn an_i386_addend_too_wide_for_its_bytes_is_refused() {
let mut data = part(".data", vec![0; 2]);
let kind = Reference::Address { bytes: 1 };
data.relocs = vec![Reloc { at: 0, symbol: "foo".to_owned(), kind, addend: 300, after: 0 }];
let names =
vec![Name { at: Held::Undefined, ..at("foo", 0, Sort::Untyped, Binding::Global) }];
let input = Assembled { parts: vec![data], names, subsections: false };
let Err(Error::Refused { why }) = assembled(&input, &i386()) else {
panic!("an addend of 300 went into one byte");
};
assert!(why.contains("300"), "{why}");
}
#[test]
fn a_32_bit_file_gets_its_entry_size_and_groups_in_the_right_place() {
let mut strings = part(".rodata.str1.1", b"hi\0".to_vec());
strings.shape.merge = 1;
strings.shape.strings = true;
let mut kept = part(".text.f", vec![0xc3]);
kept.group = Some(Group { symbol: "f".to_owned(), keep: Keep::Together });
let names = vec![Name {
at: Held::In { part: 1, offset: 0 },
..at("f", 0, Sort::Func, Binding::Global)
}];
let input = Assembled { parts: vec![strings, kept], names, subsections: false };
let bytes = assembled(&input, &i386()).expect("an object");
let header = elf::FileHeader32::<Endianness>::parse(&bytes[..]).expect("a 32 bit header");
let endian = header.endian().expect("an endianness");
let sections = header.sections(endian, &bytes[..]).expect("the sections");
let find = |name: &str| {
sections
.iter()
.find(|section| sections.section_name(endian, section) == Ok(name.as_bytes()))
.expect("the section")
};
assert_eq!(find(".rodata.str1.1").sh_entsize(endian), 1);
let group = find(".group");
let at = group.sh_offset(endian) as usize;
assert_eq!(&bytes[at..at + 4], &[0; 4], "a group that is not a COMDAT says so");
}
#[test]
fn a_section_that_goes_with_a_name_points_at_the_section_the_name_is_in() {
let record = |link: &str| Part {
shape: Shape { write: true, ..Shape::of("__patchable_function_entries") },
link: Some(link.to_owned()),
..part("__patchable_function_entries", vec![0; 8])
};
let parts = vec![
part(".text", vec![0xc3]),
part(".init.text", vec![0xc3]),
record("f"),
record("g"),
];
let names = vec![
Name { at: Held::In { part: 1, offset: 0 }, ..at("f", 0, Sort::Func, Binding::Global) },
at("g", 0, Sort::Func, Binding::Global),
];
let bytes = assembled(&Assembled { parts, names, subsections: false }, &target())
.expect("an object");
let header = elf::FileHeader64::<Endianness>::parse(&bytes[..]).expect("a header");
let endian = header.endian().expect("an endianness");
let sections = header.sections(endian, &bytes[..]).expect("the sections");
let index = |name: &str| {
sections
.iter()
.position(|section| sections.section_name(endian, section) == Ok(name.as_bytes()))
.expect("the section") as u32
};
let records: Vec<_> = sections
.iter()
.filter(|section| {
sections.section_name(endian, section) == Ok(&b"__patchable_function_entries"[..])
})
.collect();
assert_eq!(records.len(), 2);
for record in &records {
assert!(
record.sh_flags(endian).contains(elf::SHF_LINK_ORDER),
"a record that goes with nothing"
);
}
assert_eq!(records[0].sh_link(endian), index(".init.text"));
assert_eq!(records[1].sh_link(endian), index(".text"));
}
#[test]
fn a_section_that_goes_with_a_name_in_no_section_is_refused() {
let record = Part {
link: Some("nowhere".to_owned()),
..part("__patchable_function_entries", vec![0; 8])
};
let input = Assembled { parts: vec![record], names: Vec::new(), subsections: false };
assert!(assembled(&input, &target()).is_err());
}
}