2. 开发环境搭建:交叉编译、内核源码树与工具链

说实话,内核模块开发的第一步,往往就把不少人卡住了。不是代码多难写,而是环境没搭对。我见过太多人花了一整天装包,结果编译出来跑不了——说白了,就是交叉编译链和内核源码树没对齐。

这一章,我就带你把这套环境理清楚。咱们的目标很明确:能在你的开发机上编译出能在目标设备上运行的内核模块。不管是树莓派、ARM 开发板,还是某个工控设备,流程都一样。

2.1 交叉编译:为什么需要它?

你的开发机通常是 x86_64 架构,但目标设备可能是 ARM、RISC-V 或 MIPS。x86 上 gcc 编译出来的二进制,ARM 根本跑不了。所以我们需要交叉编译器——在 x86 上运行,但生成 ARM 指令集的工具链。

核心概念:交叉编译 = 在宿主机(Host)上编译出目标机(Target)可执行的代码。

我个人习惯把工具链放在 /opt/toolchains/ 下,方便管理。不同厂商的工具链命名规则不太一样,但核心文件就这几个:

工具 典型名称(ARM 为例) 作用
C 编译器 arm-linux-gnueabihf-gcc 编译 C 源码
C++ 编译器 arm-linux-gnueabihf-g++ 编译 C++ 源码
链接器 arm-linux-gnueabihf-ld 链接目标文件
汇编器 arm-linux-gnueabihf-as 汇编源码
归档工具 arm-linux-gnueabihf-ar 创建静态库
二进制工具 arm-linux-gnueabihf-objdump 反汇编、查看段信息

小技巧:用 which arm-linux-gnueabihf-gcc 检查工具链是否在 PATH 里。如果找不到,记得把 /opt/toolchains/xxx/bin 加到 ~/.bashrc 的 export PATH 里。

2.2 配置内核源码树

内核模块不是随便编译的。它需要依赖内核源码树里的头文件和 Makefile 规则。说白了,你得有一份与目标设备上运行的内核版本完全一致的源码。

为什么必须一致?因为内核内部的数据结构、函数签名、宏定义,每个版本都可能变。你拿 5.10 的源码编译模块,插到 5.15 的内核里,大概率 insmod 就报 Invalid module format。

我踩过这个坑。有一次客户现场升级了内核,我没同步更新源码树,结果模块加载直接 panic。嗯,从那以后我养成了习惯:每次部署前先 uname -r 确认版本。

2.2.1 获取内核源码

有两种方式:

  1. 从内核官网下载:kernel.org 上找到对应版本,解压到 /usr/src/ 或自定义目录。
  2. 从设备厂商获取:很多嵌入式板子(比如树莓派、NXP i.MX)会提供修改过的内核源码,带自己的驱动补丁。建议优先用厂商的。

解压后,进入源码目录,先做一次配置:

# 假设目标设备的内核配置已导出为 .config 文件
# 你可以从设备上拷贝 /proc/config.gz 解压得到
zcat /proc/config.gz > .config

# 或者使用默认配置(不一定匹配)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig

# 然后编译内核头文件和模块相关目标
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare

注意:modules_prepare 这一步很关键。它不会编译整个内核,但会生成 Module.symvers 和必要的头文件。没有这个文件,你的模块编译时会找不到符号。

2.2.2 设置内核源码树路径

编译模块时,Makefile 需要知道内核源码树在哪。通常通过 KERNELDIR 变量指定:

# 在你的模块 Makefile 中
KERNELDIR ?= /home/user/linux-5.10.100

# 或者通过环境变量
export KERNELDIR=/home/user/linux-5.10.100

我个人习惯在 ~/.bashrc 里加一个别名,省得每次敲路径:

export KERNEL_SRC=/opt/kernel/linux-5.10.100

2.3 安装必要工具链

除了交叉编译器,你还需要一些辅助工具。别小看它们,少了哪个都可能卡住。

工具 安装方式(Ubuntu/Debian) 用途
build-essential apt install build-essential 基础编译工具(gcc, make, ld 等)
libncurses-dev apt install libncurses-dev menuconfig 图形配置界面依赖
bc apt install bc 内核编译脚本中用到的基本计算器
flex / bison apt install flex bison 内核解析器生成工具
elfutils apt install elfutils 处理 ELF 文件,调试符号相关
openssl-dev apt install libssl-dev 内核模块签名功能需要

避坑指南:我曾经在编译 5.10 内核时,因为没装 elfutils,modules_prepare 一直报 Cannot generate ORC metadata。装完就好了。所以建议一次性把这些都装上。

2.4 验证环境:编译一个空模块

环境搭没搭好,跑一个最小模块就知道了。写一个最简单的 hello.c:

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel module!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel module!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple test module");

对应的 Makefile:

obj-m := hello.o

KERNELDIR ?= /path/to/your/kernel/source
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

执行 make,如果看到生成了 hello.ko,恭喜你,环境通了。

验证命令:file hello.ko 应该显示 ELF 32-bit LSB relocatable, ARM 或类似架构信息。

2.5 整体流程一览

下面这张图把整个搭建流程串起来了。你跟着箭头走一遍,心里就有谱了。

开发环境搭建流程 开发环境搭建核心流程 宿主机 (x86_64) 交叉编译器 (arm-linux-gnueabihf-gcc) 内核源码树 (KERNELDIR) make modules → hello.ko 目标设备 (ARM) 关键:交叉编译器 + 内核源码树版本必须与目标设备内核匹配 验证方式:file hello.ko 确认架构,uname -r 确认版本

2.6 常见问题与排查

环境搭建过程中,有几个问题几乎每个人都会遇到。我列出来,你遇到了直接对照查:

  • “找不到 ”:说明 KERNELDIR 没设对,或者内核源码没执行 modules_prepare。
  • “Invalid module format”:内核版本不匹配,或者 Module.symvers 缺失。检查 uname -r 和源码版本。
  • “/lib/modules/xxx 不存在”:目标设备上没装内核模块目录。用 make modules_install 安装,或者手动拷贝 .ko 文件。
  • “relocation truncated to fit”:链接时地址空间不够。检查内核配置中 CONFIG_ARM_MODULE_PLTS 是否开启。

重要提醒:不要在宿主机上直接 insmod 交叉编译的模块。x86 内核加载 ARM 模块会直接报错,甚至可能让宿主机内核 panic。我见过有人把开发板上的模块拷到 PC 上试,结果系统直接挂了。

好了,环境搭到这里,你已经可以编译出能在目标设备上运行的 .ko 文件了。下一件事,就是把它加载进去,看看能不能正常工作。不过那是后面章节的内容了,咱们先把基础打牢。