编译、汇编和链接
一、编译、汇编和链接的基本概念
编译:将源代码转换为汇编语言。
汇编:将汇编语言转换为机器代码。
链接:将多个对象文件和库合并成一个单一的可执行文件。
下图为编译的具体流程:

二、链接与加载
1. 目标文件的组成
每一个输入文件(如 .o 文件)不仅包含自身的代码段与数据段(SECTION),还携带一个符号表。符号表中的符号分为两类:
导出符号:由当前文件定义,供其他文件调用。
导入符号:由当前文件使用,但不由当前文件定义。
2. 链接器的两遍扫描机制
链接器在运行时会执行经典的“两遍扫描”过程:
第一遍扫描(符号解析与内存布局):链接器根据链接脚本(ld文件)规划各个段在输出文件中的内存布局,并构建全局符号表以解析符号引用。
第二遍扫描(重定位与代码生成):基于第一遍确定的内存地址,链接器读取并重定位目标代码。它会将符号引用替换为确切的机器码地址(例如,将
BL printf替换为BL 0x2100),调整代码和数据的内存地址,并将重定位后的代码、文件头部信息以及符号表等写入最终的输出文件中。
3. 调试符号与动态链接
调试符号:几乎所有的目标代码格式都支持调试符号。如果在编译时加入了
-g选项,生成的.o文件会包含调试信息。在链接阶段,链接器会将所有.o文件的调试符号进行整合,并统一放置在最终 ELF 文件的底部。动态库绑定:对于动态链接库,链接器在链接阶段只会解析并识别出所需的共享库名称,将其记录在输出文件中,而不是将库代码直接链入程序。真正的符号绑定与地址解析,会延迟到程序加载时由动态链接器完成。
4. 体积优化策略
在链接过程中,如果直接链接 .o 文件,该文件会被全量加入;如果链接静态库(.a),链接器会按对象文件(.o)为单位进行按需提取,这往往会导致大量无用代码被打包,增加最终文件体积。为此,通常采用以下编译与链接选项进行优化:
编译阶段:在 CMake 中开启
-ffunction-sections和-fdata-sections,使编译器将每一个函数和数据分别放入独立的段中。链接阶段:配合使用
-Wl,--gc-sections,让链接器在生成最终目标文件时,自动剔除(Garbage Collect)那些未被引用的独立段,从而实现“按需打包”。
5. 核心总结
整体而言,链接过程是将编译产物转化为可执行文件的关键步骤。我们通过编译器将源码转化为包含代码、数据和符号表的 .o 或 .lib 文件,并在 CMake 中指定基底文件。链接器通过两次扫描,建立全局符号表、分配虚拟地址、执行重定位(如将符号转换为机器码),并最终按照链接脚本拼接代码段与数据段。
在这个过程中,必须注意目标文件的加入机制:.o 文件会被全量加入,而静态库(.a)则是按需提取。为了有效控制最终 ELF 或 BIN 文件的体积,强烈建议结合使用 -ffunction-sections、-fdata-sections 与 -Wl,--gc-sections,以确保只保留程序真正需要的代码与数据段。
补充内容:如果还要进一步压缩体积和提升性能,可以开启 LTO (
-flto)。它允许链接器跨越不同的.o文件进行全局优化。比如,它能发现某个函数虽然在 A 文件中定义了,但全工程都没调用,即使没有切成独立段,LTO 也能把它干掉;同时还能进行跨文件的内联优化。
在我看来,链接器与编译器是比较可恶的,如果优化等级过高或者设置的优化策略过强,可能会导致很多问题的出现,除非不得已,那么尽量不要损失产品的稳定性来换取那么一丁点的内存或运存空间!!!
博主在优化等级上遇到的BUG:GOTO语句乱跳,变量凭空消失或值不对,代码执行顺序被打乱,自作主张删代码导致出现HardFault,指针越界,DEBUG调试器出现随时被滴答打断,内存对齐失败导致HardFault,符号链接失效导致无法正确进入中断向量表。
三、RAM与FLASH的内容
受限于个人水平,文中难免有理解不到位或笔误之处。恳请各位大佬不吝赐教,欢迎邮件交流:sharkey_z@163.com
