【嵌入式Linux】Makefile 实战教程:从零看懂一个完整的项目编译脚本
本文以嵌入式 Linux 网关项目(集成 SQLite/MQTT)为例,手把手教你编写 Makefile。针对习惯 IDE 开发者的痛点,文章提供了一份可直接复用的通用模板,并深入拆解交叉编译、头文件引用 (-I)、静态链接 (-static) 等核心参数。重点剖析了“库链接顺序导致 undefined reference”等经典坑点(如为何 -lpthread 必须后置)。无论你是新手还是进阶工
0.前言
最近在做嵌入式 Linux 网关项目(基于 STM32 + LoRa + 树莓派/Linux 板),涉及到了交叉编译、第三方库(SQLite, MQTT)的链接。刚开始看 Makefile 觉得像天书,各种 -I, -L, -l 满天飞。
经过一番摸索和 AI 的辅助,我终于彻底搞懂了每一行的含义。为了防止以后忘记,也为了帮助同样被 Makefile 折磨的同学,特此记录一下这个“从入门到实战”的 Makefile 模板。
1. 为什么我们需要 Makefile?
在 Windows 上用 Keil 或 VSCode 编程时,点击一下“绿色的三角”就自动编译了。但在 Linux 命令行世界里,我们需要手动告诉编译器:
- 去哪里找头文件? (
.h) - 去哪里找库文件? (
.so / .a) - 用什么编译器? (
gcc还是arm-linux-gnueabihf-gcc?) - 依赖哪些库? (
pthread, math, sqlite...)
Makefile 就是一个“自动化脚本”,把这些复杂的指令写好,以后只需要敲一个 make 命令,它就会自动帮我们干完所有的活。
2. 实战代码:通用 Makefile 模板
这是我项目中实际使用的 Makefile,适用于包含第三方库(如 SQLite, MQTT)的嵌入式 C 语言项目。
# =======================================================
# 嵌入式 Linux 项目通用 Makefile
# 目标:编译网关程序,链接 SQLite 和 MQTT 库
# =======================================================
# 1. 指定编译器 (交叉编译工具链)
CC = arm-linux-gnueabihf-gcc
# 2. 定义生成的程序名字
TARGET = gateway_app
# 3. 定义第三方库的安装路径 (这是重点!)
# 这里存放了我们在 PC 上交叉编译好的库文件
LIB_PATH = /home/xiaoguan/gateway_project/libs_install
# 4. 指定头文件路径 (-I)
# 告诉编译器去哪里找 .h 文件 (相当于 Keil 里的 Include Paths)
INCLUDES = -I$(LIB_PATH)/include
# 5. 指定库文件路径和链接库 (-L, -l)
# -static: 静态编译 (生成的程序大,但无需配置开发板环境,拷进去就能跑)
# -L: 指定库文件所在的目录
# -l: 指定具体要链接的库名字 (注意去掉 lib 前缀和 .a/.so 后缀)
# 注意:-lpthread -ldl -lm 等系统底层库必须放在最后!
LIBS = -static -L$(LIB_PATH)/lib \
-lpaho-mqtt3c -lsqlite3 -lpthread -ldl -lm
# 6. 默认编译规则 (make all)
all:
$(CC) main.c $(INCLUDES) $(LIBS) -o $(TARGET)
@echo "Build Success! Generated: $(TARGET)"
# 7. 清理规则 (make clean)
clean:
rm -f $(TARGET)
@echo "Cleaned."
3. 逐行深度解析 (保姆级)
很多初学者(包括之前的我)最容易晕的地方就在于第 4、5 步。下面逐一拆解:
3.1 变量定义 (CC, LIB_PATH)
CC = arm-linux-gnueabihf-gcc
LIB_PATH = ...
这就像 C 语言里的 #define 宏。把编译器名字和长路径定义成变量,后面用$(CC)和 $(LIB_PATH) 引用。好处就是如果以后换了编译器(比如换成 gcc 跑电脑版),或者库移动了位置,只需要改这两行,不用去改下面的复杂命令。
3.2 头文件路径 (INCLUDES = -I…)
INCLUDES = -I$(LIB_PATH)/include
-I (大写 i):告诉编译器**“去哪里找说明书”**。
如果不写这行,代码里的#include "sqlite3.h" 就会报错 No such file or directory。这里我们将路径指向了第三方库安装目录下的 include 文件夹。
3.3 库链接配置 (LIBS = …) —— 核心难点
LIBS = -static -L$(LIB_PATH)/lib -lpaho-mqtt3c -lsqlite3 -lpthread ...
这里包含了 3 个关键点:
1 -static (静态编译):
- 作用:把
SQLite和MQTT的代码全部“塞”进我们的程序里。 - 优点:移植性无敌。把生成的
gateway_app拷贝到任何一个同样 CPU 架构的 Linux 板子上都能直接运行,不需要在板子上安装库,也不会报library not found。 - 缺点:程序体积大(几 MB)。如果去掉它就是动态编译,程序小,但在新板子上运行可能缺库。
2 -L vs -l (大写 L vs 小写 L):
-L(Library Path):告诉链接器去哪个文件夹找库(比如/home/xxx/lib)。-l(Link Library):告诉链接器要链接哪个具体的文件。 写法规则:掐头去尾。比如库文件名是 libsqlite3.a,在Makefile里就写-lsqlite3。
3 为什么 -lpthread 必须放在最后?
- 这是一个经典坑!Linux 链接器是从左到右处理依赖的。
- 我们的代码依赖
sqlite3,而sqlite3内部依赖pthread(多线程)。 - 如果把 -lpthread 放在最前面,链接器看没人用它,就把它扔了;等后面读到 sqlite3需要线程支持时,发现线程库没了,就会报错
undefined reference。 - 口诀:越基础的系统库(pthread, m,dl),越要往后放。
3.4 编译与清理
$(CC) main.c $(INCLUDES) $(LIBS) -o $(TARGET)
这行命令翻译过来就是:
“请用交叉编译器,编译 main.c,参考 INCLUDES 里的头文件,打包 LIBS 里的库文件,输出名为 TARGET 的程序。”
rm -f $(TARGET)
- -f:强制删除,就算文件不存在也不报错。
- 作用:在打包代码发给别人,或者想彻底重新编译时,执行 make clean,让项目回归清爽状态。
4. 总结与避坑指南
- 找不到头文件? 检查
INCLUDES里的路径对不对,是否用了-I。 Undefined reference?检查LIBS 里是不是漏了-l,或者-lpthread放错位置了。
开发板运行报错"No such file"?- 可能是动态编译但板子缺库。建议新手开发阶段直接用 -static 静态编译,省心省力。
Makefile缩进:Makefile的规则(如 $(CC) … 那一行)必须以 Tab 键 开头,不能用空格!否则会报错missing separator。
希望这篇文章能帮你搞懂 Makefile!如果你也在做嵌入式 Linux 开发,欢迎在评论区交流。
Original Article by [菜菜小关] 记录学习,分享技术。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)