1. C/C++ 编译的四个步骤
从源代码到可执行文件,通常分为:预处理 → 编译 → 汇编 → 链接。

1.1 预处理(Preprocessing)
处理以 # 开头的预处理指令,例如展开 #include 引入的头文件、替换宏、处理条件编译,并去掉注释。
bash
gcc -E main.c -o main.i
-E 表示只进行预处理。C 的预处理结果通常以 .i 结尾,C++ 通常以 .ii 结尾。
1.2 编译(Compilation)
对预处理后的代码进行语法、语义检查和优化,生成目标架构的汇编代码。
bash
gcc -S main.i -o main.s
-S 表示执行到生成汇编代码为止。这里的"编译"指四个步骤中的这一步;平时也会用"编译"泛指整个构建过程。
1.3 汇编(Assembly)
把汇编代码转换成机器指令,生成目标文件。
bash
gcc -c main.s -o main.o
-c 表示执行到生成目标文件为止,不进行链接。.o 文件通常包含机器代码、符号表、重定位信息等,还不能直接作为程序运行。
1.4 链接(Linking)
把目标文件与其他目标文件、所需库以及启动代码组合起来,解析函数和变量等符号的引用,完成必要的重定位,生成可执行文件。
以 test.c 和 v.c 两个源文件为例,先分别生成目标文件,再将它们链接为一个程序:
bash
# 分别生成目标文件(完成预处理、编译和汇编,不进行链接)
gcc -c test.c -o test.o
gcc -c v.c -o v.o
# 将两个目标文件链接成可执行文件 test
gcc test.o v.o -o test
# 运行程序(要求架构与当前系统兼容)
./test
其中,test.o 和 v.o 是链接的输入,-o test 指定生成的可执行文件名。
例如,test.c 中使用了在 v.c 中定义的全局变量或函数,需要在 test.c 中提供相应声明。分别生成目标文件后,对这些外部符号的引用通常仍待解析;链接器会在 v.o 中找到对应定义,把两个目标文件组合成完整程序。如果漏掉 v.o,且其他输入也没有提供这些定义,通常会报 undefined reference(未定义的引用)。
代码中调用 printf() 等库函数时,链接还需要处理对相应库的引用。动态链接的程序在运行时还需要相应的动态库。
1.5 一条命令完成整个过程
bash
# C 程序
gcc main.c -o main
# C++ 程序
g++ main.cpp -o main
# 运行生成的程序(要求架构与当前系统兼容)
./main
上面的单条构建命令会完成四个步骤,默认不会保留中间的 .i、.s、.o 文件。C++ 通常使用 g++,它会自动链接常用的 C++ 标准库。
记忆:预处理展开代码,编译生成汇编,汇编生成目标文件,链接生成可执行文件。
2. 交叉编译工具链(以 i.MX6ULL 为例)
2.1 什么是交叉编译
交叉编译是指:在一种平台上编译,生成供另一种平台运行的程序。
例如,在 x86_64 架构的 Linux 电脑上编译代码,生成能在 i.MX6ULL 开发板上运行的 ARM 程序。电脑是开发主机,开发板是目标平台。
i.MX6ULL 是 NXP 的处理器,采用 32 位 ARM Cortex-A7 内核 。因此,为它编译程序时,需要使用适合目标 ARM 平台的工具链。参考:NXP i.MX6ULL 产品资料
2.2 工具链一般从哪里获取
一般优先使用芯片厂商或开发板厂商随 BSP/SDK 提供、推荐的交叉编译工具链。
- BSP(板级支持包):包含与开发板相关的启动、内核、驱动及构建配置等内容。
- SDK(软件开发套件):通常包含交叉编译工具链、目标系统的头文件和库,以及环境配置脚本。
以 i.MX6ULL 为例,先查看 NXP 或所用开发板厂商的配套资料,使用与板上系统版本相匹配的工具链和 SDK。工具链不仅要匹配 CPU 架构,还要考虑 ABI(如浮点调用约定)、C 库及目标系统的库版本;仅看到名称里有 arm,还不足以确认兼容。
根据本次提供的百问网开发包目录,实际使用的工具链前缀是 arm-buildroot-linux-gnueabihf-,后续示例采用这一前缀。
2.3 为什么叫"工具链"
它是一组配合完成编译、汇编、链接等工作的工具。以当前开发包的 arm-buildroot-linux-gnueabihf- 前缀为例:
| 工具名称 | 主要用途 |
|---|---|
arm-buildroot-linux-gnueabihf-gcc |
C 编译驱动,也可调度汇编、链接等步骤 |
arm-buildroot-linux-gnueabihf-g++ |
C++ 编译驱动 |
arm-buildroot-linux-gnueabihf-as |
汇编器 |
arm-buildroot-linux-gnueabihf-ld |
链接器 |
arm-buildroot-linux-gnueabihf-ar |
创建静态库 |
arm-buildroot-linux-gnueabihf-objcopy |
复制、提取或转换目标文件内容 |
生成的 test 应复制到匹配的 i.MX6ULL Linux 开发板上运行;在普通 x86_64 主机上通常不能直接运行。
记忆:在电脑上编译,在开发板上运行;工具链优先选厂商配套版本。
3. ELF 文件格式
3.1 什么是 ELF
ELF(Executable and Linkable Format,可执行与可链接格式)是 Linux 平台上广泛使用的二进制文件格式。 前面生成的 .o 目标文件、最终的可执行程序,以及 .so 共享库,都可以采用 ELF 格式。
ELF 文件不仅保存机器指令和数据,还保存文件头、加载信息、符号表、调试信息等,供链接器、系统加载器和调试器使用。Linux 执行 ELF 程序时,需要根据其中的加载信息建立程序的内存映像。
3.2 ELF 文件结构示意图
下图按课堂照片右侧的内容重绘,保留各层名称和分组:

重点记住:
- 文件头与头表:记录文件基本信息、各部分的位置及加载方式。
.text:存放机器指令。.rodata:存放只读数据,如字符串常量。.data与.bss:存放全局变量和静态变量;初值非零的通常放在.data,未显式初始化或初值为零的通常放在.bss。- 符号与调试信息 :
.symtab记录函数、变量等符号,图中的.stab保存调试信息。
记忆:ELF 不只是代码和数据,还包含供链接、加载和调试使用的信息。
4. file 和 readelf 命令
4.1 用两种编译器编译同一个 test.c
在 Linux 开发主机上,进入 test.c 所在目录,分别使用普通 gcc 和开发板配套的交叉编译器编译同一份源码。以下假设交叉编译工具链的 bin 已加入 PATH。
bash
# 普通 gcc:生成供当前 Linux 主机运行的程序
gcc test.c -o test_pc
# i.MX6ULL 配套交叉编译器:生成供开发板运行的程序
arm-buildroot-linux-gnueabihf-gcc test.c -o test_arm
两个命令使用同一个 test.c,分别输出 test_pc 和 test_arm。
4.2 分别使用 file 查看两个文件
成功生成两个文件后,在同一目录执行:
bash
file test_pc
file test_arm
file 用于识别文件类型。重点比较输出中的位数和目标架构:

| 对比项 | test_pc |
test_arm |
|---|---|---|
| 位数 | 64-bit,64 位 |
32-bit,32 位 |
| 目标架构 | x86-64,面向当前电脑 |
ARM,面向 i.MX6ULL 开发板 |
| 类型描述 | shared object |
executable |
| 动态加载器 | /lib64/ld-linux-x86-64.so.2 |
/lib/ld-linux-armhf.so.3 |
两者都是 ELF 文件,LSB 表示小端字节序,dynamically linked 表示动态链接。
结论:同一个 test.c,普通 gcc 生成 x86-64 程序,交叉编译器生成 ARM 程序;两者面向不同平台,通常不能直接互换运行。
4.3 使用 readelf -h 查看文件头
readelf -h 用于查看 ELF 文件头 ,比 file 提供更详细的信息。分别查看两个文件:
bash
readelf -h test_pc
readelf -h test_arm
这里的 -h 表示查看文件头。也可使用工具链配套的 arm-buildroot-linux-gnueabihf-readelf -h test_arm 查看 ARM 程序。
test_pc 的实际输出:

test_arm 的实际输出:

作用:确认程序的位数、目标架构、文件类型和入口等信息,帮助检查是否编译成了目标平台需要的程序。
| 字段 | test_pc |
test_arm |
|---|---|---|
Class(位数) |
ELF64,64 位 |
ELF32,32 位 |
Machine(架构) |
x86-64 | ARM |
Type(类型) |
DYN,结合前面的编译方式,可能为 PIE 程序 |
EXEC,可执行文件 |
Flags(架构相关标志) |
0x0 |
EABI5、hard-float ABI(硬浮点调用约定) |
Entry point address 是程序入口,通常对应启动代码,不能直接当作 main() 的地址。
5. BIN 文件与 ELF 的区别
这里的 BIN 指原始二进制镜像(Raw Binary),通常保存程序的机器指令和数据,没有 ELF 文件头、程序头表、节头表等结构信息,常用于裸机程序或固件烧录。
| 对比 | ELF | BIN(原始二进制镜像) |
|---|---|---|
| 内容 | 代码、数据,以及加载、链接等结构信息,可含符号和调试信息 | 主要是代码和数据的原始字节 |
| 加载方式 | 加载器可以从文件头和程序头等获取加载位置与入口 | 加载位置和入口通常由启动流程或外部约定指定 |
| 常见用途 | Linux 应用程序、目标文件、共享库及调试 | 裸机程序、固件镜像及烧录 |
| 信息查看 | 可用 readelf 查看内部结构 |
没有 ELF 结构,不能直接用 readelf 解析 |
两者都可以包含 CPU 执行的机器指令。 BIN 也需要正确加载和启动;把普通 Linux 应用的 ELF 转成 BIN,并不会自动变成能独立运行的裸机程序。GNU objcopy 的原始二进制输出说明可参考 官方文档。
记忆:ELF 带有结构和描述信息,BIN 是按约定使用的原始字节镜像。 .bin 只是常见扩展名,不能仅凭后缀确定实际格式。
6. 交叉编译工具链中常用的工具
6.1 arm-buildroot-linux-gnueabihf-size 工具
作用:统计目标文件或可执行文件中代码、数据等节的大小,帮助了解程序的空间占用。
bash
arm-buildroot-linux-gnueabihf-size test_arm
实际输出:

大小的单位都是字节。
| 列名 | 本次值 | 含义 |
|---|---|---|
text |
865 | 程序指令和只读内容(如字符串)占多大 |
data |
288 | 静态区,全局变量 |
bss |
4 | 程序启动时要清零的数据占多大,如没写初值的全局变量 |
dec |
1157 | 前面三项加起来,用十进制表示 |
hex |
485 | 同一个总数,换成十六进制表示 |
filename |
test_arm | 查看的是哪个文件 |
6.2 arm-buildroot-linux-gnueabihf-nm 工具
作用:查看目标文件或可执行文件的符号表,了解函数、变量的名称、类型和地址等信息。
bash
arm-buildroot-linux-gnueabihf-nm test_arm

可以把符号表理解成程序的**"索引表",主要记录函数、变量等的名字、位置和类别**,方便链接器、调试器查找。
例如图中的 0001040c T main:
main:函数的名字。0001040c:这个函数在该程序中的地址,用十六进制表示。T:表示符号位于代码区。
图中的 U puts@GLIBC_2.4 表示:程序使用了 puts,但它的定义由外部库提供,当前文件中没有定义。
普通符号表通常不影响已链接程序的正常运行。 链接完成后,CPU 按机器指令执行,调用函数不需要再查找 main 等名字。因此,去掉普通符号表(.symtab)通常仍能运行,只是查看函数名、调试和定位问题会更困难。动态链接所需的动态符号表(.dynsym)仍需保留。
6.3 arm-buildroot-linux-gnueabihf-strip 工具
作用:去掉程序中运行时不需要的符号、调试信息等,减小文件体积。
bash
# 查看处理前的文件信息
file test_arm
# 使用交叉工具链的 strip,直接修改 test_arm
arm-buildroot-linux-gnueabihf-strip test_arm
# 再次查看文件信息
file test_arm

- 处理前
not stripped:尚未剥离普通符号表等信息。 - 处理后
stripped:已剥离这些信息,文件通常会变小。
图中处理前后仍是 32 位 ARM ELF 可执行文件,动态链接方式也没有改变。去掉的是运行时不需要的辅助信息,通常不影响程序正常运行。
6.4 arm-buildroot-linux-gnueabihf-objdump 工具
作用:反汇编,把程序中的机器码显示成汇编指令,方便查看程序实际执行的指令。
bash
arm-buildroot-linux-gnueabihf-objdump -d test_arm

-d 表示反汇编,查看 test_arm 中的机器码所对应的汇编指令。
6.5 arm-buildroot-linux-gnueabihf-objcopy 工具
作用:复制、提取或转换目标文件中的内容,例如把 ELF 文件转换成 BIN 文件。
bash
arm-buildroot-linux-gnueabihf-objcopy -O binary test_arm test_arm.bin

-O binary 指定输出为原始二进制格式,test_arm 是输入文件,test_arm.bin 是生成的文件。格式转换不会让这个 Linux 应用自动变成能独立运行的裸机程序。
为什么转成 BIN 后,仍不能直接脱离 Linux 运行?
因为 objcopy 只转换文件格式,不改变程序原来的依赖。当前的 test_arm 是 Linux 动态链接程序,依赖 Linux 的加载器、库和系统服务,转成 BIN 后这些需求仍然存在。
转换有什么意义?
BIN 方便交给按地址写入原始字节的烧录工具或加载程序使用。裸机开发中,程序会事先配好启动代码、链接地址和必要的硬件初始化,先生成 ELF 供调试,再转换成 BIN 供烧录或加载:
text
按裸机要求编写的程序 → ELF(供链接、调试)→ BIN(供烧录、加载)
因此,本次把 Linux 应用转成 BIN,主要是练习格式转换;它不会因此获得裸机运行能力。能否脱离操作系统,取决于程序怎么编写和构建,而不是文件后缀。