Make
面向 Linux / C/C++ / 交叉编译(ARM、MIPS uClibc 工具链)的构建笔记。
Make 基础概念
Make 通过比较目标(target)与其依赖(prerequisite)的修改时间(mtime)决定是否重建:只要任一依赖比目标新,就执行该目标的命令,并沿依赖图递归向上。
常用选项
- -C :Change directory(切换目录)
Makefile 中第一个 target 是默认目标(下文示例里是 all),make 不带参数时构建它。
- 常用调试开关:
make -n:通常只打印 recipe;含 $(MAKE) 的递归调用行、带 + 前缀的行以及某些重建 makefile 的操作仍可能执行,不能当作绝对无副作用的模式。make -p:打印内部规则与变量数据库,同时仍可能正常构建;只查询时可用 make -qp,注意其退出码也表示目标是否需要重建。make -d:打印为什么某个目标被判定需要重建。
- NFS/时钟偏差可能导致 mtime 误判,必要时
make clean重来。
交叉编译静态库示例
一个完整的交叉编译静态库 Makefile(已修正原示例中的 ar 修饰符与缺失的 .PHONY):
# 交叉编译器前缀
CROSS_COMPILER := arm-mol-linux-uclibcgnueabihf-
CXX := $(CROSS_COMPILER)g++
AR := $(CROSS_COMPILER)ar
PROJECT_ROOT := /home/meian/work/SVN/nvr/plh/eavs_FH_list/eavs_mc6830/project
# 编译选项:只放编译期标志。
# -L ./lib -lfdk-aac 属于“链接期”选项,不应放进 CXXFLAGS(编译 .o 阶段用不到)
CXXFLAGS = -std=c++11 -Wall -O0
FDK_AAC_INC = -I./include -I./include/fdk-aac -I../mc6830/include
# 静态库名称
LIBNAME = libG711toAAC.a
# 源文件列表
CPPSRC := AACEncoder.cpp G711toAAC.cpp ../mc6830/G711.cpp
# 目标文件列表(把 .cpp 后缀替换成 .o)
CPPOBJ = $(CPPSRC:.cpp=.o)
# 默认目标
all: lib
# 模式规则:编译每个 .cpp -> .o
%.o: %.cpp
$(CXX) $(CXXFLAGS) $(FDK_AAC_INC) -c $< -o $@
# 归档静态库
# 说明:原示例用的是 rcu,其中 u(仅更新更新的成员)在现代 binutils 默认的
# “确定性归档”模式(D)下会被忽略并告警,故改为 rcs:s 显式表明写入符号索引
lib: $(CPPOBJ)
$(AR) rcs $(LIBNAME) $(CPPOBJ)
# 仅当使用该 ARM 交叉编译器时,把生成的静态库拷到工程 lib 目录
# 该 ifeq 为解析期条件,紧跟 lib 规则,其下的 cp 会追加到 lib 的 recipe
ifeq ($(CROSS_COMPILER),arm-mol-linux-uclibcgnueabihf-)
cp $(LIBNAME) $(PROJECT_ROOT)/lib_mc6830/
endif
# 清理生成文件
clean:
rm -f $(LIBNAME) $(CPPOBJ)
# 声明伪目标:避免与同名文件冲突(见“伪目标”一节)
.PHONY: all lib clean
关于
ar修饰符:GNU ar 的r操作会自动写入符号索引(armap),rc/rcs均可直接链接,无需再跑ranlib;只有大写S才会抑制索引。rcs里的s只是把“写索引”的意图显式化,并对非 GNU 的 ar 更具可移植性。
Makefile 变量与赋值
四种赋值运算符的差异会直接影响标志拼接与工具链切换,是最容易踩的坑:
| 运算符 | 名称 | 展开时机 | 典型用途 |
|---|---|---|---|
= | 递归 / 延迟展开 | 每次引用时重新求值(可引用后面才定义的变量) | 一般变量;注意 X = $(X)... 会无限递归 |
:= | 立即 / 简单展开 | 定义时求值一次 | 路径、CROSS_COMPILE 前缀、$(shell ...) 结果,避免反复求值 |
?= | 条件赋值 | 仅当变量未定义时赋值 | 允许命令行/环境覆盖,如 CROSS_COMPILE ?= arm-mol-...- |
+= | 追加 | 继承左侧原有的展开语义 | CXXFLAGS += -g |
其它要点:
$(VAR)与${VAR}等价。- 用
:=冻结$(shell ...)结果,用=则每次引用都会重新执行 shell:
GITVER := $(shell git rev-parse --short HEAD) # 只执行一次
GITVER2 = $(shell git rev-parse --short HEAD) # 每次引用都执行一次
- recipe 里访问 shell 变量要写两个
$:$$HOME(单个$会被 make 当成自己的变量)。
优先级
同一个变量名同时从多个地方定义时,有优先级考虑
override > make 命令行变量 > Makefile 普通赋值 > Shell 环境变量 > Make 内置默认变量
- override最高优先级
MCU_VERSION = FROM_MAKEFILE
override MCU_VERSION = FROM_OVERRIDE
all:
@echo $(MCU_VERSION)
@echo origin=$(origin MCU_VERSION)
####### 执行 make MCU_VERSION=FROM_COMMAND_LINE
输出结果
FROM_OVERRIDE
origin=override
即使命令行指定make MCU_VERSION=FROM_COMMAND_LINE,输出结果依然不变
- make 命令行变量
MCU_VERSION = FROM_MAKEFILE
all:
@echo $(MCU_VERSION)
@echo origin=$(origin MCU_VERSION)
####### 执行 make MCU_VERSION=MCU_CIU32 (这就是command line)
输出结果
MCU_CIU32
origin=command line
这是command line > Makefile 普通赋值
- 普通赋值
MCU_VERSION = FROM_MAKEFILE
all:
@echo $(MCU_VERSION)
@echo origin=$(origin MCU_VERSION)
- shell环境变量
这个需要与make XXX=xxx区分
all:
@echo $(MCU_VERSION)
@echo origin=$(origin MCU_VERSION)
Target、Prerequisite、Recipe
规则的三要素:
target: prerequisites
recipe # 必须以 TAB 开头
- recipe 行必须以真实 TAB 开头,用空格会报
*** missing separator. Stop.——编辑器把 TAB 自动转空格是最高频踩坑。 - 每条 recipe 行在独立的 shell 中执行;跨行要么行尾加
\续行,要么用;连接(如cd build && make)。 - order-only 依赖:
target: normal-prereqs | order-only-prereqs。竖线右侧的依赖只保证“先存在”,其 mtime 变化不会触发重建,常用于先建输出目录:
OBJDIR := build
$(OBJDIR)/%.o: %.cpp | $(OBJDIR) # $(OBJDIR) 是 order-only 依赖
$(CXX) $(CXXFLAGS) -c $< -o $@
$(OBJDIR):
mkdir -p $@
模式规则与自动变量
自动变量表
| 变量 | 含义 |
|---|---|
$@ | 目标名 |
$< | 第一个依赖(编译规则里就是源文件) |
$^ | 全部依赖,去重(写链接行的对象列表用它) |
$+ | 全部依赖,不去重、保持顺序(链接顺序敏感时有用) |
$? | 比目标新的依赖 |
$* | 模式规则中 % 匹配到的茎(stem) |
$(@D) / $(@F) | 目标的目录部分 / 文件名部分 |
链接规则示例:
app: $(CPPOBJ)
$(CXX) $^ -o $@ $(LDFLAGS) # $^ = 所有 .o
静态模式规则与 VPATH
- 静态模式规则:只对指定目标列表套用模式,比通配
%.o: %.cpp更可控:
$(CPPOBJ): %.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
- 源文件散落在多目录(如本例的
../mc6830/)时,用vpath让 make 跨目录搜索:
vpath %.cpp ../mc6830 # 只对 .cpp 生效
# VPATH = ../mc6830:../common # 对所有类型生效
自动头文件依赖生成
%.o: %.cpp 只把 .cpp 列为依赖,头文件对 make 不可见——改了 .h 不会触发重编,会链接进过期的 .o,是典型的隐蔽 bug。让编译器生成依赖文件即可解决:
CXXFLAGS += -MMD -MP
-include $(CPPOBJ:.o=.d) # 放在 Makefile 末尾
-MMD:生成每个.o对应的.d依赖文件,只记录用户头文件(-MD连系统头也记)。-MP:为每个头文件生成一个空的伪目标,避免头文件被删后 make 报“无规则可用”。
编译和链接选项
编译分阶段
-E:只做预处理,输出到 stdout。-S:编译到汇编(.s)。-c:编译+汇编到目标文件.o,不链接(编译多文件工程的常态)。- 不带上述选项:一路编译并链接成可执行文件。
gcc -c hello.c -o hello.o # 只编译,产出 .o
gcc hello.o -o hello # 链接
编译期 vs 链接期选项
| 阶段 | 常用选项 |
|---|---|
| 编译期 | -pthread -I<dir> -D<宏> -std= -O0/-O2/-Os -g -Wall -Wextra -Werror -f*(如 -fPIC) |
| 链接期 | -L<dir> -l<name> -Wl,<原样传给 ld 的选项> -shared -static -pthread |
-I(大写 i):头文件搜索目录——把<dir>加入预处理时#include的搜索路径(参数是目录,不是某个头文件)。-L(大写 L):库文件搜索目录。-l(小写 L):链接指定库(-lworld找libworld.so或libworld.a)。
gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld
- 嵌入式常用体积/优化选项:
-Os(为体积优化,flash 受限时常用)、-Og(可调试的优化);配合-ffunction-sections -fdata-sections+ 链接期-Wl,--gc-sections剔除未用段;strip(或链接加-s)去符号进一步减小体积。
-fpic、-fPIC、-fpie、-fPIE 的区别
(原为空标题,此处补齐。)
-fPIC:生成位置无关代码(Position-Independent Code),共享库(.so)必需——凡是要进.so的.o都得用它编译。-fpic(小写):同样是位置无关代码,但用更小/更快的 GOT 寻址序列,受平台 GOT 表大小限制;MIPS/ARM 上大型库容易溢出,拿不准就用-fPIC。-fPIE/-fpie:位置无关可执行文件(Position-Independent Executable),配合 ASLR;-fPIE是编译期标志,链接期还要加-pie。- PIC vs PIE:PIC 允许符号插入(interposition,用于库);PIE 面向主程序、可做更激进优化。
- 交叉/嵌入式注意:很多 uClibc 工具链默认构建非 PIE;若最终被抽取并链接进 .so 的静态库成员含不适用于共享对象的非 PIC 重定位,可能报 recompile with -fPIC;应以 PIC 方式重新编译相关成员,不能仅给最终链接命令加 -fPIC。
静态库与动态库
创建
- 静态库(.a):把多个
.o打包,链接时只抽取被引用到的成员,所需代码进入最终产物,不再运行时加载这个 .a;但程序仍可能依赖 libc 等其他动态库。
$(CROSS)gcc -c a.c b.c
$(CROSS)ar rcs libfoo.a a.o b.o # s = 写符号索引
- 动态库(.so):编译加
-fPIC,链接加-shared,运行期由动态加载器解析(体积小、多进程共享)。
$(CROSS)gcc -fPIC -c a.c b.c
$(CROSS)gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 a.o b.o
soname 与三级命名
- 用
-Wl,-soname,libfoo.so.1记录 soname。 - 磁盘惯例:真实文件
libfoo.so.1.0.0← 运行期符号链接libfoo.so.1(= soname)← 开发期符号链接libfoo.so(-lfoo链接时找它)。
链接顺序敏感(高频 undefined reference 根源)
链接器从左到右单遍处理,库必须出现在引用它符号的对象/库之后,否则报 undefined reference:
# G711toAAC 用到 fdk-aac,所以 fdk-aac 放在它右边
$(CXX) main.o libG711toAAC.a -L./lib -lfdk-aac -o app
- 两个静态库相互依赖时:重复列出,或用
-Wl,--start-group libA.a libB.a -Wl,--end-group。 -Wl,-Bstatic/-Wl,-Bdynamic可强制某段按静态/动态链接。
运行期库查找与排查
- glibc 加载器在依赖名不含斜杠时,大致依次考虑:仅在无 DT_RUNPATH 时使用 DT_RPATH → LD_LIBRARY_PATH(安全执行模式会忽略)→ DT_RUNPATH → /etc/ld.so.cache → 默认目录。RUNPATH 只用于直接依赖,其他 libc 的规则可能不同。
- 嵌入搜索路径:
-Wl,-rpath,/usr/local/lib,或用$ORIGIN相对可执行文件定位-Wl,-rpath,'$ORIGIN/../lib'。若写在 Makefile recipe 中,要把 $ 写成 $$,例如-Wl,-rpath,'$$ORIGIN/../lib'。 - 交叉编译时为传递性
.so依赖指定仅链接期搜索路径:-Wl,-rpath-link,<dir>。 - 全静态可执行文件(最小 rootfs 常用):
-static;只静态化工具链运行时:-static-libgcc -static-libstdc++。 - 排查:
arm-...-readelf -d a.out看NEEDED/RPATH;readelf -h a.out看架构与 ABI。
Linux 下的符号可见性(对应 Windows 的 dllexport/dllimport)
下面这段 xlog_API 宏是 Windows 的库导出/导入写法(__declspec(dllexport/dllimport));在 Linux 分支上它展开为空,也就没法控制 .so 到底导出哪些符号。
#ifndef XLOG_H
#define XLOG_H
// __declspec(dllexport) 导出 xLog 类的函数到 lib 文件中
// __declspec(dllimport) 导入 xLog 类的函数到 exe 文件中
#ifndef _WIN32 // linux
# define xlog_API
#else
# ifdef xlog_STATIC // 静态库
# define xlog_API
# else
# ifdef xlog_EXPORTS
# define xlog_API __declspec(dllexport) // 库项目
# else
# define xlog_API __declspec(dllimport) // 调用方
# endif
# endif
#endif
class xlog_API XLog
{
public:
XLog();
~XLog() = default;
};
#endif // XLOG_H
Linux/GCC 的对应机制是 visibility 属性:
- 用
__attribute__((visibility("default")))标记要导出的符号,编译.so时加-fvisibility=hidden把其余符号默认隐藏。 - 即把上面 Linux 分支从
#define xlog_API(空)改为构建共享库时定义为__attribute__((visibility("default")))。 - 好处:减小
.so符号表、加快目标机加载,是 dllexport/dllimport 的跨平台对应物。
伪目标
避免和同名文件冲突
如果一个规则并不产生与目标同名的文件,则其命令在每次 make 该目标时都应执行。例如:
clean:
rm *.o temp
因为 rm 命令并不产生名为 clean 的文件,正常情况下每次 make clean 都会执行。但如果目录里恰好出现了名为 clean 的文件,规则就失效了:它没有依赖,clean 文件永远被视为“最新”,命令永远不执行。用 .PHONY 声明可避免此问题:
.PHONY: clean
clean:
rm *.o temp
这样 make clean 会无视是否存在 clean 文件,始终执行。
常备伪目标清单
工程里常声明为伪目标的:all clean distclean install uninstall test。可集中声明:
.PHONY: all clean install
.PHONY 除避免同名文件冲突外,还会让 make 跳过该目标的隐式规则搜索。
递归 Make
内核模块编译
-C <dir>:进入<dir>目录后再执行其 Makefile。M=:传给内核 Makefile 的变量,指定外部模块源码目录。
make -C $(KDIR) M=$(PWD) modules
跨目录递归要点
- 子目录调用用
$(MAKE)而非裸make——这样-jN、MAKEFLAGS、jobserver 才能正确下传,并行才生效:
sub:
$(MAKE) -C subdir
export VAR把变量下传给子 make;命令行传入的变量(如make CROSS_COMPILE=...)默认自动下传。make -jN并行构建;个别有竞态的目标用.NOTPARALLEL强制串行,或用 order-only 依赖保证顺序。- 递归 make 的固有缺点是跨目录依赖不完整(Recursive Make Considered Harmful);大工程可考虑非递归单 Makefile,或改用 CMake。
常见问题
典型编译/链接/交叉错误排查
| 报错 | 原因与处理 |
|---|---|
*** missing separator. Stop. | recipe 用了空格而非 TAB |
undefined reference to X | 缺库,或库顺序错(库要放在引用它的对象右侧),循环依赖用 --start-group |
cannot find -lfoo | -L 路径或库名不对 |
wrong ELF class / file 显示架构不符 | 误用了主机 gcc 或错的工具链 |
version 'GLIBC_2.xx' not found / ABI 报错 | 目标机 libc 与工具链不匹配(uClibc 与 glibc 混用尤甚) |
fatal error: xxx.h: No such file or directory | 缺 -I,或交叉编译未设 --sysroot 导致系统头缺失 |
找不到头文件(CMake)
main.cpp:10:10: fatal error: gpio.h: No such file or directory
#include "gpio.h"
^~~~~~~~
排查思路:确认头文件实际所在目录,然后用 target_include_directories(见“CMake Target 化用法”)把该目录加入目标的头文件搜索路径;交叉编译还要确认系统头是否在 sysroot 内。
待确认:原笔记此处「目录结构」示意图缺失,具体工程的目录布局需本人补充。
设置 gdb 调试模式(CMake)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -Wall")
单配置生成器按 CMAKE_BUILD_TYPE 选择对应的 CMAKE_CXX_FLAGS_