Bootstrap

Linux内核入门(一)Hello World 驱动程序


前言

注意:建议在虚拟机上另外安装一个 linux 操作系统进行开发与调试,本文旨在技术分享与探讨,并不承担任何风险。
本文主要探讨如何在 linux下编写一个Hello World入门级别的驱动程序以及演示驱动程序的加载、卸载以及调试的方法与步骤,由于笔者现在手头上没有开发板因此先暂时在ubuntu 22.04 server上进行演示。


一、准备工作

1.1 安装内核头文件

sudo apt-get update
sudo apt-get install build-essential linux-headers-$(uname -r)

1.2 编写驱动程序代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

#define HELLO_WORLD_DEBUG
#undef PDEBUG             /* undef it, just in case */

#ifdef HELLO_WORLD_DEBUG
    #ifdef __KERNEL__
     /* This one if debugging is on, and kernel space */
        #define PDEBUG(level, fmt, args...) printk(level fmt, ## args)
    #else
     /* This one for user space */
        #define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
    #endif
#else
    #define PDEBUG(level, fmt, args...) /* not debugging: nothing */
#endif

static int number = 10;

static int __init hello_init(void) {
    //printk(KERN_WARNING "Hello, world!\n");
    PDEBUG(KERN_WARNING, "number:%d, Hello, world !\n", number);
    number--;
    return 0;
}

static void __exit hello_exit(void) {
    //printk(KERN_WARNING "Goodbye, world!\n");
    PDEBUG(KERN_WARNING, "number:%d, Goodbye, world !\n", number);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("JeffChong");
MODULE_DESCRIPTION("A simple Hello World driver");
MODULE_VERSION("1.0");

1.3 编写Makefile

obj-m += hello_world.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

1.4 编译驱动程序

jeff@jeff:~/jeffPro/kernel/hello_world$ ls
hello_world.c  Makefile
jeff@jeff:~/jeffPro/kernel/hello_world$ make
make -C /lib/modules/5.15.0-112-generic/build M=/home/jeff/jeffPro/kernel/hello_world modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-112-generic'
  CC [M]  /home/jeff/jeffPro/kernel/hello_world/hello_world.o
  MODPOST /home/jeff/jeffPro/kernel/hello_world/Module.symvers
  CC [M]  /home/jeff/jeffPro/kernel/hello_world/hello_world.mod.o
  LD [M]  /home/jeff/jeffPro/kernel/hello_world/hello_world.ko
  BTF [M] /home/jeff/jeffPro/kernel/hello_world/hello_world.ko
Skipping BTF generation for /home/jeff/jeffPro/kernel/hello_world/hello_world.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-112-generic'
jeff@jeff:~/jeffPro/kernel/hello_world$ ls
hello_world.c   hello_world.mod    hello_world.mod.o  Makefile       Module.symvers
hello_world.ko  hello_world.mod.c  hello_world.o      modules.order
jeff@jeff:~/jeffPro/kernel/hello_world$

二、驱动程序代码分析

该驱动程序是一个简单的 “Hello World” 内核驱动模块。它包括模块的初始化和卸载函数,以及一些宏定义来描述模块的元数据。

2.1 头文件

#include <linux/module.h>:
#include <linux/kernel.h>:
#include <linux/init.h>:

module.h: 包含与模块相关的宏和函数声明。
kernel.h: 包含内核的一些基本定义和函数声明。
init.h: 包含初始化和清理函数相关的宏。

2.2 模块初始化函数

static int __init hello_init(void) {
    //printk(KERN_WARNING "Hello, world!\n");
    PDEBUG(KERN_WARNING, "number:%d, Hello, world !\n", number);
    number--;
    return 0;
}

__init 宏表示此函数仅在初始化时使用,之后会被丢弃以释放内存。
printk() 表示向内核日志打印一条消息,KERN_WARNING表示消息的级别。
PDEBUG 宏用于更灵活的调试打印。

2.3 模块退出函数

static void __exit hello_exit(void) {
    //printk(KERN_WARNING "Goodbye, world!\n");
    PDEBUG(KERN_WARNING, "number:%d, Goodbye, world !\n", number);
}

__exit 宏表示此函数仅在模块卸载时使用。

2.4 模块初始化和清理宏

module_init(hello_init);
module_exit(hello_exit);

module_initmodule_exit 是两个宏,分别用来声明驱动模块初始化和卸载函数。

2.5 模块元数据

MODULE_LICENSE("GPL");
MODULE_AUTHOR("JeffChong");
MODULE_DESCRIPTION("A simple Hello World driver");
MODULE_VERSION("1.0");

MODULE_LICENSE 宏声明模块的许可证为 GPL(GNU General Public License),确保模块符合集成到 Linux 内核中的开源许可证要求。
MODULE_AUTHOR 宏指定模块的作者。
MODULE_DESCRIPTION 宏对模块的简要描述。
MODULE_VERSION 宏指定模块的版本号。

三、加载、卸载驱动程序

3.1 加载驱动程序

使用指令 sudo insmod hello_world.ko 加载驱动程序 (注意要以超级用户的权限加载) 可以看到驱动程序已经加载到内核中了

jeff@jeff:~/jeffPro/kernel/hello_world$ sudo insmod hello_world.ko
jeff@jeff:~/jeffPro/kernel/hello_world$ lsmod | grep hello_world
hello_world            16384  0

使用指令 tail -f /var/log/syslogsudo dmesg -w 可以实时监控内核日志信息的打印

jeff@jeff:~$ tail -f /var/log/syslog
Jun 30 07:41:27 jeff systemd[1]: Starting Daily apt upgrade and clean activities...
Jun 30 07:41:36 jeff systemd[1]: apt-daily-upgrade.service: Deactivated successfully.
Jun 30 07:41:36 jeff systemd[1]: Finished Daily apt upgrade and clean activities.
Jun 30 07:41:36 jeff systemd[1]: apt-daily-upgrade.service: Consumed 7.268s CPU time.
Jun 30 07:48:04 jeff systemd[1]: Started Session 3 of User jeff.
Jun 30 07:48:04 jeff systemd[1]: Started Session 4 of User jeff.
Jun 30 07:49:03 jeff systemd[1]: Started Session 5 of User jeff.
Jun 30 07:49:04 jeff systemd[1]: Started Session 6 of User jeff.
Jun 30 07:49:40 jeff systemd[1]: Started Session 7 of User jeff.
Jun 30 07:49:40 jeff systemd[1]: Started Session 8 of User jeff.
Jun 30 07:50:26 jeff kernel: [ 1521.664186] hello_world: loading out-of-tree module taints kernel.
Jun 30 07:50:26 jeff kernel: [ 1521.664233] hello_world: module verification failed: signature and/or required key missing - tainting kernel
Jun 30 07:50:26 jeff kernel: [ 1521.666424] number:10, Hello, world !
Jun 30 07:50:47 jeff kernel: [ 1542.553050] number:9, Goodbye, world !

3.1.1 遇到的问题

我们留意到内核打印的日志消息中有这么一个警告:

hello_world: loading out-of-tree module taints kernel.
hello_world: module verification failed: signature and/or required key missing - tainting kernel

污染内核: taints kernel 表明加载一个树之外的模块可能会污染内核环境。即存在未经认证的代码,这通常用于帮助开发者和系统管理员识别潜在的不稳定性或兼容性问题。

模块验证失败: 我们编译的模块是外部模块(不是由内核源代码树内直接编译的模块),因此,未能通过内核的签名验证。现代Linux内核(尤其是有安全强化的内核)可能会要求加载的模块有一个有效的数字签名,这个签名用于确保模块未被篡改并且是由受信任的源编译的。

3.1.2 解决的方法

在开发环境中可以临时禁用模块签名验证,但是这样做的话会降低系统的安全性,只适合做开发测试,而且这个方法需要重新配置并编译内核,因此在这里不适用。

为了在生产环境中加载模块而不显示此警告,最好为该模块进行签名。在使用该方法前先安装一下 openssl,具体的安装方法这里就不再说了。

以下是如何为驱动模块进行签名的基本步骤:

1. 生成签名密钥

openssl req -new -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=Module Signing/"

2. 创建PKCS#12文件

openssl pkcs12 -export -out sign-key.p12 -inkey key.pem -in cert.pem -password pass:yourpassword

3. 使用内核提供的 sign-file 工具签名

/lib/modules/$(uname -r)/build/scripts/sign-file sha256 key.pem cert.pem hello_world.ko

4. 在内核中添加签名密钥(如果使用自定义密钥)
将 cert.pem 拷贝到 /lib/modules/$(uname -r)/build/certs 并更新内核信任:

sudo cp cert.pem /lib/modules/$(uname -r)/build/certs/
sudo update-initramfs -u

5. 加载签名后的模块

sudo insmod hello_world.ko

至此再查看一下内核的打印信息已经没有警告了

jeff@jeff:~$ tail -f /var/log/syslog
Jun 30 07:41:27 jeff systemd[1]: Starting Daily apt upgrade and clean activities...
Jun 30 07:41:36 jeff systemd[1]: apt-daily-upgrade.service: Deactivated successfully.
Jun 30 07:41:36 jeff systemd[1]: Finished Daily apt upgrade and clean activities.
Jun 30 07:41:36 jeff systemd[1]: apt-daily-upgrade.service: Consumed 7.268s CPU time.
Jun 30 07:48:04 jeff systemd[1]: Started Session 3 of User jeff.
Jun 30 07:48:04 jeff systemd[1]: Started Session 4 of User jeff.
Jun 30 07:49:03 jeff systemd[1]: Started Session 5 of User jeff.
Jun 30 07:49:04 jeff systemd[1]: Started Session 6 of User jeff.
Jun 30 07:49:40 jeff systemd[1]: Started Session 7 of User jeff.
Jun 30 07:49:40 jeff systemd[1]: Started Session 8 of User jeff.
Jun 30 07:50:26 jeff kernel: [ 1521.664186] hello_world: loading out-of-tree module taints kernel.
Jun 30 07:50:26 jeff kernel: [ 1521.664233] hello_world: module verification failed: signature and/or required key missing - tainting kernel
Jun 30 07:50:26 jeff kernel: [ 1521.666424] number:10, Hello, world !
Jun 30 07:50:47 jeff kernel: [ 1542.553050] number:9, Goodbye, world !
Jun 30 07:53:29 jeff kernel: [ 1704.978089] number:10, Hello, world !
Jun 30 08:13:27 jeff kernel: [ 2903.171240] number:9, Goodbye, world !

3.2 卸载驱动程序

卸载驱动程序使用指令 sudo rmmod hello_world

jeff@jeff:~/jeffPro/kernel/hello_world$ lsmod | grep hello_world
hello_world            16384  0
jeff@jeff:~/jeffPro/kernel/hello_world$ sudo rmmod hello_world
jeff@jeff:~/jeffPro/kernel/hello_world$ lsmod | grep hello_world
jeff@jeff:~/jeffPro/kernel/hello_world$

四、调试驱动程序

关于驱动程序的调试方法先简单介绍一下 hello world 级别的调试方法,后续再专门写一篇文章详细介绍一下其他的调试方法。

4.1 使用 printk 函数打印调试信息

在驱动程序中使用 printk 函数打印调试信息,例如:

printk(KERN_WARNING"Hello , world !\n");

printk 函数的功能跟 printf 函数是一样的,不同之处在于 printk 函数有日志级别的划分,可以在打印字符串前面加上一个宏用于设置该消息的日志级别,例如上面例子中的 KERN_WARNING 。

4.1.1 查看调试信息

使用指令 sudo dmesg -w 或者 tail -f /var/log/syslog 可以实时观察内核信息的打印

jeff@jeff:~$ tail -f /var/log/syslog
Jun 30 07:41:27 jeff systemd[1]: Starting Daily apt upgrade and clean activities...
Jun 30 07:41:36 jeff systemd[1]: apt-daily-upgrade.service: Deactivated successfully.
Jun 30 07:41:36 jeff systemd[1]: Finished Daily apt upgrade and clean activities.
Jun 30 07:41:36 jeff systemd[1]: apt-daily-upgrade.service: Consumed 7.268s CPU time.
Jun 30 07:48:04 jeff systemd[1]: Started Session 3 of User jeff.
Jun 30 07:48:04 jeff systemd[1]: Started Session 4 of User jeff.
Jun 30 07:49:03 jeff systemd[1]: Started Session 5 of User jeff.
Jun 30 07:49:04 jeff systemd[1]: Started Session 6 of User jeff.
Jun 30 07:49:40 jeff systemd[1]: Started Session 7 of User jeff.
Jun 30 07:49:40 jeff systemd[1]: Started Session 8 of User jeff.
Jun 30 07:50:26 jeff kernel: [ 1521.664186] hello_world: loading out-of-tree module taints kernel.
Jun 30 07:50:26 jeff kernel: [ 1521.664233] hello_world: module verification failed: signature and/or required key missing - tainting kernel
Jun 30 07:50:26 jeff kernel: [ 1521.666424] number:10, Hello, world !
Jun 30 07:50:47 jeff kernel: [ 1542.553050] number:9, Goodbye, world !
Jun 30 07:53:29 jeff kernel: [ 1704.978089] number:10, Hello, world !
Jun 30 08:13:27 jeff kernel: [ 2903.171240] number:9, Goodbye, world !

4.1.2 内核日志消息级别宏定义分析

KERN_WARNING 是一个在内核日志系统中用于指定日志消息级别的宏,它的定义位于内核头文件目录中 include/linux/kern_levels.h 处。

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __KERN_LEVELS_H__
#define __KERN_LEVELS_H__

#define KERN_SOH        "\001"          /* ASCII Start Of Header */
#define KERN_SOH_ASCII  '\001'

#define KERN_EMERG      KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT      KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT       KERN_SOH "2"    /* critical conditions */
#define KERN_ERR        KERN_SOH "3"    /* error conditions */
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions */
#define KERN_NOTICE     KERN_SOH "5"    /* normal but significant condition */
#define KERN_INFO       KERN_SOH "6"    /* informational */
#define KERN_DEBUG      KERN_SOH "7"    /* debug-level messages */

#define KERN_DEFAULT    ""              /* the default kernel loglevel */

我们可以看到内核的日志消息分为8个级别,数字越小的级别越高比如 KERN_EMERG 的级别最高,KERN_DEBUG 的级别最低。
我们可以使用指令 cat /proc/sys/kernel/printk 来查看一下当前系统内核日志优先级别的相关配置情况。

jeff@jeff:/usr/src$ cat /proc/sys/kernel/printk
4       4       1       7

以下是依次对这四个数字的详细说明:

console_loglevel: 这是当前的控制台日志级别。内核会将优先级低于或等于这个级别的消息发送到控制台(syslog日志文件或内核日志文件)。数字为 4 表示 KERN_WARNING 级别及更高级别的消息(KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING)将被输出到控制台。

default_message_loglevel: 这是默认的消息日志级别。任何没有明确指定日志级别的内核消息将被赋予这个优先级。数字为 4 表示如果调用 printk 函数时没有指定日志级别将被默认设置其日志级别为 KERN_WARNING 。

minimum_console_loglevel: 这是可设置的最低的控制台日志级别。

default_console_loglevel: 这是控制台日志级别的缺省值。如果没有设置控制台日志级别的话将会使用这个缺省值。也就是说原来的第一个数字 4 的缺省值是 7。

简而言之我们只需要关心第一个数字也就是 console_loglevel 的级别就行了。数字为 4 表示 KERN_WARNING 级别及更高级别的消息将被输出到控制台。

我们可以通过指令如 echo 7 > /proc/sys/kernel/printk 来修改当前控制台日志级别为 7 ,即 KERN_INFO。

经测试我发现当我的控制台日志级别设置为 KERN_WARNING 时 ,设置的 KERN_INFO 级别的日志消息也能打印到控制台,具体原因暂时并没有查明。

4.2 使用 printk 函数调试驱动程序存在的不足

试想一下假如我们在进行一个复杂的驱动开发,为了调试会在源码中加入成百上千的 printk 语句,而当调试完毕形成最终产品的时候必然要将这些 printk 语句删除或者注释掉,当产品有问题了需要调试的时候又得把他们都打开,这样岂不是很麻烦?

4.3 如何优雅地解决使用 printk 函数在日常开发与维护之间切换的问题

我们可以通过在驱动中定义一些宏来解决这个问题

#define HELLO_WORLD_DEBUG
#undef PDEBUG             /* undef it, just in case */

#ifdef HELLO_WORLD_DEBUG
    #ifdef __KERNEL__
     /* This one if debugging is on, and kernel space */
        #define PDEBUG(level, fmt, args...) printk(level fmt, ## args)
    #else
     /* This one for user space */
        #define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
    #endif
#else
    #define PDEBUG(level, fmt, args...) /* not debugging: nothing */
#endif

#define PDEBUG(level, fmt, args…) 这行代码的意思就是定义 PDEBUG 为空,也就是无。
这样当我们不需要调试的时候只需要把 #define HELLO_WORLD_DEBUG 注释掉就行了。

总结

千里之行,始于足下。linux内核的学习之路才正开始呢。其实无论是应用程序的开发还是内核驱动的开发,最基本的东西只要掌握好了那后面的路才会走得踏实。纸上得来终觉浅,绝知此事要躬行,加油吧,少年!共勉。

;