这个表叫 hash 表,它的作用是用于快速索引符号在符号表中的位置。
先看一张图:
查找一个符号的位置步骤如下:
-
首先,计算出符号字符串的 hash 值
-
然后,根据 hash 值获取 bucket 索引 index
-
bucket[index] 里面储存的是符号表的 index,但是由于 hash 冲突问题,所以又建立了一个 chain 来当链表结构。
chain 里面储存的是符号的 hash 值,但是只使用了前 31 位,后 1 位是用于表示该链表是否结束,奇数表示结束,偶数表示后面还有元素。
-
拿到 sym_tab[index] 后,可得 chain[index],然后根据 chain[index] 的值来与符号 hash 值比较,相等则找到,未相等则判断是否为链表结尾,开始循环遍历。
结构分析
.gnu.hash节由4部分构成:
Header。
谨记:header的内容决定下面3部分内容的大小。header又分别由4个条目构成,每个条目大小是固定的4个字节,这4个条目分别是:
- nbuckets。4个字节,决定了第3部分即Hash Buckets的条目数量,1个Hash Buckets占4个字节,如果nbuckets是2,则Hash Bucekts就占8个字节,依次类推,所以说nbuckets的值决定了Hash Bucekts的大小。如上图我的示例文件nbuckets是2。
- symndx。4个字节,symndx是动态符号表中可通过哈希表访问的第一个符号的索引。symndx决定了第4部分即Hash Values的条目数量,1个Hash Values占4个字节,(dynsymcount -- symndx)*4的结果就是Hash Values的实际大小,dynsymcount如果有人不清楚是什么,这里我明确告诉你们,dynsymcount表示的是动态符号表的条目数量。
- maskwords。4个字节,决定了第2部分即Bloom Filter的条目数量,1个Bloom Filter在32位系统占4个字节,在64位系统占8个字节,我是64位系统所以是8个字节,maskwords如果是1,那么Bloom Filter就是8个字节,如果maskwords是2,那么Blooom Filter就是2*8=16,那么Bloom Filter的实际大小就是16个字节,以此类推,所以说maskwords的值决定了Bloom Filter的大小。
- shift2。4个字节,Bloom Filter使用的移位计数。
Bloom Filter
32位系统单个条目占4个字节,64位系统占8个字节,条目数量由第一部分即header里的maskwords的值决定,我这儿是64位系统,Bloom Filter的大小是maskwords * 8。
Hash Buckets
单个条目占4个字节,条目数量由第一部分即header里的nbuckets的值决定,Hash Buckets的大小是nbuckets * 4。
Hash Values
单个条目占4个字节,条目数量由第一部分即header里的symndx的值决定(准确说是由dynsymcount和symndx的值决定)。Hash Values的大小是(dynsymcount -- symndx) * 4。
例子分析
我们去到 d_ptr 的位置,即 3090:
- 第 1 个 4 字节,表示 bucket 的size,这里为3
- 第 3 个 4 字节,值为 1,它用于计算 bucket 的起始位置。计算方式为
0x3090 + 0x10 + 0x8
贴一段源码:
ini
case DT_GNU_HASH:
gnu_nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
// skip symndx
gnu_maskwords_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[2];
gnu_shift2_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[3];
gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>(load_bias + d->d_un.d_ptr + 16);
gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);
// amend chain for symndx = header[1]
gnu_chain_ = gnu_bucket_ + gnu_nbucket_ -
reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
所以 bucket 起始位置在下图黄色位置:
可以看到从黄色开始一共有 6 项,前面说过, bucket size 为 3,所以前 3 项是 bucket,由于 chain 的大小与 bucket 一样,所以后3项是 chain 。
bucket 的第一项是 00000159,这个就是 sym_tab 的索引,实际上我们看第二个 4 字节位置,它也是 00000159,这个值其实表示的是 chain 表的第一个有效索引,由于 sym_tab 与 chain 大小一致,所以这个索引的值也是一致的。
有一点需要注意,hash 表是用于linker快速索引符号的,而这些符号的合集是符号表的子表,所以只会使用到符号表的部分索引。这也是第一张图里面,在 sym_tab 与 chain 表中都留出了空白的原因。在我们的例子中,hash表里面就3个元素。
0x159 的符号是:
css
symtab[345] [U] _edata
0x15B 的符号是:
css
symtab[347] [U] __bss_start
由于 chain 的 0x159 位置储存的 hash 值是 ECD54542,是一个偶数。所以如果符号不是 _edata,还会去 chain 的 0x160 位置去寻找,其对应的符号是:
css
symtab[346] [U] _end