feat(ld.so): lazy binding

Signed-off-by: Anhad Singh <andypython@protonmail.com>
This commit is contained in:
Anhad Singh
2024-12-19 00:29:06 +11:00
parent 30970e111a
commit 5ae1b8e974
4 changed files with 341 additions and 61 deletions
+11 -2
View File
@@ -7,7 +7,11 @@ use core::{
sync::atomic::{AtomicUsize, Ordering},
};
use crate::{c_str::CStr, ld_so::tcb::Tcb, platform::types::*};
use crate::{
c_str::CStr,
ld_so::{linker::Resolve, tcb::Tcb},
platform::types::*,
};
pub const RTLD_LAZY: c_int = 0x0001;
pub const RTLD_NOW: c_int = 0x0002;
@@ -42,6 +46,11 @@ pub unsafe extern "C" fn dladdr(addr: *mut c_void, info: *mut Dl_info) -> c_int
#[no_mangle]
pub unsafe extern "C" fn dlopen(cfilename: *const c_char, flags: c_int) -> *mut c_void {
//TODO support all sort of flags
let resolve = if flags & RTLD_NOW == RTLD_NOW {
Resolve::Now
} else {
Resolve::Lazy
};
let filename = if cfilename.is_null() {
None
@@ -69,7 +78,7 @@ pub unsafe extern "C" fn dlopen(cfilename: *const c_char, flags: c_int) -> *mut
let cbs_c = linker.cbs.clone();
let cbs = cbs_c.borrow();
let id = match (cbs.load_library)(&mut linker, filename) {
let id = match (cbs.load_library)(&mut linker, filename, resolve) {
Err(err) => {
ERROR.store(ERROR_NOT_SUPPORTED.as_ptr() as usize, Ordering::SeqCst);
return ptr::null_mut();
+4 -4
View File
@@ -1,11 +1,11 @@
use super::linker::Linker;
use super::linker::{Linker, Resolve};
use crate::platform::types::c_void;
use alloc::boxed::Box;
use goblin::error::Result;
pub struct LinkerCallbacks {
pub unload: Box<dyn Fn(&mut Linker, usize)>,
pub load_library: Box<dyn Fn(&mut Linker, Option<&str>) -> Result<usize>>,
pub load_library: Box<dyn Fn(&mut Linker, Option<&str>, Resolve) -> Result<usize>>,
pub get_sym: Box<dyn Fn(&Linker, usize, &str) -> Option<*mut c_void>>,
}
@@ -23,8 +23,8 @@ fn unload(linker: &mut Linker, lib_id: usize) {
linker.unload(lib_id)
}
fn load_library(linker: &mut Linker, name: Option<&str>) -> Result<usize> {
linker.load_library(name)
fn load_library(linker: &mut Linker, name: Option<&str>, resolve: Resolve) -> Result<usize> {
linker.load_library(name, resolve)
}
fn get_sym(linker: &Linker, lib_id: usize, name: &str) -> Option<*mut c_void> {
+37 -6
View File
@@ -1,3 +1,5 @@
//! See <https://refspecs.linuxfoundation.org/elf/elf.pdf>.
use super::{
debug::{RTLDDebug, _r_debug},
linker::Symbol,
@@ -33,10 +35,32 @@ use goblin::elf64::{
sym,
};
use goblin::{
elf::Elf,
elf::{
sym::{STB_GLOBAL, STB_WEAK},
Dynamic, Elf,
},
error::{Error, Result},
};
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum SymbolBinding {
/// Global symbols are visible to all object files being combined. One
/// file's definition of a global symbol will satisfy another file's
/// undefined reference to the same global symbol.
Global = STB_GLOBAL,
/// Weak symbols resemble global symbols, but their definitions have lower
/// precedence.
Weak = STB_WEAK,
}
impl SymbolBinding {
#[inline]
pub fn is_global(&self) -> bool {
matches!(self, Self::Global)
}
}
/// Use to represent a library as well as all the symbols that is loaded withen it.
#[derive(Default)]
pub struct DSO {
@@ -57,6 +81,9 @@ pub struct DSO {
pub tls_module_id: usize,
pub tls_offset: usize,
pub use_count: usize,
pub dynamic: Option<Dynamic>,
pub dynsyms: Vec<goblin::elf::sym::Sym>,
}
impl DSO {
@@ -106,15 +133,19 @@ impl DSO {
0
},
tls_offset: tls_offset,
dynamic: elf.dynamic.map(|dynamic| dynamic),
dynsyms: elf.dynsyms.iter().collect(),
};
return Ok((dso, tcb_master));
Ok((dso, tcb_master))
}
pub fn get_sym(&self, name: &str) -> Option<(Symbol, bool)> {
pub fn get_sym(&self, name: &str) -> Option<(Symbol, SymbolBinding)> {
if let Some(value) = self.global_syms.get(name) {
Some((*value, true))
Some((*value, SymbolBinding::Global))
} else if let Some(value) = self.weak_syms.get(name) {
Some((*value, false))
Some((*value, SymbolBinding::Weak))
} else {
None
}
@@ -404,7 +435,7 @@ impl DSO {
.iter()
.filter(|s| s.sh_type == SHT_INIT_ARRAY || s.sh_type == SHT_FINI_ARRAY)
{
let addr = if is_pie_enabled(&elf) {
let addr = if is_pie_enabled(elf) {
mmap_addr + section.vm_range().start
} else {
section.vm_range().start
+289 -49
View File
@@ -6,7 +6,12 @@ use alloc::{
};
use core::{cell::RefCell, mem::transmute, ptr};
use goblin::{
elf::{program_header, reloc, sym::STT_TLS, Elf},
elf::{
dynamic::{DT_PLTGOT, DT_STRTAB},
program_header, reloc,
sym::STT_TLS,
Elf,
},
error::{Error, Result},
};
@@ -19,8 +24,9 @@ use crate::{
unistd::F_OK,
},
io::{self, Read},
ld_so::dso::SymbolBinding,
platform::{
types::{c_int, c_void},
types::{c_char, c_int, c_uint, c_void},
Pal, Sys,
},
};
@@ -68,6 +74,16 @@ impl Symbol {
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub enum Resolve {
/// Resolve all undefined symbols immediately.
Now,
/// Perform lazy binding (i.e. symbols will be resolved when they are first
/// used).
#[default]
Lazy,
}
bitflags::bitflags! {
#[derive(Debug, Default)]
pub struct DebugFlags: u32 {
@@ -141,53 +157,50 @@ impl Linker {
}
pub fn load_program(&mut self, path: &str, base_addr: Option<usize>) -> Result<usize> {
self.load_object(path, &None, base_addr, false)?;
return Ok(self.objects.get(&root_id).unwrap().entry_point);
self.load_object(path, &None, base_addr, false, Resolve::default())?;
// TODO(andypython): make self.load_object() return a reference to the
// loaded object, thereby remove the ugly unwrap().
Ok(self.objects.get(&root_id).unwrap().entry_point)
}
pub fn load_library(&mut self, name: Option<&str>) -> Result<usize> {
pub fn load_library(&mut self, name: Option<&str>, resolve: Resolve) -> Result<usize> {
match name {
Some(name) => {
if let Some(id) = self.name_to_object_id_map.get(name) {
let obj = self.objects.get_mut(id).unwrap();
obj.use_count += 1;
return Ok(*id);
Ok(*id)
} else {
let parent_runpath = &self
.objects
.get(&root_id)
.and_then(|parent| parent.runpath.clone());
let lib_id = self.next_object_id;
self.load_object(name, parent_runpath, None, true)?;
self.load_object(name, parent_runpath, None, true, resolve)?;
return Ok(lib_id);
Ok(lib_id)
}
}
None => return Ok(root_id),
None => Ok(root_id),
}
}
pub fn get_sym(&self, lib_id: usize, name: &str) -> Option<*mut c_void> {
match self.objects.get(&lib_id) {
Some(obj) => {
return obj.get_sym(name).map(|(s, strong)| {
if s.sym_type != STT_TLS {
s.as_ptr()
} else {
unsafe {
let mut tls_index = dl_tls_index {
ti_module: obj.tls_module_id as u64,
ti_offset: s.value as u64,
};
__tls_get_addr(&mut tls_index)
}
}
});
}
_ => {
return None;
}
}
self.objects.get(&lib_id).and_then(|obj| {
obj.get_sym(name).map(|(s, _binding)| {
if s.sym_type != STT_TLS {
s.as_ptr()
} else {
let mut tls_index = dl_tls_index {
ti_module: obj.tls_module_id as u64,
ti_offset: s.value as u64,
};
unsafe { __tls_get_addr(&mut tls_index) }
}
})
})
}
pub fn unload(&mut self, lib_id: usize) {
@@ -219,6 +232,7 @@ impl Linker {
runpath: &Option<String>,
base_addr: Option<usize>,
dlopened: bool,
resolve: Resolve,
) -> Result<()> {
unsafe { _r_debug.state = RTLDState::RT_ADD };
_dl_debug_state();
@@ -335,7 +349,7 @@ impl Linker {
}
}
self.relocate(&new_objects, &objects_data)?;
self.relocate(&new_objects, &objects_data, resolve)?;
self.run_init(&new_objects);
for obj in new_objects.into_iter() {
@@ -420,7 +434,7 @@ impl Linker {
)?;
}
return Ok(());
Ok(())
}
fn search_object(&self, name: &str, parent_runpath: &Option<String>) -> Result<String> {
@@ -474,7 +488,102 @@ impl Linker {
return Ok(data);
}
fn relocate(&self, new_objects: &Vec<DSO>, objects_data: &Vec<Vec<u8>>) -> Result<()> {
fn resolve_sym<'a>(name: &str, objs: impl Iterator<Item = &'a DSO>) -> Option<Symbol> {
let mut res = None;
for dso in objs {
if let Some((sym, binding)) = dso.get_sym(name) {
if binding.is_global() {
return Some(sym);
}
res = Some(sym);
}
}
res
}
/// Perform lazy relocations.
fn lazy_relocate<'a>(
&self,
obj: &DSO,
new_objects: &[DSO],
elf: &Elf,
resolve: Resolve,
) -> Result<()> {
let object_base_addr = obj.mmap.as_ptr() as u64;
if let Some(dynamic) = elf.dynamic.as_ref() {
// Global Offset Table
let got = if let Some(ptr) = {
dynamic
.dyns
.iter()
.find(|r#dyn| r#dyn.d_tag == DT_PLTGOT)
.map(|r#dyn| r#dyn.d_val)
} {
(object_base_addr + ptr) as *mut usize
} else {
assert_eq!(dynamic.info.jmprel, 0);
return Ok(());
};
unsafe {
got.add(1).write(obj.id);
got.add(2).write(__plt_resolve_trampoline as usize);
}
for rel in elf.pltrelocs.iter() {
match (rel.r_type, resolve) {
(reloc::R_X86_64_JUMP_SLOT, Resolve::Lazy) => unsafe {
*((object_base_addr + rel.r_offset) as *mut u64) += object_base_addr;
},
(reloc::R_X86_64_JUMP_SLOT, Resolve::Now) => {
let sym = elf.dynsyms.get(rel.r_sym).ok_or(Error::Malformed(format!(
"missing symbol for relocation {:?}",
rel
)))?;
let name =
elf.dynstrtab
.get_at(sym.st_name)
.ok_or(Error::Malformed(format!(
"missing name for symbol {:?}",
sym
)))?;
// FIXME(andypython): warn on unresolved symbols?
let resolved = Linker::resolve_sym(
name,
self.objects.values().chain(new_objects.iter()),
)
.map(|sym| sym.as_ptr())
.unwrap_or(ptr::null_mut());
let addend = rel.r_addend.unwrap_or(0) as u64;
unsafe {
*((object_base_addr + rel.r_offset) as *mut u64) =
resolved as u64 + addend;
}
}
_ => todo!("unsupported relocation type {:?}", rel.r_type),
}
}
}
Ok(())
}
fn relocate(
&self,
new_objects: &Vec<DSO>,
objects_data: &Vec<Vec<u8>>,
resolve: Resolve,
) -> Result<()> {
let symbols_lookup_objects: Vec<&DSO> =
self.objects.values().chain(new_objects.iter()).collect();
@@ -489,12 +598,7 @@ impl Linker {
let b = mmap.as_ptr() as usize;
// Relocate
for rel in elf
.dynrelas
.iter()
.chain(elf.dynrels.iter())
.chain(elf.pltrelocs.iter())
{
for rel in elf.dynrelas.iter().chain(elf.dynrels.iter()) {
trace!(
" rel {}: {:x?}",
reloc::r_to_str(rel.r_type, elf.header.e_machine),
@@ -520,21 +624,20 @@ impl Linker {
reloc::R_X86_64_COPY => 1,
_ => 0,
};
for lookup_id in lookup_start..symbols_lookup_objects.len() {
let lookup_obj = &symbols_lookup_objects[lookup_id];
if let Some((s, strong)) = lookup_obj.get_sym(name) {
for lookup_obj in symbols_lookup_objects.iter().skip(lookup_start) {
if let Some((s, binding)) = lookup_obj.get_sym(name) {
trace!(
"symbol {} from {} found in {} ({})",
"symbol {} from {} found in {} ({:?})",
name,
obj.name,
lookup_obj.name,
if strong { "strong" } else { "weak" }
binding
);
symbol = Some(s);
t = lookup_obj.tls_offset;
found = true;
// Stop looking if any strong symbol is found
if strong {
if binding.is_global() {
break;
}
}
@@ -607,7 +710,7 @@ impl Linker {
let sym = symbol
.as_ref()
.expect("R_X86_64_COPY called without valid symbol");
ptr::copy_nonoverlapping(sym.as_ptr() as *const u8, ptr, sym.size as usize);
ptr::copy_nonoverlapping(sym.as_ptr() as *const u8, ptr, sym.size);
},
_ => {
panic!(
@@ -639,7 +742,7 @@ impl Linker {
prot |= sys_mman::PROT_WRITE;
}
let res = unsafe {
unsafe {
let ptr = if is_pie_enabled(&elf) {
mmap.as_ptr().add(vaddr)
} else {
@@ -649,18 +752,22 @@ impl Linker {
Sys::mprotect(ptr as *mut c_void, vsize, prot).map_err(|_| {
Error::Malformed(format!("failed to mprotect {}", obj.name))
})?;
};
}
}
}
return Ok(());
for (i, obj) in new_objects.iter().enumerate() {
self.lazy_relocate(obj, new_objects, &Elf::parse(&objects_data[i])?, resolve)?;
}
Ok(())
}
fn run_init(&self, objects: &Vec<DSO>) {
use crate::platform::{self, types::*};
for obj in objects.iter().rev() {
if let Some((symbol, true)) = obj.get_sym("__relibc_init_environ") {
if let Some((symbol, SymbolBinding::Global)) = obj.get_sym("__relibc_init_environ") {
unsafe {
symbol
.as_ptr()
@@ -673,3 +780,136 @@ impl Linker {
}
}
}
// GOT[1] = object_id
// GOT[2] = __plt_resolve_trampoline
//
// The stubs in .plt will push the relocation index and the object ID onto the stack and jump to
// [`__plt_resolve_trampoline`]. The trampoline will then call this function to resolve the symbol
// and update the respective GOT entry. The trampoline will then jump to the resolved symbol.
//
// FIXME(andypython): 32-bit
extern "C" fn __plt_resolve_inner(object_id: usize, relocation_index: c_uint) -> *mut c_void {
let tcb = unsafe { Tcb::current() }.unwrap();
assert!(!tcb.linker_ptr.is_null());
let linker = unsafe { &*tcb.linker_ptr }.lock();
let obj = linker.objects.get(&object_id).unwrap();
let obj_base = obj.mmap.as_ptr() as usize;
let dynamic = obj.dynamic.as_ref().unwrap();
let jmprel = dynamic.info.jmprel as usize;
let rela = unsafe {
&*((obj_base + jmprel) as *const reloc::reloc64::Rela).add(relocation_index as usize)
};
assert_eq!(
reloc::reloc64::r_type(rela.r_info),
reloc::R_X86_64_JUMP_SLOT
);
let sym = obj
.dynsyms
.get(reloc::reloc64::r_sym(rela.r_info) as usize)
.expect("symbol not found");
assert_ne!(sym.st_name, 0);
let strtab_offset = dynamic
.dyns
.iter()
.find(|r#dyn| r#dyn.d_tag == DT_STRTAB)
.unwrap()
.d_val;
let name = unsafe {
CStr::from_ptr((strtab_offset + sym.st_name as u64 + obj_base as u64) as *const c_char)
};
let resolved = Linker::resolve_sym(name.to_str().unwrap(), linker.objects.values())
.expect("symbol not found")
.as_ptr();
unsafe {
println!(
"@plt: {} -> *mut {:#x}",
name.to_string_lossy(),
resolved as usize
);
*((obj_base as u64 + rela.r_offset) as *mut u64) = resolved as u64;
}
resolved
}
extern "C" {
fn __plt_resolve_trampoline() -> usize;
}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!(
"
.global __plt_resolve_trampoline
.hidden __plt_resolve_trampoline
__plt_resolve_trampoline:
push rsi
push rdi
mov rdi, qword ptr [rsp + 0x10]
mov rsi, qword ptr [rsp + 0x18]
// stash the floating point argument registers
sub rsp, 128
movdqu [rsp + 0x00], xmm0
movdqu [rsp + 0x10], xmm1
movdqu [rsp + 0x20], xmm2
movdqu [rsp + 0x30], xmm3
movdqu [rsp + 0x40], xmm4
movdqu [rsp + 0x50], xmm5
movdqu [rsp + 0x60], xmm6
movdqu [rsp + 0x70], xmm7
push rax
push rcx
push rdx
push r8
push r9
push r10
push rbp
mov rbp, rsp
and rsp, 0xfffffffffffffff0
call {__plt_resolve_inner}
mov r11, rax
mov rsp, rbp
pop rbp
pop r10
pop r9
pop r8
pop rdx
pop rcx
pop rax
movdqu xmm7, [rsp + 0x70]
movdqu xmm6, [rsp + 0x60]
movdqu xmm5, [rsp + 0x50]
movdqu xmm4, [rsp + 0x40]
movdqu xmm3, [rsp + 0x30]
movdqu xmm2, [rsp + 0x20]
movdqu xmm1, [rsp + 0x10]
movdqu xmm0, [rsp + 0x00]
add rsp, 128
pop rdi
pop rsi
add rsp, 0x10
jmp r11
ud2
.size __plt_resolve_trampoline, . - __plt_resolve_trampoline
",
__plt_resolve_inner = sym __plt_resolve_inner
);