1 动态链接器
动态链接器(Dynamic Linker)是操作系统的一部分,它能够在程序运行时动态地链接程序所需的共享库。
两大libc——glibc和musl中都带有自己的动态链接器(ld.so)。通常来说,使用什么工具链编译,最终得到的PIE文件中INTERP段就会包含工具链对应libc的ld.so的路径。
比如使用 x86_64-linux-gnu-gcc编译,INTERP段中就包含着glibc中ld.so的路径(/lib64/ld-linux-x86-64.so.2);使用x86_64-linux-musl-gcc编译,INTERP段中就包含着glibc中ld.so的路径(/lib/ld-musl-x86_64.so)。
在可执行程序(例如图中的test程序)加载启动时,系统会根据INTERP段中的内容找到可执行程序对应的动态链接器来加载可执行程序。
2 dlopen
dlopen 是一个 POSIX 标准的 C 语言函数,它用于在程序运行时动态加载共享库(动态链接库),它是由动态链接器(ld.so)实现的。
这个函数是动态链接加载 API 的一部分,它允许程序在不包含库的所有代码的情况下运行,而是在需要时才加载所需的库。
例如,加载一个名为 libexample.so 的共享库,并获取其中的 example_function 函数的代码如下:
void *handle = dlopen("libexample.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return;
}
void (*example_function)() = (void (*)())dlsym(handle, "example_function");
if (!example_function) {
fprintf(stderr, "%s\n", dlerror());
dlclose(handle);
return;
}
example_function();
dlclose(handle);
在这个例子中,首先使用 dlopen 打开共享库,然后使用 dlsym 获取函数的地址,最后调用该函数并在完成后使用 dlclose 关闭共享库。
3 动态链接器的实现
我用Rust实现了一个简化版的动态链接器——dlopen-rs。
它目前还不能做到自举,即不能像musl/glibc ld.so一样自己为自己做重定位从而作为一个PIE程序的启动器,不过我实现了类似dlopen、dlsym等的api,这使得我们可以使用dlopen-rs在程序执行时加载动态库。
此外它还支持no_std环境,并且可以在一些没有系统动态链接器的嵌入式设备上加载动态库。
3.1 相关概念
有关elf文件的详细介绍可以看本系列的第一篇,这里只做简单的介绍
ELF文件结构:ELF 文件由以下几个部分组成:
-
ELF 头部(ELF Header):包含了文件的总体信息。
-
程序头表(Program Header Table):描述了文件中各个段(segment)的信息。
-
节头表(Section Header Table):描述了文件中的各个节(section)的信息。
-
节(Sections):保存程序实际的代码和数据。
Segment:一个segment包含着多个section,在动态库加载时,动态链接器是以segment为基本单位将动态库中的内容加载到内存中去的(只需要加载类型为LOAD的segment到内存中)。PS:不是所有的section都被包含在segment中的,有些section不需要被加载到内存中,例如.debug开头的一些section。
Program Header:Program Header指明了segment的类型,大小,在文件中的偏移量以及在内存中的偏移量。其中DYNAMIC类型的segment是动态链接器最为关注的部分,它里面包含了动态链接器对动态库进行动态加载所需要的全部信息。
Section:section是elf文件中最基本的单位,通常来说可以通过section的名字来确定其所包含的内容,比如.text就是代码节,其中包含着程序的代码。
Section Header:描述了文件中的各个节(sections)的信息,比如section的类型、在文件中的偏移量、大小等信息。
Program Header与Section Header的区别:section header对应着section,section header主要在链接器链接多个可重定位目标文件(.o为后缀的那些文件)时使用;program header就如之前所说,对应着segment,它主要在动态库加载时被动态链接器使用。也就是说,在实现一个动态链接器时不需要关注section headers,只需要获取program headers中的内容就足够了。
重定位:总的来说重定位可以分为两类:静态重定位和动态重定位。静态重定位是在程序执行之前完成,由链接器(比如gcc中的ld和llvm中的lld)负责。动态重定位是在程序执行过程中进行的,由动态链接器(ld.so)负责。在实现动态链接器时,我们只需要关注动态重定位即可。
至于为什么要进行动态重定位,这是因为程序在编译时可能不包含所有需要的代码或数据,这些不被包含的代码或数据是通过动态链接的方式被使用的。你可以通过ldd查看某个库依赖的其他动态库。
这里需要说明的是不同CPU架构的重定位类型也有所不同,下面给出amd64(即x86-64)重定位类型的相关文档,具体内容见4.4节Relocation。
refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
Thread Local Storage:线程本地存储(简称TLS)。在多线程模式下,有些变量需要支持每个线程独享一份的功能,这种每个线程独享的变量会放到每个线程专有的存储区域,允许每个线程拥有自己单独的变量实例,简而言之,我们可以说每个线程都可以有自己独立的变量实例,而不会干扰其他线程,在多线程环境下,使用TLS来实现线程私有的数据存储,确保了数据的安全性和隔离性。
PS:TLS的初始化也是由动态链接器完成的。
3.2 dlopen的实现
在程序运行时加载一个动态库,大致可以分为以下几步:
-
打开对应的动态库文件。
-
读取并解析文件的ELF Header,验证该elf文件能否在本机上加载。
-
利用ELF Header读取program headers。
-
根据program headers的内容将需要加载的段加载到内存中,并获得.dynamic section的地址。
-
利用.dynamic section获取需要重定位的符号、符号表、字符串表等后续需要使用到的信息。
-
加载重定位符号所需要的依赖库。
-
使用从.dynamic section中获取的信息和加载的依赖库进行符号的重定位。
-
完成动态库的加载。
下面这段代码是7~8步的核心代码。下面这个链接是其在源码中的位置。
elf_loader/src/relocation.rs at main · weizhiao/elf_loader
fn relocate_impl<F>(mut self, libs: &[RelocatedDylib], find: F) -> Self
where
F: Fn(&str) -> Option<*const ()>,
{
let mut relocation = core::mem::take(&mut self.relocation);
#[inline(never)]
fn find_symdef<'a, T: ThreadLocal, U: Unwind>(
elf_lib: &'a ElfDylib<T, U>,
libs: &'a [RelocatedDylib],
dynsym: &'a ElfSymbol,
syminfo: SymbolInfo,
) -> Option<SymDef<'a>> {
if dynsym.st_shndx != SHN_UNDEF {
Some(SymDef {
sym: dynsym,
base: elf_lib.segments.base(),
#[cfg(feature = "tls")]
tls: elf_lib.tls.as_ref().map(|tls| unsafe { tls.module_id() }),
})
} else {
libs.iter().find_map(|lib| {
lib.inner.symbols.get_sym(&syminfo).map(|sym| SymDef {
sym,
base: lib.base(),
#[cfg(feature = "tls")]
tls: lib.inner.tls,
})
})
}
}
/*
A Represents the addend used to compute the value of the relocatable field.
B Represents the base address at which a shared object has been loaded into memory during execution.
S Represents the value of the symbol whose index resides in the relocation entry.
*/
if let Some(rela_array) = &mut relocation.pltrel {
// 开启lazy bind后会跳过plt相关的重定位
if self.lazy {
rela_array.relocate(|rela, _, _, _| {
let r_type = rela.r_type();
let r_sym = rela.r_symbol();
// S
// 对于.rela.plt来说通常只有这一种重定位类型
assert!(r_sym != 0 && r_type == REL_JUMP_SLOT as usize);
let ptr = (self.base() + rela.r_offset()) as *mut usize;
// 即使是延迟加载也需要进行简单重定位,好让plt代码能够正常工作
unsafe {
let origin_val = ptr.read();
let new_val = origin_val + self.base();
ptr.write(new_val);
}
});
} else {
rela_array.relocate(|rela, idx, bitmap, deal_fail| {
let r_type = rela.r_type();
let r_sym = rela.r_symbol();
// S
// 对于.rela.plt来说通常只有这一种重定位类型
assert!(r_sym != 0 && r_type == REL_JUMP_SLOT as usize);
let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
if let Some(symbol) = find(syminfo.name)
.or(find_symdef(&self, libs, dynsym, syminfo).map(|symdef| symdef.into()))
{
self.write_val(rela.r_offset(), symbol as usize);
} else {
deal_fail(idx, bitmap);
return;
};
});
}
}
if let Some(rela_array) = &mut relocation.dynrel {
rela_array.relocate(|rela, idx, bitmap, deal_fail| {
let r_type = rela.r_type();
let r_sym = rela.r_symbol();
match r_type as _ {
// B + A
REL_RELATIVE => {
self.write_val(rela.r_offset(), self.segments.base() + rela.r_addend());
}
// REL_GOT: S REL_SYMBOLIC: S + A
REL_GOT | REL_SYMBOLIC => {
let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
if let Some(symbol) = find(syminfo.name)
.or(find_symdef(&self, libs, dynsym, syminfo)
.map(|symdef| symdef.into()))
{
self.write_val(rela.r_offset(), symbol as usize + rela.r_addend());
} else {
deal_fail(idx, bitmap);
return;
};
}
// ELFTLS
#[cfg(feature = "tls")]
REL_DTPMOD => {
if r_sym != 0 {
let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
if let Some(symdef) = find_symdef(&self, libs, dynsym, syminfo) {
self.write_val(rela.r_offset(), symdef.tls.unwrap());
} else {
deal_fail(idx, bitmap);
return;
};
} else {
self.write_val(rela.r_offset(), unsafe {
self.tls.as_ref().unwrap().module_id()
});
}
}
#[cfg(feature = "tls")]
REL_DTPOFF => {
let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
if let Some(symdef) = find_symdef(&self, libs, dynsym, syminfo) {
// offset in tls
let tls_val = (symdef.sym.st_value as usize + rela.r_addend())
.wrapping_sub(TLS_DTV_OFFSET);
self.write_val(rela.r_offset(), tls_val);
} else {
deal_fail(idx, bitmap);
return;
};
}
REL_NONE | REL_JUMP_SLOT => {
return;
}
_ => unimplemented!("symbol: {},rel type: {}", r_sym, r_type),
}
});
}
self.relocation = relocation;
self.dep_libs.extend_from_slice(libs);
self
}
3.3 dlsym的实现
从已经加载的动态库中获取符号,可以分为以下几步:
-
计算符号名称的hash值
-
在动态库.gun.hash section中保存的哈希表中查找是否存在对应的符号
-
找到名字一样的符号后检查其版本号与需要的符号是否一致。(这一步与.gnu.version section有关)
-
返回找到的符号或返回None
3.4 示例程序
首先需要设置环境变量LD_LIBRARY_PATH,dlopen会在该路径下寻找被加载库依赖的动态库。此外还可以通过设置环境变量LD_BIND_NOW来强制关闭/开启延迟绑定(lazy binding)。
export LD_LIBRARY_PATH=/lib
export LD_BIND_NOW=0
示例程序:
use dlopen_rs::ElfLibrary;
use std::path::Path;
fn main() {
let path = Path::new("./target/release/libexample.so");
let libexample = ElfLibrary::dlopen(path).unwrap();
let add = unsafe { libexample.get::<fn(i32, i32) -> i32>("add").unwrap() };
println!("{}", add(1, 1));
let print = unsafe { libexample.get::<fn(&str)>("print").unwrap() };
print("dlopen-rs: hello world");
}
执行效果:
2
dlopen-rs: hello world
除了这个例子外github仓库里提供了5个example以及example使用的示例动态库libexample.so。下面是examples的代码链接和执行example的命令。PS:--example后面跟着的是example的名字。
dlopen-rs/examples at main · weizhiao/dlopen-rs
git clone https://github.com/weizhiao/dlopen-rs.git
cd dlopen-rs
cargo build -r -p example_dylib
cargo r -p dlopen-rs --example dlopen
最后再次附上dlopen-rs的链接,对动态链接器感兴趣的小伙伴可以看看,有所帮助的话欢迎点个star。:)
4 参考文献
ELF and ABI Standards:
refspecs.linuxfoundation.org/elf/index.html
标签:self,elf,let,rela,动态,链接,dlopen From: https://blog.csdn.net/justdoyaya/article/details/144376760