在之前的两个章节中,我们具体尝试了将一个 .c 源码编译为内置驱动和模块驱动,其中我们发现大量使用了 Makefile 文件,并进行了一些简单的编写,接下来我们就详细解释下 Makefile语法 和 make 工具。
一、什么是Makefile?
- Makefile 就是一个普通的文本文件,里面写了编译程序的规则和命令。
- 只要我们用
make命令,make工具会自动读Makefile,按照里面的指令一步步帮我们编译代码。 - 用
Makefile能让我们不用每次手敲长长的编译命令,非常方便,尤其是有很多源文件的时候。
二、make工具什么?
make 工具是编译辅助工具。解决使用命令编译工程非常繁琐的问题。
- 读取当前目录下的
Makefile文件,然后自动帮你完成:- 编译
- 链接
- 清理(删除临时文件)。
- 只要源文件有改动,它才会重新编译,没改就不用动。
- 编译内核驱动、很多大型项目,都是靠
make+Makefile来自动化编译。
三、make和Makefile是什么关系?
- Makefile:一本“说明书”,写清楚:
- 我要生成什么文件(目标)
- 这些文件依赖哪些源文件
- 要用什么命令去生成
- make:一个“执行说明书的小工”
- 它不会自己猜怎么编译
- 它只是读 Makefile
- 然后一步一步执行里面的命令
平时使用方式:
make # 默认会执行 Makefile 里的第一个目标
make clean # 执行 Makefile 中名为 clean 的目标2
四、Makefile的最基本结构
先看一眼最小版本的 Makefile 结构:
目标: 依赖1 依赖2 ...
<Tab>命令1
<Tab>命令22
3
几点关键说明:
目标:想要生成的文件名,或者一个“任务名称”依赖:生成这个目标之前,必须先准备好的文件/目标命令:真正执行的 shell 命令 注意:这里前面必须是 Tab 键,而不是空格!
举个超级简单的例子:
hello: hello.c
gcc hello.c -o hello2
含义是:
- 目标:
hello - 依赖:
hello.c - 命令:
gcc hello.c -o hello
运行:
make # 会根据 Makefile 生成 hello
./hello # 运行程序2
五、从零写一个简单Makefile(用户态程序)
创建一个简单的文件 main.c ,并写入内容:
#include <stdio.h>
int main(void)
{
printf("Hello, Makefile!\n");
return 0;
}2
3
4
5
6
7
1. 创建Makefile
在同一目录下新建一个文件,名字就叫 Makefile。
写入:
# 1. 定义变量
CC = gcc # 指定使用的编译器
TARGET = main # 生成的可执行文件名
# 2. 编译规则
$(TARGET): main.c
$(CC) main.c -o $(TARGET)
# 3. 清理命令
clean:
rm -f $(TARGET)2
3
4
5
6
7
8
9
10
11
逐行说明:
CC = gcc:定义一个变量 CC,值为gccTARGET = main:变量 TARGET,表示生成的程序名$(TARGET): main.c:- 目标是
main - 依赖是
main.c
- 目标是
$(CC) main.c -o $(TARGET):- 实际运行就是:
gcc main.c -o main
- 实际运行就是:
clean::- 定义一个叫
clean的“任务” rm -f $(TARGET)就是删除生成的程序
- 定义一个叫
2. 实际操作
在终端中:
# 第一次编译
make
# 运行程序
./main
# 清理
make clean2
3
4
5
6
7
8
你会看到:
make时会自动调用gcc main.c -o mainmake clean会删除main文件
六、多个源文件
创建三个文件,并编写内容:
main.cadd.cadd.h
main.c 里面调用 add() 函数:
// main.c
#include <stdio.h>
#include "add.h"
int main(void)
{
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}2
3
4
5
6
7
8
9
10
// add.c
#include "add.h"
int add(int a, int b)
{
return a + b;
}2
3
4
5
6
7
// add.h
int add(int a, int b);2
3
1. 传统的编译命令(手敲)
可能会这样做:
gcc -c main.c # 生成 main.o
gcc -c add.c # 生成 add.o
gcc main.o add.o -o main2
3
4
5
每次改一点都要重新敲,很烦。
2. 用 Makefile 管理
写一个稍微完整点的 Makefile :
# 指定编译器
CC = gcc
# 编译选项
CFLAGS = -Wall -g
# 目标程序名
TARGET = main
# 源文件列表
SRCS = main.c add.c
# 根据源文件自动生成 .o 文件列表
OBJS = $(SRCS:.c=.o)
# 默认目标
all: $(TARGET)
# 链接阶段:把所有 .o 链接成最终程序
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
# 编译阶段:把 .c 编译成 .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理
clean:
rm -f $(OBJS) $(TARGET)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
简单理解一下:
SRCS = main.c add.c:所有.c文件OBJS = $(SRCS:.c=.o):把main.c add.c转成main.o add.oall: $(TARGET):默认执行 all 目标,即生成 main$(TARGET): $(OBJS):先保证.o文件都生成了,再链接%.o: %.c:- 这是一个“通用规则”
- 任意
xxx.c都可以用这个规则生出xxx.o $<表示依赖的第一个文件(这里是源文件.c)$@表示当前目标(这里是.o文件)
操作流程:
make # 自动完成编译 + 链接
./main # 运行可执行程序
make clean # 清理文件2
3
4
5
七、内核驱动相关的Makefile
在前面章节里,已经见过类似这样的驱动 Makefile:
export ARCH=arm64
# 交叉编译器绝对路径前缀
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
# 和源文件名一致
obj-m += hello_world.o
# 内核源码目录
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们一点点拆开来看。
1. obj-m += hello_world.o 讲解?
- 内核使用一套叫 Kbuild 的编译系统
- 对于模块来说,
obj-m表示“要编译成模块的对象文件” hello_world.o会被编译成hello_world.ko模块
也就是说,只要写:
obj-m += hello_world.oKbuild 就知道你是要编译一个名为 hello_world 的内核模块。
提示:
如果有多个模块,可以这样写:
obj-m += hello_world.o led_drv.o key_drv.o
2. ARCH 和 CROSS_COMPILE 讲解?
export ARCH=arm64
# 交叉编译器绝对路径前缀
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-2
3
4
ARCH:目标架构,这里是arm64,表示我们要为 arm64 平台 编译内核模块 如果是 x86_64,一般就是ARCH=x86_64。CROSS_COMPILE:交叉编译器前缀 比如你真正调用的编译器会是:aarch64-none-linux-gnu-gccaarch64-none-linux-gnu-ld- …
export 的意思是:把这两个变量导出为环境变量,让后面 make -C $(KDIR) 时,内核的 Kbuild 也能看到它们,从而使用正确的编译器和架构。
3. KDIR 和 PWD 是什么意思?
# 内核源码目录
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)2
3
KDIR:指定 内核源码/构建目录 的路径- 你在那台开发机上配置好的内核源码就在这个目录
PWD:当前模块代码所在目录$(shell pwd)表示执行pwd命令,得到当前路径?=表示“如果外面没有定义 PWD,就用这个值”
后面我们会把这两个变量传给 make:
-C $(KDIR):先切到内核源码目录,再执行makeM=$(PWD):告诉内核:我要编译的模块源码在当前目录
4. all 目标
all:
make -C $(KDIR) M=$(PWD) modules2
等价于在终端手动敲:
make -C /home/lckfb/TaishanPi-3-Linux/kernel-6.1 M=$(pwd) modules含义是:
-C $(KDIR):切换到内核源码目录/home/lckfb/TaishanPi-3-Linux/kernel-6.1,在那里执行makeM=$(PWD):把当前模块代码目录告诉Kbuildmodules:让内核构建系统只编译 “ 外部模块 ”
你可以理解为:
把“编译内核模块要用的一大串命令”包装成了一个简单的
make。 以后在模块目录里,只需要敲make就能完成模块编译,生成hello_world.ko。
5. clean 目标
clean:
make -C $(KDIR) M=$(PWD) clean2
这行命令的意思是:
- 仍然切到内核源码目录
$(KDIR)下 - 让 Kbuild 根据
M=$(PWD),去你的模块目录里 - 自动清理当前模块相关的中间文件和目标文件,比如:
hello_world.ohello_world.mod.chello_world.ko.tmp_versions/等
实际使用中,你只要在模块目录里敲
make clean就能把之前编译产生的文件都清理干净,目录恢复到“只剩源代码”的状态。
八、练习
练习 1:单文件程序
- 写一个
hello.c,打印一句话:
#include <stdio.h>
int main() {
printf("Hello, Makefile!\n");
return 0;
}2
3
4
5
6
- 写一个最简单的
Makefile:
hello: hello.c
gcc hello.c -o hello
clean:
rm -f hello2
3
4
5
- 操作:
make
./hello
make clean2
3
练习 2:多文件程序
创建三个文件,并编写内容:
main.cadd.cadd.h
main.c 里面调用 add() 函数:
// main.c
#include <stdio.h>
#include "add.h"
int main(void)
{
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}2
3
4
5
6
7
8
9
10
// add.c
#include "add.h"
int add(int a, int b)
{
return a + b;
}2
3
4
5
6
7
// add.h
int add(int a, int b);2
3
编写 Makefile 文件:
# 指定编译器
CC = gcc
# 编译选项:-Wall 打开常见警告,-g 方便调试(可选)
CFLAGS = -Wall -g
# 最终生成的可执行文件名
TARGET = main
# 源文件
SRCS = main.c add.c
# 把 .c 列表转换成 .o 列表,比如 main.c -> main.o
OBJS = $(SRCS:.c=.o)
# 默认目标:执行 make 时,如果不写别的目标,先执行 all
all: $(TARGET)
# 链接:由多个 .o 生成最终可执行程序
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
# 通用规则:任意 xxx.c 编译成 xxx.o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 运行目标:先保证 $(TARGET) 存在,然后执行它
run: $(TARGET)
./$(TARGET)
# 清理目标:删除中间文件和最终程序
clean:
rm -f $(OBJS) $(TARGET)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- 在
Makefile中加了一个run目标,例如:
run: $(TARGET)
./$(TARGET)2
这样你可以:make run 一次完成编译+运行。
当然也可以先 make 再 ./main 运行:
练习 3:简单内核模块
我们直接参照 【编写第一个驱动】那个章节的讲解进行白那些即可:
我们将 hello_world.ko 复制到开发板中:
运行下面的命令加载驱动模块:
sudo insmod hello_world.ko有些用户可能就发现了,加载之后没有任何的反应,这是因为日志自动被系统归类隐藏了,运行下面的命令即可查看 hello_world 相关的日志:
dmesg | grep -E 'hello'到此我们第一个 hello world 驱动就完成了。
九、常见坑
- 命令前面用空格,不是 Tab
- 结果:
make报错,或者提示missing separator - 解决:确认命令行前是一个 Tab 字符
- 结果:
- 文件名/目标名写错
- 结果:
No rule to make target 'xxx' - 解决:检查 Makefile 里的目标、依赖名称,和真实文件是否一致
- 结果:
- 忘记 clean
- 修改了代码,但旧的可执行文件/模块还在
- 习惯养成:需要彻底重新编译时执行一次
make clean
- 大小写问题
- Linux 文件系统是区分大小写的
Makefile和makefile虽然都能识别,但建议统一用Makefile
十、总结
- Makefile = 编译说明书,告诉 make 目标是什么、依赖是什么、怎么编译。
- make = 自动执行工具,按照 Makefile 指令帮你完成繁琐命令。
- 基本语法只有一条:
目标: 依赖+ 下一行 Tab 开头的命令。 - 多练习:
- 先从一个
.c文件开始 - 再到多个
.c - 再到简单内核模块
- 先从一个
- 遇到看不懂的 Makefile,不用怕:
- 先找出:目标、依赖、命令
- 一条条对照理解