程序员的基本修养之代码编译#
| 代码编译过程介绍,避坑指南,一些常用代码查看工具使用介绍
预处理#
1.预处理的作用#
宏替换:
替换 #define 定义的宏。
1
2
3
4
| #define PI 3.14159
double circle_area(double radius) {
return PI * radius * radius; // 替换后:3.14159 * radius * radius
}
|
头文件包含
替换 #include 指令为头文件的内容。
1
| #include <iostream>// 替换为 <iostream> 文件的完整内容
|
条件编译
根据条件选择性地编译代码。
1
2
3
| #ifdef DEBUG
std::cout << "Debug mode is on" << std::endl;
#endif
|
宏展开
处理函数式宏。
1
2
| #define SQUARE(x) ((x) * (x))
int result = SQUARE(5); // 替换为 ((5) * (5))
|
注释删除
移除源代码中的注释内容。
2.查看预处理结果#
通过 编译器选项 可以仅执行预处理步骤。例如gcc/clang:
1
| g++ -E main.cpp -o main.i
|
- -E 选项表示仅执行预处理。
- 输出文件 main.i 包含预处理后的源代码。
cmake可以通过添加配置保存中间产物
1
| set_target_properties(${PROJECT_NAME} PROPERTIES COMPILE_FLAGS "-save-temps=obj")
|
注:一些复杂的宏操作可以通过这种方式确定最终展开后的形式
3.预处理注意事项#
宏展开陷阱
注意宏的嵌套展开可能引发意外行为,用括号保护表达式。
1
| #define ADD(x, y) ((x) + (y))
|
头文件滥用
导出了所有头文件并加入了搜索路径,当存在多个同名头文件时,可能会引起一些诡异的编译问题,或者运行时崩溃
- 头文件的搜索顺序
1.搜索当前目录(一般是#include “header.h”,双引号方式引用头文件)
2.通过-I指定的目录,多个目录按加入的顺序搜索
3.标准系统目录
| 编译是从源文件(.c/.cpp)生成目标文件(*.o)的过程
Q1:目标文件里面包含了哪些信息?#
1
| llvm-objdump -s /path/to/objfile # 显示目标文件中所有Section的内容
|

1.目标文件类型、目标架构#
查看命令:
1
2
| # 可以直接使用ndk里面的工具,目标文件/静态库/动态库/可执行文件都可以查看
llvm-readelf -h 目标文件
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| File: /Users/marshall/Workspace/projects/test_compile/build/lib_src/CMakeFiles/lib_src.dir/conv1d.o
Format: Mach-O arm64
Arch: aarch64
AddressSize: 64bit
MachHeader {
Magic: Magic64 (0xFEEDFACF)
CpuType: Arm64 (0x100000C)
CpuSubType: CPU_SUBTYPE_ARM64_ALL (0x0)
FileType: Relocatable (0x1)
NumOfLoadCommands: 4
SizeOfLoadCommands: 520
Flags [ (0x2000)
MH_SUBSECTIONS_VIA_SYMBOLS (0x2000)
]
Reserved: 0x0
}
|
2.只读内容存在__TEXT段#
a.__text节保存了编译后的机器码:#
内容:源代码编译后的二进制机器指令,对应程序的函数和逻辑
查看命令:
注:通过查看中间产物汇编文件(*.s)可以初步分析是否值得做
比如通过查看conv1d.s文件,发现已经做了循环展开,就不需要在c代码上手动做循环展开了(NEON类的SIMD在代码编译是否会进行编译优化待确定)
b.__cstring节保存了字符串#
c.__const节保存了学常量#
3.全局变量和静态变量保存在__DATA段#
nm命令介绍
nm命令可以用来分析二进制分析的符号信息
1
2
| # -A 选项在符号名前附加文件名,适用于分析静态库(.a):
nm -A 静态库路径
|

注:该命令可以用来辅助分析Undefined symbol一类的编译问题
Q2:同一份代码,保持编译参数不变的情况,两次编译最终的目标文件是否是一样的?#
- 通常一致的场景
如果满足以下条件,两次编译的目标文件大概率相同:
- 代码完全不变:未修改任何源码文件(包括头文件)
- 编译参数严格一致:包括优化级别(如 -O2)、调试选项(如 -g)、路径参数(如 -I)等
- 编译器版本一致:同一版本的编译器(如 GCC 12.3)和链接器
- 环境无干扰:
a.无时间戳或随机化因素嵌入二进制文件(如代码中未使用 DATE、TIME 宏)
b.编译路径和文件系统结构相同
- 时间戳或随机化因素
若源码使用 DATE、TIME 等宏,编译后生成的二进制文件会包含编译时间戳,导致两次编译结果不同。
1
| printf("Build Time: %s %s\n", __DATE__, __TIME__); // 每次编译结果不同
|
- 调试信息中的路径差异
调试信息(.debug_line 段)默认包含源码绝对路径。若两次编译的源码目录不同,目标文件会不同。
1
2
3
4
5
| # 第一次编译路径:/home/user/project/
gcc -g main.c -o main
# 第二次编译路径:/tmp/build/
gcc -g main.c -o main # 调试信息中的路径不同,目标文件哈希值不同
|
| 链接就是把所有目标文件合并到起,同时目标文件中在未知的地址(如在其它文件中实现的函数调用)替换成最终的地址

注:左侧是目标文件main.o,右侧是最终的可执行程序main
``` Shell
llvm-objdump -d 目标文件
```
动态库与静态库对比#

macos环境下查看依赖
1
| otool -L /path/to/binary
|
1
2
3
| main:
@rpath/liblib_src.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
|
注:@rpath 会根据不同应用的配置解析到对应的目录