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:
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> #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 ; p = current; pr_info("current pid: %d\n" ,p->pid); pr_info("current name: %s\n" ,p->comm); 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 ) { 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);