【彻底搞懂】驱动程序编译之前没有注意到的知识点

第一部分:核心概念的建立——为什么不能像普通程序一样编译驱动?

这是一个最重要、也最基础的问题。我们平时写一个C语言的“Hello, World!”程序,只需要一个命令:

gcc hello.c -o hello

这个命令做了几件事:

  1. 预处理:处理 #include#define 等。
  2. 编译:将C代码转换成汇编代码。
  3. 汇编:将汇编代码转换成机器码(目标文件 .o)。
  4. 链接:将你的目标文件和系统提供的标准C库(比如 libc.so)链接起来,生成最终的可执行文件 hello。标准库里包含了 printf 这类函数的具体实现。

但是,驱动程序完全不同。

  1. 运行环境不同:普通程序运行在操作系统的“用户空间”(User Space),而驱动程序运行在“内核空间”(Kernel Space)。内核空间是操作系统的核心,拥有直接操作硬件的最高权限。
  2. 依赖的函数库不同:普通程序依赖标准C库(libc),可以使用 printf, scanf, fopen 等函数。而驱动程序不能使用这些函数,它需要依赖内核自身提供的函数库,比如 printk (内核里的打印函数), kmalloc (内核里的内存申请函数) 等。
  3. 必须与内核版本精确匹配:你编译的驱动模块,将来是要加载到一个特定版本、特定配置的内核中去的。如果你的驱动在编译时使用的内核头文件、配置、编译器选项与目标内核不一致,加载时轻则失败,重则导致整个系统崩溃(内核恐慌, 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命令,我们编译驱动所依赖的那个内核,它的源代码放在哪里。

为什么必须指定它?
因为前面说了,编译驱动需要:

  1. 内核的头文件(#include <linux/module.h> 等)。
  2. 内核的配置信息(.config 文件)。
  3. 内核的符号表(Module.symvers 文件),这样你的模块才能调用内核里的函数。
  4. 内核顶层的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 这个指令,并编译对应的文件。

总结第一条命令的执行流程:

  1. make程序启动。
  2. 看到 -C /home/book/.../linux-4.4,于是cd到这个内核源码目录。
  3. 在内核源码目录里,开始执行它自己的主Makefile
  4. 它发现我们给它传递了 M=/path/to/our/driver 和目标 modules
  5. 于是它执行内部的 modules 规则,这个规则会做很多准备工作(设置编译器、头文件路径等),然后返回到我们指定的 M 目录。
  6. 回到我们的驱动目录后,它会寻找这里的Makefile文件(就是你提供的这个),读取obj-m += hello_drv.o这行。
  7. 根据这行指令,调用正确的交叉编译器(后面会讲),使用正确的参数,编译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     # 清理用户空间的中间文件
  1. $(MAKE) -C $(KERN_DIR) M=$(CURDIR) modules clean

    • 进入内核目录,清理内核模块编译的所有中间文件
  2. rm -f $(TARGET)

    • 删除最终生成的可执行程序(比如你的测试程序)
  3. 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)。

总结:完整的逻辑流程

现在,我们把所有知识点串起来,形成一个完整的、从无到有的逻辑流程:

  1. 准备阶段 (Setup)
    a. 获取并配置内核: 在你的Ubuntu工作站上,获取目标开发板对应的Linux内核源码。进入源码目录,使用开发板厂商提供的配置文件(.config),为目标板(比如ARCH=arm64)配置并编译一遍内核。这一步至关重要,它会生成后续编译驱动所需的一切。
    b. 获取交叉编译器: 下载并解压与你的内核和开发板匹配的交叉编译工具链(Toolchain)。
    c. 编写代码: 编写你的驱动源代码 hello_drv.c 和用户空间测试代码 hello_drv_test.c
    d. 编写Makefile: 编写我们上面详细分析的那个Makefile文件,确保KERN_DIR指向你准备好的内核源码目录,obj-m指向你的驱动目标文件。

  2. 编译阶段 (Build)
    a. 打开终端: 进入存放驱动代码和Makefile的目录。
    b. 设置环境变量: 在这个终端里,执行export命令,正确设置ARCH, CROSS_COMPILEPATH 这三个环境变量。
    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. 内核构建系统根据ARCHCROSS_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

  3. 部署与测试阶段 (Deploy & Test)
    a. 拷贝文件: 将编译生成的hello_drv.kohello_drv_test两个文件,通过网络、SD卡等方式,传输到你的开发板上。
    b. 加载驱动: 在开发板的终端上,使用 insmod ./hello_drv.ko 命令加载内核模块。如果一切顺利,驱动就会被加载到内核中。你可以用lsmod命令查看是否加载成功。
    c. 运行测试: 运行./hello_drv_test程序,通过它来和你的驱动进行交互,验证驱动功能是否正常。

希望这个从根源、到细节、再到完整流程的讲解,能让你彻底明白编译一个内核模块背后的原理和每一个步骤的意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值