C++的编译

C++从代码文件到程序可以执行主要经过了几个步骤:

  1. 预处理;
  2. 编译;
  3. 汇编;
  4. 链接。

预处理

生成.i文件。

  1. 会替换宏定义内容,比如#define, #ifndef
  2. 删除注释代码
  3. 导入头文件做替换(是真的把头文件的所有内容都替换,所以内容很多很多)

编译

生成.s文件。汇编指令,会检查语法错误问题。

经过词法分析,语法分析,生成中间代码,优化,然后是目标代码的生成(就是指定平台的汇编代码)。

汇编

生成的是.o可执行文件。

汇编中最重要的一个东西就是符号表。可以使用nm指令来查看可执行文件的符号表。程序具体运行时,各种运行函数和访问参数都是靠这张符号表来表示的。

1
2
3
4
5
6
7
8
⋊> ~/code_directory nm answer2.o                                                             23:10:52
0000000100000498 T __Z7gcd_resxx
U __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEErsERi
U __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEErsERx
U __ZNSt3__13cinE
0000000100000000 T __mh_execute_header
0000000100000524 T _main
U _printf

每一行包含了符号的地址,符号的类型和符号名称。U代表undefined,这个标志着需要在链接的时候解析。T代表符号在.text段,通常是函数或者代码。还有D:.data,B:.bss,R:.rodata。

由于我是macOS电脑,所以包含了__mh_executor_header,是动态链接的入口点,未定义符号会在运行时通过动态链接解析,所以如果只是调用了库函数不需要手动链接。

ELF段(Section)信息

当然,除了符号表,还有很多重要信息。以linux系统为例,ELF是可执行文件的标准格式,我们可以使用readelf指令来查看可执行文件的基本信息。

1
readelf -S elf_example

主要段的作用

段名 作用 内容
.text 代码段 可执行指令
.data 已初始化数据段 初始化的全局/静态变量
.bss 未初始化数据段 未初始化的全局/静态变量
.rodata 只读数据段 字符串常量、const变量
.symtab 符号表 所有符号信息
.strtab 字符串表 符号名称字符串
.rel.text 重定位表 代码段的重定位信息
重定位表就是后续需要在链接中找到地址的内容。

链接

将目标文件和库文件进行链接。动态库可以被多个程序链接,链接分为了静态和动态。

静态链接

在编译阶段,将所需的库代码直接嵌入到生成的可执行文件中。

生成的可执行文件是一个完全独立的文件,运行时不需要额外的库文件支持。

静态链接的指令需要额声明是-static

1
g++ main.cpp -o main -static -L. -lmy_library

动态链接

在编译阶段,生成的可执行文件中只包含对库的引用,而不嵌入库代码。库代码在程序运行时由操作系统动态加载。
生成的可执行文件依赖外部动态库(如 .dll.so 文件)。

动态链接的指令表明了默认链接使用的是动态链接方法。

1
g++ main.cpp -o main -L. -lmy_library

我们可以总结两种链接方式的优缺点。

特点 静态链接 动态链接
文件体积 可执行文件体积较大,库代码嵌入其中。 可执行文件体积较小,库代码独立存在。
内存占用 每个进程加载独立的库代码,占用更多内存。 多个进程共享动态库代码,占用内存较少。
运行时性能 启动速度快,运行时性能更好。 启动速度稍慢,运行时性能略低。
库更新 无法独立更新库,需重新编译程序。 库可以独立更新,无需重新编译程序。
部署复杂度 部署简单,无需考虑库文件依赖。 部署复杂,需要确保库文件路径和版本正确。
调试难度 调试简单,运行环境固定。 调试复杂,需考虑动态库路径和符号解析。
适用场景 嵌入式设备、独立分发、对稳定性要求高的场景。 多程序共享库、插件系统、需要频繁更新的场景。

如何生成一个动态库

macOS上的动态库是.dylib, linux上是.so。一般可以使用下面指令进行动态链接的创建:

1
g++ -shared -fPIC -o libmy_library.so my_library.cpp

-fPIC表示的是position independent code,说明动态库使用的地址都是相对地址,方便其他程序动态链接。

特性 动态库符号表 可执行文件符号表
导出符号 包含导出的符号,供其他程序使用。 不导出符号,符号仅供程序自身使用。
未定义符号 可能包含未定义符号,依赖其他库解析。 可能包含未定义符号,依赖动态库解析。
符号地址 使用位置无关代码(PIC),地址通常是相对地址。 地址通常是绝对地址,因为程序加载时地址固定。
符号可见性控制 可以通过编译器选项控制符号是否导出。 符号不可见,无法被其他程序使用。
运行时作用 动态库的符号表用于动态链接器解析符号。 可执行文件的符号表用于解析程序自身的符号。
对外部的依赖 动态库可能依赖其他库,符号表中包含依赖的符号信息。 可执行文件依赖动态库,符号表中包含库的符号引用。