ELF 通过 Sysv Hash & Gnu Hash 查找符号的实现及对比

简介

在做elf plt hook的时候,我们需要根据符号名去查找符号信息(除了hook,动态连接器等程序也需要查找符号),通常有3种方法:

  1. 顺序遍历符号表,这种方法最直观,但性能较差
  2. 基于 sysv hash 表查找(DT_HASH)
  3. 基于 gnu hash 表查找(DT_GNU_HASH)

在做符号查找之前,需要一些准备工作:

  1. 需要找到 dynamic segment (PT_DYNAMIC) 在内存中的位置
  2. 解析 dynamic segment,从中解析出:
    1. 动态符号表(DT_SYMTAB)的位置
    2. 动态字符串表(DT_STRTAB)的位置
    3. sysv hash 表(DT_HASH)的位置,如果有的话
    4. 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部分数据:

  1. nbucket: uint32_t 类型,表示 bucket 的数目
  2. nchain: uint32_t 类型,表示 chain 的数目(跟动态符号表的符号个数相同)
  3. buckets: nbucket 个 uint32_t 元素的数组,通过 symbolNameHash % nbucket 可以找到符号对应的 bucket,其value就是链中第一个符号在符号表中的索引
  4. 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;  
};

bucketschains中的元素都是符号表中符号的索引。

基于sysv hash表的查找步骤
  1. 根据上面给出的sysvHash函数计算出符号名的哈希值hash
  2. 通过 hash % nbucket 找到符号所对应的 bucket,而 index = bucket[hash % nbucket] 就是链中的第一个元素,index 对应的就是符号在符号表中的索引
  3. 如果 index == 0 则未找到该符号,流程结束。否则判断符号表中索引为index的符号是否是我们要找的符号,判断方法就是看当前符号名跟要找的符号名是否相等
  4. 如果符号名相等,则找到目标符号,流程结束。否则根据 indexchain 中找到下一个符号索引:index = chain[index]
  5. 跳转到第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表的结构
  1. nbucket: uint32_t 类型,表示 buckets的元素个数
  2. symndx: uint32_t 类型,表示gnu hash支持查找符号索引 >= symndx 的符号,索引小于这个值的不支持直接通过 gnu hash 查找,可以遍历 [1, symndx) 挨个比对符号名查找
  3. bloomSizebloomShiftblooms:布隆过滤器需要的数据,用于快速判断一个符号是否查不到
  4. buckets: nbucket 个 uint32_t 元素的数组,通过 symbolNameHash % nbucket 可以找到符号对应的 bucket,其value就是链中第一个符号在符号表中的索引
  5. 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部分:

  1. 前面一部分主要是 UNDEF symbol 以及 FILE symbol之类的,这些不能通过 gnu hash 查找,他们的索引区间为:[0, symndx),这部分符号不需要有序
  2. 后面一部分 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表的查找步骤
  1. 通过上面的gnuHash函数计算出符号名的哈希值 hash
  2. 通过布隆过滤器来判断符号是否一定查不到,如果是,则结束流程。
  3. 通过 hash % nbucket 找到符号所对应的 bucket,而 index = buckets[hash % nbucket] 就是链中的第一个元素,index 对应的就是符号在符号表中的索引,如果 index == 0,说明当前bucket是空的,符号未找到,结束流程。
  4. 因为chains中存有符号名的哈希值(不包括最后一位),因此我们可以先对比哈希值
    1. 如果哈希值相同,再对比符号名,如果符号名相同,则找到符号,结束流程。
    2. 如果哈希值的最后一位是 1,表明已经到达链尾,符号未找到,结束流程。
    3. 因为相同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 查找的对比

  1. gnu hash对符号表内的符号排序有要求,而sysv hash没有要求,因此在同一个elf文件中,这2种hash表可以共存。可以通过连接器的--hash-style参数来指定hash表的类型:sysv, gnu or both
  2. sysv hash 可用于查找 UNDEF symbol,而 gnu hash 查找UNDEF symbol则需要对 [1, symndx)区间内的符号名遍历对比,这个case下 sysv hash可能性能更好
  3. 通过布隆过滤器,对于一些不存在的符号,gnu hash(可能但不一定)可以快速判断出来,这个case下gnu hash性能更好
  4. gnu hash中 chains 中存的是符号名的哈希值(不包含最后一个bit),因此在比较符号名之前,可以先比较哈希值是否相同,uint32_t 类型的哈希值比字符串比较要快得多(符号名有时候会很长,比如c++参数类型也是包含在函数符号名中的),这个case下gnu hash性能更好
  5. gnu hash中,同一个bucket中的符号连续存放,而sysv hash链中元素是链式非连续的,这个case下gnu hash cache更友好,性能更好
  6. 如果要获取动态符号表中符号的个数(section header table没mmap的内存,且elf文件无权限访问的话),sysv hash比较方便: nchain跟符号个数相同。而通过gnu hash就比较麻烦:需要遍历 最大的bucket(buckets[nbucket-1],如果其值为0,也就是bucket为空,则往前推一个)链到最后一个元素,他的 index + 1 就是符号个数

备注

  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 之类的也类似。
  2. 查找符号的时候其实还需要考虑Symbol Versioning,不过这个跟本文讨论的关系不大,此处忽略
相关推荐
debug_cat1 小时前
AndroidStudio Ladybug中编译完成apk之后定制名字kts复制到指定目录
android·android studio
编程洪同学6 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
氤氲息8 小时前
Android 底部tab,使用recycleview实现
android
Clockwiseee8 小时前
PHP之伪协议
android·开发语言·php
小林爱8 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
小何开发9 小时前
Android Studio 安装教程
android·ide·android studio
开发者阿伟10 小时前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_4381509910 小时前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu11 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd
木头没有瓜11 小时前
ruoyi 请求参数类型不匹配,参数[giftId]要求类型为:‘java.lang.Long‘,但输入值为:‘orderGiftUnionList
android·java·okhttp