动态链接过程分析

以下内容均在Ubuntu上使用llvm的代码工具,测试者也可以自己选择其他合适的环境来工作。

术语约定

接下来的内容中,区分以下术语以减少歧义:

链接代表将库中的符号映射到可执行文件的符号表并生成可执行文件的行为,可能也被称为“静态链接”。

装载代表将库或者可执行文件的代码内容映射到内存中的行为,可能也被称为“动态链接”。

加载代表将文件内容复制到内存并不(尚未)做任何保护(诸如读写保护和执行保护)的行为,和仅调用read()的意义相同。

操作

首先,我们有两个源文件:bin.cmagic.c。他们的内容分别如下:

1
2
3
4
5
6
7
/* bin.c */
#include <stdio.h>
extern int get_magic();
int main() {
printf("%d\n", get_magic());
return 0;
}
1
2
3
4
5
/* magic.c */
#include <stdio.h>
int get_magic() {
return 12345;
}

生成libmagic.so,也即动态库文件:

1
clang --shared magic.c -o libmagic.so

再生成bin这个可执行文件:

1
clang -L. -lmagic bin.c -o bin

其中-L.表示链接时添加当前目录.为库搜索路径。

最后,测试执行这个bin

1
LD_LIBRARY_PATH=".;$LD_LIBRARY_PATH" ./bin

输出12345,代表链接库装载成功。其中环境变量LD_LIBRARY_PATH代表执行时需要装载的库搜索路径。

过程分析

  1. 链接操作伊始,我们拥有libmagic.soclang生成的bin.o(化名)中间文件。

  2. 由于是调用了clang同时编译和链接,clang会传给ld.lld一些预设的参数,所以接下来用clang指代链接器。

  3. clanglibmagic.so中发现了与bin.o中标记为*UND*(即undefined,未定义)的符号get_magic,然后在.plt节中添加相应的指向.got的桩代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ; 示例桩代码(伪汇编)
    NR_GET_MAGIC equ <<.got中存放get_magic的条目编号>>

    .plt:
    push qword [rel .got + 8] ; .got[1]: TODO
    jmp qword [rel .got + 16] ; .got[2]: 存放查询符号并修改.got的对应条目的函数地址,
    ; 函数最后也会跳转到被查询的函数

    get_magic@plt:
    jmp qword [rel .got + 24 + 8 * NR_GET_MAGIC] ; .got[3 + NR_GET_MAGIC]:
    ;存放get_magic的函数地址,
    ; 默认为下一行的指令所在地址
    push NR_GET_MAGIC
    jmp .plt
  4. 之后在可执行文件里加入相应的默认依赖:libc.sold.so,分别是C标准库和装载器(即俗称的动态链接器),并指定ld.so为它的解释器。(没错,就算是编译型语言在处理动态链接时也需要解释器(interpreter),不过功能和解释型语言的解释器大不相同;稍后将会讲到它的功能。)

  5. 链接完成,生成bin文件。

  6. 在shell里执行LD_LIBRARY_PATH=".;$LD_LIBRARY_PATH" ./bin

  7. shell将bin装载到内存并解析文件头,发现其拥有解释器信息,然后将ld.so一并加载到内存,最后通过create_process之类的系统调用跳转到ld.so的入口点。ld.so在此时并没有正确装载,仅仅是裸文件内容。以下将ld.so一并称为装载器。

  8. 装载器将自身正确装载后,再继续装载其他依赖,诸如VDSO和libmagic.so等等,并重写bin.got[1][2]为实际函数地址,完成后跳转到bin的入口点。

  9. bin中调用到get_magic的桩代码get_magic@plt(第一次)。

  10. 此时.got对应条目为桩代码的下一行地址,桩代码继续执行,跳转到ld.so的解析符号函数(可以用dlsym()类比)。

  11. ld.so查询到libmagic.so中的实际函数get_magic,并将.got的对应条目替换为该函数的地址。最后跳转到get_magic

  12. get_magic返回12345。

  13. bin中调用到get_magic的桩代码get_magic@plt(第二次等等)。

  14. 此时.got对应条目为实际函数地址,代码将直接跳转到get_magic并返回12345。

结论

在链接、装载、调用三步骤中,链接器、装载器均发挥了巨大用处,是C库必不可少的组成部分。如果想要开发操作系统,必须要实现或者沿用一个C库,和它对应的装载(解释)器。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信