目录
项目架构和配置
不同的项目架构不同,一般都会有不同命名的文件夹表示项目中不同代码的含义。这里可以着重注意两个文件,一个是build文件夹,一个是CMakeLists.txt文件夹。
build文件夹
存放构建过程中的临时文件(.o目标文件等)和最终产物(即可执行文件),一般将整个项目编译连接之后产生的文件(中间文件和最终产物)存放于此。它主要作用是和源码分离,保持源码干净。
CMakeLists.txt文件夹
这个文件涉及元构建系统/构建系统生成器 CMake。CMake并不直接编译代码,它读取CMakeLists.txt,检测环境(编译器版本、库路径),然后生成适合平台的构建文件(Linux下生成Makefile、Windows下生成Visual Sudio Solution,或者生成Ninja文件),包括定义项目名、定义生成文件类型和定义依赖关系。
构建自动化
根据构建文件(Makefile)(由cmake文件自动生成或者项目本身就有),Make或者Ninja自动构建整个项目。具体包括如下几个阶段:
预处理
处理所有以#开头的指令,包括将宏定义(#define)进行替换,处理条件编译(#ifdef,#endif),最重要的是将#include "header.h"的内容直接复制粘贴到.cpp文件中。
编译
运用编译器g++或者gcc等,将预处理后的C++代码翻译成汇编语言。可以加入指令,这时编译器核心部分,分析代码逻辑,进行循环展开,死代码消除、指令重拍等操作。最终产出.s汇编文件。
汇编
运用汇编器,将汇编语言翻译成机器能读懂的机器码,产出.o目标文件。此时.o文件中虽然包含机器码,但里面函数调用还是悬空。即.o文件内部不仅仅有机器码,还包含两张至关重要的表,符号表和重定位表。在实际汇编中,当汇编器遇到非本文件的函数调用时,由于其并不知道具体地址,所以他将此地址留空,并在重定位表中写下记录告知链接器在代码段什么地址处,需要用到什么函数的真实地址,待链接器填写。并维护以下两个表。
-
符号表。当前.o文件的"资产负债表",即记录有什么函数,缺少什么函数。例如如下:
0000000000000000 T main <-- T 表示 Text,我定义了 main 函数 (资产) U add <-- U 表示 Undefined,我用到了 add,但我不知道它在哪 (负债) U printf <-- U 表示 Undefined,我用到了 printf (负债) -
重定位表。这是.o文件的补全指南,告诉链接器在什么地址用到了哪个函数,但本文件没有需要链接器填写。如下所示:
RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000001a R_X86_64_PLT32 add-0x0000000000000004 0000000000000025 R_X86_64_PLT32 printf-0x0000000000000004
链接
将无数个.o目标文件和库文件组合成最终产品的过程。分为静态链接和动态链接:
- 静态链接.库文件.a(Linux)和.lib(Windows)。静态库本质就是一堆.o文件的压缩包,链接器会将代码中用到的函数或者部分直接拷贝到可执行文件中,这样函数地址在链接期就直接确定。优点是部署方便,不依赖外部环境。缺点是生成文件太大,如果库更新了,需要重新编译整个项目。
- 动态链接.库文件.so(Linux)和.dll(Windows)和.dylib(macOS)。链接器不拷贝代码,只在可执行文件中记录在哪个库文件中用到哪个函数,当程序启动时,操作系统会加载这些库文件,自动寻找所需部分填入可执行文件中,这样函数地址在运行或第一次调用时才确定。优点是节省磁盘和内存,库更新只需替换文件,无需重编程序。
- 符号解析.链接器扫描所有输入文件,建立全局符号表,确保每个函数还有一个定义、每个被调用函数都能找到定义。(全局符号表并不是一个单独的物理文件,而是在内存中的一种复杂数据结构)
常见术语
- GCC是什么?GCC是GUN编译器套件,是一个编译器系统,不是单个程序,包含一系列编译工具的集合,包含gcc、g++等编译器(这些都是广义上的汇编,即包含预处理、编译、汇编、链接全流程)。
- ...