Contents

Linux 内核模块入门:Hello, kernel world!

本文代码地址:https://github.com/Shibo-Zhu/linux-try/tree/master/kernel_module_hello

对于许多 Linux 爱好者和开发者来说,深入内核层面的编程是一项充满挑战又极具吸引力的任务。内核模块(Kernel Module)为我们提供了一种在不重新编译整个内核的情况下,动态地向内核添加或移除功能的强大机制。

本文将通过一个最简单的 “Hello, World!” 内核模块示例,带你走过从编写、编译、加载到卸载的全过程。我们还将探讨两种最常见的编译方式:交叉编译在目标机上本地编译,这对于嵌入式设备(如树莓派)的开发尤其重要。

1. 我们的第一个内核模块:hello.c

让我们从代码开始。这是一个极简的内核模块,它在加载时向内核日志打印一条 “Hello” 消息,并在卸载时打印 “Goodbye”。

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <generated/utsrelease.h> // 包含 UTS_RELEASE 宏,用于获取内核版本

// 模块元信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zs");
MODULE_DESCRIPTION("Simple Hello World kernel module");
MODULE_VERSION("1.0");

// 模块加载时执行的函数
static int __init hello_init(void)
{
    // pr_info 是内核推荐的打印宏,级别为 KERN_INFO
    pr_info("HelloModule: Hello, kernel world! (ver=%s)\n", UTS_RELEASE);
    return 0; // 返回 0 表示成功
}

// 模块卸载时执行的函数
static void __exit hello_exit(void)
{
    pr_info("HelloModule: Goodbye, kernel world!\n");
}

// 注册初始化和退出函数
module_init(hello_init);
module_exit(hello_exit);

代码解析:

  • #include <linux/module.h>: 编写内核模块所必需的头文件。
  • MODULE_*: 这些是模块的元数据宏,用于声明模块的许可证、作者、描述和版本。MODULE_LICENSE("GPL") 是非常重要的一项,否则加载模块时内核会发出“污染(taint)”警告。
  • module_init(hello_init): 指定当模块被加载 (insmod) 时,hello_init 函数将被调用。
  • module_exit(hello_exit): 指定当模块被卸载 (rmmod) 时,hello_exit 函数将被调用。

2. 构建模块的 “遥控器”:Makefile

与普通的用户态程序不同,内核模块的编译依赖于内核自身的构建系统。我们需要一个特殊的 Makefile 来“告诉” make 命令如何编译我们的模块。

# Makefile
obj-m += hello.o

# 'all' 是默认目标
all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

# 'clean' 用于清理编译生成的文件
clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

Makefile 解析:

  • obj-m += hello.o: 这行代码告诉内核构建系统,我们要将 hello.c 编译成一个名为 hello.ko 的可加载模块。
  • -C $(KDIR): make 命令将切换到 $(KDIR) 变量指定的目录去执行。KDIR 必须指向包含了内核头文件和构建配置的内核源码树。
  • M=$(PWD): 告诉内核构建系统,我们的模块源码在当前目录 (PWD),构建完成后再返回。
  • modules: 这是内核构建系统预定义的一个目标,专门用于编译外部模块。

3. 方法一:交叉编译(适用于嵌入式设备)

在嵌入式开发中,我们通常在性能更强的开发主机(如 PC)上编译代码,然后将生成的目标文件部署到资源有限的目标机(如树莓派)上运行。这个过程就是“交叉编译”。

假设我们的开发主机是 x86 架构,而目标机是 aarch64(ARM64)架构的树莓派。

步骤 1: 在主机上准备内核构建环境

在编译模块之前,我们需要先在内核源码树中准备好模块编译所需的一切。

# 进入你的内核源码目录
cd /media/zs/ubuntu_disk/rt_linux/rpi-5.8/linux

# 使用目标架构和交叉编译工具链前缀来准备模块构建环境
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules_prepare

这个命令会生成 include/generated/utsrelease.hModule.symvers 等关键文件,为模块编译做好准备。

步骤 2: 编写交叉编译脚本 build_cross.sh

这个脚本封装了交叉编译所需的 ARCHCROSS_COMPILE 环境变量。

#!/bin/bash
set -e

# ====== 请根据你的环境修改这两项 ======
CROSS_COMPILE=aarch64-linux-gnu-
ARCH=arm64
KDIR=/media/zs/ubuntu_disk/rt_linux/rpi-5.8/linux   # 指向交叉编译用的内核源码
# ====================================

PWD=$(pwd)
export ARCH CROSS_COMPILE

echo "[*] Using KDIR=${KDIR}"
echo "[*] Building module for ARCH=${ARCH} with CROSS_COMPILE=${CROSS_COMPILE}"

make -C "${KDIR}" M=${PWD} modules

echo "[*] Build done: $(pwd)/hello.ko"
modinfo hello.ko | grep vermagic || true

步骤 3: 执行编译

# 赋予脚本执行权限
chmod +x build_cross.sh

# 运行脚本
./build_cross.sh

编译成功后,当前目录会生成 hello.ko 文件。我们可以使用 modinfo 命令查看它的信息,特别是 vermagic 字段,它必须与目标机的内核版本完全匹配。

$ modinfo hello.ko
filename:       /media/zs/ubuntu_disk/linux-try/kernel_module_hello/hello.ko
version:        1.0
description:    Simple Hello World kernel module
author:         zs
license:        GPL
...
vermagic:       5.8.18-v8+ SMP preempt mod_unload modversions aarch64

步骤 4: 在目标机上加载和测试模块

  1. 将模块文件复制到树莓派

    scp hello.ko rpi4-1@192.168.1.105:/tmp/
  2. 加载模块

    ssh rpi4-1@192.168.1.105
    sudo insmod /tmp/hello.ko
  3. 查看内核日志

    dmesg | tail -n 1
    # [ 3700.463603] HelloModule: Hello, kernel world! (ver=5.8.18-v8+)

    你看到了!我们的模块成功加载并打印了消息。

  4. 确认模块已加载

    lsmod | grep hello
    # hello                  16384  0
  5. 卸载模块

    sudo rmmod hello
  6. 再次查看内核日志

    dmesg | tail -n 1
    # [ 3751.164989] HelloModule: Goodbye, kernel world!

    卸载消息也成功打印,实验完成!

4. 方法二:在目标机上本地编译

有时,我们希望直接在目标设备上编译内核模块。这需要我们将内核头文件和构建所需的文件部署到目标机上。

步骤 1: 在主机上打包内核头文件

# 进入内核源码目录
cd /media/zs/ubuntu_disk/rt_linux/rpi-5.8/linux

# 将整个源码树(或至少是编译模块所需的文件)打包
tar czf /tmp/linux-headers-5.8.18-v8+.tar.gz .

步骤 2: 在目标机上部署内核头文件

  1. 将打包文件拷贝到目标机

    scp /tmp/linux-headers-5.8.18-v8+.tar.gz rpi4-1@192.168.1.105:/tmp/
  2. 在目标机上解压并建立符号链接 登录到目标机后执行:

    # 建议将头文件放在 /usr/src 目录下
    cd /usr/src
    sudo tar xzf /tmp/linux-headers-5.8.18-v8+.tar.gz
    
    # 建立一个符号链接,让内核构建系统能找到头文件
    sudo rm -f /lib/modules/$(uname -r)/build
    # 这是标准做法,Makefile 中的 `$(uname -r)` 会解析到这个路径
    sudo ln -s /usr/src/linux-headers-5.8.18-v8+ /lib/modules/$(uname -r)/build

步骤 3: 在目标机上编译

  1. 安装编译工具

    sudo apt update
    sudo apt install -y make gcc build-essential
    sudo apt install -y flex bison libncurses5-dev libssl-dev bc
  2. 进入模块源码目录,准备编译环境

    cd /usr/src/linux-headers-5.8.18-HCBS-v8+
    sudo make clean
    sudo make modules_prepare
  3. 编写本地编译脚本 build.sh 这个脚本更简单,因为它不需要 CROSS_COMPILEARCH

    #!/bin/bash
    set -e
    
    KDIR=/lib/modules/$(uname -r)/build   # 直接使用系统默认的内核构建路径
    PWD=$(pwd)
    
    echo "[*] Using KDIR=${KDIR}"
    
    make -C "${KDIR}" M=${PWD} modules
    
    echo "[*] Build done: $(pwd)/hello.ko"
    modinfo hello.ko | grep vermagic || true
  4. 执行编译hello.c, Makefile, build.sh 拷贝到目标机的某个工作目录(例如 ~/hello_module),然后运行:

    cd ~/hello_module
    chmod +x build.sh
    ./build.sh

    编译成功后,你会在当前目录看到 hello.ko。接下来的加载和卸载步骤与交叉编译一节中的完全相同。