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,不过这个跟本文讨论的关系不大,此处忽略
相关推荐
_一条咸鱼_1 小时前
揭秘 Android View 位移原理:源码级深度剖析
android·面试·android jetpack
_一条咸鱼_1 小时前
深度剖析:Android View 滑动原理大揭秘
android·面试·android jetpack
_一条咸鱼_1 小时前
深度揭秘:Android View 滑动冲突原理全解析
android·面试·android jetpack
_一条咸鱼_1 小时前
揭秘 Android View 惯性滑动原理:从源码到实战
android·面试·android jetpack
ansondroider2 小时前
Android adb 安装应用失败(安装次数限制)
android·adb·install
艾小逗4 小时前
uniapp中检查版本,提示升级app,安卓下载apk,ios跳转应用商店
android·ios·uni-app·app升级
tangweiguo030519876 小时前
Android Kotlin ViewModel 错误处理:最佳 Toast 提示方案详解
android·kotlin
火柴就是我6 小时前
android 基于 PhotoEditor 这个库 开发类似于dlabel的功能_2
android
每次的天空7 小时前
Android学习总结之Java篇(一)
android·java·学习