【随笔】静态链接和动态链接

参考资料

1. 深入浅出计算机组成原理_组成原理_计算机基础《深入浅出计算机组成原理》徐文浩

2. 深入浅出静态链接和动态链接_kang___xi的博客-CSDN博客_动态链接

3. Linux 中的动态链接库和静态链接库是干什么的? - 知乎 (zhihu.com)

4. CSAPP《深入理解计算机系统》

5. bottomupcs:http://bottomupcs.sourceforge.net/csbu/c3673.htm

6. https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html

7, What is the Symbol Table and What is the Global Offset Table? - CodeProject

 

零、概述

将源码文件编译成可执行文件的过程,知乎问答上面这张图很直接。CSAPP第七章也有提及。

Linux上的静态库.a文件,动态库.so文件(share object);Windows上的静态库.lib文件,动态库.dll文件(dynamic-link libary)。

Linux上的可执行文件格式是ELF(executable and linkable format),Windows上的是PE格式(portable executable)。

 

一、静态链接

静态链接生成可执行文件,是将所有的目标文件或静态库都链接成为一个可执行文件,换句话说,这个生成的可执行文件包含了所有的目标文件或静态库,目标文件或静态库被复制、嵌入到静态链接生成的可执行文件中。

《深入浅出计算机组成原理》上给了一个非常直接的示例(CSAPP上有也类似的示例),如下。

1. 先准备两个源码文件add_lib.c和link_example.c

// add_lib.c
int add(int a, int b)
{
    return a+b;
}
// link_example.c
#include <stdio.h>
int main()
{
    int a = 10;
    int b = 5;
    int c = add(a, b);
    printf("c = %d\n", c);
}

2. 将这两个源码文件编译成目标文件add_lib.o和link_example.o

$ gcc -g -c add_lib.c link_example.c
$ objdump -d -M intel -S add_lib.o
$ objdump -d -M intel -S link_example.o

3. 通过objdump工具查看这两个目标文件的汇编代码。两个汇编文件都比较容易理解,先保存rbp,然后将rsp赋值给rbp,这两步的目的是为了后面方便让rbp作为栈偏移地址来做栈顶寻址;接着就是操作数入栈(方法入参或局部变量赋值),然后操作数出栈放到通用寄存器(ax、bx等),执行函数内的代码,其中包括通过在link_example.o中通过call调用add_lib.o子程序(汇编上叫子程序,高级语言叫函数,Java叫方法,都是一个意思)。

add_lib.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
   7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
   a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
   d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  10:   01 d0                   add    eax,edx
  12:   5d                      pop    rbp
  13:   c3                      ret    
link_example.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
   f:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
  16:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
  19:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  1c:   89 d6                   mov    esi,edx
  1e:   89 c7                   mov    edi,eax
  20:   b8 00 00 00 00          mov    eax,0x0
  25:   e8 00 00 00 00          call   2a <main+0x2a>
  2a:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  2d:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
  30:   89 c6                   mov    esi,eax
  32:   48 8d 3d 00 00 00 00    lea    rdi,[rip+0x0]        # 39 <main+0x39>
  39:   b8 00 00 00 00          mov    eax,0x0
  3e:   e8 00 00 00 00          call   43 <main+0x43>
  43:   b8 00 00 00 00          mov    eax,0x0
  48:   c9                      leave  
  49:   c3                      ret    

4. 然后将两个可重定位目标文件链接成可执行文件link_example并执行

$ gcc -o link-example add_lib.o link_example.o
$ ./link_example
c = 15

5. 最后再通过objdump工具查看这个可执行文件的汇编代码,你会发现add_lib.o和link_example.o中的汇编程序都在这个文件里面,当然在Linux上这个可执行文件都是ELF格式,否则加载程序的时候会失败的

link_example:     file format elf64-x86-64
Disassembly of section .init:
...
Disassembly of section .plt:
...
Disassembly of section .plt.got:
...
Disassembly of section .text:
...
 
 6b0:   55                      push   rbp
 6b1:   48 89 e5                mov    rbp,rsp
 6b4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
 6b7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
 6ba:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 6bd:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 6c0:   01 d0                   add    eax,edx
 6c2:   5d                      pop    rbp
 6c3:   c3                      ret    
00000000000006c4 <main>:
 6c4:   55                      push   rbp
 6c5:   48 89 e5                mov    rbp,rsp
 6c8:   48 83 ec 10             sub    rsp,0x10
 6cc:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
 6d3:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
 6da:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
 6dd:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
 6e0:   89 d6                   mov    esi,edx
 6e2:   89 c7                   mov    edi,eax
 6e4:   b8 00 00 00 00          mov    eax,0x0
 6e9:   e8 c2 ff ff ff          call   6b0 <add>
 6ee:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
 6f1:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
 6f4:   89 c6                   mov    esi,eax
 6f6:   48 8d 3d 97 00 00 00    lea    rdi,[rip+0x97]        # 794 <_IO_stdin_used+0x4>
 6fd:   b8 00 00 00 00          mov    eax,0x0
 702:   e8 59 fe ff ff          call   560 <printf@plt>
 707:   b8 00 00 00 00          mov    eax,0x0
 70c:   c9                      leave  
 70d:   c3                      ret    
 70e:   66 90                   xchg   ax,ax
...
Disassembly of section .fini:
...

所以,回到本段开头的那句话,“静态链接生成可执行文件,是将所有的目标文件或静态库都链接成为一个可执行文件,换句话说,这个生成的可执行文件包含了所有的目标文件或静态库,目标文件或静态库被复制、嵌入到静态链接生成的可执行文件中”,这样就很直观的明白静态链接的意思。当然,静态链接中的符号解析和重定位没有提及,本文定位只是随笔,而且我的能力确实有限。

所以,静态链接的过程是这样的(图来自《深入浅出计算机组成原理》):

CSAPP第七章也有图示:

最后总结一下,静态链接的优缺点:

  • 优点:通过静态链接生成的可执行文件包含了程序执行所需要的所有资料,所以他运行很快(这个是相对下文动态链接而言)
  • 缺点:
    • 浪费空间:如果多个可执行文件都包含同样的目标文件,那么这个目标文件会存在多份,在每个可执行文件中都存在(相对而言,下文提及动态链接在这样的情况下只会存在一份,怎么做到的,下文再谈)
    • 更新困难:因为静态链接生成的可执行文件是包含了所有的目标文件,如果包含的某一个目标文件更新了,那么需要重新链接生成新的可执行文件;

 

二、动态链接

动态链接克服了静态链接的主要缺点。在动态链接的过程中,链接的不再是磁盘上的目标文件,而是加载到内存中的共享库,这个内存中的共享库在Windows上就是DLL文件,在Linux上就是.SO文件。

动态链接的形式大概是这样的(图来自《深入浅出计算机组成原理》):

存在的问题是:动态链接在编译的时候源码文件和共享库并不是像静态链接那样生成一个包含两者的可执行文件,所以没有办法像静态链接那样可以通过编译时确定好的相对地址寻址。等到共享库被加载到内存(可执行文件加载一次,共享库加载一次,各加载各的),可执行文件在运行时怎样知晓共享库的内存地址并寻址到共享库的函数?

《深入浅出计算机组成原理》上也给了一个非常直观的示例(CSAPP上有也类似的示例):

1. 先准备一个头文件lib.h和他的实现lib.c,其中包含了做成动态链接库的函数show_me_the_money

// lib.h
#ifndef LIB_H
#define LIB_H
 
void show_me_the_money(int money);
 
#endif
// lib.c
#include <stdio.h>
 
 
void show_me_the_money(int money)
{
    printf("Show me USD %d from lib.c \n", money);
}

2. 接着准备一个show_me_poor.c的源码文件,其中调用了lib.c的函数show_me_the_money

// show_me_poor.c
#include "lib.h"
int main()
{
    int money = 5;
    show_me_the_money(money);
}

3. 然后将lib.c编译成动态链接库lib.so文件,参数-fPIC是生成地址无关代码的意思

$ gcc lib.c -fPIC -shared -o lib.so

4. 然后将show_me_poor.c编译成可执行文件show_me_poor,这个可执行文件动态链接了lib.so

$ gcc -o show_me_poor show_me_poor.c ./lib.so

5. 最后通过objdump工具查看可执行文件show_me_poor的汇编代码

$ objdump -d -M intel -S show_me_poor
……
0000000000400540 <show_me_the_money@plt-0x10>:
  400540:       ff 35 12 05 20 00       push   QWORD PTR [rip+0x200512]        # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8>
  400546:       ff 25 14 05 20 00       jmp    QWORD PTR [rip+0x200514]        # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10>
  40054c:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]
 
0000000000400550 <show_me_the_money@plt>:
  400550:       ff 25 12 05 20 00       jmp    QWORD PTR [rip+0x200512]        # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>
  400556:       68 00 00 00 00          push   0x0
  40055b:       e9 e0 ff ff ff          jmp    400540 <_init+0x28>
……
0000000000400676 <main>:
  400676:       55                      push   rbp
  400677:       48 89 e5                mov    rbp,rsp
  40067a:       48 83 ec 10             sub    rsp,0x10
  40067e:       c7 45 fc 05 00 00 00    mov    DWORD PTR [rbp-0x4],0x5
  400685:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  400688:       89 c7                   mov    edi,eax
  40068a:       e8 c1 fe ff ff          call   400550 <show_me_the_money@plt>
  40068f:       c9                      leave  
  400690:       c3                      ret    
  400691:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  400698:       00 00 00 
  40069b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
……

先看main函数的汇编代码,rbp和rsp姿势一样,都是为了方便函数栈顶寻址;接着常量0x5进栈,赋值到eax,在mov到edi,做好函数调用的准备;然后call   400550 <show_me_the_money@plt>,即调用show_me_the_money函数,重点是这个函数在文件中的后缀@plt。

call   400550 <show_me_the_money@plt>

plt是procedure link table或procedure lookup table,即程序连接表,这个表在show_me_poor可执行文件的代码段中。

由于可执行文件中并没有要调用的共享库函数的相对地址,所以通过PLT程序连接表来做中介,这就意味着要去程序连接表plt中寻找对应的函数地址。所以接下来,call 400550指令会跳转去执行地址400550的汇编代码。

0000000000400550 <show_me_the_money@plt>:
  400550:       ff 25 12 05 20 00       jmp    QWORD PTR [rip+0x200512]        # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>
  400556:       68 00 00 00 00          push   0x0
  40055b:       e9 e0 ff ff ff          jmp    400540 <_init+0x28>
  • 一开始就jmp到GOT中show_me_the_money函数对应的地址<_GLOBAL_OFFSET_TABLE_+0x18>,即GOT[3],因为GOT一条表项是8byte,GOT[0~2]都是专属的,所以本例中该函数+0x18即24byte,整好到GOT[3];
  • 如果是第一次跳转,那么GOT[3]中会返回plt的下一条指令地址,即push 0x0,其中0x0是show_me_the_money的函数ID,这是给接下来动态链接器去加载包含该函数的动态库做准备;
  • 最后jmp 400540,400540 <show_me_the_money@plt-0x10>,这意思是show_me_the_money函数在plt中的地址减去0x10字节,即减去16byte,因为plt一个表项是16byte,所以该地址即为plt[0],也就意味着接下来会跳转到动态链接器的入口;

查看400540 <show_me_the_money@plt-0x10>对应的汇编源码,即plt[0]表项的内容:

0000000000400540 <show_me_the_money@plt-0x10>:
  400540:       ff 35 12 05 20 00       push   QWORD PTR [rip+0x200512]        # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8>
  400546:       ff 25 14 05 20 00       jmp    QWORD PTR [rip+0x200514]        # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10>
  • push QWORD PTR [rip+0x200512] # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8>:将GOT中偏移地址为_GLOBAL_OFFSET_TABLE_+0x8、长度为QWORD的数据压栈,如上截图所示,got是一个在数据段的数组结构,其一个条目是8字节,GOT[0]、GOT[1]包含了动态链接器在解析函数地址时会使用的信息,所以这里其实是将GOT[1]的数据压栈;
  • jmp QWORD PTR [rip+0x200514] # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10>:跳转到GOT中偏移地址为_GLOBAL_OFFSET_TABLE_+0x10的地方执行,0x10=16,即跳转到GOT[2]执行,而GOT[2]是动态链接器在ld-linux.so模块中的入口;

所以很明了了,第一次调用共享库函数的时候,没有对应的地址,这就需要通过动态链接器到已经加载到内存中的共享库寻址(如果没加载,先加载),寻址之后回填到GOT对应的表项;以后就可以直接寻址,即只需要plt中第一个jmp就行了,本例中以后的调用只需要执行jmp QWORD PTR [rip+0x200512] # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>(因为第一次调用的时候动态链接器已经寻址并将地址更新到GOT中,GOT对应的条目中不再只是简单的返回其PLT下一条指令的地址),后续的push、跳转到<show_me_the_money@plt-0x10>调用动态链接器寻址等都不再需要了。那这个过程就是所谓的延迟绑定。

CSAPP中相关的图示:

《Computer Science from the Bottom Up》第九章

What happens at this point is the dynamic linker function _dl_runtime_resolve is run. It finds the relocation; remember how the relocation specified the name of the symbol? It uses this name to find the right function; this might involve loading the library from disk if it is not already in memory, or otherwise sharing the code.

The relocation record provides the dynamic linker with the address it needs to "fix up"; remember it was in the GOT and loaded by the initial PLT stub? This means that after the first time the function is called, the second time it is loaded it will get the direct address of the function; short circuiting the dynamic linker.

这个过程大概是这样的(图来自《深入浅出计算机组成原理》和《Computer Science from the Bottom Up》):

最后,总结一下动态链接的优缺点:

  • 优点
    • 空间:内存中只存在一个共享库,所有进程共享,N:1的方式;不像静态链接,需要将目标文件或静态库复制、嵌入可执行文件形成1:1的方式,浪费更多存储空间
    • 更新:因为是动态链接,共享库的更新可以是无感的,当然如果兼容做的好
  • 缺点:比静态链接速度慢

 

补充:本文还有一些未详尽甚至错漏的地方,还请不吝指正!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值