#![allow(clippy::unwrap_used, reason = "test failures should be loud")]
mod common;
use object_lang::{Architecture, FileKind, Object, Section, SectionKind, Symbol};
const EXIT42_X86_64: [u8; 12] = [
0xbf, 0x2a, 0x00, 0x00, 0x00, 0xb8, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x05, ];
const EXIT42_AARCH64: [u8; 12] = [
0x40, 0x05, 0x80, 0xd2, 0xa8, 0x0b, 0x80, 0xd2, 0x01, 0x00, 0x00, 0xd4, ];
fn check_structure(bytes: &[u8], arch: Architecture, code: &[u8]) {
let elf = common::parse(bytes);
assert_eq!(elf.e_type, 2, "ET_EXEC");
let page = arch.page_size();
let address = object_lang::elf::code_address(arch);
assert_eq!(elf.entry, address);
let types: Vec<u32> = elf.segments.iter().map(|s| s.ty).collect();
assert_eq!(types, [1, 1, 0x6474_e551]);
assert_eq!(elf.segments[0].flags, 4, "headers are read-only");
assert_eq!(elf.segments[1].flags, 5, "code is read+execute");
assert_eq!(elf.segments[2].flags, 6, "the stack is not executable");
let memory = common::Memory::load(bytes, &elf, page);
let (loaded, flags) = memory.read(address, code.len() as u64).unwrap();
assert_eq!(loaded, code);
assert!(flags.iter().all(|&f| f == 5));
assert!(memory.read(elf.segments[0].vaddr + 64, 56 * 3).is_some());
let obj = object_lang::elf::read(bytes).unwrap();
assert_eq!(obj.kind(), FileKind::Executable);
assert_eq!(obj.architecture(), arch);
assert_eq!(obj.entry(), address);
assert_eq!(obj.sections()[0].data(), code);
assert_eq!(obj.symbols()[0].name, "_start");
}
#[cfg(target_os = "linux")]
fn run(bytes: &[u8], name: &str) -> std::process::Output {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("object-lang-exec-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(name);
std::fs::write(&path, bytes).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
const ETXTBSY: i32 = 26;
let mut attempts = 0;
let output = loop {
match std::process::Command::new(&path).output() {
Err(e) if e.raw_os_error() == Some(ETXTBSY) && attempts < 100 => {
attempts += 1;
std::thread::sleep(std::time::Duration::from_millis(10));
}
result => break result.unwrap(),
}
};
let _ = std::fs::remove_file(&path);
output
}
#[test]
fn exit42_x86_64() {
let bytes = object_lang::elf::executable(Architecture::X86_64, &EXIT42_X86_64).unwrap();
check_structure(&bytes, Architecture::X86_64, &EXIT42_X86_64);
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
let output = run(&bytes, "exit42");
assert_eq!(output.status.code(), Some(42), "{output:?}");
}
}
#[test]
fn exit42_aarch64() {
let bytes = object_lang::elf::executable(Architecture::Aarch64, &EXIT42_AARCH64).unwrap();
check_structure(&bytes, Architecture::Aarch64, &EXIT42_AARCH64);
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
{
let output = run(&bytes, "exit42");
assert_eq!(output.status.code(), Some(42), "{output:?}");
}
}
struct Layout {
text: u64,
rodata: u64,
data: u64,
bss: u64,
}
impl Layout {
fn new(arch: Architecture) -> Layout {
let page = arch.page_size();
let text = object_lang::elf::code_address(arch);
Layout {
text,
rodata: text + page,
data: text + 2 * page,
bss: text + 2 * page + 4,
}
}
}
fn program(arch: Architecture, code: Vec<u8>) -> Object {
let at = Layout::new(arch);
let mut obj = Object::executable(arch);
let len = code.len() as u64;
let text = obj.add_section(
Section::new(".text", SectionKind::Text)
.with_align(16)
.with_address(at.text)
.with_data(code),
);
obj.add_section(
Section::new(".rodata", SectionKind::ReadOnlyData)
.with_address(at.rodata)
.with_data(b"hello\n".to_vec()),
);
obj.add_section(
Section::new(".data", SectionKind::Data)
.with_align(4)
.with_address(at.data)
.with_data(40u32.to_le_bytes().to_vec()),
);
obj.add_section(
Section::new(".bss", SectionKind::Bss)
.with_align(4)
.with_address(at.bss)
.with_bss_size(4096),
);
obj.add_symbol(Symbol::function("_start", text, at.text, len));
obj.set_entry(at.text);
obj
}
fn x86_64_hello() -> Vec<u8> {
let at = Layout::new(Architecture::X86_64);
let mut code: Vec<u8> = Vec::new();
let rip = |code: &mut Vec<u8>, head: &[u8], target: u64, tail: &[u8]| {
let end = at.text + (code.len() + head.len() + 4 + tail.len()) as u64;
let disp = (target as i64 - end as i64) as i32;
code.extend_from_slice(head);
code.extend_from_slice(&disp.to_le_bytes());
code.extend_from_slice(tail);
};
code.extend_from_slice(&[0xb8, 1, 0, 0, 0]); code.extend_from_slice(&[0xbf, 1, 0, 0, 0]); rip(&mut code, &[0x48, 0x8d, 0x35], at.rodata, &[]); code.extend_from_slice(&[0xba, 6, 0, 0, 0]); code.extend_from_slice(&[0x0f, 0x05]); rip(&mut code, &[0xc7, 0x05], at.bss + 4092, &[2, 0, 0, 0]); rip(&mut code, &[0x8b, 0x3d], at.data, &[]); rip(&mut code, &[0x03, 0x3d], at.bss + 4092, &[]); rip(&mut code, &[0x03, 0x3d], at.bss, &[]); code.extend_from_slice(&[0xb8, 0x3c, 0, 0, 0]); code.extend_from_slice(&[0x0f, 0x05]); code
}
fn aarch64_hello() -> Vec<u8> {
let at = Layout::new(Architecture::Aarch64);
let mut insns: Vec<u32> = Vec::new();
let pc = |insns: &Vec<u32>| at.text + 4 * insns.len() as u64;
let adrp = |pc: u64, rd: u32, target: u64| {
let imm = ((target >> 12) as i64 - (pc >> 12) as i64) as u32;
0x9000_0000 | ((imm & 3) << 29) | (((imm >> 2) & 0x7ffff) << 5) | rd
};
let add_lo12 = |rd: u32, rn: u32, target: u64| {
0x9100_0000 | (((target & 0xfff) as u32) << 10) | (rn << 5) | rd
};
insns.push(0xd280_0020); let p = pc(&insns);
insns.push(adrp(p, 1, at.rodata)); insns.push(add_lo12(1, 1, at.rodata)); insns.push(0xd280_00c2); insns.push(0xd280_0808); insns.push(0xd400_0001); let p = pc(&insns);
insns.push(adrp(p, 3, at.data)); insns.push(add_lo12(3, 3, at.data)); insns.push(0xb940_0060); let p = pc(&insns);
insns.push(adrp(p, 4, at.bss + 4092)); insns.push(add_lo12(4, 4, at.bss + 4092)); insns.push(0x5280_0045); insns.push(0xb900_0085); insns.push(0xb940_0086); insns.push(0x0b06_0000); insns.push(0xd280_0ba8); insns.push(0xd400_0001); insns.iter().flat_map(|i| i.to_le_bytes()).collect()
}
fn check_program(obj: &Object) -> Vec<u8> {
let bytes = object_lang::elf::write(obj).unwrap();
let elf = common::parse(&bytes);
let loads: Vec<u32> = elf
.segments
.iter()
.filter(|s| s.ty == 1)
.map(|s| s.flags)
.collect();
assert_eq!(loads, [4, 5, 4, 6]);
let rw = elf.segments.iter().filter(|s| s.ty == 1).nth(3).unwrap();
assert_eq!((rw.filesz, rw.memsz), (4, 4 + 4096));
let memory = common::Memory::load(&bytes, &elf, obj.architecture().page_size());
let at = Layout::new(obj.architecture());
assert_eq!(memory.read(at.rodata, 6).unwrap().0, b"hello\n");
assert_eq!(memory.read(at.data, 4).unwrap().0, 40u32.to_le_bytes());
assert!(memory.read(at.bss, 4096).unwrap().0.iter().all(|&b| b == 0));
assert_eq!(&object_lang::elf::read(&bytes).unwrap(), obj);
bytes
}
#[test]
fn hello_x86_64_uses_every_kind_of_segment() {
let bytes = check_program(&program(Architecture::X86_64, x86_64_hello()));
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
let output = run(&bytes, "hello");
assert_eq!(output.stdout, b"hello\n");
assert_eq!(output.status.code(), Some(42), "{output:?}");
}
let _ = bytes;
}
#[test]
fn hello_aarch64_uses_every_kind_of_segment() {
let bytes = check_program(&program(Architecture::Aarch64, aarch64_hello()));
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
{
let output = run(&bytes, "hello");
assert_eq!(output.stdout, b"hello\n");
assert_eq!(output.status.code(), Some(42), "{output:?}");
}
let _ = bytes;
}
#[test]
fn executable_tier_one_refuses_empty_code() {
assert!(object_lang::elf::executable(Architecture::X86_64, &[]).is_err());
}