7.动态链接和加载

前面了解了静态链接,以及一点点动态链接的东西。

这部分继续动态链接和加载。

1. 动态链接

为了实现库和应用程序分开,一个重要的技术是生成位置无关代码。

比如说一个函数 foo,静态地址为 0x40000000,用 movq $0x40000000, %rax 然后 call *(%rax) 这样调用函数并不是位置无关的。这样操作地址是写死的。

一个可行的方法是外部库函数的调用通过查表来实现。即 call TABLE(x),在运行时填表。

有了查表这个机制的时候,就有这么个情况,一个有趣的问题,编译器遇到函数调用,应该翻译成哪种指令?比如一个 hello.c 会调用 foo 和 printf

  • 如果 foo 来自同一个动态链接库

    • foo 函数实现在另一个模块 foo.c 中

    • foo.c 和 hello.c 链接成 a.out

    • call foo

  • 如果 foo 来自另一个动态链接库

    • call TABLE(foo)

hello.c 在源代码层次,是分不清是在 foo.c 还是在 foo.so 中的。对于调用 foo.c 中的情况,是不需要查表的,call 的时候直接相对跳转就能跳过去了,foo.so 中的函数,是必须要查表的,在程序刚刚加载的时候,根本不知道 foo 的地址在哪里,.so 可能被加载到任何位置。

如果把所有的调用都翻译成查表,当然可以实现,但是并不好。

C 语言这个语言里,在编译的时候无法区分谁最后会链接到一起,只有链接的时候才知道。从历史的角度看,没有动态链接,没有查表。

这种情况下,发明了 PLT(Procedure Linkage Table) ,比如库函数,我们调用 printf,编译时有 call printf,翻译成 call printf@PLT,然后加一段代码 PLT,在 PLT 里面查表,PLT 里面 jmp *TABLE[printf] ,查的这个表就是 GOT(Global Offset Table)

反汇编的 printf 可以看到翻译成 puts@plt

2. 实现动态加载

3. ELF 动态链接和加载的细节

ELF 和前面的小的 dl 格式无本质区别,但是在工程实践上还有海量的细节。

可执行文件就是状态机一个初始状态的描述,我们想可视化的看到,一定会有这么个工具。即解析这个文件,打印出一些信息给我们查看,即 ldd 工具。ldd 假装加载一个程序,解析依赖的二进制库。

比如 ldd /bin/ls

ubuntu@VM-4-15-ubuntu ~> ldd /bin/ls
        linux-vdso.so.1 (0x00007ffd96ac8000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f6e5513b000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6e54f13000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f6e54e7c000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f6e55197000)

vdso 不陷入内核的系统调用,pcre2 正则表达式匹配的库,还有 /lib64/ld-linux-x86-64.so.2 这是个加载器,这个程序在依赖的库加载完成之前是不能执行的,所以需要这个加载器来加载依赖库。

下面的内容是真实的更多的细节。

一个问题,execve 执行后初始状态是什么?execve 后的一瞬间,这个程序就开始运行了吗?

用 gdb 调试动态链接的程序,starti 可以看到,马上要执行的是 /lib64/ld-linux-x86-64.so.2 中的 _start() 事实上这是程序的加载器。(了解到这一步,对动态链接和程序加载以及了解的比较好了)

更多的问题,为什么是 /lib64/ld-linux-x86-64.so.2 的,能不能换成自己的?

如果去百度,那么基本上不太可能找到答案,这是一个足够小众的问题。

AI 时代,问 AI 就好了。AI 读过世界上所有的书,看过所有的手册,逛过所有的论坛。
  • What are the first a few steps exected after execve() of a ELF dynamic link binary?

  • How can I compile an ELF binary that use an alternative dynamic loader than the default ld.so?

这部分代码 作为一个 loader,打印一个 loader,这段代码如果能跑起来,那么任何一段 C 语言代码都能跑起来,我们可以实现任何事情。编译成一个 .so。

关于知识的壁垒,形成了很大的障碍。

如果参考的社区是百度和 CSDN,那么看的东西和 github,google,StackOverflow,是另一个风景。

现在,未来要来了,GPT 时代,知识的获取会变得更加容易,人学习知识的能力也会变得更强大。

当第一次 readelf 的时候,翻了下然后退出,到现在带着理论、带着理解、带着调过代码的经验去看,发现有些东西能看懂了。就算是有些东西不完全懂,也知道这个东西目前和我没啥关系,但在我需要的时候,该用什么方式去搞懂,这是很惊人的能力。

接触 system 的东西总是很痛苦的,用电脑,用 linux,反复重装,逐渐地会发现,不怕看文档了,那么就是进步了。

重新思考 PLT 的设计。为什么要用动态链接,而不是在动态的时候静态链接。即,不要查表,查 PLT,直接跳转。明显好处是变快了,可以少一次间接跳转,但是在初始加载的时候付出了很大代价,如果有几万个跳转,那得把所有跳转都得 patch 一遍,如果对于一个运行时间很短的程序,这个代价是很大的。

所以进一步思考,是否可以设计一种新的二进制格式文件,两种方法都支持,谁快选谁。

如果不那么在乎性能,这种方式的好处是省空间,这点并不显然。如果 libc 有 1MB 的代码,所有的程序都链接 libc,但是整个操作系统里只有一份这个代码,通过虚拟内存的页面共享实现,物理内存里只有一份拷贝。但是如果做了链接,每个跳转的地址都被改成了具体的地址,算来算去,这部分的性能也不是那么重要,就保留了这个设计。

代码的问题解决了,还有数据的问题。

最后更新于