原文地址:动态链接库如何函数寻址
最近我们发现 go
编译为 so
之后,内存占用涨了好多,初步分析下来,是动态符号导致的,感觉不太符合常识
趁着娃还在外面放假,正好学习学习~
hello world
int main() {
printf("hello world\n")
}
在最简单的
hello world
中,printf
最终也是来自 libc.so
这个动态链接库通过
objdump
,我们可以找到这一行:400638:call400520
这里的
printf@plt
,表示 printf
这个函数是依赖的外部函数,是要动态寻址的。为什么需要函数寻址 不同于静态链接,函数地址在链接期(执行前),就可以确定下来了。
然而,动态链接库的地址,是在程序执行的时候,加载 so 文件时才能确定的。那么,要调用动态库中的函数,是没有办法提前知道的地址的,所以需要一套机制来寻找函数的地址。
具体而言,分为两种寻址:
- so 中导出的函数地址
- so 内部调用非导出的函数地址
dlsym(x, "printf")
来寻址,然后 dlsym
会在 so 文件里找 printf
的地址第二种,是通过偏移量来寻址的,虽然绝对地址不固定,但是 so 文件内部,两个函数之间的偏移量是固定的。
缓存加速 通过字符串来查找,想想也知道是比较低效的,那有什么办法提速呢?原理也简单,就是加缓存。
具体而言呢,是通过可执行文件中的两个段的配合,其中
.plt
可执行,.got.plt
可写,来实现缓存的效果。还是从这一行
call
指令开始400638:call400520
400520
来自 .plt
段,而且 .plt
是可执行的继续用
objdump
可以看指令:400520:jmpQWORD PTR [rip+0x200afa]# 601020 400526:push0x0
40052b:jmp400500 <.plt>
这里有两个
jmp
:第一个
jmp
的地址来自 601020
,而这个 601020
来自 .got.plt
段,.got.plt
是可写的首次执行的时候,
601020
里存的就是 400526
,此时意味着慢路径,需要动态查找。当查到地址之后,会修改
601020
中的值,这样后续就可以直接一个 jmp
就完成寻址了,不需要再按照字符串查找了。查找逻辑 至于慢路径查找,最终会调用到
_dl_lookup_symbol_x
,大体而言是这么个逻辑:- 先在当前可执行文件中,通过
0x0
这个偏移量,找到函数名,也就是printf
- 然后再从 so 文件中,根据
printf
来查找函数地址
.dynsym
用来存符号,也就是Elf_Sym
这个结构,这个结构体里存了函数偏移地址,名称偏移地址等.dynstr
用来存字符,比如printf
这个字符串本身就存在这里
nm -D
可以看到类似这样的数据,其中 U
表示 undefined
,需要外部寻址的函数00000000004004e8 T _init
U printf
内部调用 这个就简单很多了,偏移量是固定的,不用动态查找,直接调用
call
指令就行了。x86 上的 call
执行也有好几个,其中有一个就是按照偏移量来的。这里有一个有意思的小细节,比如这个例子:
000000000040061e :
40061e:48 83 ec 08subrsp,0x8
400622:bf 01 00 00 00movedi,0x1
400627:e8 e6 ff ff ffcall400612
40062c:89 c6movesi,eax
call指令的跳转地址是
0x400612
,这个是怎么来的呢?e8
表示按照相对地址寻址,然后就有了这么结果:0x40062c + 0xffffffe6 = 0x400612
平常用
objdump
和 gdb
看到 call
指令的地址,也都是计算后的,不注意的话,会以为都是绝对地址。总结
- 调用动态链接库中的函数,是通过函数名动态查找的
- 导出的函数,以及依赖的外部函数,都在
.dysym
里记录了元信息 - 函数名字符串,是存在
.dynstr
里的 .plt
和.got.plt
这一对配合,用于寻址缓存- 内部调用直接用偏移量,
call
指令有一种就是按照偏移量来计算的
【动态链接库如何函数寻址】
文章图片