【linux】基础开发工具(3)gcc/g++,动静态库
c/c++源代码到可执行程序“四级跳”
作为c/c++开发者,理解编译的过程至关重要,理解c/c++从代码一步步编译成可执行程序的过程是我们学习的必经之路。
从 .c/.cpp 源代码到可执行程序,需经历预处理、编译、汇编、链接四个阶段,每一步都决定了程序的最终形态:
1.预处理:“文本替换” 的预处理
- 处理
#include(展开头文件)、#define(宏替换)、条件编译指令等,生成.i文件。 - 示例命令:
gcc -E code.c -o code.i

执行完成后生成一个.i文件。
2. 编译:“高级→汇编” 的翻译
- 将预处理后的代码转换为汇编语言(
.s文件)。 - 示例命令:
gcc -S code.i -o code.s

编译先将代码编译为了汇编语言,为什么不直接编制成机器码呢?
我们的每一步都是基于前人走出来来的,最开始的编程就是纯二进制纸带打孔(孔洞能否被光透过代表0和1),但是这样操作繁杂效率低下且人类不容易理解。
在此之上,二十世纪四五十年代,汇编语言被发明出来,汇编相比二进制更容易被人理解。但是无法被机器理解。由此编译器产生了。
汇编的第一代编译器必然是用机器语言(二进制)实现的,因为汇编语言没有编译器就不能被机器识别,所以用二进制写了一个解析汇编语言的程序叫编译器。

直接将高级语言翻译成二进制(机器码)成本会非常高,汇编语言可以作为一个中间的跳板,但是现代语言中,这已经不是必然选项。这其中还有很多细节,本文不再赘述。
编译器的自举
有了二进制的第一代编译器之后,就可以用汇编语言实现自己的编译器,这一个过程叫做编译器的自举。
3. 汇编:“汇编→机器码” 的转化
- 将汇编代码转换为机器码(
.o目标文件),这是计算机能直接执行的二进制指令。 - 示例命令:
gcc -c code.s -o code.o

注意:

.o文件虽然是机器码,但是默认不带可执行的属性。要想生成可执行程序,要将.o文件转化为可执行程序。

.o文件可以形成可执行文件,多个.o文件可以链接形成一个可执行程序(不同功能的代码分开编译,最终形成一个可执行文件)。
4. 链接:“目标文件→可执行程序” 的合并
将多个 .o 文件、依赖的库文件 “合并” 成可执行程序。
静态链接:把静态库的代码直接拷贝到可执行程序中。
静态链接在编译时就会完成链接,大体分为以下七步:
1.收集目标文件:解释编译器将文件编译成目标文件(.o或.obj)。
2.解析符号:链接器扫描所有目标文件,构建一个全局符号表,记录每个符号的定义位置。如果同一个符号被多次定义,链接器会报错(除非是弱符号)。如果某个符号未定义,链接器会从静态库中查找。
3.提取库文件:链接器按照命令行指定的顺序处理静态库。当遇到未解析的符号时,链接器会在静态库中查找包含该符号定义的目标文件,并将其加入到链接中。这个过程可能会重复,因为库中的目标文件可能又引用了其他符号。
4.合并段:链接器将每个目标文件中的代码段(如.text)合并到可执行文件的代码段,数据段(如.data、.bss)合并到数据段。同时,链接器会为每个段分配运行时内存地址。
5.重定位:在目标文件中,对于外部函数和变量的引用通常是暂时用相对地址或0地址表示。链接器根据符号的实际地址修改这些引用,填上正确的地址。重定位信息存储在目标文件的重定位表中。
6.生成可执行文件:完成重定位后,链接器生成可执行文件,其中包含程序运行的所有代码和数据,以及程序入口点信息。
动态链接:可执行程序仅记录动态库的 “引用”,运行时才加载库。
动态链接发生在运行时,但链接过程实际上分为两个阶段:编译时链接和运行时链接。
编译时链接(生成可执行文件时):
符号解析:与静态链接类似,链接器解析程序中的符号引用。但是,对于动态库中的符号,链接器不会将代码复制到可执行文件中,而是记录这些符号的名称和它们所在的动态库信息。
生成可执行文件:可执行文件中包含一个动态段(如.dynamic),其中列出了所依赖的动态库名称,以及一个符号表(如.dynsym),记录了需要从动态库中解析的符号。
运行时链接(程序加载时):
加载动态库:当程序运行时,动态链接器(如ld-linux.so)首先检查可执行文件所依赖的动态库,然后在系统预设的路径(如/lib, /usr/lib)或用户指定路径(如LD_LIBRARY_PATH)中查找这些库文件,并将它们加载到内存中。
符号解析与重定位:动态链接器解析可执行文件和动态库中的符号引用,将其与动态库中的实际地址绑定。这个过程可能涉及大量的重定位操作。
地址分配:动态链接器为动态库分配内存地址,并修改可执行文件和动态库中的地址引用。由于动态库可以被多个进程共享,因此通常使用位置无关代码(PIC)来避免与绝对地址的耦合。
运行时链接的两种方式:
加载时链接:在程序启动时由动态链接器完成所有符号的解析和重定位。
运行时链接:程序在运行过程中通过动态链接器提供的API(如dlopen, dlsym)动态加载库和解析符号。
理解运行时和编译时链接:
静态链接,链接器会将代码复制到可执行文件中,在运行之前就已经链接好了,所以称之为编译时链接。
动态链接,链接器不会将代码复制到可执行文件中,而是记录要用到的动态库中的方法,在运行时调用动态库中的方法,所以称之为运行时链接。
对比:
动态链接运行时需要链接动态库,速度稍慢,但是无需将代码加载到可执行文件中,所以生成的文件较小,有较高的灵活性,可使用模块化实现,库更新时替换库文件就行,无需重新编译。但是如果可执行文件的库文件缺失会导致链接失败,程序无法运行。使用同一个动态库的进程,如果动态库缺失未启动的进程由于动态库缺失会启动失败。运行中的进程会崩溃。
静态链接会把代码加载到可执行文件,文件体积较大(每一个用到静态库的程序都需要将代码拷贝一份),库更新时需重新编译;一次编译之后,即使失去库文件,可执行文件仍然可以运行。使用同一个静态库的进程没有关联性,一旦编译过后,静态库就影响不了进程运行了。
静态库和动态库没有好坏之分,它们有着不同的适用场景。
静态库的哲学是:一次打包,终身可用。
应用:嵌入式设备,各种无需频繁更新的场景。
动态库的哲学是:共享资源,动态更新。
应用: 各种游戏的地图和武器资源(app同理),按需自由下载。需要频繁更新的场景。
gcc/g++编译
gcc编译时,默认动态链接

静态链接生成的可执行程序比动态链接大得多。有的系统可能默认没有安装动态库,使用静态链接会报错。
ldd
使用ldd可以查看可执行程序所需要的库。

自动化构建-make和makefile基础
使用gcc/g++编译时需要需要选择选项,每编译一次,就重新写一次会极大的浪费我们的时间,由此引入了自动化构建工具。
make是一个命令,makefile是一个文件,make是一个经典的构建自动化工具,主要用于管理软件编译和构建过程。它通过读取 Makefile 文件来自动化执行编译、链接、安装等任务。

makefile第一行是依赖关系,如图表示code依赖code.c生成。第二行是依赖方法,表示如果通过code.c生成code可执行文件。注意:依赖方法必须以Tab键开头,不能使用空格


有了clean,makefile就基本能使用了

make 命令扫描makefile文件时,从上向下扫描,默认形成第一个。
如果将makefile改成这样

使用make时则默认执行:
.PHONY
.PHONY是 Makefile 中的一个特殊指令,用于声明伪目标。伪目标指像clean这样要执行的动作,而不是要生成的文件。被.PHONY修饰的会一直执行。
make没有被.PHONY修饰,当目录中存在一个code,且源代码没有被修改,输入make不会继续生成可执行文件。

原理
文件=内容+属性。使用stat可以查询文件的属性。

每一个文件都是自己的acm时间,make时会比较源代码文件和可执行文件的modify时间新旧,如果源代码被修改了,modify时间更新。make会重新生成可执行程序。被.PHONY修饰的不会去比较这两个文件的新旧,直接执行。
