首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【Linux内核模块】模块的编译:从代码到可加载模块的 “变身术“

【Linux内核模块】模块的编译:从代码到可加载模块的 “变身术“

作者头像
用户12001910
发布2026-01-21 19:50:55
发布2026-01-21 19:50:55
110
举报

如果你刚开始学内核模块开发,可能会被编译过程搞得一头雾水 —— 为什么不能像编译普通 C 程序那样用 gcc 直接编译?内核模块的编译到底特殊在哪里?今天咱们就从最基础的 Makefile 写起,一步步掌握从代码到可加载模块的 "变身术"。


一、内核模块编译的特殊性:为什么不能直接用 gcc?

普通 C 程序编译很简单,gcc hello.c -o hello就行,但内核模块可不行。这就像做面包和做蛋糕的区别 —— 虽然都是面粉做的,但烤箱温度、配料比例完全不同。

1.1 内核模块的 "特殊身份"

内核模块不是独立程序,而是要嵌入到内核中的 "插件",意味着:

  • 必须使用与内核完全一致的编译选项(比如字节序、对齐方式)
  • 不能依赖标准 C 库(glibc),只能用内核提供的函数(如 printk)
  • 必须与内核版本严格匹配(不同内核版本的头文件差异很大)

1.2 举个直观的例子

如果你用系统默认的 gcc 编译模块,会得到类似这样的错误:

代码语言:javascript
复制
error: 'printk' undeclared (first use in this function)

这不是因为 printk 不存在,而是因为没有正确引入内核头文件和编译选项。

1.3 内核模块编译的三大要素

二、编译前的准备:搭建 "工作台"

在开始编译前,需要准备好必要的工具和环境,就像做蛋糕前要准备好烤箱和原料。

2.1 安装内核开发包

最关键的是要安装与当前内核版本匹配的内核源码或开发包:

Ubuntu/Debian 系统

代码语言:javascript
复制
# 查看当前内核版本
uname -r
# 安装对应版本的内核开发包
sudo apt-get install linux-headers-$(uname -r)

CentOS/RHEL 系统

代码语言:javascript
复制
sudo yum install kernel-devel-$(uname -r)

这些包会安装编译模块所需的头文件和配置文件,通常放在/lib/modules/$(uname -r)/build目录下。

2.2 准备测试代码

创建一个简单的测试模块hello.c

代码语言:javascript
复制
#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void) {
    printk(KERN_INFO "Hello, 内核模块世界!\n");
    return 0;
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, 内核模块世界!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("一个简单的测试模块");

这个模块加载时会打印一句欢迎语,卸载时打印告别语。

三、核心工具:Makefile 的写法

内核模块的编译完全依赖 Makefile,这是整个过程的 "指挥中心"。一个最简单的内核模块 Makefile 只有几行,但每一行都有特殊含义。

3.1 最基础的 Makefile

创建文件Makefile(注意首字母大写):

代码语言:javascript
复制
# 声明要编译的模块
obj-m += hello.o

# 内核源码树路径(自动获取当前系统内核)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build

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

# 默认编译目标
default:
    # 进入内核源码树,执行编译
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

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

  • obj-m += hello.o:告诉内核构建系统,要编译一个名为hello.ko的模块(.o文件会被链接成.ko模块)
  • KERNELDIR:指定内核源码树的位置,/lib/modules/$(uname -r)/build是标准位置
  • PWD:记录当前目录路径,让内核构建系统知道模块代码在哪里
  • (MAKE) -C (KERNELDIR) M=(PWD) modules:这是核心命令,意思是: -C (KERNELDIR):进入内核源码目录M=(PWD):告诉内核构建系统,模块源码在当前目录modules:执行内核模块编译目标

3.2 编译模块

在终端执行make命令:

代码语言:javascript
复制
make

如果一切顺利,会看到类似这样的输出:

代码语言:javascript
复制
make -C /lib/modules/5.4.0-100-generic/build M=/home/user/kernel-modules modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-100-generic'
  CC [M]  /home/user/kernel-modules/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/user/kernel-modules/hello.mod.o
  LD [M]  /home/user/kernel-modules/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-100-generic'

编译成功后,目录下会生成几个文件:

  • hello.ko:最终可加载的内核模块(最重要的文件)
  • hello.o:编译产生的目标文件
  • hello.mod.c:模块依赖信息(自动生成)
  • hello.mod.o:模块依赖目标文件
  • modules.orderModule.symvers:模块顺序和符号信息

四、多文件模块编译:如何编译多个源代码文件

当模块代码复杂时,通常会分成多个文件。比如我们有main.chelper.c两个文件,该如何编译呢?

4.1 多文件模块的 Makefile

只需将多个文件合并成一个模块名:

代码语言:javascript
复制
# 将main.o和helper.o合并成一个demo.ko模块
obj-m += demo.o
demo-objs := main.o helper.o  # 指定组成demo.ko的目标文件

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

关键是demo-objs := main.o helper.o,它告诉构建系统:demo.komain.ohelper.o链接而成。

4.2 多文件模块的代码结构

main.c(主文件):

代码语言:javascript
复制
#include <linux/init.h>
#include <linux/module.h>
#include "helper.h"  // 包含辅助函数头文件

static int __init demo_init(void) {
    printk(KERN_INFO "主模块初始化\n");
    helper_function();  // 调用辅助函数
    return 0;
}

static void __exit demo_exit(void) {
    printk(KERN_INFO "主模块退出\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");

helper.c(辅助文件):

代码语言:javascript
复制
#include <linux/kernel.h>
#include "helper.h"

void helper_function(void) {
    printk(KERN_INFO "这是辅助函数\n");
}

helper.h(头文件):

代码语言:javascript
复制
#ifndef _HELPER_H_
#define _HELPER_H_

void helper_function(void);

#endif

执行make后,会生成demo.ko模块,包含了两个文件的功能。

五、编译选项的高级配置:自定义编译规则

有时需要添加额外的编译选项,比如警告级别、宏定义等,这可以通过EXTRA_CFLAGS实现。

5.1 添加编译警告选项

让编译器更严格地检查代码:

代码语言:javascript
复制
obj-m += hello.o
EXTRA_CFLAGS += -Wall -Wextra  # 开启更多警告

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

-Wall开启基本警告,-Wextra开启额外警告,帮助发现潜在问题。

5.2 定义宏常量

在编译时传递宏定义,控制代码条件编译:

代码语言:javascript
复制
obj-m += debug_module.o
EXTRA_CFLAGS += -DDEBUG=1  # 定义DEBUG宏,值为1

# 或者根据条件定义
ifdef DEBUG
EXTRA_CFLAGS += -DDEBUG=1
endif

在代码中可以这样使用:

代码语言:javascript
复制
#ifdef DEBUG
printk(KERN_DEBUG "调试信息:变量x的值为%d\n", x);
#endif

编译时如果定义了DEBUG=1,就会输出调试信息。

5.3 包含额外头文件目录

如果模块需要引用其他目录的头文件:

代码语言:javascript
复制
obj-m += mymodule.o
EXTRA_CFLAGS += -I$(PWD)/include  # 包含当前目录下的include子目录

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

六、交叉编译:为其他平台编译模块

有时需要为嵌入式设备编译模块(比如 ARM 架构),这就需要交叉编译。

1. 安装交叉编译工具链

以 ARM 架构为例,在 Ubuntu 上安装:

代码语言:javascript
复制
sudo apt-get install gcc-arm-linux-gnueabihf

2. 交叉编译的 Makefile

代码语言:javascript
复制
obj-m += hello_arm.o

# 目标架构的内核源码路径
KERNELDIR := /path/to/arm-linux-kernel
# 交叉编译器前缀
CROSS_COMPILE := arm-linux-gnueabihf-
# 当前目录
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) modules

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

  • ARCH=arm:指定目标架构为 ARM
  • CROSS_COMPILE=arm-linux-gnueabihf-:指定交叉编译器前缀

执行make后,会生成能在 ARM 设备上运行的hello_arm.ko模块。

七、常见编译错误及解决方法

即使最简单的模块,也可能遇到编译错误。这里列举几个最常见的问题及解决办法。

7.1 "没有规则可制作目标" 错误

代码语言:javascript
复制
make: *** 没有规则可制作目标“modules”。 停止。

原因:内核源码树路径错误或未安装内核开发包。 解决

  • 检查KERNELDIR是否指向正确的内核源码目录
  • 确保已安装linux-headers-$(uname -r)

7.2 "隐式声明函数" 错误

代码语言:javascript
复制
error: implicit declaration of function ‘printk’ [-Werror=implicit-function-declaration]

原因:忘记包含必要的头文件。 解决:在代码开头添加#include <linux/kernel.h>(printk 的声明在这个头文件中)。

7.3 "未声明的标识符" 错误

代码语言:javascript
复制
error: ‘KERN_INFO’ undeclared (first use in this function)

原因KERN_INFO等日志级别宏定义在linux/kern_levels.h中,而该文件通常通过linux/kernel.h间接包含。 解决:添加#include <linux/kernel.h>

7.4 "许可证污染" 警告

代码语言:javascript
复制
WARNING: modpost: missing MODULE_LICENSE() in /home/user/hello.o

原因:模块未声明许可证。 解决:在代码中添加MODULE_LICENSE("GPL");(或其他合法许可证)。

7.5 版本不匹配错误

代码语言:javascript
复制
WARNING: "some_function" [/home/user/mymodule.ko] undefined!

原因:模块使用的内核函数在当前内核版本中不存在或未导出。 解决

  • 检查函数是否存在于当前内核版本
  • 确认函数是否被EXPORT_SYMBOL导出
  • 可能需要根据内核版本调整代码

八、模块编译后的验证:确保模块可用

编译出.ko文件后,还需要验证模块是否可以正常加载到内核中。

1. 加载模块

代码语言:javascript
复制
sudo insmod hello.ko

2. 检查模块是否加载成功

代码语言:javascript
复制
lsmod | grep hello

如果输出类似hello 16384 0,说明模块加载成功。

3. 查看模块输出日志

代码语言:javascript
复制
dmesg | tail

会看到模块初始化函数打印的信息:Hello, 内核模块世界!

4. 卸载模块

代码语言:javascript
复制
sudo rmmod hello

再次查看日志:dmesg | tail,会看到退出函数的输出:Goodbye, 内核模块世界!

如果这几步都没问题,说明编译的模块是正常可用的。


内核模块的编译虽然看起来复杂,但核心原理很简单:遵循内核的编译规则,使用内核提供的工具链,确保与内核版本匹配

掌握模块编译需要记住三个关键点:

  1. 必须有匹配的内核头文件和开发包
  2. 正确编写 Makefile,指定模块名和内核源码路径
  3. 理解模块编译与普通程序编译的区别(无标准库、特殊链接方式)

刚开始可能会被各种错误信息搞得头疼,但只要多练习几次,熟悉了常见问题的解决方法,你会发现 —— 编译内核模块其实就那么几步固定操作。就像学骑自行车,一开始觉得难,熟练后就成了本能。

编译过程:

附录:万能Makefile模板

代码语言:javascript
复制
# 目标模块名
obj-m += ultimate_module.o

# 多文件支持
ultimate_module-objs := main.o utils.o

# 自定义源文件目录
src_dir := src
obj_dir := build

# 自动收集源文件
srcs := $(wildcard $(src_dir)/*.c)
objs := $(patsubst $(src_dir)/%.c,$(obj_dir)/%.o,$(srcs))

# 内核目录
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build

# 编译规则
all:
    @mkdir -p $(obj_dir)
    make -C $(KERNEL_DIR) M=$(PWD) modules

clean:
    make -C $(KERNEL_DIR) M=$(PWD) clean
    rm -rf $(obj_dir)

# 版本信息注入
EXTRA_CFLAGS += -DBUILD_TIMESTAMP=\"$(shell date +%Y-%m-%dT%H:%M:%S%z)\"

下一篇文章,我们将学习内核模块的调试技巧,看看如何解决模块加载后的运行时问题。如果你在编译过程中遇到了其他问题,欢迎在评论区留言讨论!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、内核模块编译的特殊性:为什么不能直接用 gcc?
    • 1.1 内核模块的 "特殊身份"
    • 1.2 举个直观的例子
    • 1.3 内核模块编译的三大要素
  • 二、编译前的准备:搭建 "工作台"
    • 2.1 安装内核开发包
    • 2.2 准备测试代码
  • 三、核心工具:Makefile 的写法
    • 3.1 最基础的 Makefile
    • 3.2 编译模块
  • 四、多文件模块编译:如何编译多个源代码文件
    • 4.1 多文件模块的 Makefile
    • 4.2 多文件模块的代码结构
  • 五、编译选项的高级配置:自定义编译规则
    • 5.1 添加编译警告选项
    • 5.2 定义宏常量
    • 5.3 包含额外头文件目录
  • 六、交叉编译:为其他平台编译模块
  • 七、常见编译错误及解决方法
    • 7.1 "没有规则可制作目标" 错误
    • 7.2 "隐式声明函数" 错误
    • 7.3 "未声明的标识符" 错误
    • 7.4 "许可证污染" 警告
    • 7.5 版本不匹配错误
  • 八、模块编译后的验证:确保模块可用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档