从实例分析ELF格式的.gnu.hash区与glibc的符号查找
前言
ELF格式的.gnu.hash节在设计上比较复杂,直接从glibc源码进行分析的难度也比较大。今天静下心来看了这篇精彩的文章,终于将布隆滤波器、算数运算转为位运算等一系列细节搞懂了(值得一提的是,这篇博客十分值得花一些时间读懂,它不仅对总体有一个较好的描述,而且还涉及了许多有益的实现细节)。但本人愚钝异常,没有一个完整的walkthrough就不能觉得自己真的搞懂了一个东西。所以本文从查找一个符号的真实情况出发,把ELF格式是如何组织一个符号,以及动态链接器如何读取并处理这些信息以进行符号查询的全过程详细地讲清楚。
本文假定读者已经读过上文中提到的博客,并理解布隆滤波器,GNU hash采用的单一哈希策略,把取模转为取与这些名词。在后续有时间时我可能会对它们进行简单介绍,但珠玉在前让人确实不想献丑。
本文的实现以及so文件均以glibc 2.31为准。
符号哈希,符号表与字符表
一个符号的相关信息会在ELF文件中dynamic section的三块出现:.gnu.hash对应的符号哈希,.dynsym对应的动态符号表,.dynstr对应的字符表。在查找符号时,动态链接器首先从.gnu.hash中进行查询,得到该符号在动态符号表中的偏移。动态链接器根据这个偏移读出一个符号,并找到这个符号的名字在字符表中的偏移。从字符表中读出符号的名称如果与要查找的符号匹配,则找到了这个符号,再从符号表中读出符号的相关信息并返回。
64位ELF格式的符号定义如下:
// in <elf.h>
typedef struct
{
// 32 bits
Elf64_Word st_name; /* Symbol name (string tbl index) */
// 8 bit
unsigned char st_info; /* Symbol type and binding */
// 8 bit
unsigned char st_other; /* Symbol visibility */
// 16 bits
Elf64_Section st_shndx; /* Section index */
// 64 bits
Elf64_Addr st_value; /* Symbol value */
// 64 bits
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
这个数据结构占用内存的大小为24B,这也是合理安排成员顺序以节约文件大小的一个例子。
.gnu.hash的结构
glibc使用如下函数从ELF文件中读取符号哈希相关信息:
// in elf/dl-lookup.c
void
_dl_setup_hash (struct link_map *map)
{
Elf_Symndx *hash;
if (__glibc_likely (map->l_info[ELF_MACHINE_GNU_HASH_ADDRIDX] != NULL))
{
// 一个指向32位长内存的指针,用来读取哈希相关变量,故名hash32
Elf32_Word *hash32
= (void *) D_PTR (map, l_info[ELF_MACHINE_GNU_HASH_ADDRIDX]);
map->l_nbuckets = *hash32++;
Elf32_Word symbias = *hash32++;
Elf32_Word bitmask_nwords = *hash32++;
/* Must be a power of two. */
assert ((bitmask_nwords & (bitmask_nwords - 1)) == 0);
map->l_gnu_bitmask_idxbits = bitmask_nwords - 1;
map->l_gnu_shift = *hash32++;
map->l_gnu_bitmask = (ElfW(Addr) *) hash32;
hash32 += __ELF_NATIVE_CLASS / 32 * bitmask_nwords;
map->l_gnu_buckets = hash32;
hash32 += map->l_nbuckets;
map->l_gnu_chain_zero = hash32 - symbias;
/* Initialize MIPS xhash translation table. */
ELF_MACHINE_XHASH_SETUP (hash32, symbias, map);
return;
}
// 以下处理古老的DT_HASH项,现已不用
if (!map->l_info[DT_HASH])
return;
hash = (void *) D_PTR (map, l_info[DT_HASH]);//Q: what about some non-GNU ELFs
map->l_nbuckets = *hash++;
/* Skip nchain. */
hash++;
map->l_buckets = hash;
hash += map->l_nbuckets;
map->l_chain = hash;
}
上述代码读取了关键变量赋值:l_nbuckets,symbias,bitmask_nwords,l_gnu_shift,l_gnu_buckets,l_gnu_chain_zero。其中,以“l”开头的变量存储在ELF文件的link_map中,具体定义见<link.h>。还有不是从文件中读出的变量l_gnu_bitmask_idxbits,它们的具体含义为:
- l_nbuckets:使用哈希桶的数量
- symbias:动态符号表中外部不能访问的符号数量,但它们仍然占用了动态符号表项
- bitmask_nwords:使用bitmask_nwords个字作为布隆滤波器的向量
- l_gnu_shift:为使用同一哈希函数实现k=2的布隆滤波器,需要右移的位数
- l_gnu_buckets:哈希桶的开始地址
- l_gnu_chain_zero:符号哈希值的开始地址
- l_gnu_bitmask_idxbits:为对bitmask_nwords取模化为取与,由bitmask_nwords-1而来
为了便于理解,将.gnu.hash节中的内容画成示意图:
以libc为例。检查对应字段的值:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep .gnu.hash -A 5
Contents of section .gnu.hash:
38a0 (f3030000) (0c000000) (00010000) (0e000000) ................
->l_nbuckets=1011 ->symbias=12 ->bitmask_nwords=256 ->l_gnu_shift=14
38b0 (00301044 a0200201) (8803e690 c5458c00) .0.D. .......E..
->第一个bloom word 0x010220a044103000
38c0 c4005800 07840070 c280010d 8a0c4104 ..X....p......A.
38d0 10008840 32082a40 88543c2d 200e3248 ...@2.*@.T<- .2H
38e0 2684c08c 04080002 020ea1ac 1a0666c8 &.............f.
可以看到symbias=12,即有12个内部符号:
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | head -n 20
Symbol table '.dynsym' contains 2367 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __libpthread_freeres
2: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND _rtld_global@GLIBC_PRIVATE (33)
3: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND __libc_enable_secure@GLIBC_PRIVATE (33)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __tls_get_addr@GLIBC_2.3 (34)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _dl_exception_create@GLIBC_PRIVATE (33)
6: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND _rtld_global_ro@GLIBC_PRIVATE (33)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __tunable_get_val@GLIBC_PRIVATE (33)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _dl_find_dso_for_object@GLIBC_PRIVATE (33)
9: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _dl_starting_up
10: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __libdl_freeres
11: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND _dl_argv@GLIBC_PRIVATE (33)
12: 00000000000ab970 33 FUNC GLOBAL DEFAULT 16 __strspn_c1@GLIBC_2.2.5
13: 0000000000089260 352 FUNC GLOBAL DEFAULT 16 putwchar@@GLIBC_2.2.5
14: 00000000001324f0 20 FUNC GLOBAL DEFAULT 16 __gethostname_chk@@GLIBC_2.4
15: 00000000000ab9a0 44 FUNC GLOBAL DEFAULT 16 __strspn_c2@GLIBC_2.2.5
16: 000000000014f580 218 FUNC GLOBAL DEFAULT 16 setrpcent@@GLIBC_2.2.5
可见符号0-11为内部符号。
查找符号
下面以查找符号printf为例,介绍符号查找的过程。
首先使用下面的哈希函数生成符号的32位哈希:
// in elf/dl-lookup.c
static uint_fast32_t
dl_new_hash (const char *s)
{
uint_fast32_t h = 5381;
for (unsigned char c = *s; c != '\0'; c = *++s)
h = h * 33 + c;
return h & 0xffffffff;
}
得到printf的哈希值为0x156b2bb8。
随后计算布隆滤波器需要的两个hashbit:
unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
unsigned int hashbit2 = ((new_hash >> l->l_gnu_shift) & (__ELF_NATIVE_CLASS - 1));
得到hashbit1 = 56,hashbit2 = 44。
找到该hash对应的bloom word:
const Elf64_Addr *bitmask = l->l_gnu_bitmask;
// l->l_gnu_bitmask_idxbits = bitmask_nwords - 1,将取模变为取与
// (new_hash / __ELF_NATIVE_CLASS) & l->l_gnu_bitmask_idxbits = 174
Elf64_Addr bitmask_word = bitmask[(new_hash / __ELF_NATIVE_CLASS) & l->l_gnu_bitmask_idxbits];
printf对应的hash在第174个bloom word处,它的值位于bloom word的开始地址0x38b0+174*8=3e20
检查3e20处对应的值:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 3e20 "
3e20 d0884a41 c0703429 10ec4303 92003103 ..JA.p4)..C...1.
其bloom word为0x293470c0414a88d0。
将其右移56位:0b0010 1001
将其右移44位:0b10 1001 0011 0100 0111
二者的最后一位均为1,说明布隆滤波器不能拒绝这个哈希值。
这时在对应的哈希桶上进行寻找:
Elf32_Word bucket = l->l_gnu_buckets[new_hash % l->l_nbuckets];
由于0x156b2bb8 % 1011 = 295,需要找到第296个哈希桶。
而哈希桶的起始地址为l_gnu_bitmask + 64 / 32 * bitmask_nwords = 0x40b0,对应哈希桶的地址为0x40b0+295*4=0x454c。
查看0x454c处对应的哈希桶内容:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 4540 "
4540 77020000 00000000 7a020000 **7c020000** w.......z...|...
哈希桶的内容为0x27c。
而l_gnu_chain_zero的地址为:
l_gnu_chain_zero = l_gnu_buckets + l_nbuckets - symbias;
可计算出l_gnu_chain_zero的地址为0x504c,所以第296个哈希桶包含的真正哈希位于0x504c+27c*4=0x5a3c
查看具体的哈希内容:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 5a30 " -A 2
5a30 ade8dbbb 142dcb13 bb86f85f e6952000 .....-....._.. .
5a40 **b82b6b15** 0a05f1d5 deb6427f 856177fd .+k.......B..aw.
5a50 1ae585e7 ec296fa8 1ae585e7 29ce248f .....)o.....).$.
于0x5a40处找到我们之前计算的哈希0x156b2bb8(注意小端序)。
此时,这个符号在.gnu.hash的下标,就是它在动态符号表中的(下标-symbias)。但由于之前l_gnu_chain_zero已经整体减掉了symbias,所以此处用该符号的地址减掉l_gnu_chain_zero可直接得到符号在符号表中的下标。
0x5a40 - 0x504c = 0x9f4 = 2548,由于一个哈希值为4字节,故下标为2548 / 4 = 637
找到动态符号表的起始地址:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep .dynsym -A 1
Contents of section .dynsym:
07548 00000000 00000000 00000000 00000000 ................
上文中提到,64位ELF文件中一个符号的长度位24字节,故符号在符号表上的起始地址应当为0x7548 + 24*637 = 0xb100
找到动态符号表对应位置的内容:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 0b0f8 " -A 1
0b0f8 16000000 00000000 **f3040000** 12001000 ................
0b108 104e0600 00000000 cc000000 00000000 .N..............
读出符号在字符表上的偏移量为0x4f3。
找到字符表的起始地址:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep .dynstr -A 1
Contents of section .dynstr:
15330 00786472 5f755f6c 6f6e6700 5f5f7763 .xdr_u_long.__wc
起始地址为0x15330,故该符号的地址为0x15330 + 0x4f3 = 0x15823
读出字符表对应位置的值:
$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 15820 " -A 1
15820 494f5f**70** 72696e74 66007265 67697374 IO_printf.regist
15830 65725f70 72696e74 665f6675 6e637469 er_printf_functi
查找到了符号printf,它是IO_printf的别名,在字符表中为了节省空间将二者合并了。
这样,就完成了一次符号查询的全过程。
从实例分析ELF格式的.gnu.hash区与glibc的符号查找的更多相关文章
- 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main | 百篇博客分析OpenHarmony源码 | v51.04
百篇博客系列篇.本篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | ...
- 实例分析ELF文件静态链接
参考文献: <ELF V1.2> <程序员的自我修养---链接.装载与库>第4章 静态链接 开发平台: [thm@tanghuimin static_link]$ uname ...
- 实例分析ELF文件动态链接
参考文献: <ELF V1.2> <程序员的自我修养---链接.装载与库>第6章 可执行文件的装载与进程 第7章 动态链接 <Linux GOT与PLT> 开发平台 ...
- 鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 | 百篇博客分析OpenHarmony源码 | v53.02
百篇博客系列篇.本篇为: v53.xx 鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应 ...
- Linux ELF格式分析
http://www.cnblogs.com/hzl6255/p/3312262.html ELF, Executable and Linking Format, 是一种用于可执行文件.目标文件.共享 ...
- ELF格式文件分析以及运用
基于本文的一个实践<使用Python分析ELF文件优化Flash和Sram空间的案例>. 1.背景 ELF是Executable and Linkable Format缩写,其官方规范在& ...
- elf格式分析
近期研究了一下elf文件格式,发现好多资料写的都比較繁琐,可能会严重打击学习者的热情,我把自己研究的结果和大家分享,希望我的描写叙述可以简洁一些. 一.基础知识 elf是一种文件格式,用于存储Linu ...
- C# format 日期 各种 符号 实例分析如何精确C#日期格式到毫秒
摘 自: http://developer.51cto.com/art/200908/141145.htm 实例分析如何精确C#日期格式到毫秒 2009-08-03 10:48 paulfzm jav ...
- Lab_1:练习4——分析bootloader加载ELF格式的OS的过程
一.实验内容 通过阅读bootmain.c,了解bootloader如何加载ELF文件.通过分析源代码和通过qemu来运行并调试bootloader&OS, bootloader如何读取硬盘扇 ...
随机推荐
- 1038 Recover the Smallest Number
Given a collection of number segments, you are supposed to recover the smallest number from them. Fo ...
- ASP程序写的项目与微信服务号(公众号)完美结合。仅需一个DLL组建WeixinDLL
因ASP程序开发有很多优点,早年间ASP风靡全球,因此如今还在继续运营的ASP开发的项目仍在运行着,但是随着社交网络不断发达,特别是微信支付.微信通讯.小程序等的出现,导致很多ASP项目对接起来就比较 ...
- php的call_user_func_array()使用场景
1..动态调用普通函数时,比如参数和调用方法名称不确定的时候很好用 function sayEnglish($fName, $content) { echo 'I am ' . $content; } ...
- 基于Xposed Hook实现的Android App的协议算法分析小工具-CryptoFucker
本文博客地址:https://blog.csdn.net/QQ1084283172/article/details/80962121 在进行Android应用的网络协议分析的时候,不可避免涉及到网络传 ...
- Photoshop 第二课 工具-钢笔的使用
钢笔的使用 钢笔→ 是一个非常实用(主要用于)但是非常难操作(会者不难哦~)的工具. 钢笔属性中有三种状态:1.路径:2.形状:3.像素.其中路径和形状是我们最常用的状态.路径是一条用来圈定需要操作的 ...
- jQuery数组($.grep,$.each,$.inArray,$.map)处理函数详解
1.jQuery.grep( array, function(elementOfArray, indexInArray) [, invert ] ) 描述: 查找满足过滤函数的数组元素.原始数组不受影 ...
- 简述MySQL优化
数据库的优化可以从四个方面来优化: 1.结构层: web服务器采用负载均衡服务器,mysql服务器采用主从复制,读写分离 2.储存层: 采用合适的存储引擎,采用三范式 3.设计层: 采用分区分表,索引 ...
- python三元(三目)运算
传统的if,else写法 三元运算 name="alex" if 1==1 else "SB"
- Java解析xml文件遇到特殊符号&会出现异常的解决方案
文/朱季谦 在一次Java解析xml文件的开发过程中,使用SAX解析时,出现了这样一个异常信息: Error on line 60 of document : 对实体 "xxx" ...
- 高阶函数 / abs方法
abs()求绝对值,填括号里面