静态链接和动态链接简述

由于对静态链接和动态链接的概念已经动作有所不了解,因此特意写了这篇文章进行初步的梳理,主要参考《深入理解计算机系统》这本书。

1 静态链接

1.1 静态链接概念

静态链接以一组可重定位的目标文件为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节组成。 为了创建可执行文件,链接器必须完成两个主要任务:符号解析和重定位。

1.2 符号解析

程序的链接——符号解析 链接器解析符号表.symtab,将每个符号引用和符号定义联系起来。如下图所示,符号解析就是去干箭头干的活。 Image

符号解析的整体过程如下:

  • 程序中有定义和引用的符号(包括函数和变量)
  • 编译器将定义的符号放在符号表中
    • 符号表是一个结构数组
    • 每个表项包含符号名、长度位置等信息
  • 链接器将每个符号的引用都与一个确定的符号定义建立关联

1.3 重定位

合并输入模块,为每个符号分配运行时地址。

1.3.1 重定位表目

汇编器遇到对最终位置未知的目标引用的时候,就会生成一个可重定位的表目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。.relo.text.relo.data这两个节存放了这些表目。

1.3.2 重定位流程

.o文件中,可以看出符号引用位置的值都是未知的,因此需要在重定位的时候,将其填充为正确的地址。如main函数的地址d处的4个全为0的字节,这部分需要在链接阶段通过链接器将array的地址填充进去。

   d:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi      # 14 <main+0x14>
test@iZuf6bogmr00a004vw0sbeZ:~/ctest/csapp_test$ objdump -S main.o

main.o:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
 ************************************************************************/
int sum(int *a, int n);

int array[2] = {1, 2};

int main() {
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
    int val = sum(array, 2);
   8:   be 02 00 00 00          mov    $0x2,%esi
   d:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 14 <main+0x14>
  14:   e8 00 00 00 00          callq  19 <main+0x19>
  19:   89 45 fc                mov    %eax,-0x4(%rbp)
    return val;
  1c:   8b 45 fc                mov    -0x4(%rbp),%eax
}
  1f:   c9                      leaveq
  20:   c3                      retq

1.4 静态库

静态库是一系列可重定位目标文件的集合,在Linux系统下,通过ar工具将其打包为一个.a文件。 静态库出现的原因:使用静态库而不是直接使用可重定位的目标文件的原因在于,如果直接使用一个大的可重定位目标文件,那么链接器会将整个可重定位目标文件链接到可执行文件中,如果由多个可执行文件,就会导致每个可执行文件都有这样的拷贝,可执行文件的大小大大增加。而静态库打包了一组可重定位目标文件,在链接的时候,只会拷贝那些被程序引用的目标模块。 静态库解析顺序:在符号解析阶段,链接器从左到右来扫描可重定位目标文件和存档文件。

动态链接

一文看懂动态链接 动态链接和静态链接不同,动态链接在链接阶段并不会将共享库拷贝到可执行文件中,而是在程序加载或者运行时进行通过动态链接器进行加载。

大概思路是这样,程序a和b都依赖模块c,那么等程序a运行的时候,才链接c,c此时被加载进内存。程序b运行时,直接跟内存中的c进行链接,不再单独加载。 这样,无论是在磁盘还是内存中,都只有一份c模块,完美。

这里需要强调一下,这里所谓的共享,并不是共享整个Lib.c的内容,而是特指共享它的代码部分。 对于Lib.c中的数据部分,每个进程都需要一份自己的拷贝,因为它们可能需要独立地修改Lib.c中的数据。

动态链接的GOT和PLT表

动态库有两个特性,一是所有进程共享同一个动态库,即整个内存空间中只有一个动态库被加载;二是,动态库共享的是代码,而其数据区是不共享的,每个进程有自己单独的数据区。

GOT

因此动态库代码区如果引用到了数据区的数据,需要使用GOT来确定这个数据,保证每个进程读取到的是自己进程的数据。

PLT

可执行程序在调用到动态库的函数时,是不知道其地址的,在链接阶段也不知道。只有在程序加载进内存调用到动态链接器的时候,才知道函数的地址,但是此时我们是在加载阶段,并不能直接修改代码区的数据,因此可以定义一个标记指向数据区的地址,这个地址保存着被调函数的地址,可以在动态加载的时候被修改,这个区域就叫PLT。(其实上述PLT的功能和GOT的功能是一样的),编译器加入PLT的主要作用是实现了延迟绑定。

延迟绑定

在一开始的时候,GOT表中的函数地址并不是指向真的函数。可执行程序第一次执行到共享库的函数的时候,会先调用动态链接器,将GOT表中的函数地址修改为真正的函数地址,等到第二次执行到共享库函数的时候,会直接调用到GOT的地址,而不会再调用动态链接器了。

Tags: Linux
Share: X (Twitter) Facebook LinkedIn