【0基础嵌入式学习日志】Day04:GCC 编译流程、.o 目标文件与 Makefile 分步编译
一、前言
今天继续进行嵌入式 C 语言基础学习。前几天已经完成了 C 工程目录搭建、结构体、故障码、函数封装、结构体指针、多文件拆分以及 Makefile 多源文件编译。
Day03 中,工程已经从单个main.c拆分成了多个.h和.c文件:
system.h / system.c sensor.h / sensor.c fault.h / fault.c main.c但是 Day03 的 Makefile 仍然是一次性把多个.c文件直接编译成最终可执行程序:
gcc src/main.c src/system.c src/sensor.c src/fault.c-Wall-Wextra-Iinclude-obuild/day03_test这种写法可以运行,但不够接近真实 C 工程的编译过程。因此,Day04 的重点是学习 GCC 的分步编译流程:
.c 源文件 → .o 目标文件 → 可执行程序也就是先将每个.c文件分别编译成.o目标文件,然后再把多个.o文件链接成最终程序。
二、Day04 学习目标
本次 Day04 主要学习以下内容:
- 理解
.c、.h、.o和可执行文件之间的关系; - 理解 GCC 编译过程中“编译”和“链接”的区别;
- 学会使用
gcc -c生成.o目标文件; - 学会使用多个
.o文件链接生成最终可执行程序; - 学会使用 Makefile 管理分步编译流程;
- 理解
make clean、make、make run的作用; - 进一步熟悉嵌入式 C 工程的编译组织方式。
三、Day04 工程结构
Day04 工程是在 Day03 的基础上复制并修改得到的,工程结构如下:
day04 ├── Makefile ├── include │ ├── fault_code.h │ ├── fault.h │ ├── sensor.h │ ├── system.h │ └── system_type.h ├── src │ ├── fault.c │ ├── main.c │ ├── sensor.c │ └── system.c └── build ├── fault.o ├── main.o ├── sensor.o ├── system.o └── day04_test其中:
include:存放头文件;src:存放源文件;build:存放编译生成的中间文件和最终程序;Makefile:管理编译、运行和清理命令。
需要注意的是,build目录下的.o文件和day04_test是本地编译生成的产物,一般不会上传到 GitHub,因为它们可以由源码重新生成。
四、.c、.h、.o 和可执行文件的关系
在 C 工程中,常见文件类型有以下几种。
1..c源文件
.c文件是源代码文件,用来写函数的具体实现。
例如:
src/main.c src/system.c src/sensor.c src/fault.c这些文件是真正写功能逻辑的地方。
2..h头文件
.h文件是头文件,通常用于放:
宏定义 结构体定义 函数声明例如:
include/system_type.h include/fault_code.h include/system.h include/sensor.h include/fault.h头文件的作用是让不同.c文件之间能够共享类型、宏定义和函数声明。
3..o目标文件
.o文件是目标文件,是.c文件经过 GCC 编译后生成的中间结果。
例如:
src/main.c → build/main.o src/system.c → build/system.o src/sensor.c → build/sensor.o src/fault.c → build/fault.o.o文件不能直接运行,但是它已经是源代码编译后的结果。
4. 可执行文件
多个.o文件最终需要链接成一个可执行程序:
build/main.o build/system.o build/sensor.o build/fault.o ↓ build/day04_test最终生成的day04_test才可以运行。
五、GCC 编译流程理解
Day04 的核心流程可以概括为:
第一步:每个 .c 文件分别编译成 .o 文件 第二步:多个 .o 文件链接成最终可执行程序对应关系如下:
src/main.c → build/main.o src/system.c → build/system.o src/sensor.c → build/sensor.o src/fault.c → build/fault.o然后:
build/main.o + build/system.o + build/sensor.o + build/fault.o → build/day04_test这个流程比 Day03 更接近真实 C 工程的编译方式。
六、为什么要生成 .o 文件?
在小工程中,可以直接一次性编译多个.c文件:
gcc src/main.c src/system.c src/sensor.c src/fault.c-Wall-Wextra-Iinclude-obuild/day03_test但是在真实嵌入式工程中,源文件可能很多,例如:
main.c uart.c can.c i2c.c spi.c adc.c motor.c sensor.c fault.c system.c如果每次修改一个文件都重新编译所有.c文件,效率会比较低。
使用.o文件后,可以将每个.c文件先单独编译成目标文件。这样以后如果只修改了某一个.c文件,理论上只需要重新编译对应的.o文件,再重新链接即可。
这就是大型 C 工程和嵌入式工程常用的编译方式。
七、Day04 Makefile 内容
Day04 的Makefile内容如下:
CC = gcc CFLAGS = -Wall -Wextra -Iinclude TARGET = build/day04_test OBJS = build/main.o build/system.o build/sensor.o build/fault.o all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) build/main.o: src/main.c $(CC) $(CFLAGS) -c src/main.c -o build/main.o build/system.o: src/system.c $(CC) $(CFLAGS) -c src/system.c -o build/system.o build/sensor.o: src/sensor.c $(CC) $(CFLAGS) -c src/sensor.c -o build/sensor.o build/fault.o: src/fault.c $(CC) $(CFLAGS) -c src/fault.c -o build/fault.o run: $(TARGET) ./$(TARGET) clean: rm -f build/*.o $(TARGET)需要注意,Makefile 中命令行前面必须使用Tab 键,不能使用普通空格。例如:
$(CC) $(OBJS) -o $(TARGET)这一行前面必须是 Tab。
八、Makefile 逐句理解
1. 指定编译器
CC = gccCC表示 C Compiler,也就是 C 语言编译器。
后面使用:
$(CC)就等价于:
gcc2. 指定编译参数
CFLAGS = -Wall -Wextra -Iinclude其中:
-Wall:开启常见警告;-Wextra:开启更多警告;-Iinclude:告诉 GCC 到include目录下查找头文件。
因为代码中使用了:
#include"system.h"#include"sensor.h"#include"fault.h"这些头文件都在include目录中,所以需要加上:
-Iinclude3. 指定最终可执行文件
TARGET = build/day04_test这表示最终生成的程序是:
build/day04_test运行时可以使用:
./build/day04_test4. 指定目标文件列表
OBJS = build/main.o build/system.o build/sensor.o build/fault.oOBJS表示 object files,也就是目标文件列表。
这里的意思是:
最终程序需要 main.o、system.o、sensor.o 和 fault.o5. 默认目标 all
all: $(TARGET)当在终端输入:
make默认执行第一个目标,也就是all。
这句的意思是:
执行 make 时,需要生成 $(TARGET)而$(TARGET)对应:
build/day04_test6. 生成最终程序
$(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET)展开后等价于:
build/day04_test: build/main.o build/system.o build/sensor.o build/fault.o gcc build/main.o build/system.o build/sensor.o build/fault.o -o build/day04_test这表示:
要生成 day04_test,必须先有 main.o、system.o、sensor.o 和 fault.o然后通过 GCC 把多个.o文件链接成最终可执行程序。
7. 编译 main.c
build/main.o: src/main.c $(CC) $(CFLAGS) -c src/main.c -o build/main.o这表示:
src/main.c → build/main.o展开后等价于:
gcc-Wall-Wextra-Iinclude-csrc/main.c-obuild/main.o其中-c表示:
只编译,不链接所以这一步只生成main.o,不会生成最终程序。
8. 编译 system.c
build/system.o: src/system.c $(CC) $(CFLAGS) -c src/system.c -o build/system.o这表示:
src/system.c → build/system.o9. 编译 sensor.c
build/sensor.o: src/sensor.c $(CC) $(CFLAGS) -c src/sensor.c -o build/sensor.o这表示:
src/sensor.c → build/sensor.o10. 编译 fault.c
build/fault.o: src/fault.c $(CC) $(CFLAGS) -c src/fault.c -o build/fault.o这表示:
src/fault.c → build/fault.o11. 运行程序
run: $(TARGET) ./$(TARGET)这表示执行:
makerun时,会运行:
./build/day04_test12. 清理编译产物
clean: rm -f build/*.o $(TARGET)这表示执行:
makeclean时,会删除:
build/*.o build/day04_test其中:
rm表示删除;-f表示强制删除,不提示;build/*.o表示build目录下所有.o文件。
九、编译与运行过程
进入 Day04 工程目录:
cd/root/Embedded_14Days/day04清理旧编译产物:
makeclean执行编译:
make查看build目录:
lsbuild可以看到:
day04_test fault.o main.o sensor.o system.o这说明.o目标文件和最终可执行程序都已经成功生成。
运行程序:
makerun运行结果如下:
LED state:0Voltage:9.50V Current:2.50A temperature:72.00C Fault code: 0x0007 Fault: Low voltage Fault: Over current Fault: Over temperature程序成功完成了传感器数据更新、故障检测和系统状态打印。
十、Day03 和 Day04 的区别
Day03 的编译方式是:
多个 .c 文件 → 直接生成可执行程序对应命令类似:
gcc src/main.c src/system.c src/sensor.c src/fault.c-Wall-Wextra-Iinclude-obuild/day03_testDay04 的编译方式是:
多个 .c 文件 → 多个 .o 文件 → 可执行程序也就是:
src/main.c → build/main.o src/system.c → build/system.o src/sensor.c → build/sensor.o src/fault.c → build/fault.o build/main.o + build/system.o + build/sensor.o + build/fault.o → build/day04_test相比 Day03,Day04 更接近真实工程中的编译方式。
十一、遇到的问题和解决方法
1. Makefile 命令前面必须使用 Tab
Makefile 对格式非常敏感,规则下面的命令行前面必须使用 Tab 键。
例如:
all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET)如果使用普通空格,可能会出现:
missing separator因此,在写 Makefile 时要特别注意缩进格式。
2. build 目录下的文件不上传 GitHub
本地编译后,build目录下会生成:
main.o system.o sensor.o fault.o day04_test这些文件属于编译产物,不是源码。通常不需要上传到 GitHub。
因为只要源码和 Makefile 存在,就可以重新执行:
make再次生成这些文件。
3. GitHub push 可能受网络影响
上传 GitHub 时,如果出现类似:
Failed to connect to github.com port443一般是 WSL 网络连接 GitHub 不稳定,不是代码问题。
只要本地已经成功执行:
gitcommit说明代码已经保存到本地 Git 版本库。网络恢复后重新执行:
gitpush即可。
十二、今日总结
通过 Day04 的学习,主要掌握了以下内容:
- 理解了
.c、.h、.o和可执行文件之间的关系; - 学会了使用
gcc -c将.c文件编译成.o目标文件; - 理解了
-c表示只编译、不链接; - 理解了多个
.o文件需要链接后才能生成最终可执行程序; - 学会了使用 Makefile 管理分步编译流程;
- 掌握了
make clean、make、make run的基本作用; - 进一步理解了真实 C 工程中的编译组织方式。
Day04 的核心可以总结为一句话:
.c 源文件先分别编译成 .o 目标文件, 多个 .o 文件再链接成最终可执行程序。这一步对后续学习嵌入式工程、驱动开发、交叉编译和大型项目构建非常重要。
十三、项目源码
本次 Day04 学习代码已上传至 GitHub:
https://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day04