第一部分:核心概念的建立——为什么不能像普通程序一样编译驱动?
这是一个最重要、也最基础的问题。我们平时写一个C语言的“Hello, World!”程序,只需要一个命令:
gcc hello.c -o hello
这个命令做了几件事:
- 预处理:处理
#include和#define等。 - 编译:将C代码转换成汇编代码。
- 汇编:将汇编代码转换成机器码(目标文件
.o)。 - 链接:将你的目标文件和系统提供的标准C库(比如
libc.so)链接起来,生成最终的可执行文件hello。标准库里包含了printf这类函数的具体实现。
但是,驱动程序完全不同。
- 运行环境不同:普通程序运行在操作系统的“用户空间”(User Space),而驱动程序运行在“内核空间”(Kernel Space)。内核空间是操作系统的核心,拥有直接操作硬件的最高权限。
- 依赖的函数库不同:普通程序依赖标准C库(
libc),可以使用printf,scanf,fopen等函数。而驱动程序不能使用这些函数,它需要依赖内核自身提供的函数库,比如printk(内核里的打印函数),kmalloc(内核里的内存申请函数) 等。 - 必须与内核版本精确匹配:你编译的驱动模块,将来是要加载到一个特定版本、特定配置的内核中去的。如果你的驱动在编译时使用的内核头文件、配置、编译器选项与目标内核不一致,加载时轻则失败,重则导致整个系统崩溃(内核恐慌, Kernel Panic)。
结论:基于以上三点,我们得出一个核心结论:编译驱动程序,本质上不是一个独立的过程,而是整个内核编译体系的一部分。 我们必须借助内核的源代码和它的构建系统(Build System)来完成编译,而不能自己简单地调用gcc。
第二部分:解决方案——利用内核的构建系统
既然我们必须依赖内核来编译驱动,那么具体要怎么做呢?
Linux内核的开发者们早就想到了这个问题。他们提供了一套非常强大的构建系统(基于make工具),我们只需要编写一个简单的Makefile,告诉这套系统“我要编译哪个文件”,然后调用它,它就会自动帮我们处理好所有复杂的细节(比如使用哪个编译器、加哪些编译选项、链接哪些内核函数等)。
你图片里的Makefile,就是我们与内核构建系统沟通的“指令文件”。
第三部分:逐行解析你的Makefile
现在,我们来逐行、逐字地分析你提供的Makefile。
# 1. 使用不同的开发板内核时,一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译,为了能编译内核,要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE,比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f hello_drv_test
obj-m += hello_drv.o
我们将这个文件拆成几个功能块来理解。
功能块1:obj-m += hello_drv.o —— 核心指令
这是整个Makefile中最核心的一句,虽然它看起来最简单。
obj-m: 这是内核构建系统能看懂的一个特殊变量。它的意思是 “Object, as a Module”,即“编译成一个模块的目标文件”。+=: 表示追加。hello_drv.o: 表示我们希望最终生成的目标模块,是由hello_drv.c这个源文件编译而来的。内核构建系统会自动推导:既然你需要hello_drv.o,那我就去找一个叫hello_drv.c的文件来编译。
所以,这行代码的完整含义是:请内核构建系统将 hello_drv.c 文件编译成一个可加载的内核模块。
如果我们有多个文件要编译成一个模块,可以写成 obj-m += my_module.o,然后在下面加上 my_module-objs := file1.o file2.o。如果想编译多个模块,就写多行,如:
obj-m += a.o
obj-m += b.o
功能块2:KERN_DIR = ... —— 指定内核源码位置
KERN_DIR: Kernel Directory,内核目录。- 作用: 这个变量告诉
make命令,我们编译驱动所依赖的那个内核,它的源代码放在哪里。
为什么必须指定它?
因为前面说了,编译驱动需要:
- 内核的头文件(
#include <linux/module.h>等)。 - 内核的配置信息(
.config文件)。 - 内核的符号表(
Module.symvers文件),这样你的模块才能调用内核里的函数。 - 内核顶层的
Makefile,这是整个构建系统的入口。
所有这些东西,都在KERN_DIR指向的那个目录里。
一个至关重要的前提:KERN_DIR指向的这个内核目录,必须是已经为你的目标开发板配置和编译过的。仅仅下载一份纯净的内核源码是不行的。因为在配置和编译过程中,才会生成模块编译所依赖的各种头文件和配置文件。这就是注释中“内核要事先配置、编译”的含义。
功能块3:all: 目标与命令 —— 执行编译
这是Makefile的默认目标。当你在终端里只输入 make 命令而不指定目标时,make就会执行 all: 下面的命令。
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
这里有两条命令,我们分开看。
第一条命令(编译驱动模块):make -C $(KERN_DIR) M=\pwd` modules`
这是触发内核构建系统来编译我们驱动模块的神奇咒语。让我们把它拆开:
make: 调用make程序。-C $(KERN_DIR): 这是make的一个选项。-C(大写C) 的意思是Change Directory。- 它告诉
make程序:不要在当前目录下找Makefile执行,请先切换到$(KERN_DIR)所指定的目录(也就是内核源码根目录)。 - 为什么要切换过去?因为真正的、能编译内核和模块的、那个复杂的、主要的
Makefile文件,在内核源码根目录下。
M=\pwd``: 这是传递给内核构建系统的一个变量。M的意思是Module。
modules: 这是指定给内核Makefile的一个目标(Target)。- 它告诉内核
Makefile:我这次执行的任务是,编译M变量所指定的外部模块。 内核Makefile中定义了名为modules的规则,专门用于此目的。 - 它告诉内核的
Makefile:你要编译的模块的源代码,不在你(内核)的源码树里,而是在我来的那个地方。 \pwd`是一个shell命令,pwd(Print Working Directory) 会返回当前所在的目录的绝对路径。所以这句话的意思就是把当前目录的路径赋值给变量M`。- 这样,内核构建系统在执行时,处理完自己的事情后,就会返回到
M指定的目录(我们的驱动代码目录)来查找obj-m这个指令,并编译对应的文件。
总结第一条命令的执行流程:
make程序启动。- 看到
-C /home/book/.../linux-4.4,于是cd到这个内核源码目录。 - 在内核源码目录里,开始执行它自己的主
Makefile。 - 它发现我们给它传递了
M=/path/to/our/driver和目标modules。 - 于是它执行内部的
modules规则,这个规则会做很多准备工作(设置编译器、头文件路径等),然后返回到我们指定的M目录。 - 回到我们的驱动目录后,它会寻找这里的
Makefile文件(就是你提供的这个),读取obj-m += hello_drv.o这行。 - 根据这行指令,调用正确的交叉编译器(后面会讲),使用正确的参数,编译
hello_drv.c生成hello_drv.o,最后链接生成hello_drv.ko。
第二条命令(编译测试程序):$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
- 注意:这条命令与生成
.ko文件无关! - 它的作用: 编译一个用户空间的测试应用程序。驱动程序安装后,通常会在
/dev目录下创建一个设备文件,比如/dev/hello。我们需要一个普通的应用程序来打开、读取、写入或控制这个设备文件,从而测试我们的驱动功能是否正常。hello_drv_test.c就是这个测试程序的源代码。 $(CROSS_COMPILE)gcc:CROSS_COMPILE是一个环境变量(或Makefile变量),它指定了交叉编译器的前缀。比如,aarch64-linux-gnu-。- 所以
$(CROSS_COMPILE)gcc展开后就变成了aarch64-linux-gnu-gcc。 - 什么是交叉编译? 如果你在一个x86架构的电脑(比如你的Ubuntu PC)上,想编译一个能在ARM架构的开发板上运行的程序,这个过程就叫交叉编译。因为编译器运行的平台(x86)和程序要运行的目标平台(ARM)不同。
aarch64-linux-gnu-gcc就是这样一个能在x86上生成ARM64可执行文件的编译器。
-o hello_drv_test: 指定输出的可执行文件名为hello_drv_test。hello_drv_test.c: 指定输入的源代码文件。
功能块4:clean: 目标与命令 —— 清理编译结果
当你在终端输入 make clean 时,就会执行 clean: 下面的命令。它的作用是删除所有编译过程中生成的文件,让目录恢复到干净的状态。
clean:
$(MAKE) -C $(KERN_DIR) M=$(CURDIR) modules clean
rm -f $(TARGET) # TARGET是变量,不是硬编码
rm -f *.o *.d # 清理用户空间的中间文件
-
$(MAKE) -C $(KERN_DIR) M=$(CURDIR) modules clean- 进入内核目录,清理内核模块编译的所有中间文件
-
rm -f $(TARGET)- 删除最终生成的可执行程序(比如你的测试程序)
-
rm -f *.o *.d- 删除用户空间编译产生的临时文件(.o是目标文件,.d是依赖文件)
一句话总结: 第一条清内核模块,第二条删最终程序,第三条清编译垃圾。
功能块5:顶部的注释和环境变量 —— 配置编译环境
这部分是整个流程的“准备工作”,也是最容易出错的地方,注释里已经解释得很清楚了,我们再深入一下。
# 为了能编译内核,要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE,比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/path/to/toolchain/bin
在执行 make 命令之前,你必须在你的shell(终端)中正确设置这些环境变量。内核构建系统会读取这些环境变量来决定如何编译。
export ARCH=arm64:ARCH: Architecture,架构。- 作用: 告诉内核构建系统,我们的目标平台是
arm64架构。构建系统会根据这个变量去寻找arch/arm64目录下的配置和代码,并生成适用于ARM64架构的机器码。如果你在x86电脑上为x86本身编译,就不需要设置这个。
export CROSS_COMPILE=aarch64-linux-gnu-:CROSS_COMPILE: 交叉编译工具链前缀。- 作用: 告诉构建系统,在调用
gcc,ld,objcopy等编译工具时,不要直接用系统默认的(那是为本机编译的),而要在这些工具名前面加上这个前缀。 - 例如,需要调用
gcc时,它会自动变成调用aarch64-linux-gnu-gcc。需要ld(链接器)时,就会调用aarch64-linux-gnu-ld。
export PATH=...:PATH: 系统环境变量,用于指定可执行文件的搜索路径。- 作用: 当内核构建系统要去调用
aarch64-linux-gnu-gcc这个程序时,它总得知道这个程序安装在系统的哪个位置。我们将交叉编译工具链的bin目录添加到PATH环境变量中,系统就能找到了。
这三个环境变量是相互关联的:
你告诉系统目标是 ARCH=arm64,那么你就必须提供一个能编译arm64代码的交叉编译链(通过 CROSS_COMPILE 指定),并且你必须让系统能找到这个交叉编译链(通过修改 PATH)。
总结:完整的逻辑流程
现在,我们把所有知识点串起来,形成一个完整的、从无到有的逻辑流程:
-
准备阶段 (Setup)
a. 获取并配置内核: 在你的Ubuntu工作站上,获取目标开发板对应的Linux内核源码。进入源码目录,使用开发板厂商提供的配置文件(.config),为目标板(比如ARCH=arm64)配置并编译一遍内核。这一步至关重要,它会生成后续编译驱动所需的一切。
b. 获取交叉编译器: 下载并解压与你的内核和开发板匹配的交叉编译工具链(Toolchain)。
c. 编写代码: 编写你的驱动源代码hello_drv.c和用户空间测试代码hello_drv_test.c。
d. 编写Makefile: 编写我们上面详细分析的那个Makefile文件,确保KERN_DIR指向你准备好的内核源码目录,obj-m指向你的驱动目标文件。 -
编译阶段 (Build)
a. 打开终端: 进入存放驱动代码和Makefile的目录。
b. 设置环境变量: 在这个终端里,执行export命令,正确设置ARCH,CROSS_COMPILE和PATH这三个环境变量。
bash export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- export PATH=$PATH:/path/to/your/toolchain/bin
c. 执行编译: 输入make命令并回车。
d. 幕后发生的事情:
i.make执行all目标下的第一条命令。
ii.make切换到KERN_DIR,带着M=当前目录和modules目标,调用内核的主Makefile。
iii. 内核构建系统根据ARCH和CROSS_COMPILE环境变量,配置好交叉编译环境。
iv. 它返回到我们的驱动目录,读取这里的Makefile,发现了obj-m += hello_drv.o。
v. 它调用aarch64-linux-gnu-gcc等工具,编译hello_drv.c,最终生成hello_drv.ko文件。
vi.all目标下的第一条命令执行完毕,接着执行第二条。
vii.make调用aarch64-linux-gnu-gcc编译hello_drv_test.c,生成用户空间测试程序hello_drv_test。 -
部署与测试阶段 (Deploy & Test)
a. 拷贝文件: 将编译生成的hello_drv.ko和hello_drv_test两个文件,通过网络、SD卡等方式,传输到你的开发板上。
b. 加载驱动: 在开发板的终端上,使用insmod ./hello_drv.ko命令加载内核模块。如果一切顺利,驱动就会被加载到内核中。你可以用lsmod命令查看是否加载成功。
c. 运行测试: 运行./hello_drv_test程序,通过它来和你的驱动进行交互,验证驱动功能是否正常。
希望这个从根源、到细节、再到完整流程的讲解,能让你彻底明白编译一个内核模块背后的原理和每一个步骤的意义。
171万+

被折叠的 条评论
为什么被折叠?



