C/C++基本功之编译链接原理(一)

非学无以广才,非志无以成学。


前言

通常的开发环境都是流行的IDE,比如Visual Studio,这些IDE将编译和链接的过程一步完成,成为build(构建)。IDE提供的编译和链接设置对于大部分应用程序开发足够使用,但是软件的运行机制被隐藏了,很多程序上的错误让我们无法定位。
C/C++是本地编译型语言,了解编译链接原理对C/C++编程至关重要,当出现编程问题有助于你快速定位bug,解决复杂问题。
本文参考《深入理解计算机系统》和《程序员的自我修养》的相关内容,介绍了C/C++基本功之编译链接原理的基本内容。


一、编译

经过预编译->编译->汇编,生成二进制可重定位的目标文件(*.obj / .o)
给出如下两个源文件main.c和swap.c,它们将作为贯穿本文的示例,帮助我们理解编译和链接的过程。(使用GCC来编译)

/*swap.c*/
extern int buf[];
int* bufp0 = &buf[0];
int* bufp1;
void swap(){
	int temp;
	bufp1 = &buf[1];
	temp = *bufp0;
	*bufp0 = *bufp1;
	*bufp1 = temp;
}
/*main.c*/
void swap();
int buf[2] = {1, 2};
int main(){
	swap();
	return 0;
}

1. 预编译

首先源代码文件和相关的头文件被预编译器cpp预编译成一个.i文件。第一步预编译过程可以用如下命令执行:

$gcc -E main.c -o main.i

或者用cpp命令:

$cpp main.c > main.i

预编译过程主要处理源代码中以#开始的预编译指令,处理规则如下:

  • 将#define删除,并展开其定义的所有宏
  • 处理所有条件预编译指令,比如#if、#ifdef、#elif、#else、#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置(递归进行)
  • 删除所有的注释
  • 添加行号和文件名标识,比如#3"main.c"3,便于编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
  • 保留所有的#pragma编译器指令

2. 编译

编译过程就是把预编译完的文件进行词法分析、语法分析、语义分析及优化后生成汇编代码。编译过程可以通过以下命令执行:

$gcc -S main.i -o main.s

通常gcc把预编译和编译两个步骤合并成一个步骤,得到汇编输出文件,可以使用如下命令:

$gcc -S main.c -o main.s

3. 汇编

汇编是将汇编代码转变成机器可以执行的指令,每一条汇编语句对应一条机器指令。汇编命令如下:

$as main.o -o main.o

或者使用gcc命令:

$gcc -c main.s -o main.o

4. 小结

上面gcc编译分解过程如下图所示。
gcc编译过程分解图

除了上面分三步骤使用gcc命令,也可以使用gcc命令可以直接从c源代码文件,经过预编译、编译和汇编直接输出目标文件:

gcc -c main.c -o main.o

二、链接

1. 静态链接

静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载运行的可执行目标文件作为输出。
可重定位目标文件是由不同的代码和数据节组成的。指令在一个节中,初始化的全局变量在另一个节中,未初始化的变量又在另外一个节中。
链接器必须完成两个主要任务:

  • 符号解析(symbol resolution),目标文件定义和引用符号,符号解析的目的是将每个符号引用和一个符号定义联系起来。
  • 重定位(relocation)。编译器和汇编器生成从地址零开始的代码和数据节。链接器通过把每个符号定义与存储地址联系起来,修改所有对这些符号的引用,重定位这些节。

2. 目标文件

目标文件包括:

  • 可重定位目标文件。比如main.o,包含二进制代码和数据,可以和其他可重定位目标文件合并起来生成一个可执行目标文件。
  • 可执行目标文件。比如a.out,包含二进制代码和数据,可以直接拷贝到存储器并执行。
  • 共享目标文件。特殊类型的可重定位目标文件,在加载或运行时动态地加载到存储器并连接。

3. 可重定位目标文件

如下图是一个典型的ELF可重定位目标文件。
可重定位目标文件
上一章我们已经编译得到了一个main.o和swap.o的可重定位目标文件,使用如下objdump可以查看.o文件的结构:

$objdump -h swap.o

4. 符号和符号表

每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号信息。在链接器的上下文中,有三种不同的符号:

  • m定义并能够被其他模块引用的全局符号,对应非静态函数和非静态的全局变量。
  • 由其他模块定义并被模块m引用的全局符号,如external。
  • 只被模块m定义和引用的本地符号,对应静态函数和静态变量,这些符号在模块m中任何地方都是可见的,但是不能被其他模块引用。

符号表是由汇编器构造的,使用编译器输出到汇编语音.s文件中的符号。

5. 符号解析

  • 链接错误
    当编译器遇到一个不是在当前模块中定义的符号(变量或函数名),会假设该符号是在其他某个模块中定义的,生成一个链接器符号表表目,交给链接器处理。
    如果链接器在任何输入的模块中没有找到这个被引用的符号,报出链接错误

  • 解析多处定义的全局符号
    在编译时,编译器输出每个全局符号给汇编器,分为强或弱类型,汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。
    函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。如本文代码示例中,buf、bufp0、main和swap是强符号,bufp1是弱符号。

  • 处理多处定义的符号遵循如下规则:

    • 规则1:禁止有多个强符号
    • 规则2:如果有一个强符号和多个弱符号,选择强符号
    • 规则3:如果有多个弱符号,从中任意选一个

**注意:**规则2和规则3的应用会造成一些不易察觉的运行时错误,尤其是如果重复定义还有不同的类型时。

6. 与静态库链接

静态库也可以用做链接器的输入,在链接时,链接器将只拷贝被被应用程序引用的目标模块。
在linux系统中,静态库以一种称为archive的特殊文件格式存放在磁盘中。archive文件是一组连接起来的可重定位目标文件的集合,文件名由后缀.a标识

  • 示例:在linux x86平台制作一个计算向量加法和乘法的静态库,源代码如下图。
    静态库源代码
  • 使用AR工具制作静态库:
$gcc -c addvector.c multvector.c
$ar rcs libvector.a addvector.o multvector.o
  • 编写test.c使用生成的libvector.a静态库:
#include <stdio.h>
void addVector(int*, int*, int*, int);
void multVector(int*, int*, int*, int);
int v1[3] = {1, 2, 3};
int v2[3] = {5, 2, 6};
int v3[3];
int main()
{
	addVector(v1, v2, v3, 3);
	printf("v3 = {%d %d %d}\n", v3[0], v3[1], v3[2]);
	return 0;
}
  • 编译和链接输入文件test.o和libvector.a:
$gcc -O2 -c test.c
$gcc -static -o p1 test.o ./libvector.a

上面链接过程如下图所示:
与静态库链接过程

7. 重定位

链接器完成了符号解析这一步,就把代码中每个符号引用和确定的一个符号定义关联起来。链接器就知道它的输入目标模块中的代码节和数据节的确切大小。现在就可以重定位操作,合并输入模块,并为每个符号分配运行时地址。重定位分为两步:

  • 重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。
  • 重定位节的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

当汇编器生成一个目标模块时,并不知道数据和代码最终将存放在存储器中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。
无论何时汇编器遇到对最终位置未知的目标引用,就会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用
代码的重定位表目放在.relo.text中,已初始化数据的重定位表目放在.relo.data中
ELF定义了11种不同的重定位类型,重点是两种最基本的重定位类型:

  • R_386_PC32:重定位一个使用32位PC相关的地址引用。
  • R_386_32:重定位一个使用32位绝对地址的引用。

8. 可执行目标文件

可执行目标文件的格式类似于可重定位目标文件的格式。


总结

首先介绍了从源代码到最终可执行文件的4个步骤:预编译、编译、汇编和链接,分析了他们的作用及相互关系。重点介绍了静态链接的基本概念:重定位、符号、可重定位目标文件、可执行目标文件、静态库等概念。
然后介绍了目标文件和可执行文件的ELF格式,包括代码段、数据段和bss段。无论是可执行文件、目标文件或库,都是一样基于段的文件或者是这种文件的集合。
下一篇将结合更多的代码示例进一步剖析。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值