Skip to main content

编译和链接

· 6 min read

程序被执行的过程

预编译--编译--汇编--链接

gcc hello.c
./a.out

预编译

(1)展开#define宏定义

(2)处理条件预编译指令 #ifdef #if #elif #endif

(3)删除所有注释

编译

经过编译器生成.s

汇编

经过汇编器生成.o

链接

处理各个模块间相互引用的部分,主要包括地址和空间分配、符号决议、重定位

目标文件

目标文件、静态库、动态库、可执行文件在结构上都差不多,这里以目标文件代替。比如linux中的ELF文件

以段的形式组合,主要包括:

ELF header
.text代码段
.data初始化了的全局变量和局部静态变量
.bss未初始化的全局变量和局部静态变量
.rodataconst修饰的变量、字符串常量
.symtab符号表
.rel.text对.text的重定位表,比如调用printf
......

段表长度、段表位置、段数量、ELF文件类型(.o/.so/可执行文件,系统通过这个字段判断类型,而不是后缀)等信息

符号表(.symtab)

符号名(函数/变量名) —— 符号值(距段首的偏移地址)——符号所在段

C++符号修饰和函数签名

不同编译器可能不同

int func(int) ===> _Z4funci
int C::func(int) ===> _ZN1C4funcEi

extern "C" 主要就用来控制符号修饰,因为C和C++的符号修饰规则不同,如果需要调用C语言库的函数,采用C++的符号修饰,就会导致找不到符号的错误

强符号和弱符号

强符号:目标文件A和目标文件B同时定义全局变量global,会报错multiple definition of 'global'

TODO: 补充

链接

为什么不把不同目标文件直接顺序拼接

因为可能会存在很多很小的段,每个段会进行分页,分页后很短的段会出现内部碎片

分为两步

  • 空间与地址分配(空间: 多个.o变成.exe后,elf文件空间怎么分配;地址: 每个段的虚拟地址和长度 )
  • 符号解析与重定位(符号解析、重定位、调整代码中的地址)

空间与地址分配

(1)链接器扫描所有输入的目标文件,计算合并后每个段的位置和长度(比如a的.text和b的.text合并到一起);

在链接之前,所有目标文件的虚拟地址(VMA)是0,链接时就会进行分配,链接后的文件会记录每个段的虚拟地址。

(2)收集所有的符号定义和符号引用,计算他们的虚拟地址,并放到全局的符号表。

每个符号在各自段的offset记录在原先的.o文件中的符号表里,在确定段在全局的虚拟地址后,便可以确定这些符号的虚拟地址了

符号解析与重定位

一个文件如果用到外部文件函数/变量时,编译成目标文件后,编译器将相应语句对应的地址暂时设置为0。同时,编译器会做两件事:

  • 符号表:记录某个符号所在段、在所属段的偏移地址
  • 重定位表:比如.rel.text .rel.data

重定位表的结构:

  • offset: 距离段首的偏移值
  • info:重定位入口的类型和符号在符号表中的下标

链接器处理每个重定位入口,查找全局符号表,找到相应符号进行重定位

如果找不到会报"undefined reference to xxx"

C++链接过程的一些问题

重复代码消除

C++模版、虚函数表在每一个用到的文件都会编译成同样的内容,直接链接会造成重复,链接器会进行重复代码消除。

全局构造与析构

全局对象的构造函数在main之前执行,在main之后析构

所以C++的ELF文件中,还额外定义了两种段

  • .init: main之前会执行的代码
  • .fini: main之后会执行的代码

API和ABI

API是源代码级别的接口;ABI是二进制级别的接口

比如C++的对象内存布局是ABI的一部分; POSIX是一个API标准

静态库链接

静态库是一组目标文件的集合,多目标文件压缩、打包

链接器链接过程中,会把需要的目标文件从库中解压出来,并链接成一个文件