dlsym 是动态链接器实现的一个函数,它可以在运行时从已加载的动态库中获取符号(通常是函数或全局变量)的地址。这个函数的原型定义在 <dlfcn.h> 头文件中,其定义如下:
void *dlsym(void *handle, const char *symbol);
-
handle 是一个动态链接库的句柄,通常由 dlopen 函数返回。
-
symbol 是你要查找的符号的名称。
-
函数返回一个 void* 类型的指针,这个指针指向符号在内存中的地址。
我们可以使用 dlsym 来获取动态链接库中函数的地址,然后通过函数指针调用这个函数,或者获取全局变量的地址进行操作。如果函数调用失败,它会返回 NULL,并且可以通过 dlerror 函数获取错误信息。
dlsym 函数允许程序在运行时决定是否使用某个库,或者在不重启程序的情况下加载新的库版本。
1 相关知识
dlsym的实现主要涉及到elf文件中的三个section:.dynsym section、.dynstr section和.gnu.hash section,因此这部分主要介绍这三个section中的内容,如果想了解elf的基础知识可以看本系列的第一篇:
1.1 .dynsym section
.dynsym中包含了动态链接时所需的符号信息,对于动态库来说(.so文件),这个section是必须存在的。在 ELF 文件中,有两种类型的符号表:.symtab(符号表)和 .dynsym(动态符号表)。

.symtab 包含了程序中定义和引用的所有符号的信息,包括局部符号和全局符号。它对于调试和链接过程(这里的链接是指对.o文件的链接)非常有用,但并不需要在程序运行时被加载到内存中。
与 .symtab 相比,.dynsym 只包含了与动态链接相关的符号信息,它是 .symtab 的一个子集。.dynsym 中通常包含了动态库中定义的全局符号,这些符号在程序运行时可能会被使用,动态链接器需要使用.dynsym 在运行时获得符号的信息,因此.dynsym需要在程序运行时被加载到内存中。

在某些情况下,为了减少二进制文件的大小,可能会使用 strip 命令移除 .symtab 信息,但 .dynsym 会被保留,因为它对于程序的运行是必需的。
以64位的ELF为例,.dynsym中保存着一个Elf64_Sym数组,Elf64_Sym的各个字段的作用如下:
-
st_name:符号名在.dynstr或.strtab中的索引(如果是.dynsym中保存的Elf64_Sym结构体,那么保存的就是.dynstr中的索引)。
-
st_info:低4位保存符号的类型(例如,是否是函数、对象、文件等),高4位保存符号的binging(例如,局部、全局、弱等)。
-
st_other:符号的可见性。
-
st_shndx:符号所在的section的索引。
-
st_value:符号的值或地址。对于动态库,这里的地址指的是符号相对于动态库基地址的偏移(基地址通常是0)。
-
st_size:符号的大小。
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
PS:elf.h中有这个结构体的定义,此外还有很多其他与elf相关的结构体的定义,感兴趣的小伙伴可以看一下这个头文件。
1.2 .dynstr section
.dynstr 包含了动态链接器在运行时解析动态符号时所需的字符串,这个section也是动态库中必须存在的。.dynstr 节中的字符串通常以空字符(\0)结尾,这些字符串通常是符号的名称(函数名或全局变量名)。

.dynstr 节是 .dynsym 的辅助节,.dynsym 节包含了动态链接需要的符号,而 .dynstr 节则包含了这些符号的名称,动态链接器在寻找符号时会将这两个节配合使用——.dynsym 节中的每个符号条目都会包含一个字符串表索引(之前提到的st_name字段),该索引指向 .dynstr 中的一个字符串,即符号的名称。这样,当动态链接器需要查找或解析一个符号时,它可以使用 .dynsym 中的信息和 .dynstr 中的字符串来确定符号的具体名称。
1.3 .gnu.hash section
.gnu.hash用于在动态链接时加速符号的查找过程。这个节是一个哈希表,它存储了.dynsym中符号的哈希值,以便在动态链接器查找符号时能够快速确定符号是否存在于对象中。
.gnu.hash 节通常包含以下四个部分:
(1)Header:包含四个条目,每个条目占用4个字节,分别是:
-
nbuckets:哈希桶的数量,决定了哈希桶数组的大小。
-
symndx:动态符号表中可以通过哈希表访问的第一个符号的索引。
-
maskwords:布隆过滤器中使用的字数,决定了布隆过滤器的大小。
-
shift2:布隆过滤器使用的移位计数。
(2)Bloom Filter:用于快速检查一个符号是否可能存在于哈希表中。布隆过滤器是一种空间效率很高的概率型数据结构,用于测试一个元素是否是一个集合的成员。
(3)Hash Buckets:包含一系列桶,每个桶包含一个.dynsym中符号的索引。
(4)Hash Values:包含.dynsym中符号的哈希值。(Hash Values中并不包含.dynsym中所有符号的hash值,其包含hash值的个数=.dynsym中符号的个数-symndx)

.gnu.hash 节是 GNU 扩展的一部分,它比传统的 ELF .hash 节提供了更快的符号查找速度,并且支持更大和更复杂的符号表。
2 实现方式
当程序调用dlsym时,动态链接器就会使用.dynsym、.dynstr和.gnu.hash这三个section来查找动态库中dlsym参数传入的符号名所对应的符号。
大致步骤如下:
-
计算dlsym传入的字符串参数symbol的hash值(记作symbol_hash)。
-
使用symbol_hash检查布隆过滤器,以确定是否有可能存在该符号。
-
如果布隆过滤器表示符号可能存在,链接器会找到symbol_hash所对应的哈希桶,从而获得hash桶中保存的.dynsym中符号的索引dynsym_idx。
-
令chain_idx=dynsym_idx-symndx。(symndx是前文所提到的.dynsym中可以通过哈希表访问的第一个符号的索引)
-
使用chain_idx作为Hash values的开始索引(可以说找到了哈希桶对应的chain),之后以索引不断+1的方式,比较symbol_hash和当前索引处Hash values中保存的hash值(将后者记作chain_hash),如果symbol_hash等于chain_hash,再比较符号名是否相同,直到找到符号或哈希桶所对应的chain结束(chain_hash&1!=1成立时表示chain结束)。
PS:每个哈希桶都对应Hash values中的一段连续的部分(将其称作chain),一个chain包含多个hash值(前文的那张图也许可以帮助理解chain)。此外不光是dlsym会使用上述流程寻找符号,动态链接器在重定位符号时也是使用是上述的流程在依赖库中寻找所需的符号。
3 具体实现
dlsym是动态链接器的基础功能,musl和glibc中的动态链接器都实现了dlsym。下面是我写的一个动态链接器中实现dlsym的核心代码。下面这个链接是其在源码中的位置:
struct SymbolData {
/// .gnu.hash
pub hashtab: ELFGnuHash,
/// .dynsym
pub symtab: *const ELFSymbol,
/// .dynstr
pub strtab: ELFStringTable<'static>,
#[cfg(feature = "version")]
/// .gnu.version
pub version: Option<version::ELFVersion>,
}
// self的类型是SymbolData
pub(crate) fn get_sym(&self, symbol: &SymbolInfo) -> Option<&ELFSymbol> {
let hash = ELFGnuHash::gnu_hash(symbol.name.as_bytes());
let bloom_width: u32 = 8 * size_of::<usize>() as u32;
let bloom_idx = (hash / (bloom_width)) as usize % self.hashtab.blooms.len();
let filter = self.hashtab.blooms[bloom_idx] as u64;
if filter & (1 << (hash % bloom_width)) == 0 {
return None;
}
let hash2 = hash.shr(self.hashtab.nshift);
if filter & (1 << (hash2 % bloom_width)) == 0 {
return None;
}
let table_start_idx = self.hashtab.table_start_idx as usize;
let chain_start_idx = unsafe {
self.hashtab
.buckets
.add((hash as usize) % self.hashtab.nbucket as usize)
.read()
} as usize;
if chain_start_idx == 0 {
return None;
}
let mut dynsym_idx = chain_start_idx;
let mut cur_chain = unsafe { self.hashtab.chains.add(dynsym_idx - table_start_idx) };
let mut cur_symbol_ptr = unsafe { self.symtab.add(dynsym_idx) };
loop {
let chain_hash = unsafe { cur_chain.read() };
if hash | 1 == chain_hash | 1 {
let cur_symbol = unsafe { &*cur_symbol_ptr };
let sym_name = self.strtab.get(cur_symbol.st_name as usize);
#[cfg(feature = "version")]
if sym_name == symbol.name && self.check_match(dynsym_idx, &symbol.version) {
return Some(cur_symbol);
}
#[cfg(not(feature = "version"))]
if sym_name == symbol.name {
return Some(cur_symbol);
}
}
if chain_hash & 1 != 0 {
break;
}
cur_chain = unsafe { cur_chain.add(1) };
cur_symbol_ptr = unsafe { cur_symbol_ptr.add(1) };
dynsym_idx += 1;
}
None
}
PS:check_match函数会比较符号的版本号,这是在dlvsym和重定位带有版本号信息的符号时会使用到的,它与.gnu.version、.gnu.version_r、.gnu.version_d这三个section有关,我之后会写一篇文章来专门介绍版本号的相关内容。