6

给实习生讲明白 Lazy/Non-lazy Binding

 2 years ago
source link: https://juejin.cn/post/7001842254495268877
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

给实习生讲明白 Lazy/Non-lazy Binding

2021年08月29日 13:25 ·  阅读 959

0x0 参考资料

《程序员的自我修养》

Mach-O 与动态链接 | 张不坏的博客

iOS程序员的自我修养-MachO文件动态链接(四)

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 动态库中定义的 kHelloPrefixsay 符号信息在 Mach-OExport 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 中使用了 saykHelloPrefix 这两个符号。

使用 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 ,在链接时链接器会对它们进行重定位:

制作可执行文件并执行

使用 clangmain.olibsay.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 中找到了 kHelloPrefixsay,并将他们在可执行文件 main 中标记出来,但并没有计算出它们的地址,如图:

等到执行可执行文件 main 时,mainlibsay.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。

下文详细说明了程序首次访问动态库中函数符号的流程。

  1. 使用 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

  1. 使用 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

  1. 地址 0x100008000 位于 __DATA __la_symbol_ptr 中,存储的值是 100003FA4
  2. 100003FA4__TEXT __stub_helper 中汇编指令的地址。
  3. 使用 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 地址中存储的值去执行。

  1. 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

再作一张图进行总结:

我觉得实习生应该能明白了。。

安装掘金浏览器插件
多内容聚合浏览、多引擎快捷搜索、多工具便捷提效、多模式随心畅享,你想要的,这里都有!
前往安装

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK