编译步骤
GCC 编译器在编译一个C语言程序时需要经过以下 4 步:
1. 预处理
将C语言源程序预处理,生成.i文件。{预编译处理(.c)
a.宏定义指令:将所有的#define删除,并且展开所有的宏定义
b.条件编译指令:处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
c.头文件包含指令:处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置
d.特殊符号指令:预编译器可研识别一些特殊的符号,例如:删除所有注释 “//”和”/* */”
e.添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号
f.保留所有的#pragma编译器指令,因为编译器需要使用它们
2. 汇编文件
预处理后的.i文件编译成为汇编语言,生成.s文件。{优化程序(.s .asm)
- 编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码
3. 目标文件
将汇编语言文件经过汇编,生成目标文件.o文件。{汇编程序(.obj、 .o、.a、 .ko)
- 汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。根据汇编指令和机器指令的对照表一一翻译即可。用一下指令进行汇编
4. 可执行文件
将各个模块的.o文件链接起来生成一个可执行程序文件。{链接程序(.exe、 .elf、 .axf 等
通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件., 链接的主要内容是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地衔接,链接分为静态链接和动态链接。
静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。静态库文件:是一个二进制文件,存放的功能函数实现,在文件编译时要访问文件,编译之后静态库文件可以删除
而动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去,动态库文件:是一个二进制文件,存放的功能函数实现,在文件执行时要访问文件,编译时不需要动态库文件
gcc的详细编译过程
编写代码
为了能够演示编译的整个过程,首先创建一个工作目录test4,然后生成一个 C语言编写的hello.c程序,代码如下
//hello.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
编译过程
对于这个程序,一步到位的编译指令是: gcc test.c -o test
实质上,编译过程是分为四个阶段进行的,即预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和连接(Linking)
预处理
预处理的过程主要包括以下过程:
(1) 将所有的#define 删除,并且展开所有的宏定义,并且处理所有的条件预编 译指令,比如#if #ifdef #elif #else #endif 等
(2) 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置
(3) 删除所有注释“//”和“/* */”
(4) 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号
(5)保留所有的#pragma 编译器指令,后续编译过程需要使用它们
gcc 进行预处理的命令:gcc -E hello.c -o hello.i
如图,发现输出 test.i 文件中存放着 test.c 经预处理之后的代码,gcc 的-E 选项,可以让编译器在预处理后停止,并输出预处理结果.
编译为汇编语言
输入gcc -S hello.i -o hello.s指令将预处理生成的 hello.i 文件编译生成汇编程序 hello.s
GCC 的选项-S 使 GCC 在执行完编译后停止,生成汇编程序
汇编
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相 对于编译过程比较简单,通过调用 Binutils 中的汇编器 as根据汇编指令和处理 器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o 目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部 分了,但是在链接之前还不能执行。
输入gcc -c hello.s -o hello.o
指令将编译生成的 hello.s 文件汇编生成目标文件 hello.o
或者直接调用Binutils 中的 as将 hello.s 文件汇编生成目标文件,输入命令as -c hello.s -o hello.o
注意:hello.o 目标文件为 ELF(Executable and Linkable Format)格式的可重定向文件
链接(连接)
gcc 连接器是 gas 提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件
链接分为静态链接和动态链接,其要点如下:
(1) 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链 接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和 重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)
(2)动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统 中把相应动态库加载到内存中去
对于生成的 hello.o,输入命令gcc hello.o -o hello将其与C标准输入输出库进行连接,最终生成程序 hello然后执行
补充
多个程序文件的编译
通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用 GCC 能够很好地管理这些编译单元
假设有一个由 test1.c 和 test2.c 两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序 test,可以使用命令gcc test1.c test2.c -o test
如果同时处理的文件不止一个,GCC 仍然会按照预处理、编译和链接的过程依次进行。如果深究起来,上面这条命令大致相当于依次执行如下三条命令:gcc -c test1.c -o test1.o、gcc -c test2.c -o test2.o、gcc test1.o test2.o -o test
检错
gcc -pedantic illcode.c -o illcode
-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助Linux 程序员离这个目标越来越近。换句话说,-pedantic 选项能够帮助程序员发现一些不符合 ANSI/ISO C标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的那些情况,才有可能被 GCC 发现并提出警告。
除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W 开头,其中最有价值的当数-Wall,使用它能够使 GCC 产生尽可能多的警告信息。
gcc -Wall illcode.c -o illcode
GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优 秀的 Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror 选项,那 么 GCC会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:
gcc -Werror test.c -o test
库文件连接
开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、或lib、dll)的集合。
虽然 Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下;Windows所使用的库文件主要放在 Visual Stido 的目录下的 include 和 lib,以及系统文件夹下。但有的时候,我们要用的库不再这些目录下,所以 GCC 在编译时必须用自己的办法来查找所需要的头文件和库文件。
例如:我们的程序 test.c 是在 linux 上使用 c 连接 mysql,这个时候我们需要去 mysql 官网下载 MySQL Connectors 的 C 库,下载下来解压之后,有一个 include 文件夹,里面包含mysql connectors 的头文件,还有一个 lib 文件夹,里面包含二进制 so 文件 libmysqlclient.so,其中 inclulde 文件夹的路径是/usr/dev/mysql/include,lib 文件夹是/usr/dev/mysql/lib
编译成可执行文件
执行gcc –c –I /usr/dev/mysql/include test.c –o test.o
命令编译 test.c 为目标文件
链接
把所有目标文件链接成可执行文件:
gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
Linux 下的库文件分为两大类分别是动态链接库(通常以.so 结尾)和静态链接库(通常以.a结尾),二者的区别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的
强制链接时使用静态链接库
默认情况下, GCC 在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static 选项,强制使用静态链接库。
在/usr/dev/mysql/lib 目录下有链接时所需要的库文件 libmysqlclient.so 和 libmysqlclient.a,为了让GCC 在链接时只用到静态链接库,使用下面的命令:gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
静态库链接时搜索路径顺序:
ld 会去找 GCC 命令中的参数-L
再找 gcc 的环境变量 LIBRARY_PATH
再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的
动态链接时、执行时搜索路径顺序:
编译目标代码时指定的动态库搜索路径
环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
配置文件/etc/ld.so.conf 中指定的动态库搜索路径
默认的动态库搜索路径/lib
默认的动态库搜索路径/usr/lib
有关环境变量:
LIBRARY_PATH 环境变量:指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH 环境变量:指定程序动态链接库文件搜索路径
ELF文件的分析
ELF全称Executable and Linkable Format,即可执行和可链接的格式,是UNIX系统实验室(USL)为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,是所有类UNIX系统的主要可执行文件格式,windows系统对应的可执行文件格式简称PE,两者都是COFF格式的变种。
Linux上的ELF文件主要有三种:
1、可重定向文件,即通过汇编产生的文件,后缀是.o,该文件不能直接运行,
2、可执行文件,将多个可重定向文件和共享库文件通过链接产生,可以直接运行
3、共享库,如libc的共享库libc.so,该文件同样不能直接运行,同可重定向文件相比,最大的区别在于该文件不需要经过重定向处理。
ELF文件的段
ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都是段
一个典型ELF文件包含的段 | 含义 |
---|---|
.text | 已编译程序的指令代码段 |
.rodata | :ro 代表 read only,即只读数据(例如常数 const) |
.data | 已初始化的 C 程序全局变量和静态局部变量 |
.bss | 未初始化的 C 程序全局变量和静态局部变量 |
.debug | 调试符号表,调试器用此段的信息帮助调试 |
- ELF header: 描述整个文件的组织,包含ELF文件类型,硬件平台类型,程序执行入口, sections和segments的数量和起始偏移位置,大小等。
- Program Header Table: 描述文件中的各种segments,通常一个segment包含若干个属性(如读写权限等)相同的section,将section合并成segment是为了减少内存空间浪费,方便内存管理,section的大小是任意的,但是segment的大小必须是所在操作系统的内存页(如4KB)大小的整数倍。操作系统加载可执行文件时会把LOAD类型的segment映射至虚拟地址空间。可重定向文件中没有此项,只有可执行文件中才有。
sections 或者 segments:具体的sections,sections是将汇编代码文件中的各种数据做归类保存,方便对其做内存分配与管理, 如.text section是可执行指令的集合,.data section包含初始化的全局变量,.bss section保存的是未初始化的全局变量和局部静态变量,.dynsym section记录了所有需要重定向处理的符号等。segments是从程序加载和运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序。 - Section Header Table: 包含了文件各个section的属性信息,比如起始偏移位置,大小等。
使用 readelf -S 查看其各个 section 的信息
例如:输入readelf -S hello命令
反汇编ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。
使用objdump -D 对其进行反汇编如下
输入命令objdump -D hello进行反汇编
使用 objdump -S 将其反汇编并且将其C语言源代码混合显示出来
输入命令gcc -o hello -g hello.c、objdump -S hello
符号解析和重定位
对多个可重定位目标文件和其引用的共享库文件进行链接时,首先会逐一查找校验可重定位文件使用的所有变量或者函数,包括本模块内定义的和引入自其他模块的,是否存在合法的唯一的定义,如果查找校验失败就会报错符号未找到(undefined reference)。所谓的符号就是源代码中使用的函数名或者变量名,符号是为了提高代码的可读性,方便编程使用,编译时需要将所有的符号替换成内存中的相对地址或者绝对地址,因为底层的机器指令只认识内存地址。上述查找校验符号并将其替换成内存地址的过程就称为符号解析。
查找校验完符号后就会将多个可重定位文件按照输入的文件顺序以section为维度进行合并,一个一个的拼接,因为单个可重定位目标文件中使用的相对地址的起始地址都是0,所以合并时需要将原来的相对于0的地址都加上一个偏移地址,并改写对应section,最后更新对应的section Table。计算偏移地址的时候除了考虑文件拼接因素外,还需要考虑section对应segment在虚拟地址空间中的分布,考虑内存页的大小,即内存布局优化,这个过程就称为地址和空间分配,地址和空间指的是虚拟地址和空间。
单个源代码文件在编译时并不知道其引用的其他模块中的全局变量和函数的具体内存地址,因此编译后的汇编代码(.text section)中此类未知符号都有对应的特定内存地址表示,并在可重定位表(.text.rel section)中记录了这类未知符号。链接时会查找这类未知符号的真实地址,并改写汇编代码中使用的特定地址,这个过程就是重定位,即将代码指令中使用的假地址替换成真实内存地址的操作,重定位是符号解析的核心。在程序静态编译环节发生的重定位叫静态重定位,在程序加载完成,动态链接过程产生的重定位称为动态重定位。
参考: 程序的链接和装入及Linux下动态链接的实现
ELF学习--重定位文件
ELF学习--可执行文件
静态链接和动态链接
在静态编译环节将多个存在依赖关系的文件(模块或者库)做合理拼接就称为静态链接。如果多个进程即对应的多个可执行文件都依赖了同一个模块,在静态链接下,该模块的代码和数据则会在硬盘,内存中都各保存一份,实际上代码是可以多个进程共享的,这样就导致了内存硬盘存储空间的浪费。如果该模块代码更新,就必须对可执行文件进行二次编译才能使用更新后的模块代码。
解决上述问题的方法就是动态链接,即将符合解析中核心操作重定位推迟到程序运行时进行,具体而言就是在代码指令运行过程中只有用到了某个来自其他模块的全局变量或者函数才会触发对应的重定位,该重定位由动态链接重定位表,符号表和动态链接器完成,符号表和重定位表记录需要动态链接的符号及其所属的模块ID,函数名等,动态连接器根据重定位表的信息找到符号对应的真实地址。动态链接下,彼此相互依赖的多个模块可以独立开发,独立编译,模块间通过定义全局变量和函数的头文件调用,因为同一个头文件可以有不同的实现,所以可以极大提高程序的可扩展性和兼容性;编译时不需要将依赖的其他模块文件合并进来,所以生成的最终可执行文件体积更小,当其他模块出现更新时不需要对本模块二次编译。动态链接的问题是依赖的模块更新后可能跟原来的接口不兼容且有一定的性能损耗(与静态链接比,在5%以下)。
某个库文件通过静态链接还是动态链接的方式编译由库文件本身决定,编译形成共享库文件时,可以通过参数指定形成静态链接库文件和动态链接库文件,前者.a结尾,后者.so结尾,默认是动态链接库文件,在链接时链接器判断符号所属的库文件是动态链接库就会做特殊处理,将这类符号放在单独的动态链接符号表和可重定位表中。静态链接库文件中每个函数对应一个目标文件,如printf函数对应printf.o文件,这样拆分是为了避免引入其他不需要的函数而导致最终的可执行文件体积过大。动态链接库文件是在程序被装载的时候由动态链接器加载到对应进程的虚拟地址空间内(即完成库文件的内存映射),在完成动态链接后才将控制权交给可执行文件的入口地址,由动态连接器保证内存中的库文件只有一份,但数据是每个进程独立的。动态链接器的库文件路径在可执行文件的.interp section内,Linux下通常是/lib/ld-linux.so.2。
参考: 《程序员的自我修养》
c语言程序编译运行过程;静态链接,动态链接
ELF--动态链接