2. 开发环境搭建:从零开始,让内核跑起来
说实话,驱动开发最让人头疼的往往不是写代码本身,而是环境搭了半天,连个“Hello World”都跑不出来。我见过太多新手卡在这一步,最后放弃了整个课程。
这一章,我们就来把地基打牢。我会带你一步步搞定四件事:拿到内核源码、装好交叉编译工具链、配好QEMU虚拟机、最后写出并运行你的第一个内核模块。
整个过程大概需要30分钟。嗯,咱们开始吧。
2.1 获取Linux内核源码
你可能会问:“我自己的电脑就是Linux,为什么还要专门下载内核源码?”
原因很简单:驱动模块需要和内核版本严格对应。你编译出来的.ko文件,必须链接到目标内核的符号表。版本差一个补丁号,都可能加载失败。我在项目中就吃过这个亏——开发机上好好的,放到客户的内核上直接报“Invalid module format”。
获取源码有几种方式,我列在下面:
| 方式 | 命令/操作 | 适用场景 |
|---|---|---|
| 发行版源码包 | apt-get install linux-source |
快速开始,版本匹配 |
| 内核官网 | wget https://cdn.kernel.org/pub/linux/kernel/... |
需要特定版本 |
| Git克隆 | git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git |
跟踪主线开发 |
我个人习惯用Git克隆,因为方便切换分支。但如果你只是做驱动开发,用发行版自带的源码包最省事。
uname -r 查看当前内核版本,然后 make kernelversion 确认源码版本。两者必须一致。
2.2 安装交叉编译工具链
交叉编译,说白了就是在你的x86电脑上,编译出能在ARM(或其他架构)上运行的代码。为什么需要?因为嵌入式设备通常性能有限,没法直接在上面编译。
安装命令很简单,以ARM64为例:
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install binutils-aarch64-linux-gnu
装完后验证一下:
aarch64-linux-gnu-gcc --version
看到版本信息就说明成功了。
2.3 配置QEMU虚拟机
QEMU是我们用来模拟目标硬件的利器。没有它,你每次测试驱动都得烧录到真实设备上,效率极低。
安装QEMU:
sudo apt-get install qemu-system-arm
sudo apt-get install qemu-utils
然后我们需要一个根文件系统。我推荐用Buildroot生成,或者直接下载现成的Debian镜像。这里给一个快速启动的脚本:
#!/bin/bash
# 启动QEMU虚拟机
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-smp 2 \
-m 1024 \
-kernel arch/arm64/boot/Image \
-append "console=ttyAMA0 root=/dev/vda" \
-drive file=rootfs.ext4,format=raw,if=virtio \
-netdev user,id=net0 \
-device virtio-net,netdev=net0 \
-nographic
这个脚本启动了一个带2核CPU、1GB内存的ARM64虚拟机。你想想看,有了这个环境,你可以在宿主机上编译驱动,然后scp到虚拟机里测试,完全不用碰真实硬件。
2.4 第一个Hello World驱动模块
终于到了写代码的环节。一个最简单的内核模块长这样:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World! Module loaded.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, World! Module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
代码很简单,但有几个关键点我要强调:
- __init 和 __exit:这两个宏告诉内核,初始化函数在运行后可以释放内存,退出函数在模块卸载时调用。
- printk:内核版的printf,输出到内核日志。用
dmesg查看。 - MODULE_LICENSE:必须声明许可证,否则内核会报“tainted”警告。
接下来是Makefile:
# Makefile
obj-m += hello.o
KDIR := /path/to/linux-source
CROSS_COMPILE := aarch64-linux-gnu-
CC := $(CROSS_COMPILE)gcc
all:
make -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
make -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) clean
执行 make,你会得到一个 hello.ko 文件。把它传到QEMU虚拟机里:
scp hello.ko root@<虚拟机IP>:/root/
然后在虚拟机里加载:
insmod hello.ko
dmesg | tail
看到“Hello, World! Module loaded.”了吗?恭喜你,第一个驱动模块跑起来了!
insmod 报错“Exec format error”,八成是架构不匹配。检查一下你的 ARCH 和 CROSS_COMPILE 是否设置正确。我曾经在x86上编译了ARM模块,然后试图在x86虚拟机里加载,结果折腾了半小时才发现问题。
2.5 本章知识体系
下面这张图总结了整个开发环境的搭建流程和模块编译的完整链路:
这张图把整个流程分成了三块:左侧是环境准备,右侧是模块编译,底部是测试验证。箭头表示依赖关系——没有左侧的环境,右侧的编译就是空中楼阁。
好了,环境搭好了,第一个模块也跑通了。接下来你就可以在这个框架上,开始写真正有功能的驱动了。记住,驱动开发的核心就是“编译-测试-调试”这个循环,而我们现在已经把循环的每个环节都打通了。