深入理解计算机系统 第七章 链接

从代码到可执行文件步骤(以gcc ,main.c为例)

1 .预处理(cpp) main.i
命令:gcc -E main.c -o main.i
2 .编译(cc1) main.s
命令:gcc -S main.i -o main.s
或gcc -S main.c -o main.s
3 .汇编(as) main.o 可重定位目标文件
命令:gcc -c main.s -o main.o
或gcc -c main.c -o main.o
4 .链接(ld)
将多个.o文件及一些必要的系统文件组合起来,创建一个可执行目标文件
命令:gcc -static -o myproc main.o test.o
将main.o,test.o链接成可执行目标文件myproc

链接主要任务

  1. 符号解析 将函数,全局变量,静态变量(static)的定义和引用关联起来
  2. 重定位 链接器通过把每个符号定义和一个存储器位置联系起来,修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定义这些节。

目标文件

1 .可重定位目标文件
包含二进制代码和数据,可以在编译时与其他可重定位目标文件合并,组成可执行文件。
2 . 可执行文件
包含二进制代码和数据,可直接拷贝到存储器中执行。
3 . 共享目标文件
特殊的可重定位目标文件,可在加载或运行时被动态加载到存储器,并链接。

可重定位目标文件

  1. ELF头,以一个16字节的序列开始,描述生成该文件的系统的字的大小和字节顺序。剩下字节包括帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(可重定位、可执行、共享的)、机器类型(如IA32)、节头部表的文件偏移,以及节头部表(见图最后)中的条目大小和数量。夹在ELF和节头部表中间的都是节。
  2. .text,已编译的机器代码。
  3. .rodata,read only data,只读数据,比如printf语句中的格式串和switch的跳转表。
  4. .data,已初始化的全局C变量。局部变量在运行时保存在栈中,既不在.data,也不在.bss中。
  5. .bss,未初始化的全局C变量。这个节不占据实际的磁盘空间。区分初始化和未初始化是为了空间效率。(意思是,.data磁盘实际保存的只有初始化的全局变量)
  6. .symtab,符号表,程序中定义和引用的函数和全局变量的信息。每个ELF文件都有。
  7. .rel.text,当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。然而可执行目标文件中并不需要重定位信息,除非用户指定。
  8. .rel.data,被模块引用或定义的任何全局变量的重定位信息。一般而言,任何已被初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
  9. .debug,调试符号表。条目是程序中定义的局部变量和类型定义,定义和引用的全局变量,原始的C源文件。
  10. .line,原始C源程序行号和.text节机器指令之间的映射关系。要求-g编译。
  11. .strtab,一个字符串表,每个字符串以null结尾。包括.symtab和.debug中的符号表,节头部中的节名字。

以下面代码main.c 为例,查看其生成的可重定位目标文件的ELF头

/* main.c */
/* $begin main */
int sum(int *a, int n);

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

int main() 
{
    int val = sum(array, 2);
    return val;
}
/* $end main */

命令:gcc -c main.c -o main.o
readelf -h main.o
运行结果如下:

ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          720 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         64 (字节)
  节头数量:         12
  字符串表索引节头: 11

Magic:45 4c 46分别是E L F三个字母的ASCII码
Class(类别):格式为64位
Data(数据):补码表示,按小端方式存放
Version(版本):1
OS/ABI: 表示操作系统的类型 这里是UNIX - System V
Type:ELF文件类型为可重定位文件REL
Machine:在64位机器上编译的目标代码
程序入口地址为0,是可重定位的文件类型,是链接式,不是执行式,不可运行
程序头起点:没有程序头表,偏移量为0
节头表起始位置:起始地址为720字节
本头的大小:ELF(这个头)节头大小为64字节
程序头大小:0
程序头的数目:0
节头大小:64字节
节头数量:12个表项
字符串表索引节头:节头表中第11项是字符串表

符号和符号表

每个可重定位目标模块m都有一个符号表(.symtab)
1.由m定义并能被其他模块引用的全局符号。 eg.非静态的C函数以及非静态的C全局变量。
2.只被m定义和引用的本地符号。 eg.带static的C函数和static全局变量。以及static局部变量。
3.由其他模块定义,并被m引用的全局符号,称为外部符号。 eg.定义在其他模块中的C函数和变量。
PS:在函数内部定义的static变量,不在栈中管理。而是在.data和.bss中为每个定义分配空间,并且在.symtab中创建一个名字唯一的本地符号。

符号解析

符号解析是链接的两个主要任务之一,方法是将每个引用和一个确切的定义联系起来。那如果多个目标文件同时定义了相同的符号怎么办?

  • 强、弱符号
    强符号:函数和已初始化的全局变量。
    弱符号:未初始化的全局变量。
  • Linix链接器处理多重定义的符号的规则:
    1.不允许有多个强符号
    2.一个强符号,多个弱符号,选强符号
    3.多个弱符号,随便选一个

静态链接过程

1.链接器按照命令行输入顺序,从左到右扫描可重定位目标文件和存档文件(静态库)。
2.在此次扫描中,链接器维持一个可重定位目标文件的集合E(这个集合的文件会被合并起来形成可执行文件),一个未解析符号集合U(引用了但是尚未定义),以及一个已定义符号集合D(前面输入文件已定义)。刚开始E、U、D都是空的。
3.

  • 对于命令行的每个输入文件f,先判断f是目标文件还是存档文件。目标文件直接把f加入E,然后根据f的内容修改U,D集合。
  • 若f是存档文件,尝试匹配U中未解析的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加入E中,并且根据m的内容来修改U,D集合。对存档文件的所有成员反复进行这个过程,直到U,D不再发生变化。然后继续处理下一个文件。
  • 如果链接器完成了对所有输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。若U为空,则表明各个符号解析成功,它会合并和重定位E中的目标文件,输出可执行目标文件。
  1. 所以,一般将库放在命令行的结尾。若是有特殊的需求,比如循环引用,也可以在命令行上重复导入某个库。(出现这种情况,更好的办法应该是,这两个相互依赖的模块放在同一个.a存档文件中)。

重定位

  1. 重定位节和符号定义
    这一步链接器将所有相同类型的节合并为同一类型的新的节。例如来自输入模块的.data节全部被合并成一个节,这个节成为输出的可执行目标文件中的.data节。这一步完成时,程序中的每一个指令和全局变量都有唯一的运行时存储器地址了。
  2. 重定位节中的符号引用
    链接器修改代码节和数据节中对每个符号的引用,使它们指向正确的运行时地址。这一步依赖于代码和数据的重定位条目。.rel.text和.rel.data节。前者存放代码的重定位条目,后者存放已初始化数据的重定位条目。

可执行目标文件


ELF头部描述文件的总体格式。它还包括程序的入口点,也就是程序运行时第一条执行的指令地址。.text、.rodata、.data节和之前的可重定位目标文件中的对应节类似,只是它们已经被重定位到最终的运行时存储器地址。.init节定义了一个小函数_init,程序的初始化代码会调用它。由于可执行文件时完全链接(重定位过),因此不再需要.rel.text和.rel.data节。

以main.c和sum.c为例,查看可执行目标文件的elf头。

/* sum.c */
/* $begin sum */
int sum(int *a, int n)
{
    int i, s = 0;
    
    for (i = 0; i < n; i++) { 
        s += a[i];
    }
    return s;
}        
/* $end sum */

命令:gcc -c main.c sum.c
gcc -static -o pro main.o sum.o
readelf -h pro
运行结果如下:

ELF 头:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - GNU
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x400a30
  程序头起点:          64 (bytes into file)
  Start of section headers:          842616 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         6
  节头大小:         64 (字节)
  节头数量:         33
  字符串表索引节头: 32

区别:
Type:ELF文件类型为可执行文件EXEC
程序入口地址为0x400a30,是可执行的文件类型
程序头起点:偏移量为64
程序头大小:56B

动态链接共享库

  1. 即使使用静态库,加载时也会出现重复加载到存储器的浪费情况,比如C标准库。
  2. 共享库(shared library)也称共享目标(shared object),Unix中,通常用.so后缀表示。(静态库是.a)。Windows中用.dll文件表示(dynamic linking library)。
    给定的文件系统中,对于一个库只能有一个.so文件;其次,在存储器中,一个共享库的.text节的一个副本可以被不同的运行进程共享。
  3. 链接时,不会有任何.so的代码和数据节被拷贝到可执行目标文件中,只拷贝了一些重定位和符号表信息,以便于运行时可以解析对.so中的代码和符号引用。
  4. 对共享库的引用有两种,一是在程序执行之前,被加载时,动态加载器加载和链接共享库;二是,在运行时通过动态加载器加载、链接特定的库,而无需再编译时链接那些库到应用中。
本博客对第七章的内容做了简单的总结,内容并不全面,仅供参考,如有想要深入学习的同学建议观看南京大学袁春风老师的网课和课本《深入理解计算机系统》。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值