链接

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行

大致过程如下

20203219101

2022622212733

一个链接的例子

// main.cint sum(int *a, int n);int array[2] = {1,2};int main(){  int val = sum(array, 2);  return val;}// sum.cint sum(int *a, int n){  int i, s = 0;  for (i = 0; i < n; n++) {    s += a[i];    return s;  }}
gcc -0g -o prog main.c sum.c

不管是读数据,调用函数还是读指令,对于 CPU 而言都是一个个的内存地址。因此,这里就需要一个连接 CPU 与程序员之间的桥梁,把程序中的符号转换成 CPU 执行时的内存地址。这个桥梁就是链接器,它负责将符号转换为地址

链接器的第一个作用就是把多个中间文件合并成一个可执行文件,多个中间文件的代码段会被合并到可执行文件的代码段,它们数据段也会被合并为可执行文件的数据段。链接器在合并多个目标文件的时候并不是简单地将各个 section 合并就可以了,还需要考虑每个目标中的符号的地址,即重定位,就是当被调用者的地址变化了,要让调用者知道新的地址是什么

两步链接

  1. 链接器需要对编译器生成的多个目标(.o)文件进行合并,一般采取的策略是相似段的合并,最终生成共享文件 (.so) 或者可执行文件。这个阶段中,链接器对输入的各个目标文件进行扫描,获取各个段的大小,并且同时会收集所有的符号定义以及引用信息,构建一个全局的符号表,根据符号表,也就能确定了每个符号的虚拟地址
  2. 对整个文件再进行第二遍扫描,这一阶段,会利用第一遍扫描得到的符号表信息,依次对文件中每个符号引用的地方进行地址替换。也就是对符号的解析以及重定位过程

静态链接

静态链接

目标文件

可重定位目标文件

2022622212215

ELF 文件格式把各种信息,分成一个一个的 Section 保存起来

  1. .text Section,也叫作代码段或者指令段(Code Section),用来保存程序的代码和指令
  2. .data Section,也叫作数据段(Data Section),用来保存程序里面设置好的初始化数据信息
  3. .rodata:只读数据,例如字符串常量、const 的变量
  4. .bss:未初始化全局变量,运行时会置 0
  5. .strtab:字符串表、字符串常量和变量名
  6. .rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是当前的文件里面未知的一些函数跳转地址,比如printf函数
  7. .symtab Section,叫作符号表(Symbol Table)。符号表保留了我们所说的当前文件里面定义的函数名称和对应地址的地址簿

符号和符号表

/* ELF符号表条目 */typedef struct {  int name; /* String table offset */  int value; /* Section offset,or VM address */  int size; /* Obiect size in bytes */  char type:4, /* Data,func,soction,or src file name (4 bits) */      binding:4; /* Local or global (4 bits) */  char reserved; /* Unused */  char section; /* Soction hoader index,ABS.UNDEF */} Elf_Symbol;

符号解析

处理多重定义的全局符号

与静态库链接

链接器使用静态库解析引用

重定位

各种符号的处理方式

占位符处理

由编译器填 0 之后,链接器就会根据目标文件中的重定位表,链接器在处理目标文件的时候,需要对目标文件里代码段和数据段引用到的符号进行重定位,而这些重定位的信息都记录在对应的重定位表里

每个重定位项都会包含需要重定位的偏移、重定位类型和重定位符号。重定位表的数据结构是这样的:

typedef struct {  Elf64_Addr›   r_offset; /* 重定位表项的偏移地址 */  Elf64_Xword›  r_info;   /* 重定位的类型以及重定位符号的索引 */  Elf64_Sxword› r_addend; /* 重定位过程中需要的辅助信息 */} Elf64_Rela;

对于类型为 R_X86_64_PC32 的符号,如全局变量、外部变量,重定位计算方式为:S + A – P

对于静态变量,由于只在本编译单元内可见,所以最终地址就是本编译单元的.data 段的最终地址

可执行目标文件

20221020152013

加载可执行目标文件

动态链接

动态链接常见共享库为 so 文件或 dll 文件

在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中

在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享

20203219214

2022622213753

得益于虚拟内存的存在,使得不同进程即使内存地址不同,也能通过动态链接加载同一份代码与数据

多了两个 section,一个是.plt,过程链接表(Procedure Linkage Table,PLT),一个是.got.plt,全局偏移量表(Global Offset Table,GOT)

动态链接带来的代价:

  1. 每次对全局符号的访问都要转换为对 GOT 表的访问,进行间接寻址,会比直接寻址慢
  2. 动态链接将链接中重定位的过程推迟到程序加载时进行。因此在程序启动的时候,动态链接器需要对整个进程中依赖的动态库进行加载和链接

从应用程序中加载和链接共享库

JNI

位置无关代码

如果两个共享库之间有引用关系的话,引用者和被引用者之间的相对位置就不能确定了,这时就需要引入地址无关代码技术。对于内部函数或数据访问,因为其相对偏移是固定的,所以可以通过相对偏移寻址的方式来生成代码;对于外部和全局函数或数据访问,则通过 GOT 表的方式,利用间接跳转将对绝对地址的访问转换为对 GOT 表的相对偏移寻址

可以加载而无需重定位的代码称为位置无关代码

延迟绑定

为了避免在加载时就把 GOT 表中的符号全部解析并重定位,就需要使用到延迟绑定,延迟绑定就是在 GOT 之前,插入了一个 plt

plt[x]->got[y](发现没有地址)->plt[0]->got[2](存了一个特殊的动态链接库ld-Linux.so,他会负责找到链接的函数)->将找到的地址存回got[y]

loader 通过动态修改 GOT 段,完成延迟绑定的功能

库打桩机制