简介
在做elf plt hook的时候,我们需要根据符号名去查找符号信息(除了hook,动态连接器等程序也需要查找符号),通常有3种方法:
- 顺序遍历符号表,这种方法最直观,但性能较差
- 基于 sysv hash 表查找(DT_HASH)
- 基于 gnu hash 表查找(DT_GNU_HASH)
在做符号查找之前,需要一些准备工作:
- 需要找到 dynamic segment (PT_DYNAMIC) 在内存中的位置
- 解析 dynamic segment,从中解析出:
- 动态符号表(DT_SYMTAB)的位置
- 动态字符串表(DT_STRTAB)的位置
- sysv hash 表(DT_HASH)的位置,如果有的话
- gnu hash 表(DT_GNU_HASH)的位置,如果有的话
c++
struct Dynamic {
// 动态字符串表信息 (.dynstr)
ElfW(Addr) dynstrSecAddr;// DT_STRTAB,动态字符串表地址
// 动态符号表信息 (.dynsym)
ElfW(Addr) dynsymSecAddr;// DT_SYMTAB,动态符号表地址
// hash section addr
ElfW(Addr) hashSecAddr;// DT_HASH,sysv hash
ElfW(Addr) gnuHashSecAddr;// DT_GNU_HASH,gnu hash
// ... others
};
顺序遍历查找
这个方案很好理解,但是性能比较差,尤其是查找一个当前elf中不存在的符号时。
另外实现的话似乎还有点麻烦,主要是因为 dynamic segment 中没有给出动态符号表的大小,或者动态符号表的结束地址。这个信息可以从 section header table 中获取,但是 section header table 未必会被mmap到内存中的,而 elf 文件我们也不一定有权限访问。(当然可以通过sysv hash 和 gnu hash来获得动态符号表的符号数目,文末有提到,但是既然通过顺序遍历查找,就假设不依赖sysv hash & gnu hash)
基于Sysv Hash查找符号
Sysv Hash 哈希函数
c++
static uint32_t sysvHash(const char* symbolName) {
uint32_t h = 0, g;
for (char c = *symbolName; c;) {
h = (h << 4) + c;
if ((g = h & 0xf0000000)) {
h ^= g >> 24;
}
h &= ~g;
c = *++symbolName;
}
return h;
}
Sysv Hash 表的结构
sysv hash 表包含4部分数据:
nbucket
: uint32_t 类型,表示 bucket 的数目nchain
: uint32_t 类型,表示 chain 的数目(跟动态符号表的符号个数相同)buckets
: nbucket 个 uint32_t 元素的数组,通过symbolNameHash % nbucket
可以找到符号对应的 bucket,其value就是链中第一个符号在符号表中的索引chains
: nchain 个 uint32_t 元素的数组,每个链起始于index = buckets[symbolNameHash % nbucket]
,而链中下一个符号的索引则是:chains[index]
,其中每个 index 的值都是符号在符号表中的索引。
c++
struct SysvHash {
uint32_t nbucket;
uint32_t nchain;
uint32_t* buckets;
uint32_t* chains;
};
buckets
和 chains
中的元素都是符号表中符号的索引。
基于sysv hash表的查找步骤
- 根据上面给出的
sysvHash
函数计算出符号名的哈希值hash
- 通过
hash % nbucket
找到符号所对应的 bucket,而index = bucket[hash % nbucket]
就是链中的第一个元素,index
对应的就是符号在符号表中的索引 - 如果
index == 0
则未找到该符号,流程结束。否则判断符号表中索引为index
的符号是否是我们要找的符号,判断方法就是看当前符号名跟要找的符号名是否相等 - 如果符号名相等,则找到目标符号,流程结束。否则根据
index
从chain
中找到下一个符号索引:index = chain[index]
- 跳转到第3步继续执行
查找代码
c++
uint32_t Elf::findSymbolIndexBySysvHash(const char* symbolName) const {
uint32_t hash = sysvHash(symbolName);
for (uint32_t index = sysvHash_->buckets[hash % sysvHash_->nbucket]; index; index = sysvHash_->chains[index]) {
if (strcmp(symbolName, (const char*)dynamic_.dynstrSecAddr + ((const ElfW(Sym)*)dynamic_.dynsymSecAddr)[index].st_name) == 0) {
return index;
}
}
return 0;
}
基于Gnu Hash查找符号
Gnu Hash 哈希函数
c++
static uint32_t gnuHash(const char* symbolName) {
uint32_t h = 5381;
for (char c = *symbolName; c;) {
h = (h << 5) + h + c;
c = *++symbolName;
}
return h;
}
Gnu Hash表的结构
nbucket
: uint32_t 类型,表示buckets
的元素个数symndx
: uint32_t 类型,表示gnu hash支持查找符号索引 >= symndx 的符号,索引小于这个值的不支持直接通过 gnu hash 查找,可以遍历[1, symndx)
挨个比对符号名查找bloomSize
,bloomShift
,blooms
:布隆过滤器需要的数据,用于快速判断一个符号是否查不到buckets
: nbucket 个 uint32_t 元素的数组,通过symbolNameHash % nbucket
可以找到符号对应的 bucket,其value就是链中第一个符号在符号表中的索引chains
:chains的索引跟符号表的索引是一一对应的关系,有个 symndx 的偏移。chains中的值是对应符号名的哈希值。哈希值的最后一位被用来判断是否是链尾,如果最后一位是1则表示符号是当前链的最后一个元素。
c++
struct GnuHash {
uint32_t nbucket;
uint32_t symndx;
uint32_t bloomSize;
uint32_t bloomShift;
ElfW(Addr)* blooms;// 32位elf每个元素32bit uint32_t,64位elf每个元素64bit uint64_t
uint32_t* buckets;
uint32_t* chains;
};
Gnu Hash 对符号表结构的要求
gnu hash要求符号表分为2部分:
- 前面一部分主要是 UNDEF symbol 以及 FILE symbol之类的,这些不能通过 gnu hash 查找,他们的索引区间为:
[0, symndx)
,这部分符号不需要有序 - 后面一部分 symbol ,gnu hash 要求他们是有序的:符号需要按照
symbolNameHash % nbucket
的值进行升序排列,因此同一个bucket中的符号是连续存放的
快速判断一个符号是否无法通过 gnu hash 找到
Gnu Hash中带有一个 k=2
的布隆过滤器,对同一个符号名计算两个哈希值:hash1 = gnuHash(symbolName)
,hash2 = hash1 >> bloomShift
。如果blooms[(hash1 / ELFCLASS_BITS) % bloomSize]
中 第 hash1 % ELFCLASS_BITS
位和hash2 % ELFCLASS_BITS
位不都为1,那么说明当前符号无法查到,就不必进行后续的查找过程了。(32位elf:ELFCLASS_BITS = 32,64位elf:ELFCLASS_BITS = 64) 另外这两个位都为1的情况并不代表符号就存在,需要后续的查找流程来判断。
基于gnu hash表的查找步骤
- 通过上面的
gnuHash
函数计算出符号名的哈希值 hash - 通过布隆过滤器来判断符号是否一定查不到,如果是,则结束流程。
- 通过
hash % nbucket
找到符号所对应的 bucket,而index = buckets[hash % nbucket]
就是链中的第一个元素,index
对应的就是符号在符号表中的索引,如果 index == 0,说明当前bucket是空的,符号未找到,结束流程。 - 因为chains中存有符号名的哈希值(不包括最后一位),因此我们可以先对比哈希值
- 如果哈希值相同,再对比符号名,如果符号名相同,则找到符号,结束流程。
- 如果哈希值的最后一位是 1,表明已经到达链尾,符号未找到,结束流程。
- 因为相同bucket的符号是连续存放的,所以链中下一个符号的索引就是
index + 1
,跳到第4步继续
查找代码
c++
uint32_t Elf::findSymbolIndexByGnuHash(const char* symbolName) const {
uint32_t hash = gnuHash(symbolName);
ElfW(Addr) ELFCLASS_BITS = sizeof(ElfW(Addr)) << 3;
ElfW(Addr) word = gnuHash_->blooms[(hash / ELFCLASS_BITS) % gnuHash_->bloomSize];
ElfW(Addr) mask = ((ElfW(Addr))1 << (hash % ELFCLASS_BITS))
| ((ElfW(Addr)) 1 << ((hash >> gnuHash_->bloomShift) % ELFCLASS_BITS));
if ((word & mask) != mask) {
return 0;
}
uint32_t idx = gnuHash_->buckets[hash % gnuHash_->nbucket];
if (idx == 0) {
return 0;
}
while (true) {
const char* symname = (const char*)dynamic_.dynstrSecAddr + ((ElfW(Sym)*)dynamic_.dynsymSecAddr)[idx].st_name;
const uint32_t h = gnuHash_->chains[idx - gnuHash_->symndx];
if ((hash | 1) == (h | 1)
&& strcmp(symbolName, symname) == 0) {
return idx;
}
if (h & 1) {
break;
}
++idx;
}
return 0;
}
基于gnu hash查找UNDEF符号
如果要基于 gnu hash 查找 UNDEF 的符号的话,那么可以遍历[1, symndx)
区间内的符号,对比其符号名:
c++
uint32_t Elf::findUndefineSymbolIndexByGnuHash(const char* symbolName) const {
for (uint32_t idx = 1, end = gnuHash_->symndx; idx < end; ++idx) {
const char* symname = (const char*)dynamic_.dynstrSecAddr + ((ElfW(Sym)*)dynamic_.dynsymSecAddr)[idx].st_name;
if (strcmp(symbolName, symname) == 0) {
return idx;
}
}
return 0;
}
基于Sysv Hash 和 Gnu Hash 查找的对比
- gnu hash对符号表内的符号排序有要求,而sysv hash没有要求,因此在同一个elf文件中,这2种hash表可以共存。可以通过连接器的
--hash-style
参数来指定hash表的类型:sysv, gnu or both - sysv hash 可用于查找 UNDEF symbol,而 gnu hash 查找UNDEF symbol则需要对
[1, symndx)
区间内的符号名遍历对比,这个case下 sysv hash可能性能更好 - 通过布隆过滤器,对于一些不存在的符号,gnu hash(可能但不一定)可以快速判断出来,这个case下gnu hash性能更好
- gnu hash中 chains 中存的是符号名的哈希值(不包含最后一个bit),因此在比较符号名之前,可以先比较哈希值是否相同,uint32_t 类型的哈希值比字符串比较要快得多(符号名有时候会很长,比如c++参数类型也是包含在函数符号名中的),这个case下gnu hash性能更好
- gnu hash中,同一个bucket中的符号连续存放,而sysv hash链中元素是链式非连续的,这个case下gnu hash cache更友好,性能更好
- 如果要获取动态符号表中符号的个数(section header table没mmap的内存,且elf文件无权限访问的话),sysv hash比较方便:
nchain
跟符号个数相同。而通过gnu hash就比较麻烦:需要遍历 最大的bucket(buckets[nbucket-1]
,如果其值为0,也就是bucket为空,则往前推一个)链到最后一个元素,他的index + 1
就是符号个数
备注
- 上面代码中通过
((ElfW(Sym)*)dynamic_.dynsymSecAddr)[idx]
来获取索引是idx
的符号,这个其实有个隐含要求:sizeof(ElfW(Sym)) == SYMENT
,SYMENT 为 Dynamic segment中 DT_SYMENT 所指定的符号项的大小,之前看到一个文档说:elf规范中要求的是 SYMENT >= sizeof(ElfW(Sym)),也就是说允许他们之间有 gap(比如后续扩展可能追加字段),所以稳妥的方式应该是:*((ElfW(Sym)*)dynamic_.dynsymSecAddr + idx * SYMENT)
,对于 Phdr 之类的也类似。 - 查找符号的时候其实还需要考虑Symbol Versioning,不过这个跟本文讨论的关系不大,此处忽略