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。
再作一张图进行总结:
我觉得实习生应该能明白了。。