跳转至

内核模块入门:从用户空间到内核空间的第一步

适用平台:i.MX6ULL (ARM Cortex-A7) 内核版本:Linux 6.12.49 (linux-imx) 目标读者:准备入门驱动开发的嵌入式工程师 前置知识:C语言基础、Linux基本操作


为什么要写这篇文章

如果说Linux内核是一座巨大的城堡,那内核模块就是你进入这座城堡的特制通行证。

我知道,很多刚开始学驱动开发的同学都有这种感觉:内核太复杂了,不知道从哪下手。你想写一个简单的GPIO驱动,但网上教程一上来就是一堆陌生的API,什么request_irqcdev_initfile_operations...看得你头晕眼花。

其实,所有这些复杂的东西都建立在一个基础之上:内核模块。内核模块是Linux驱动开发的起点,也是理解内核工作原理的最好切入点。

这篇文章的目标非常明确:带你彻底搞清楚什么是内核模块,它和普通程序有什么区别,内核是如何加载和卸载模块的,以及你该如何开始编写自己的第一个模块。

在正式开始之前,我要说明一下:本文所有关于内核源码的引用都基于 third_party/linux-imx 目录下的实际代码,这是NXP维护的i.MX系列处理器内核,版本是6.12.49。我们引用代码时会明确标注文件路径,方便你去查看源码。


一、什么是内核模块,为什么需要它

1.1 单内核与可加载模块

Linux采用的是单内核(Monolithic Kernel)架构。这意味着整个操作系统核心运行在一个单一的地址空间里,所有内核代码共享同一个内存空间。与之相对的是微内核(如Minix),微内核把很多服务放到用户空间作为独立进程运行。

单内核的优势是性能好——函数调用就是普通的函数调用,不需要IPC(进程间通信)的额外开销。但单内核有个大问题:一旦内核编译完成,要添加新功能就得重新编译整个内核,这在实际使用中很不方便。

内核模块(Loadable Kernel Module,LKM)就是Linux为了解决这个问题而引入的机制。它允许你在不重新编译内核的情况下,动态地向运行中的内核添加或删除代码。

你可以把内核模块理解为"内核的插件": - 编译时:模块是独立的.ko(Kernel Object)文件 - 加载时:模块的代码被加载到内核地址空间,成为内核的一部分 - 卸载时:模块的代码从内核移除,释放占用的资源

1.2 内核模块的典型应用场景

在嵌入式Linux开发中,内核模块主要用于:

  1. 设备驱动:这是最常见的用途。GPIO、I2C、SPI、UART等外设驱动通常以模块形式存在
  2. 文件系统:支持新的文件系统类型
  3. 网络协议:添加新的网络协议或过滤规则
  4. 调试和监控:如ftrace、perf等工具的核心模块
  5. 安全模块:如SELinux、AppArmor的Linux Security Module(LSM)

对于i.MX6ULL开发来说,你会频繁打交道的模块包括: - imx-sdma.ko - SDMA固件驱动 - imx-thermal.ko - 温度传感器驱动 - gpio_keys.ko - GPIO按键驱动 - 各种你自定义的设备驱动

1.3 模块 vs 静态编译

特性 模块(.ko) 静态编译(vmlinuz)
加载方式 运行时动态加载 启动时自动加载
内存占用 按需占用 始终占用
开发效率 高(无需重启) 低(需重新编译内核)
适用场景 开发调试、可选驱动 核心功能、启动必需驱动

二、用户空间 vs 内核空间:你必须理解的核心概念

在深入内核模块之前,有一个概念你必须彻底理解:用户空间和内核空间的区别。这是理解内核模块工作原理的基础,也是新手最容易踩坑的地方。

2.1 虚拟地址空间的划分

现代操作系统使用虚拟内存,每个进程都有自己独立的虚拟地址空间。在Linux中(以32位ARM为例),这个4GB的虚拟地址空间被划分为两部分(参见 Documentation/mm/highmem.rst):

+----------------------+ 0xFFFFFFFF
|                      |
|   内核空间 (1GB)     |  ← 所有进程共享
|                      |
+----------------------+ 0xC0000000 (内核空间边界)
|                      |
|                      |
|   用户空间 (3GB)     |  ← 进程私有空间
|                      |
|                      |
+----------------------+ 0x00000000

关键点: - 用户空间:0x00000000 ~ 0xBFFFFFFF(3GB) - 内核空间:0xC0000000 ~ 0xFFFFFFFF(1GB) - 每个进程有独立的用户空间,但所有进程共享同一个内核空间

对于64位系统(如i.MX8系列),这个划分会有所不同,但原理是一样的:虚拟地址空间被划分为用户空间和内核空间两部分。

2.2 用户空间的特权限制

用户空间程序运行在非特权模式(User Mode,ARMv7称为PL0),有以下限制:

  1. 不能直接访问硬件:不能直接读写寄存器
  2. 受限的内存访问:只能访问自己的虚拟地址空间
  3. 不能执行特权指令:如修改页表、开关中断等
  4. 需要系统调用进入内核:任何需要内核服务的操作都必须通过系统调用

2.3 内核空间的特权

内核代码运行在特权模式(Kernel Mode,ARMv7称为PL1),拥有:

  1. 完整的硬件访问:可以直接访问所有外设寄存器
  2. 完整的内存访问:可以访问任何物理内存
  3. 特权指令:可以修改页表、控制中断等
  4. 无限制:理论上可以做任何事情(因此内核bug可能导致系统崩溃)

2.4 用户空间与内核空间的通信

既然用户空间和内核空间是隔离的,它们如何通信?

+----------------+                          +----------------+
|                |   系统调用接口           |                |
|   用户空间     | <---------------------> |   内核空间     |
|  应用程序      |   ioctl / mmap / proc   |   内核模块     |
|                |                          |                |
+----------------+                          +----------------+

主要的通信方式包括:

方式 用途 典型应用
系统调用(syscall) 基本服务 open、read、write
ioctl 设备特定操作 配置寄存器、控制设备
mmap 内存映射 将设备寄存器映射到用户空间
procfs/sysfs 信息暴露 /proc/sys 下的文件
netlink 网络相关 路由、防火墙规则

2.5 为什么内核模块需要特别小心

正因为内核运行在特权模式,内核模块的代码bug可能导致:

  1. 系统崩溃:Oops、panic
  2. 数据损坏:误写内核数据结构
  3. 安全漏洞:权限提升、信息泄露
  4. 硬件损坏:错误配置外设寄存器(罕见但可能)

这也是为什么内核开发有严格的代码规范要求(参见 Documentation/process/coding-style.rst)。


三、内核模块的基本架构

现在让我们看看内核模块到底长什么样。一个完整的内核模块包含以下几个部分:

3.1 模块的基本结构

/* 头文件包含 */
#include <linux/module.h>      // 模块核心API
#include <linux/init.h>        // __init、__exit宏
#include <linux/kernel.h>      // printk等核心函数

/* 模块许可证声明(必须!) */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("A simple kernel module for i.MX6ULL");
MODULE_VERSION("1.0");

/* 模块初始化函数 - 加载时执行 */
static int __init my_module_init(void)
{
    printk(KERN_INFO "my_module: Hello, kernel space!\n");
    /* 初始化代码放在这里 */

    return 0;  // 返回0表示成功,非0表示失败
}

/* 模块清理函数 - 卸载时执行 */
static void __exit my_module_exit(void)
{
    printk(KERN_INFO "my_module: Goodbye, kernel space!\n");
    /* 清理代码放在这里 */
}

/* 注册模块的加载和卸载函数 */
module_init(my_module_init);
module_exit(my_module_exit);

3.2 关键宏说明

用途 必需性
MODULE_LICENSE 声明代码许可证 必需
MODULE_AUTHOR 作者信息 推荐
MODULE_DESCRIPTION 模块描述 推荐
MODULE_VERSION 模块版本 可选
module_init 指定初始化函数 必需
module_exit 指定清理函数 必需

3.3 __init__exit 标记

你可能会注意到代码中的 __init__exit 标记:

static int __init my_module_init(void)  // __init标记
{
    // ...
}

static void __exit my_module_exit(void)  // __exit标记
{
    // ...
}

这两个标记的作用(定义在 include/linux/init.h):

  • __init:标记的代码会被放在 .init.text 段,模块加载成功后这些内存会被释放
  • __exit:标记的代码会被放在 .exit.text 段,只有当模块可卸载时才链接

这是一个内存优化技术:内核的初始化代码只需要执行一次,执行完就可以把内存释放掉。

3.4 模块许可证的重要性

MODULE_LICENSE 不仅仅是法律声明,它直接影响模块的功能:

/* 来自 include/linux/module.h */
MODULE_LICENSE("GPL");              // 可以访问GPL符号
MODULE_LICENSE("GPL v2");           // 可以访问GPL符号
MODULE_LICENSE("Dual BSD/GPL");     // 可以访问GPL符号
MODULE_LICENSE("Dual MIT/GPL");     // 可以访问GPL符号
MODULE_LICENSE("Proprietary");      // 不能访问GPL符号!

如果许可证不是GPL兼容的,模块将无法使用 EXPORT_SYMBOL_GPL 导出的符号。内核中很多重要API都是用 EXPORT_SYMBOL_GPL 导出的,所以非GPL许可证的模块功能会受限。


四、模块加载流程:从 insmod 到内核函数调用

当你在命令行执行 insmod my_module.ko 时,内核到底做了什么?让我们追踪这个过程的源码实现。

4.1 系统调用入口

insmod 命令最终会调用 init_module 系统调用。这个系统调用的定义在 kernel/module/main.c

/* third_party/linux-imx/kernel/module/main.c:3066 */
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
    // ...
    err = copy_module_from_user(umod, len, &info);
    if (err) {
        mod_stat_inc(&failed_kreads);
        return err;
    }

    return load_module(&info, uargs, 0);
}

4.2 模块加载的主流程

load_module() 函数是模块加载的核心,定义在 kernel/module/main.c:2857

/* third_party/linux-imx/kernel/module/main.c:2857 */
static int load_module(struct load_info *info, const char __user *uargs,
                       int flags)
{
    struct module *mod;
    long err = 0;

    /* 1. 签名验证 */
    err = module_sig_check(info, flags);
    if (err)
        goto free_copy;

    /* 2. ELF格式验证和拷贝 */
    err = elf_validity_cache_copy(info, flags);
    if (err)
        goto free_copy;

    /* 3. 早期模块检查 */
    err = early_mod_check(info, flags);
    if (err)
        goto free_copy;

    /* 4. 布局和内存分配 */
    mod = layout_and_allocate(info, flags);
    if (IS_ERR(mod)) {
        err = PTR_ERR(mod);
        goto free_copy;
    }

    /* 5. 添加到模块列表 */
    err = add_unformed_module(mod);
    if (err)
        goto free_module;

    /* 6. 更新内核taint标志 */
    module_augment_kernel_taints(mod, info);

    /* 7. Per-CPU数据分配 */
    err = percpu_modalloc(mod, info);
    if (err)
        goto unlink_mod;

    /* 8. 模块卸载初始化 */
    err = module_unload_init(mod);
    if (err)
        goto unlink_mod;

    /* 9. 查找各个段 */
    err = find_module_sections(mod, info);
    if (err)
        goto free_unload;

    /* 10. 检查符号版本 */
    err = check_export_symbol_versions(mod);
    if (err)
        goto free_unload;

    /* 11. 设置模块信息 */
    setup_modinfo(mod, info);

    /* 12. 符号解析 */
    err = simplify_symbols(mod, info);
    if (err < 0)
        goto free_modinfo;

    /* 13. 重定位 */
    err = apply_relocations(mod, info);
    if (err < 0)
        goto free_modinfo;

    /* 14. 重定位后处理 */
    err = post_relocation(mod, info);
    if (err < 0)
        goto free_modinfo;

    /* 15. 刷新指令缓存 */
    flush_module_icache(mod);

    /* 16. 复制参数 */
    mod->args = strndup_user(uargs, ~0UL >> 1);
    if (IS_ERR(mod->args)) {
        err = PTR_ERR(mod->args);
        goto free_arch_cleanup;
    }

    /* 17. 初始化build ID */
    init_build_id(mod, info);

    /* 18. Ftrace初始化 */
    ftrace_module_init(mod);

    /* 19. 最后的初始化 - 调用你的init函数! */
    /* ... */
    return do_init_module(mod);
}

4.3 执行模块的初始化函数

do_init_module() 函数最终会调用你在模块中定义的初始化函数:

/* third_party/linux-imx/kernel/module/main.c:2516 */
static noinline int do_init_module(struct module *mod)
{
    int ret = 0;
    struct mod_initfree *freeinit;

    /* 分配init内存 */
    freeinit = kmalloc(sizeof(*freeinit), GFP_KERNEL);
    if (!freeinit) {
        ret = -ENOMEM;
        goto fail;
    }

    /* 执行构造函数 */
    do_mod_ctors(mod);

    /* 这里!调用模块的init函数 */
    if (mod->init != NULL)
        ret = do_one_initcall(mod->init);

    if (ret < 0) {
        goto fail_free_freeinit;
    }

    /* 模块状态变为LIVE */
    mod->state = MODULE_STATE_LIVE;
    blocking_notifier_call_chain(&module_notify_list,
                                 MODULE_STATE_LIVE, mod);

    /* 发送uevent */
    kobject_uevent(&mod->mkobj.kobj, KOBJ_ADD);

    /* ... */

    /* 释放init段内存 */
    if (llist_add(&freeinit->node, &init_free_list))
        schedule_work(&init_free_wq);

    return 0;

fail:
    /* 失败处理 */
    mod->state = MODULE_STATE_GOING;
    /* ... */
    return ret;
}

4.4 模块状态机

内核用 enum module_state 来表示模块的状态(定义在 include/linux/module.h):

/* third_party/linux-imx/include/linux/module.h:320 */
enum module_state {
    MODULE_STATE_LIVE,      /* 正常运行状态 */
    MODULE_STATE_COMING,    /* 正在初始化 */
    MODULE_STATE_GOING,     /* 正在卸载 */
    MODULE_STATE_UNFORMED,  /* 还在设置中 */
};

模块加载时的状态转换:

UNFORMED → COMING → LIVE
             (失败)
              GOING

4.5 模块卸载流程

当你执行 rmmod my_module 时,内核会调用 delete_module 系统调用:

/* third_party/linux-imx/kernel/module/main.c:700 */
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
                unsigned int, flags)
{
    struct module *mod;
    char name[MODULE_NAME_LEN];
    int ret;

    /* 1. 权限检查 */
    if (!capable(CAP_SYS_MODULE) || modules_disabled)
        return -EPERM;

    /* 2. 查找模块 */
    mod = find_module(name);
    if (!mod) {
        ret = -ENOENT;
        goto out;
    }

    /* 3. 检查依赖 */
    if (!list_empty(&mod->source_list)) {
        /* 有其他模块依赖我们 */
        ret = -EWOULDBLOCK;
        goto out;
    }

    /* 4. 检查状态 */
    if (mod->state != MODULE_STATE_LIVE) {
        ret = -EBUSY;
        goto out;
    }

    /* 5. 检查是否有exit函数 */
    if (mod->init && !mod->exit) {
        /* 有init但没有exit,不能卸载(除非force) */
        ret = -EBUSY;
        goto out;
    }

    /* 6. 停止模块 */
    ret = try_stop_module(mod, flags, &forced);
    if (ret != 0)
        goto out;

    /* 7. 调用模块的exit函数 */
    if (mod->exit != NULL)
        mod->exit();

    /* 8. 清理模块 */
    blocking_notifier_call_chain(&module_notify_list,
                                 MODULE_STATE_GOING, mod);
    free_module(mod);

    return 0;
}

五、内核版本与模块版本兼容性

这是新手最容易遇到的问题:为什么在开发板上编译的模块,换个内核就加载不了了?

5.1 vermagic 机制

内核模块在编译时会嵌入一个"版本魔法字符串"(vermagic),加载时内核会检查这个字符串是否匹配。vermagic的定义在 include/linux/vermagic.h

/* third_party/linux-imx/include/linux/vermagic.h:42 */
#define VERMAGIC_STRING                         \
    UTS_RELEASE " "                             \
    MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT         \
    MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS   \
    MODULE_ARCH_VERMAGIC                        \
    MODULE_RANDSTRUCT

这个字符串包含: - UTS_RELEASE:内核版本号(如 "6.12.49") - MODULE_VERMAGIC_SMP:是否启用SMP - MODULE_VERMAGIC_PREEMPT:抢占模式 - MODULE_VERMAGIC_MODULE_UNLOAD:是否支持模块卸载 - MODULE_VERMAGIC_MODVERSIONS:是否启用模块版本化 - MODULE_ARCH_VERMAGIC:架构相关配置

5.2 版本检查的实现

版本检查的实现在 kernel/module/main.ccheck_modinfo 函数:

/* third_party/linux-imx/kernel/module/main.c:2096 */
static int check_modinfo(struct module *mod, struct load_info *info, int flags)
{
    const char *modmagic = get_modinfo(info, "vermagic");
    int err;

    if (flags & MODULE_INIT_IGNORE_VERMAGIC)
        modmagic = NULL;

    /* 没有vermagic?可以用--force强制 */
    if (!modmagic) {
        err = try_to_force_load(mod, "bad vermagic");
        if (err)
            return err;
    } else if (!same_magic(modmagic, vermagic, info->index.vers)) {
        pr_err("%s: version magic '%s' should be '%s'\n",
               info->name, modmagic, vermagic);
        return -ENOEXEC;
    }

    return 0;
}

same_magic 函数在 kernel/module/version.c 中实现:

/* third_party/linux-imx/kernel/module/version.c:80 */
int same_magic(const char *amagic, const char *bmagic,
               bool has_crcs)
{
    if (has_crcs) {
        /* 如果有CRC校验,跳过内核版本部分 */
        amagic += strcspn(amagic, " ");
        bmagic += strcspn(bmagic, " ");
    }
    return strcmp(amagic, bmagic) == 0;
}

5.3 常见版本不匹配错误

当你看到这样的错误时:

my_module: version magic '6.12.49 preempt mod_unload '
    should be '6.12.49 preempt_unipi mod_unload '

说明模块是在不同的内核配置下编译的。

解决方案: 1. 正确做法:在目标内核源码目录下编译模块 2. 强制加载(不推荐):insmod -f my_module.komodprobe --force 3. 禁止版本检查(非常危险):insmod --modversion-ignore

5.4 MODVERSIONS 机制

即使启用了 CONFIG_MODVERSIONS,内核还会对每个导出的符号计算CRC值:

/* third_party/linux-imx/kernel/module/version.c:13 */
int check_version(const struct load_info *info,
                  const char *symname,
                  struct module *mod,
                  const s32 *crc)
{
    /* ... */
    for (i = 0; i < num_versions; i++) {
        u32 crcval;

        if (strcmp(versions[i].name, symname) != 0)
            continue;

        crcval = *crc;
        if (versions[i].crc == crcval)
            return 1;  /* 版本匹配 */
        goto bad_version;
    }
    /* ... */
}

这意味着,即使内核版本号相同,如果导出符号的签名改变了,模块也无法加载。

5.5 Module.symvers 文件

当你编译内核或模块时,会生成 Module.symvers 文件,记录所有导出符号及其CRC值:

/* Module.symvers 格式 */
0x12345678    printk    vmlinux    EXPORT_SYMBOL_GPL
0x9abcdef0    kmalloc   vmlinux    EXPORT_SYMBOL

编译外部模块时,构建系统会读取内核源码目录下的 Module.symvers 来验证符号版本。


六、完整可编译代码示例

现在让我们写一个完整的、可以直接在i.MX6ULL上编译运行的模块示例。

6.1 模块源码(hello_imx6ull.c)

// SPDX-License-Identifier: GPL-2.0
/*
 * hello_imx6ull.c - 一个简单的内核模块示例
 * 适用于 i.MX6ULL + Linux 6.12.49
 *
 * 编译方法:
 *   make -C <内核源码路径> M=$(pwd) modules
 *
 * 加载/卸载:
 *   insmod hello_imx6ull.ko
 *   rmmod hello_imx6ull
 *
 * 查看输出:
 *   dmesg | tail
 */

#include <linux/module.h>      /* 模块核心API,MODULE_* 宏 */
#include <linux/init.h>        /* __init、__exit 宏 */
#include <linux/kernel.h>      /* printk、pr_* 系列函数 */
#include <linux/printk.h>      /* printk 相关定义 */

/*
 * 模块元数据
 *
 * MODULE_LICENSE 是必须的,没有它模块无法使用GPL符号
 * 其他元数据有助于识别模块信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("IMX-Forge Tutorial <tutorial@example.com>");
MODULE_DESCRIPTION("A simple hello world module for i.MX6ULL");
MODULE_VERSION("1.0");

/*
 * 模块参数示例
 *
 * 可以在加载时通过命令行传入:insmod hello_imx6ull.ko count=5
 */
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of times to print hello message");

static char *name = "i.MX6ULL";
module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "Name to greet");

/*
 * hello_init - 模块初始化函数
 *
 * 这个函数在模块加载时被调用。所有的初始化工作应该在这里完成:
 *   - 注册设备
 *   - 分配内存
 *   - 注册中断
 *   - 创建proc/sysfs文件
 *
 * 返回值:0表示成功,负数表示错误码
 */
static int __init hello_init(void)
{
    int i;

    /*
     * printk 是内核版本的 printf
     * KERN_INFO 是日志级别,等价于 printk("<6>...")
     * 也可以使用 pr_info() 等简写形式
     */
    pr_info("=== hello_imx6ull module loading ===\n");
    pr_info("Hello, %s!\n", name);
    pr_info("This module was compiled for Linux %s\n", UTS_RELEASE);
    pr_info("Running on %s architecture\n", UTS_MACHINE);

    /* 打印多次,演示参数的使用 */
    for (i = 0; i < count; i++) {
        pr_info("[%d] Greeting from kernel space!\n", i + 1);
    }

    /*
     * 打印模块信息
     * THIS_MODULE 是当前模块的 struct module 指针
     */
    pr_info("Module name: %s\n", THIS_MODULE->name);
    pr_info("Module state: %d\n", THIS_MODULE->state);

    /*
     * 返回0表示成功
     * 如果返回非0,内核认为模块初始化失败,不会加载它
     */
    return 0;
}

/*
 * hello_exit - 模块清理函数
 *
 * 这个函数在模块卸载时被调用。所有在init中分配的资源应该在这里释放:
 *   - 注销设备
 *   - 释放内存
 *   - 删除proc/sysfs文件
 *   - 禁用中断
 *
 * 注意:这个函数只在模块可以卸载时才会被调用。
 *       如果模块的init函数返回失败,exit不会被调用。
 */
static void __exit hello_exit(void)
{
    /*
     * 打印退出消息
     * 注意:在exit函数中要确保已经释放了所有资源
     */
    pr_info("=== hello_imx6ull module unloading ===\n");
    pr_info("Goodbye, %s!\n", name);
    pr_info("Thank you for using this module.\n");
}

/*
 * 注册模块的加载和卸载函数
 *
 * module_init 指定模块加载时调用的函数
 * module_exit 指定模块卸载时调用的函数
 */
module_init(hello_init);
module_exit(hello_exit);

6.2 Makefile

# hello_imx6ull 模块的 Makefile
#
# 使用方法:
#   make              - 编译模块
#   make clean        - 清理编译产物
#   make install      - 安装模块到系统

# 内核源码路径 - 修改为你的实际路径
KERNEL_DIR := /home/charliechen/imx-forge/third_party/linux-imx

# 当前目录
PWD := $(shell pwd)

# 模块名称
obj-m := hello_imx6ull.o

# 编译目标
all:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

# 清理目标
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
    rm -f Module.symvers modules.order

# 安装目标
install:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install

.PHONY: all clean install

6.3 编译和测试

# 1. 进入模块目录
cd /path/to/module/directory

# 2. 编译模块
make

# 编译成功后会生成:
#   hello_imx6ull.ko    - 内核模块文件
#   hello_imx6ull.o     - 目标文件
#   hello_imx6ull.mod.o - 包含模块元信息的目标文件
#   Module.symvers      - 导出的符号列表
#   modules.order       - 模块顺序

# 3. 查看模块信息
modinfo hello_imx6ull.ko

# 4. 加载模块(使用默认参数)
sudo insmod hello_imx6ull.ko

# 5. 查看内核日志
dmesg | tail

# 6. 检查模块是否加载成功
lsmod | grep hello

# 7. 查看模块参数
cat /sys/module/hello_imx6ull/parameters/*
# 或者
systool -v -m hello_imx6ull

# 8. 卸载模块
sudo rmmod hello_imx6ull

# 9. 使用自定义参数加载
sudo insmod hello_imx6ull.ko count=3 name="Linux Learner"

七、常见错误、调试方法与内核报错解读

7.1 常见编译错误

错误1:找不到内核头文件

fatal error: linux/module.h: No such file or directory

原因KERNEL_DIR 路径不正确,或者内核源码没有配置。

解决

# 确认路径是否正确
ls $(KERNEL_DIR)/include/linux/module.h

# 如果内核源码没有配置,运行
cd $(KERNEL_DIR)
make imx_v6_defconfig    # 或其他合适的defconfig
make modules_prepare

错误2:缺少 Module.symvers

WARNING: Module version vmlinux not found
Symbol version dump
Module.symvers does not exist

原因:内核没有完整编译过。

解决

cd $(KERNEL_DIR)
make -j$(nproc)

7.2 常见加载错误

错误1:版本不匹配

hello_imx6ull: version magic '6.12.49 preempt mod_unload '
    should be '6.12.0 preempt mod_unload '
insmod: ERROR: could not insert module

原因:模块编译时使用的内核版本与运行时不同。

解决

# 在目标内核源码目录下重新编译模块
make clean
make

错误2:许可证问题

hello_imx6ull: module license 'Proprietary' taints kernel.
hello_imx6ull: module uses symbols from GPL-only module, refusing to load.

原因:模块许可证不是GPL兼容的,但使用了GPL符号。

解决:将 MODULE_LICENSE("Proprietary") 改为 MODULE_LICENSE("GPL")

错误3:未知符号

hello_imx6ull: Unknown symbol some_function (err -2)

原因:模块使用了一个不存在的内核符号。

解决:检查函数名是否拼写正确,或者该符号是否在当前内核配置下被导出。

7.3 调试技巧

技巧1:使用 dmesg 查看输出

# 实时查看内核日志
dmesg -w

# 过滤特定模块的消息
dmesg | grep hello_imx6ull

# 显示带时间戳的日志
dmesg -T

技巧2:使用 ftrace 跟踪函数

# 启用 ftrace
echo function > /sys/kernel/debug/tracing/current_tracer

# 只跟踪特定函数
echo 'do_init_module' > /sys/kernel/debug/tracing/set_ftrace_filter

# 查看跟踪结果
cat /sys/kernel/debug/tracing/trace

技巧3:使用 crash 分析 Oops

如果模块导致内核崩溃,生成的 Oops 信息可以用来分析问题:

[  123.456789] BUG: unable to handle page fault for address: 0xdeadbeef
[  123.456790] Internal error: Oops: 86000007 [#1] PREEMPT SMP ARM
[  123.456791] Modules linked in: hello_imx6ull(O+) ...
[  123.456792] CPU: 0 PID: 1234 Comm: insmod Tainted: G           O      6.12.49 ...
[  123.456793] Hardware name: Freescale i.MX6 Ultralite (Device Tree)
[  123.456794] PC is at hello_init+0x10/0x20 [hello_imx6ull]
[  123.456795] LR is at do_one_initcall+0x48/0x1b0

使用 addr2line 可以将地址转换为源码行号:

# 从模块中查找地址对应的行号
addr2line -e hello_imx6ull.o 0x10

7.4 内核 OOPS/panic 解读

当内核检测到严重错误时,会打印 Oops 或 panic 信息。理解这些信息对于调试非常重要:

类型 含义 系统状态
BUG() 内核断言失败 继续运行(但可能已损坏)
Oops 页面错误或非法访问 继续运行(但可能已损坏)
panic 致命错误,无法继续 系统停止运行

常见的 Oops 原因: 1. 空指针解引用:访问 NULL 指针 2. 用户空间指针:在内核空间访问用户空间地址 3. 栈溢出:递归过深或局部变量太大 4. 非法指令:CPU执行了无效指令


八、练习题与实战代码查看

练习题 1:带参数的模块

修改 hello_imx6ull 模块,添加以下功能:

  1. 添加一个 bool 类型的参数 verbose,控制是否输出详细日志
  2. 添加一个 int 类型的参数 delay,表示初始化时的延迟秒数
  3. verbose=true 时,打印更多调试信息

参考答案

static bool verbose = false;
module_param(verbose, bool, 0644);
MODULE_PARM_DESC(verbose, "Enable verbose output");

static int delay = 0;
module_param(delay, int, 0644);
MODULE_PARM_DESC(delay, "Delay in seconds before initialization");

static int __init hello_init(void)
{
    if (verbose) {
        pr_info("Verbose mode enabled\n");
        pr_info("Parameters: verbose=%d, delay=%d\n", verbose, delay);
        pr_info("Compile time: %s %s\n", __DATE__, __TIME__);
    }

    if (delay > 0) {
        pr_info("Sleeping for %d seconds...\n", delay);
        msleep(delay * 1000);  /* 注意:不能用 sleep()! */
    }

    pr_info("Hello, %s!\n", name);
    return 0;
}

练习题 2:查看内核源码中的模块实现

查找并阅读以下源码文件,理解它们是如何实现模块功能的:

  1. third_party/linux-imx/kernel/module/main.c - 模块加载/卸载的核心逻辑
  2. third_party/linux-imx/include/linux/module.h - 模块相关的数据结构和宏定义
  3. third_party/linux-imx/kernel/module/version.c - 版本检查机制

任务: - 在 main.c 中找到 SYSCALL_DEFINE3(finit_module, ...) 函数,理解它是如何被用户空间调用的 - 在 module.h 中找到 struct module 的完整定义,理解内核如何描述一个模块 - 在 version.c 中找到 check_version() 函数,理解CRC校验是如何工作的

练习题 3:查看系统中已加载的模块

在i.MX6ULL开发板上执行以下命令,理解输出内容:

# 1. 查看所有已加载的模块
lsmod

# 2. 查看特定模块的信息
modinfo gpio_keys

# 3. 查看模块的依赖关系
lsmod | grep -E "^Module|^gpio"
modprobe --show-depends gpio_keys

# 4. 查看 /sys/module 下的信息
ls /sys/module/gpio_keys/
cat /sys/module/gpio_keys/refcnt
cat /sys/module/gpio_keys/coresize
cat /sys/module/gpio_keys/taint

# 5. 查看 /proc/modules
cat /proc/modules | head

练习题 4:分析一个实际的驱动模块

选择一个简单的驱动模块(如 drivers/char/misc.cdrivers/gpio/gpiolib-sysfs.c),阅读它的源码:

  1. 找到 module_initmodule_exit
  2. 理解模块初始化时做了什么
  3. 理解模块清理时做了什么
  4. 找到模块导出的符号(EXPORT_SYMBOLEXPORT_SYMBOL_GPL

练习题 5:创建一个统计模块

编写一个内核模块,在 /proc 下创建一个文件,显示以下统计信息:

  1. 模块加载后的运行时间(秒)
  2. 模块被访问的次数(每次读取 /proc 文件时计数)
  3. 当前系统负载(从内核获取)

提示: - 使用 proc_create() 创建 /proc 文件 - 使用 jiffiesktime_get() 获取时间 - 使用 module_put()try_module_get() 管理引用计数


九、下一步学习建议

恭喜你!如果你读到了这里,说明你已经对内核模块有了基本的理解。下一步,你可以:

  1. 学习设备驱动基础:理解字符设备、块设备、网络设备的概念
  2. 学习并发控制:理解 spinlock、mutex、semaphore 等同步机制
  3. 学习内存管理:理解 kmalloc、vmalloc、slab 分配器的区别
  4. 实践项目:为你的i.MX6ULL板子编写一个实际的设备驱动

推荐继续阅读本教程系列: - 下一章:字符设备驱动入门 - 设备树基础与应用 - GPIO驱动实战 - I2C/SPI驱动开发


参考资料与延伸阅读

内核文档

  • Documentation/kbuild/modules.rst - 外部模块编译指南
  • Documentation/process/coding-style.rst - 内核编码风格
  • Documentation/core-api/kobject.rst - kobject 和 sysfs
  • Documentation/driver-api/ - 驱动开发API文档

推荐书籍

  1. "Linux Device Drivers, 3rd Edition" - Jonathan Corbet 等
  2. "Understanding the Linux Kernel, 3rd Edition" - Daniel P. Bovet
  3. "Linux Kernel Development, 3rd Edition" - Robert Love

在线资源

  1. Linux Kernel Newbies - 内核新手社区
  2. The Linux Kernel Module Programming Guide - 模块编程指南
  3. Linux Cross Reference - lxr.linux.no - 在线源码浏览

作者: IMX-Forge 项目组 最后更新: 2026年3月 内核版本: 6.12.49 (linux-imx)