0%

Linux-Lab1-Kernel modules

Kernel modules

  • 创建简单模块
  • 描述内核模块编译的过程
  • 介绍如何将模块与内核一起使用
  • 简单的内核调试方法

An example of a kernel module

下面是一个非常简单的内核模块示例:(源代码在 linux/tools/labs/skels/kernel_modules/1-2-test-mod/hello_mod.c 文件中)

  • 当加载到内核中时,它将生成消息 “Hello”
  • 卸载内核模块时,将生成消息 “Goodbye”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

MODULE_DESCRIPTION("Simple module");
MODULE_AUTHOR("Kernel Hacker");
MODULE_LICENSE("GPL");

static int my_hello_init(void)
{
pr_debug("Hello!\n");
return 0;
}

static void hello_exit(void)
{
pr_debug("Goodbye!\n");
}

module_init(my_hello_init);
module_exit(hello_exit);
  • 生成的消息不会显示在控制台上,但会保存在为此专门保留的内存区域中,日志记录守护程序 (syslog) 将从中提取这些消息
  • 要显示内核消息,可以使用 dmesg 命令或检查日志

Compiling kernel modules

编译内核模块不同于编译用户程序:

  • 首先,应使用其他标头(#include<>
  • 此外,模块不应链接到库
  • 最后,必须使用与加载模块的内核相同的选项来编译模块

由于上述原因,有一种标准方法用于编译内核,这种方法需要两个文件 Makefile Kbuild

Makefile

1
2
3
4
5
6
KDIR = /lib/modules/`uname -r`/build

kbuild:
make -C $(KDIR) M=`pwd`
clean:
make -C $(KDIR) M=`pwd` clean
  • /lib/modules 里面是内核模块,具有用于构建源代码的软链接
  • “-C” 选项的作用是:
    • 指将当前工作目录转移到你所指定的位置:/lib/modules/`uname -r`/build
  • “M=” 选项的作用是:
    • 当用户需要以某个内核为基础编译一个外部模块的话,需要在命令中加入 M=dir
    • 程序会自动到你所指定的 dir 目录中查找模块源码,将其编译,生成KO文件
  • 先设置当前工作目录为 KDIR(为了使用其“用于构建源代码的软链接”),然后在 kbuild 文件中寻找模块源码,最后使用 KDIR 中的软链接进行编译

Kbuild

1
2
3
ccflags-y = -Wno-unused-function -Wno-unused-label -Wno-unused-variable -DDEBUG

obj-m = hello_mod.o
  • Kbuild 中具有具体的编译参数,并且会指定模块源码的名称

直接使用 make 命令就可以编译:

1
2
3
4
5
6
7
8
9
10
➜  1-2-test-mod git:(master) ✗ make      
make -C /lib/modules/`uname -r`/build M=`pwd`
make[1]: 进入目录“/usr/src/linux-headers-5.15.0-48-generic”
CC [M] /home/yhellow/linux/tools/labs/skels/kernel_modules/1-2-test-mod/hello_mod.o
MODPOST /home/yhellow/linux/tools/labs/skels/kernel_modules/1-2-test-mod/Module.symvers
CC [M] /home/yhellow/linux/tools/labs/skels/kernel_modules/1-2-test-mod/hello_mod.mod.o
LD [M] /home/yhellow/linux/tools/labs/skels/kernel_modules/1-2-test-mod/hello_mod.ko
BTF [M] /home/yhellow/linux/tools/labs/skels/kernel_modules/1-2-test-mod/hello_mod.ko
Skipping BTF generation for /home/yhellow/linux/tools/labs/skels/kernel_modules/1-2-test-mod/hello_mod.ko due to unavailability of vmlinux
make[1]: 离开目录“/usr/src/linux-headers-5.15.0-48-generic”

不过这是内核模块的标准编译方式,使用了本机的 /lib/modules/

  • 如果想编译用于虚拟机的内核模块,则需要对 KDIR 作出修改:
1
KDIR = /home/yhellow/linux

如果编译时需要使用多个子模块,就使用如下 Kbuild 示例:(示例文件在 linux/tools/labs/skels/kernel_modules/4-multi-mod 目录中)

1
2
3
4
ccflags-y = -Wno-unused-function -Wno-unused-label -Wno-unused-variable

obj-m = supermodule.o
supermodule-y = mod1.o mod2.o

Loading/UnLoading a kernel module

要装入内核模块,使用 insmod 命令

要从内核中卸载模块,使用 rmmod 命令

1
2
insmod module.ko
rmmod module.ko
  • 加载内核模块时,将执行指定为宏参数的例程
  • 同样,当卸载模块时,将执行指定为 参数的例程

加载/卸载内核模块的完整示例如下所示:

1
2
3
4
5
➜  1-2-test-mod git:(master) ✗ sudo insmod hello_mod.ko
➜ 1-2-test-mod git:(master) ✗ dmesg | tail -1
[ 1817.709484] Hello!
➜ 1-2-test-mod git:(master) ✗ ls /sys/module | grep hello
hello_mod
1
2
3
4
5
➜  1-2-test-mod git:(master) ✗ sudo rmmod hello_mod.ko 
➜ 1-2-test-mod git:(master) ✗ dmesg | tail -2
[ 1817.709484] Hello!
[ 1943.965668] Goodbye!
➜ 1-2-test-mod git:(master) ✗ ls /sys/module | grep hello

Kernel Module Debugging

对内核模块进行故障排除比调试常规程序要复杂得多:

  • 内核模块中的错误可能导致阻塞整个系统
  • 故障排除速度大大减慢

为避免重新启动,建议使用虚拟机(qemu, virtualbox, vmware

当包含错误的模块插入内核时,它最终会生成一个 kernel oops(内核警告):

  • kernel oops 源自于内核检测到的无效操作,只能由内核生成
  • 出现 kernel oops 后,内核将继续进行工作
  • kernel oops 将会作为一个消息,内核生成的消息被保存在日志中,可以使用 dmesg 命令显示

为了确保没有内核消息丢失,建议直接从控制台插入测试内核,或定期检查内核消息(值得注意的是,由于编程错误,也可能因硬件错误而发生 kernel oops)

相对应的,如果内核发生致命错误,则会产生 kernel panic(内核崩溃):

  • 出现 kernel panic 后,系统无法返回到稳定状态

看看下面的内核模块,其中包含一个生成 kernel oops 的错误:(源代码在 linux/tools/labs/skels/kernel_modules/5-oops-mod/oops_mod.c 文件中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/slab.h>

MODULE_DESCRIPTION("Oops generating module");
MODULE_AUTHOR("So2rul Esforever");
MODULE_LICENSE("GPL");

static int my_oops_init(void)
{
char *p = 0;

pr_info("before init\n");
*p = 'a'; /* 空指针赋值 */
pr_info("after init\n");

return 0;
}

static void my_oops_exit(void)
{
pr_info("module goes all out\n");
}

module_init(my_oops_init);
module_exit(my_oops_exit);

测试结果如下:

1
2
➜  5-oops-mod git:(master) ✗ sudo insmod oops_mod.ko 
[1] 13015 killed sudo insmod oops_mod.ko
  • 内核检测到了 kernel oops,并且 kill 掉了该进程,使用 dmesg 查看下系统日志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
➜  5-oops-mod git:(master) ✗ dmesg | tail -64
[ 139.812510] before init
[ 139.812512] BUG: kernel NULL pointer dereference, address: 0000000000000000
[ 139.812515] #PF: supervisor write access in kernel mode
[ 139.812516] #PF: error_code(0x0002) - not-present page
[ 139.812517] PGD 0 P4D 0
[ 139.812519] Oops: 0002 [#1] SMP NOPTI
[ 139.812521] CPU: 1 PID: 3543 Comm: insmod Tainted: G OE 5.15.0-48-generic #54~20.04.1-Ubuntu
[ 139.812523] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
[ 139.812524] RIP: 0010:my_oops_init+0x15/0x31 [oops_mod]
[ 139.812528] Code: Unable to access opcode bytes at RIP 0xffffffffc0b1bfeb.
[ 139.812528] RSP: 0018:ffffb1bb85c6bb98 EFLAGS: 00010246
[ 139.812530] RAX: 000000000000000b RBX: 0000000000000000 RCX: 0000000000000027
[ 139.812530] RDX: 0000000000000000 RSI: ffffb1bb85c6b9e0 RDI: ffff942775e60588
[ 139.812531] RBP: ffffb1bb85c6bb98 R08: ffff942775e60580 R09: 0000000000000001
[ 139.812532] R10: 0000000000000001 R11: 000000000000000f R12: ffffffffc0b1c000
[ 139.812533] R13: ffff942695cb5ac0 R14: ffffffffc0b1e000 R15: 0000000000000000
[ 139.812534] FS: 00007f0f977db740(0000) GS:ffff942775e40000(0000) knlGS:0000000000000000
[ 139.812535] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 139.812536] CR2: ffffffffc0b1bfeb CR3: 00000001a9510003 CR4: 0000000000770ee0
[ 139.812557] PKRU: 55555554
[ 139.812558] Call Trace:
[ 139.812559] <TASK>
[ 139.812561] do_one_initcall+0x46/0x1e0
[ 139.812565] ? __cond_resched+0x19/0x40
[ 139.812568] ? kmem_cache_alloc_trace+0x15a/0x420
[ 139.812571] do_init_module+0x52/0x230
[ 139.812574] load_module+0x1376/0x1600
[ 139.812576] __do_sys_finit_module+0xbf/0x120
[ 139.812578] ? __do_sys_finit_module+0xbf/0x120
[ 139.812579] __x64_sys_finit_module+0x1a/0x20
[ 139.812581] do_syscall_64+0x59/0xc0
[ 139.812583] ? fput+0x13/0x20
[ 139.812584] ? ksys_mmap_pgoff+0x14b/0x2a0
[ 139.812586] ? exit_to_user_mode_prepare+0x3d/0x1c0
[ 139.812588] ? exit_to_user_mode_prepare+0x3d/0x1c0
[ 139.812589] ? syscall_exit_to_user_mode+0x27/0x50
[ 139.812591] ? __x64_sys_mmap+0x33/0x50
[ 139.812592] ? do_syscall_64+0x69/0xc0
[ 139.812593] ? do_syscall_64+0x69/0xc0
[ 139.812594] entry_SYSCALL_64_after_hwframe+0x61/0xcb
[ 139.812596] RIP: 0033:0x7f0f9792173d
[ 139.812598] Code: 00 c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d 23 37 0d 00 f7 d8 64 89 01 48
[ 139.812599] RSP: 002b:00007ffdee07d0f8 EFLAGS: 00000246 ORIG_RAX: 0000000000000139
[ 139.812600] RAX: ffffffffffffffda RBX: 000055e6a61767c0 RCX: 00007f0f9792173d
[ 139.812601] RDX: 0000000000000000 RSI: 000055e6a5c91358 RDI: 0000000000000003
[ 139.812602] RBP: 0000000000000000 R08: 0000000000000000 R09: 00007f0f979f8580
[ 139.812602] R10: 0000000000000003 R11: 0000000000000246 R12: 000055e6a5c91358
[ 139.812603] R13: 0000000000000000 R14: 000055e6a6176760 R15: 0000000000000000
[ 139.812604] </TASK>
[ 139.812605] Modules linked in: oops_mod(OE+) isofs xt_conntrack xt_MASQUERADE nf_conntrack_netlink nfnetlink xfrm_user xfrm_algo xt_addrtype iptable_filter iptable_nat nf_nat nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 libcrc32c bpfilter br_netfilter bridge stp llc rfcomm aufs overlay bnep vsock_loopback vmw_vsock_virtio_transport_common vmw_vsock_vmci_transport vsock binfmt_misc nls_iso8859_1 intel_rapl_msr intel_rapl_common kvm_intel kvm crct10dif_pclmul ghash_clmulni_intel aesni_intel crypto_simd vmw_balloon cryptd btusb input_leds btrtl btbcm btintel bluetooth joydev serio_raw ecdh_generic ecc vmw_vmci mac_hid sch_fq_codel vmwgfx ttm drm_kms_helper cec rc_core fb_sys_fops syscopyarea sysfillrect sysimgblt msr parport_pc ppdev drm lp parport ip_tables x_tables autofs4 hid_generic crc32_pclmul usbhid ahci libahci psmouse hid e1000 mptspi pata_acpi mptscsih mptbase i2c_piix4 scsi_transport_spi
[ 139.812636] CR2: 0000000000000000
[ 139.812637] ---[ end trace 840a29bcd63bee0c ]---
[ 139.812638] RIP: 0010:my_oops_init+0x15/0x31 [oops_mod]
[ 139.812640] Code: Unable to access opcode bytes at RIP 0xffffffffc0b1bfeb.
[ 139.812641] RSP: 0018:ffffb1bb85c6bb98 EFLAGS: 00010246
[ 139.812642] RAX: 000000000000000b RBX: 0000000000000000 RCX: 0000000000000027
[ 139.812642] RDX: 0000000000000000 RSI: ffffb1bb85c6b9e0 RDI: ffff942775e60588
[ 139.812643] RBP: ffffb1bb85c6bb98 R08: ffff942775e60580 R09: 0000000000000001
[ 139.812644] R10: 0000000000000001 R11: 000000000000000f R12: ffffffffc0b1c000
[ 139.812644] R13: ffff942695cb5ac0 R14: ffffffffc0b1e000 R15: 0000000000000000
[ 139.812645] FS: 00007f0f977db740(0000) GS:ffff942775e40000(0000) knlGS:0000000000000000
[ 139.812646] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 139.812647] CR2: ffffffffc0b1bfeb CR3: 00000001a9510003 CR4: 0000000000770ee0
[ 139.812664] PKRU: 55555554
  • 不仅标识出了触发 kernel oops 的原因,还给出了触发的位置 my_oops_init+0x15

Exercises

要解决练习,您需要执行以下步骤:

  • 从模板中准备 skeletons(具体来说就是 linux/tools/labs/skels 文件夹)
  • 构建模块
  • 将模块复制到虚拟机
  • 启动 VM 并在 VM 中测试模块

下面的练习我就挑几个有意思的挂在博客上:

6.Module parameters:

  • 编译并复制关联的模块
  • 并加载内核模块以查看 printk 消息
  • 然后从内核中卸载模块

在正常情况下载入模块,是如下的结果:

1
2
3
root@qemux86:~/skels/kernel_modules/6-cmd-mod# insmod cmd_mod.ko                
cmd_mod: loading out-of-tree module taints kernel.
Early bird gets the worm
  • 我们的目标就是把输出的 Early bird gets the worm 改为 Early bird gets tired
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

MODULE_DESCRIPTION("Command-line args module");
MODULE_AUTHOR("Kernel Hacker");
MODULE_LICENSE("GPL");

static char *str = "the worm";

module_param(str, charp, 0000);
MODULE_PARM_DESC(str, "A simple string");

static int __init cmd_init(void)
{
pr_info("Early bird gets %s\n", str);
return 0;
}

static void __exit cmd_exit(void)
{
pr_info("Exit, stage left\n");
}

module_init(cmd_init);
module_exit(cmd_exit);
  • module_param 表示向当前模块传入参数
  • 通过如下命令就可以指定参数:
1
2
root@qemux86:~/skels/kernel_modules/6-cmd-mod# insmod cmd_mod.ko str=tired
Early bird gets tired

7.Proc info:

  • 添加代码以显示当前进程的进程ID和可执行文件名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
/* TODO: add missing headers */
#include <linux/sched.h>

MODULE_DESCRIPTION("List current processes");
MODULE_AUTHOR("Kernel Hacker");
MODULE_LICENSE("GPL");

static int my_proc_init(void)
{
struct task_struct *p;
struct list_head * pos;

/* TODO: print current process pid and its name */
p = current;
pr_info("current pid: %d\n",p->pid);
pr_info("current name: %s\n",p->comm);
/* TODO: print the pid and name of all processes */
list_for_each(pos, &p->tasks)
{
p = list_entry(pos, struct task_struct, tasks);
pr_info("current pid: %d\n",p->pid);
pr_info("current name: %s\n",p->comm);
}
return 0;
}

static void my_proc_exit(void)
{
/* TODO: print current process pid and name */
struct task_struct *p;
p = current;
pr_info("current pid: %d\n",p->pid);
pr_info("current name: %s\n",p->comm);
}

module_init(my_proc_init);
module_exit(my_proc_exit);