0x0 参考资料
0x1 背景
最近被实习生问到了 Mach-O
中 __stubs
、__stub_stub_helper
、__la_symbol_ptr
、__got
相关的概念。
自己之前研究过很多遍这些 section
的运作方式,却一时无法系统地给别人描述清楚(比较尴尬)。
这块要解释清楚确实不太容易,趁着周末又系统地总结了一遍。
我怕下次别人问我的时候,又解释不清,就决定再水一篇文章记录一下吧。
Ps:实习生已经读完了 《程序员的自我修养》的前三章,读者也需要了解这三章
0x2 基本概念
链接分为静态链接和动态链接。代码中引用的「可重定位目标文件」中的符号在静态链接时被解析;引用的「动态库」中的符号将在动态链接时被解析,这个过程称为 Binding。Binding 又有Lazy Binding
和Non-lazy Binding
的概念。
以上提到的 section
都是在动态链接过程中和 Binding
相关的 section
,也是比较难理解的。
0x3 Demo 体验
为了解释清楚这个问题,必须要从一个 Demo 入手。
制作一个动态库
首先编写一个 say.c
文件:
// say.c
#include <stdio.h>
char *kHelloPrefix = "Hello";
void say(char *prefix, char *name) {
printf("%s, %s\n", prefix, name);
}
该文件定义了一个字符串常量 kHelloPrefix
以及一个函数 say
。
使用 clang
指令将其编译成动态库。
$ clang -shared say.c -o say.dylib
其中
-shared
指明生成一个动态库,可以和可执行文件进行动态链接。
参考资料中在编译动态库时还使用-fPIC
选项,笔者测试 clang -shared 应该是默认打开了该选项,加不加该选项生成的动态库没有差异,所以笔者没有加。
完成之后,可以使用 file
指令可以查看该文件的类型: dynamically linked shared library
。
$ file say.dylib
> say.dylib: Mach-O 64-bit dynamically linked shared library x86_64
Ps:say.dylib
动态库中定义的 kHelloPrefix
和 say
符号信息在 Mach-O
的 Export Info
中也有保存,在 Export Info
中的符号是 GLOBAL
性质的,可以供其它模块使用:
制作一个可重定位中间文件
再编写一个 main.c
文件:
//main.c
void say(char *prefix, char *name);
extern char *kHelloPrefix;
int main(void) {
say(kHelloPrefix, "Jack");
return 0;
}
main.c
中使用了 say
和 kHelloPrefix
这两个符号。
使用 clang
将其编译成可重定位目标文件:
$ clang -c main.c -o main.o
# -c 的含义可以使用通过 man clang 查询,意味着只生成目标文件,而不进行链接生成可执行文件
$ file main.o
> main.o: Mach-O 64-bit object x86_64
在 main.o
可重定位目标文件中,符号都被标记为 relocations
,在链接时链接器会对它们进行重定位:
制作可执行文件并执行
使用 clang
将 main.o
和 libsay.dylib
链接成可执行文件:
$ clang main.o -o main -L . -l say
$ file main
> main: Mach-O 64-bit executable x86_64
-L .
.
代表当前路径,即在当前路径下寻找要链接的库
-l say
表示链接libsay.dylib
由于在链接时指定了要链了 libsay.dylib
,链接器在 libsay.dylib
中找到了 kHelloPrefix
和 say
,并将他们在可执行文件 main
中标记出来,但并没有计算出它们的地址,如图:
等到执行可执行文件 main
时,main
和 libsay.dylib
将被加载,这些符号都将通过动态链接器 dyld
进行动态绑定,程序也将正确地被执行:
$ ./main # 执行程序
> Hello, Jack # 正确输出
0x4 Binding
从逻辑的角度,符号分为两类,「数据」和「函数」。
对这两种符号的绑定称为 Non-lazy binding 和 lazy binding。
Non-lazy binding 是指在动态链接期间立即进行绑定,解析出符号的真实地址。
lazy binding 是指在符号被用到的时候才进行绑定。
Non-Lazy Binding
在动态链接期间会解析程序使用的动态库中的「数据型符号」和 dyld_stub_binder
这个函数符号的地址,这些符号的地址都存储于 __DATA __got
中,初值都是 0,在解析完毕后将被真实地址覆盖。这就是所谓的 Non-lazy binding。
程序访问动态库中数据类型符号的流程如下文所述。
使用 otool
查看 main
可执行文件 __TEXT __text
的汇编代码:
1: $ otool -tv main
2: main:
3: (__TEXT,__text) section
4: _main:
5: 0000000100003f60 pushq %rbp
6: 0000000100003f61 movq %rsp, %rbp
7: 0000000100003f64 subq $0x10, %rsp
8: 0000000100003f68 movq 0x91(%rip), %rax
9: 0000000100003f6f movl $0x0, -0x4(%rbp)
10: 0000000100003f76 movq (%rax), %rdi
11: 0000000100003f79 leaq 0x2e(%rip), %rsi
12: 0000000100003f80 callq 0x100003f8e
13: 0000000100003f85 xorl %eax, %eax
14: 0000000100003f87 addq $0x10, %rsp
15: 0000000100003f8b popq %rbp
16: 0000000100003f8c retq
第 8
行是访问 kHelloPrefix
符号的指令。
movq 0x91(%rip), %rax
的含义是将 0x91(%rip)
的值存储到 rax
寄存器当中。
rip
寄存器中存储的是下一条指令的地址 0000000100003f6f
。
0x91(%rip)
的值是 0x91 + 0x000000100003f6f = 0x100004000
。
0x100004000
是 __DATA __got
中第一个元素的地址,里面存储的是 kHelloPrefix
的地址。
由此可见,程序在访问动态库中数据类型符号时,实际上会从 __DATA __got
中寻找该地址。
dyld_stub_binder
比较特殊,后文会介绍,必须得在动态链接阶段就提前解析出它的地址。
Lazy Binding
动态库中函数类型的符号,并不是在动态链接期间就绑定的,因为程序会大量使用动态库中的函数符号(远比数据符号多),如果在动态链接期间就解析这些函数符号的地址,会拖慢程序的启动速度。而且即使解析了这些符号,在程序运行的过程中也不一定会使用。所以为了避免浪费启动时间,这些函数符号在第一次被使用的时候才会被解析,这就是所谓的 Lazy Binding。
下文详细说明了程序首次访问动态库中函数符号的流程。
- 使用
otool
查看main
可执行文件__TEXT __text
的汇编代码:
1: $ otool -tv main
2: main:
3: (__TEXT,__text) section
4: _main:
5: 0000000100003f60 pushq %rbp
6: 0000000100003f61 movq %rsp, %rbp
7: 0000000100003f64 subq $0x10, %rsp
8: 0000000100003f68 movq 0x91(%rip), %rax
9: 0000000100003f6f movl $0x0, -0x4(%rbp)
10: 0000000100003f76 movq (%rax), %rdi
11: 0000000100003f79 leaq 0x2e(%rip), %rsi
12: 0000000100003f80 callq 0x100003f8e
13: 0000000100003f85 xorl %eax, %eax
14: 0000000100003f87 addq $0x10, %rsp
15: 0000000100003f8b popq %rbp
16: 0000000100003f8c retq
第 12
行是在调用 say
函数,会调用到 0x100003f8e
这个地址。
0x100003f8e
位于 __TEXT __stubs
中,__TEXT,__text
里中对动态库的函数型符号的引用,指向到了__stubs
。
- 使用
otool
查看__TEXT __stubs
的汇编代码:
$ otool -v main -s __TEXT __stubs
main:
Contents of (__TEXT,__stubs) section
0000000100003f8e jmpq *0x406c(%rip)
本例中只有一行指令 jmpq *0x406c(%rip)
。
jmpq
会计算出 0x406c(%rip)
所指的地址,并取里面的值(* 理解为取值)作为要跳转的地址。
rip
寄存器中存储的是下一条指令的地址,也就是 0x100003f949 = 0x0000000100003f8e + 0x6 (该指令占 6 字节)
。
0x406c(%rip)
= 0x100008000
= 0x406c
+ 0x100003f949
。
- 地址
0x100008000
位于__DATA __la_symbol_ptr
中,存储的值是100003FA4
。 100003FA4
是__TEXT __stub_helper
中汇编指令的地址。- 使用
otool
查看__TEXT __stub_helper
的汇编代码:
1: $ otool -v main -s __TEXT __stub_helper
2: main:
3: Contents of (__TEXT,__stub_helper) section
4: 0000000100003f94 leaq 0x406d(%rip), %r11
5: 0000000100003f9b pushq %r11
6: 0000000100003f9d jmpq *0x65(%rip)
7: 0000000100003fa3 nop
8: 0000000100003fa4 pushq $0x0
9: 000000100003fa9 jmp 0x100003f94
100003FA4
位于第 8
行,指令执行到第 9
行时将回到第 4
行继续执行,在第 6
行时又会跳转到 100004008 = 0x65 + 0x``100003fa3
地址中存储的值去执行。
100004008
是__DATA __got
中的第二个元素,里面存储着dyld_stub_binder
函数的地址。
此时 dyld_stub_binder
函数被调用(dyld_stub_binder
的地址已经在动态链接期间解析出来了),会去寻找 say
函数的地址,寻址到了,就把 函数符号say
的地址写入到第 3
步的 __DATA __la_symbol_ptr
数据段,替换掉原来的 100003FA4
,并调用 say
。
以上,我将程序首次访问动态库中函数符号的过程分为了 6 步。之后,当程序再调用 say
时,在第 3
步的 __DATA __la_symbol_ptr
中就可以直接找到 say
的地址,直接调用。
0x5 总结
程序引用的动态库中的数据型符号和dyld_stub_binder
这个函数符号,在动态链接期间进行绑定,是 Non-Lazy Binding。
程序引用的动态库中的函数型符号,会进行 Lazy Binding,即首次调用的时候才会绑定。
会从 __text 调用到 __stubs,再从 __stubs 找到 __la_symbol_ptr 中存储的 __stub_helper 中指令的地址并执行,然后会跳转到 __got 中执行 dyld_stub_binder 函数进行寻址并调用函数,最终找到地址后会调用函数并修改 __la_symbol_ptr 中的值且调用函数。
第二次调用该函数时,会从 __text 调用到 __stubs,再从 __stubs 找到 __la_symbol_ptr 中存储的函数地址并进行调用。
__stubs
可以理解为一个表,每个表项是一小段jmp
代码,称为「符号桩」,用于寻找并跳转到动态库的函数符号执行。
由于动态库函数符号是懒加载的,所以 __stub
首次 jmp
时需要找到 __stub_helper
中的指令,去执行 dyld_stub_binder
寻址函数进行寻址和修改。因此 __stub_helper
可以理解为 __stubs
的辅助 section
。
再作一张图进行总结:
我觉得实习生应该能明白了。。