在 Linux 平台上,动态库加载到内存之后,通常来说是被所有程序进程共享的,即不同进程通过不同的地址映射共享同一份物理内存的动态库代码。那么某个程序调用动态库中的某个函数,是如何实现的呢?
是通过延迟绑定的实现的。
这里介绍一些名称:
.plt
:过程链接表Procedure Linkage Table,ELF 文件中解决代码延迟绑定的节.got
:全局偏移表Global Offset Table,ELF 文件中解决数据延迟绑定的节.got.plt
:配合.plt
使用
延迟绑定代码
通过反汇编,如果程序调用动态库的函数 libfoo,在编译时,代码是这样安排的:
.got.plt # 数据段,可修改
... ...
.got.plt[0]: label_0 # libfoo@plt 存根数据
.got.plt[1]: label_1 # <other_foo_1@plt> 存根数据
.got.plt[2]: label_2 # <other_foo_2@plt> 存根数据
... ...
... ...
... ...
.plt # 代码段,不可修改
<default stub>: # 默认存根
push <指针标识符> # 可以表示进程的指针,也是从 .got.plt 获取的
jmp <动态链接器> # 跳转到动态链接器执行
<libfoo@plt> : # <libfoo@plt> 存根
jmp .got.plt[0] # 从存根数据中取出地址跳转去执行
label_0:
push 0x0 # 压入编号,这里编号是程序中的,不是动态库中的
jmp <default stub>
<other_foo_1@plt> : # <other_foo_1@plt> 存根
jmp .got.plt[1] # 从存根数据中取出地址跳转去执行
label_1:
push 0x1 # 根据编译,顺序压入
jmp <default stub>
<other_foo_2@plt>: # <other_foo_2@plt> 存根
jmp .got.plt[2] # 从存根数据中取出地址跳转去执行
label_2:
push 0x2 # 表示是程序中调用的第几个动态库函数
jmp <default stub>
... ...
... ...
... ...
.text # 代码段,不可修改
<main>:
... ...
# 这里正常准备参数
call libfoo@plt # main 函数调用动态库中的 libfoo 函数
... ...
模拟一下调用过程
- main 函数调用 libfoo
- 准备好参数之后,call 指令进入了存根函数中:
<libfoo@plt> :
jmp .got.plt[0] # .got.plt[0] 存放着 label_0 的地址
# 所以这里跳转到下面执行,相当于平白无故绕了个弯
label_0:
push 0x0 # 压入编号 0
jmp <default stub> # 跳转到默认存根函数中
- 进入默认存根函数 default stub 中:
<default stub>:
push <指针标识符> # 可以表示进程的指针,也是从 .got.plt 获取的
jmp <动态链接器> # 跳转到动态链接器执行
- 进入动态链接器执行,它需要完成的工作:
- 根据压入的指针标识符和序号,找到调用库函数 libfoo 真正的地址
- 根据压入的序号,将 libfoo 地址写入
.got.plt[0]
中 - 跳转到 libfoo 执行,完成调用工作
- 当第二次调用 libfoo 时,由于
.got.plt[0]
已经写入 libfoo 真正的地址,所以 call 指令之后执行的 jmp 将会直接跳转到 libfoo 函数执行。.got.plt
是数据段,可以修改,更安全一些- 所以,不能直接修改代码段,导致 call 调用多执行一条 jmp 指令
总结
最开始 .got.plt
存放着 label 的地址,也就是 jmp 下一条指令地址,经过第一次访问之后,动态链接器将修改 .got.plt
表,达到绑定的动态链接,延迟绑定的目的。之后调用动态库中的函数就不用经过动态链接中转了。