C++从代码文件到程序可以执行主要经过了几个步骤:
- 预处理;
- 编译;
- 汇编;
- 链接。
预处理
生成.i文件。
- 会替换宏定义内容,比如
#define
,#ifndef
- 删除注释代码
- 导入头文件做替换(是真的把头文件的所有内容都替换,所以内容很多很多)
编译
生成.s文件。汇编指令,会检查语法错误问题。
经过词法分析,语法分析,生成中间代码,优化,然后是目标代码的生成(就是指定平台的汇编代码)。
汇编
生成的是.o可执行文件。
汇编中最重要的一个东西就是符号表。可以使用nm
指令来查看可执行文件的符号表。程序具体运行时,各种运行函数和访问参数都是靠这张符号表来表示的。
1 | ⋊> ~/code_directory nm answer2.o 23:10:52 |
每一行包含了符号的地址,符号的类型和符号名称。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),地址通常是相对地址。 | 地址通常是绝对地址,因为程序加载时地址固定。 |
符号可见性控制 | 可以通过编译器选项控制符号是否导出。 | 符号不可见,无法被其他程序使用。 |
运行时作用 | 动态库的符号表用于动态链接器解析符号。 | 可执行文件的符号表用于解析程序自身的符号。 |
对外部的依赖 | 动态库可能依赖其他库,符号表中包含依赖的符号信息。 | 可执行文件依赖动态库,符号表中包含库的符号引用。 |