近期遇到一个服务器上内核模块无法卸载的问题。执行rmmod
命令返回错误信息:
通过Google搜索,发现其他人也遇到过类似的问题,原因基本指向是编译内核模块的gcc
版本和编译内核的gcc
版本不一致。
于是开始检查我们的环境。
我们的服务器系统是CentOS 7.8 ARM
:
1 2 3 4 [root@localhost ~] CentOS Linux release 7.8.2003 (AltArch) [root@localhost ~] Linux localhost.localdomain 4.18.0-147.8.1.el7.aarch64
查看编译内核的gcc
版本,使用的是gcc 8.3.1
:
1 2 [root@localhost ~] Linux version 4.18.0-147.8.1.el7.aarch64 (mockbuild@aarch64-02.bsys.centos.org) (gcc version 8.3.1 20190311 (Red Hat 8.3.1-3) (GCC))
查看内核模块的gcc
版本, 使用的是gcc 4.8.5
, 确实不一致:
1 2 3 4 5 [root@localhost ~] String dump of section '.comment' : [ 1] GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39) [ 2f] GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)
再去查看模块编译机器的gcc
版本,确实是gcc 4.8.5
, 而这个版本是直接通过yum
安装的。CentOS
的DVD镜像中自带的gcc
也是这个版本:
1 2 3 4 5 [root@localhost ~] gcc-4.8.5-39.el7.aarch64 libgcc-4.8.5-39.el7.aarch64 gcc-gfortran-4.8.5-39.el7.aarch64 gcc-c++-4.8.5-39.el7.aarch64
现在看,大概率的确是gcc
版本的问题,但是具体原因需要继续定位。为了进一步缩小排查范围,写了一个只有基础骨架的示例模块来复现问题。
kdemo.c
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> static int __init kdemo_init (void ) { return 0 ; } static void __exit kdemo_exit (void ) { } module_init(kdemo_init); module_exit(kdemo_exit); MODULE_LICENSE("GPL" );
Makefile
内容:
1 2 3 4 5 6 7 8 KBUILD_CFLAGS += -O0 obj-m += kdemo.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
我们安装内核编译使用的gcc 8
:
1 2 yum install -y devtoolset-8-gcc devtoolset-8-gcc-c++ source /opt/rh/devtoolset-8/enable
此时gcc
版本为:
1 2 3 4 5 [root@localhost kdemo2] gcc (GCC) 8.3.1 20190311 (Red Hat 8.3.1-3) Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
经过尝试,gcc 8.3.1
编译的kdemo.ko
可以正常卸载,而gcc 4.8.5
编译的kdemo.ko
也无法卸载。这个最简单的内核模块只有骨架,没有任何逻辑代码,于是推测可能和内核模块加载和卸载过程有关系。那就着手从卸载过程分析。
卸载模块的命令rmmod
会调用delete_module
系统调用。Device or resource busy
错误消息来源于错误码EBUSY
, 而delete_module
源码中返回EBUSY
的位置只有如下两处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (mod->state != MODULE_STATE_LIVE) { pr_debug("%s already dying\n" , mod->name); ret = -EBUSY; goto out; } if (mod->init && !mod->exit ) { forced = try_force_unload(flags); if (!forced) { ret = -EBUSY; goto out; } }
我们可以通过开发额外的内核模块来调用find_module
找到无法卸载的模块来查看具体结构内容。模块代码如下:
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/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> static int __init ms_init (void ) { struct module *mod ; mod = find_module("kdemo" ); if (mod == NULL ) { printk(KERN_INFO "kdemo not found\n" ); return -1 ; } printk(KERN_INFO "MOD: 0x%lx\n" , (unsigned long )mod); printk(KERN_INFO "STATE: %d\n" , mod->state); printk(KERN_INFO "INIT: 0x%lx\n" , (unsigned long )mod->init); printk(KERN_INFO "EXIT: 0x%lx\n" , (unsigned long )mod->exit ); printk(KERN_INFO "REFCOUNT: %u\n" , atomic_read (&mod->refcnt)); return -1 ; } module_init(ms_init); MODULE_LICENSE("GPL" );
使用gcc 8.3.1
来编译模块。加载编译后的ms.ko
模块, 可以从dmesg
或者/var/log/messages
中看到模块输出:
1 2 3 4 5 [ 703.450481] MOD: 0xffff00000a570000 [ 703.450771] STATE: 0 [ 703.450913] INIT: 0xffff0000089f0000 [ 703.451152] EXIT: 0x0 [ 703.451302] REFCOUNT: 1
可以确认内核模块无法卸载的原因是由于mod->exit
函数指针为空。
接下来需要分析mod->exit
为空的原因。
在内核实现中,模块由struct module
结构表示(位于include/linux/module.h
,省略部分成员):
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 struct module { enum module_state state ; struct list_head list ; char name[MODULE_NAME_LEN]; ...... int (*init)(void ); #ifdef CONFIG_MODULE_UNLOAD struct list_head source_list ; struct list_head target_list ; void (*exit )(void ); atomic_t refcnt; #endif ...... }
编译内核模块时,内核的modpost
工具会自动生成<module>.mod.c
文件,这个文件会包含模块的基础信息,之后会将内核模块源码文件生成的xxx.o
和<module>.mod.c
生成的<module>.mod.o
文件一起链接成<module>.ko
。
我们的示例模块生成的kdemo.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 28 29 30 31 32 33 34 35 36 #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, VERMAGIC_STRING); MODULE_INFO(name, KBUILD_MODNAME); __visible struct module __this_module __attribute__ ((section (".gnu .linkonce .this_module "))) = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif .arch = MODULE_ARCH_INIT, }; #ifdef RETPOLINE MODULE_INFO(retpoline, "Y" ); #endif static const struct modversion_info ____versions []__used __attribute__ ((section ("__versions "))) = { { 0x2c122b9f , "module_layout" }, { 0x1fdc7df2 , "_mcount" }, }; static const char __module_depends[]__used __attribute__((section(".modinfo" ))) = "depends=" ;MODULE_INFO(srcversion, "4D3060AD906F1906629370C" ); MODULE_INFO(rhelversion, "8.1" );
这个文件在.gnu.linkonce.this_module
节(section
)里存储了struct module
结构, 而.init
和.exit
分别被初始化为init_module
和cleanup_module
函数。这和我们代码里的kdemo_init
和kdemo_exit
的关系是什么呢?
宏module_init
和module_exit
定义在include/linux/module.h
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 #define module_init(initfn) \ static inline initcall_t __maybe_unused __inittest(void) \ { return initfn; } \ int init_module(void) __attribute__((alias(#initfn))); #define module_exit(exitfn) \ static inline exitcall_t __maybe_unused __exittest(void) \ { return exitfn; } \ void cleanup_module(void) __attribute__((alias(#exitfn))); #endif
这样kdemo.c
中就通过这两个宏定义了init_module
和cleanup_module
作为kdemo_init
和kdemo_exit
的别名
1 2 module_init(kdemo_init); module_exit(kdemo_exit);
因为内核模块是动态装载进内核执行的,在编译阶段模块函数地址是不确定的,需要在加载进内核后完成ELF
符号重定向,struct module
的.init
和.exit
应该是在加载的时候完成赋值的。
我们实际验证一下。
使用readelf -S kdemo.ko
查看kdemo.ko
的结构,获取到.gnu.linkonce.this_module
节的偏移位置0x000001c0
:
1 2 [11] .gnu.linkonce.thi PROGBITS 0000000000000000 000001c0 0000000000000380 0000000000000000 WA 0 0 64
使用od -Ax -j 0x000001c0 -tx1z ./kdemo.ko
查看.gnu.linkonce.this_module
节的内容:
1 2 3 4 5 0001c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< 0001d0 00 00 00 00 00 00 00 00 6b 64 65 6d 6f 00 00 00 >........kdemo...< 0001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< * 000540 64 11 00 00 04 00 00 00 00 00 08 01 00 00 00 00 >d...............<
可以确认整个section
除了.name
数组中的kdemo
几个字符外全部为\0
。
那么接下来分析模块加载过程中.init
和.exit
是如何初始化的。
内核模块的加载过程非常复杂,这两篇资料比较详细:
过程可以简化为如下步骤:
用户态进程把ko
文件加载到内存,然后调用init_module
系统调用。内核会动态分配内存并把ko
文件内容复制到内核空间。
内核校验ELF
头以及.modinfo
和__version
节中的版本信息。
版本校验通过后,内核调用自己的链接器解析所有符号重定位,把模块内的所有符号引用替换为这些符号在内存中的实际地址。
内核把模块的struct module
结构加入到内核维护的所有模块的链表中,然后调用struct module
的.init
函数。
内核实现了自己的链接器,源码实现在文件kernel/module.c
的load_module
函数:
1 2 3 4 5 6 7 8 err = simplify_symbols(mod, info); if (err < 0 ) goto free_modinfo; err = apply_relocations(mod, info); if (err < 0 ) goto free_modinfo;
符号重定位过程是由函数apply_relocations()
实现:
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 static int apply_relocations (struct module *mod, const struct load_info *info) { unsigned int i; int err = 0 ; for (i = 1 ; i < info->hdr->e_shnum; i++) { unsigned int infosec = info->sechdrs[i].sh_info; if (infosec >= info->hdr->e_shnum) continue ; if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC)) continue ; if (info->sechdrs[i].sh_flags & SHF_RELA_LIVEPATCH) continue ; if (info->sechdrs[i].sh_type == SHT_REL) err = apply_relocate(info->sechdrs, info->strtab, info->index.sym, i, mod); else if (info->sechdrs[i].sh_type == SHT_RELA) err = apply_relocate_add(info->sechdrs, info->strtab, info->index.sym, i, mod); if (err < 0 ) break ; } return err; }
通过readelf -r kdemo.ko
查看符号重定位信息:
1 2 3 4 5 6 ...... Relocation section '.rela.gnu.linkonce.this_module' at offset 0x8620 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000178 002a00000101 R_AARCH64_ABS64 0000000000000000 init_module + 0 000000000318 002900000101 R_AARCH64_ABS64 0000000000000000 cleanup_module + 0 ......
可以看到.rela.gnu.linkonce.this_module
节中有两个重定位信息,就是我们要找到的init_module
和cleanup_module
。
为了观测加载过程中.exit
的变化,可以使用kprobe
机制来定位。这里我设计了两个观测点:
符号重定向过程中
符号重定向完成后
为了找到具体的指令位置,需要对内核进行反汇编。Linux内核自带一个解压内核镜像的脚本,可以解压出内核镜像:
1 /usr/src/kernels/$(uname -r)/scripts/extract-vmlinux vmlinuz-$(uname -r) > vmlinux
但这个工具在ARM
架构上存在问题,因而我直接使用debuginfo
包中的内核镜像文件。反汇编内核镜像:
1 objdump -S /usr/lib/debug/lib/modules/4.18.0-147.8.1.el7.aarch64/vmlinux > vmlinux.out
符号重定向过程中的观测点可以选在apply_relocate_add
函数中ffff000010095ec8
:
1 2 3 4 5 6 7 8 9 10 + rel[i].r_offset; ffff000010095ec0: 8b000f19 add x25, x24, x0, lsl #3 ffff000010095ec4: f8607b09 ldr x9, [x24,x0,lsl #3] val = sym->st_value + rel[i].r_addend; ffff000010095ec8: a9409320 ldp x0, x4, [x25,#8] loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr ffff000010095ecc: b9402f81 ldr w1, [x28,#44] ffff000010095ed0: 8b011a81 add x1, x20, x1, lsl #6 switch (ELF64_R_TYPE(rel[i].r_info)) { ffff000010095ed4: 92407c02 and x2, x0, #0xffffffff
这时x9
寄存器存储的是rel[i].r_offset
的值,可以和kdemo.ko
中的数据对比。
符号重定向完成后的观测点可以选在apply_relocate_add
调用后的地址ffff000010196808
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if (info->sechdrs[i].sh_type == SHT_REL) ffff0000101967d8: b9400461 ldr w1, [x3,#4] ffff0000101967dc: 7100243f cmp w1, #0x9 ffff0000101967e0: 54004a60 b.eq ffff00001019712c <load_module+0x161c> else if (info->sechdrs[i].sh_type == SHT_RELA) ffff0000101967e4: 7100103f cmp w1, #0x4 ffff0000101967e8: 54fffd81 b.ne ffff000010196798 <load_module+0xc88> err = apply_relocate_add(info->sechdrs, info->strtab, ffff0000101967ec: b9405a62 ldr w2, [x19,#88] ffff0000101967f0: aa1403e4 mov x4, x20 ffff0000101967f4: f9401661 ldr x1, [x19,#40] ffff0000101967f8: 2a0503e3 mov w3, w5 ffff0000101967fc: b90063e5 str w5, [sp,#96] ffff000010196800: f90037e6 str x6, [sp,#104] ffff000010196804: 97fbfd91 bl ffff000010095e48 <apply_relocate_add> ffff000010196808: 2a0003f7 mov w23, w0 if (err < 0) ffff00001019680c: 37f80660 tbnz w0, #31, ffff0000101968d8 <load_module+0xdc8> ffff000010196810: b94063e5 ldr w5, [sp,#96] ffff000010196814: f9400664 ldr x4, [x19,#8] ffff000010196818: f94037e6 ldr x6, [sp,#104] ffff00001019681c: 17ffffdf b ffff000010196798 <load_module+0xc88>
此时x20
寄存器存储的是模块的指针变量struct module *mod
。
编写kprobe
模块,kp.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 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #define pr_fmt(fmt) "[%s]: " fmt, KBUILD_MODNAME #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> #include <linux/err.h> #include <linux/kprobes.h> static struct kprobe kp1 = { .addr = (unsigned int *)0xffff000010095ec8 , }; static struct kprobe kp2 = { .addr = (unsigned int *)0xffff000010196808 , }; static void handler_post1 (struct kprobe *p, struct pt_regs *regs, unsigned long flags) { pr_info("post_handler1: p->addr=0x%lx, pstate=0x%lx, x9=0x%lx\n" , (unsigned long )p->addr, (unsigned long )regs->pstate, (unsigned long )regs->regs[9 ]); } static void handler_post2 (struct kprobe *p, struct pt_regs *regs, unsigned long flags) { struct module *mod ; pr_info("post_handler2: p->addr=0x%lx, pstate=0x%lx, x20=0x%lx\n" , (unsigned long )p->addr, (unsigned long )regs->pstate, (unsigned long )regs->regs[20 ]); mod = (struct module *)regs->regs[20 ]; if (mod != NULL ) { pr_info("NAME: %s\n" , mod->name); pr_info("STATE: %d\n" , mod->state); pr_info("INIT: 0x%lx\n" , (unsigned long )mod->init); pr_info("EXIT: 0x%lx\n" , (unsigned long )mod->exit ); pr_info("REFCOUNT: %u\n" , atomic_read (&mod->refcnt)); } } static int __init kp_init (void ) { int ret; pr_info("loading\n" ); kp1.post_handler = handler_post1; kp2.post_handler = handler_post2; ret = register_kprobe(&kp1); if (ret < 0 ) { pr_err("register_kprobe failed, ret: %d\n" , ret); goto err0; } ret = register_kprobe(&kp2); if (ret < 0 ) { pr_err("register_kprobe failed, ret: %d\n" , ret); goto err1; } pr_info("kprobe1 at 0x%lx\n" , (unsigned long )kp1.addr); pr_info("kprobe2 at 0x%lx\n" , (unsigned long )kp2.addr); return 0 ; err1: unregister_kprobe(&kp1); err0: return ret; } static void __exit kp_exit (void ) { unregister_kprobe(&kp2); unregister_kprobe(&kp1); pr_info("unloaded\n" ); } module_init(kp_init); module_exit(kp_exit); MODULE_LICENSE("GPL" );
编译并加载我们的kp.ko
模块,从dmesg
可以看到加载成功:
1 2 3 [21946.542652] [kp]: loading [21946.543082] [kp]: kprobe1 at 0xffff000010095ec8 [21946.543377] [kp]: kprobe2 at 0xffff000010196808
此时去加载有问题的kdemo.ko
模块,查看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 [21946.542652] [kp]: loading [21946.543082] [kp]: kprobe1 at 0xffff000010095ec8 [21946.543377] [kp]: kprobe2 at 0xffff000010196808 [21992.480537] [kp]: post_handler1: p->addr=0xffff000010095ec8, pstate=0x20400005, x9=0xc [21992.481105] [kp]: post_handler2: p->addr=0xffff000010196808, pstate=0x60400005, x20=0xffff00000a570000 [21992.481694] [kp]: NAME: kdemo [21992.481884] [kp]: STATE: 3 [21992.482055] [kp]: INIT: 0x0 [21992.482232] [kp]: EXIT: 0x0 [21992.482408] [kp]: REFCOUNT: 2 [21992.482602] [kp]: post_handler1: p->addr=0xffff000010095ec8, pstate=0x20400005, x9=0x0 [21992.483111] [kp]: post_handler2: p->addr=0xffff000010196808, pstate=0x60400005, x20=0xffff00000a570000 [21992.483698] [kp]: NAME: kdemo [21992.483887] [kp]: STATE: 3 [21992.484059] [kp]: INIT: 0x0 [21992.484235] [kp]: EXIT: 0x0 [21992.484414] [kp]: REFCOUNT: 2 [21992.484610] [kp]: post_handler1: p->addr=0xffff000010095ec8, pstate=0x20400005, x9=0x178 [21992.485135] [kp]: post_handler1: p->addr=0xffff000010095ec8, pstate=0x80400005, x9=0x318 [21992.485652] [kp]: post_handler2: p->addr=0xffff000010196808, pstate=0x60400005, x20=0xffff00000a570000 [21992.486295] [kp]: NAME: kdemo [21992.486530] [kp]: STATE: 3 [21992.486744] [kp]: INIT: 0xffff000008b00000 [21992.487067] [kp]: EXIT: 0x0 [21992.487289] [kp]: REFCOUNT: 2
结合源码进行分析,对应的正是readelf -r kdemo.ko
的输出中的3
个section
中的4
个符号重定向:
1 2 3 4 5 6 7 8 9 10 11 12 Relocation section '.rela.init.text' at offset 0x85f0 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 00000000000c 002b0000011b R_AARCH64_CALL26 0000000000000000 _mcount + 0 Relocation section '.rela__mcount_loc' at offset 0x8608 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 000000000000 000300000101 R_AARCH64_ABS64 0000000000000000 .init.text + c Relocation section '.rela.gnu.linkonce.this_module' at offset 0x8620 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000178 002a00000101 R_AARCH64_ABS64 0000000000000000 init_module + 0 000000000318 002900000101 R_AARCH64_ABS64 0000000000000000 cleanup_module + 0
可以确定r_offset
为0x318
的cleanup_module
符号重定向确实是执行了,但执行完module.exit
依然为NULL
。
这时开始怀疑.exit
偏移量存在问题。
查看一下使用gcc 8.3.1
编译的kdemo.ko
的重定向信息:
1 2 3 4 Relocation section '.rela.gnu.linkonce.this_module' at offset 0xf010 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000178 002a00000101 R_AARCH64_ABS64 0000000000000000 init_module + 0 000000000320 002900000101 R_AARCH64_ABS64 0000000000000000 cleanup_module + 0
发现确实存在差异,4.8.5
的模块偏移是000000000318
, 而8.3.1
的模块偏移是000000000320
, 正好差8
个字节。
再写一个模块来验证exit
偏移的差异:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> static int __init offset_init (void ) { pr_info("init offset: %lu\n" , offsetof(struct module, init)); pr_info("exit offset: %lu\n" , offsetof(struct module, exit )); return -1 ; } module_init(offset_init); MODULE_LICENSE("GPL" );
gcc 4.8.5
的结果为:
1 2 [23374.470470] init offset: 376 [23374.470672] exit offset: 792
而gcc 8.3.1
的结果为:
1 2 [23467.040802] init offset: 376 [23467.041005] exit offset: 800
证实是由于exit
偏移问题导致重定向出现问题。
查看内核源码文件include/linux/module.h
的struct module
定义, 发现有这样一个宏:
1 2 3 4 #ifdef HAVE_JUMP_LABEL struct jump_entry *jump_entries ; unsigned int num_jump_entries; #endif
而这个宏定义是在include/linux/jump_label.h
:
1 2 3 #if defined(CC_HAVE_ASM_GOTO) && defined(CONFIG_JUMP_LABEL) # define HAVE_JUMP_LABEL #endif
内核构建的Makefile
(/lib/modules/4.18.0-147.8.1.el7.aarch64/build/Makefile
中会调用gcc-goto.sh
来判断gcc
是否支持asm goto
:
1 2 3 4 5 6 ifeq ($(shell $(CONFIG_SHELL) $(srctree) /scripts/gcc-goto.sh $(CC) $(KBUILD_CFLAGS) ) , y) CC_HAVE_ASM_GOTO := 1 KBUILD_CFLAGS += -DCC_HAVE_ASM_GOTO KBUILD_AFLAGS += -DCC_HAVE_ASM_GOTO endif
gcc 8.3.1
环境下,脚本判断支持asm goto
:
1 2 3 [root@localhost scripts]# /lib/modules/4.18.0-147.8.1.el7.aarch64/build/scripts/gcc-goto.sh gcc y [root@localhost scripts]#
而在gcc 4.8.5
环境下,脚本判断不支持asm goto
:
1 2 [root@localhost ~]# /lib/modules/4.18.0-147.8.1.el7.aarch64/build/scripts/gcc-goto.sh gcc [root@localhost ~]#
.init
在struct module
结构中位于HAVE_JUMP_LABEL
之前,因而两个编译器环境下的偏移量是一样的。而.exit
位于HAVE_JUMP_LABEL
之后,再加上内部结构对齐的因素,在两个编译器环境下相差了8
字节。
这样当加载gcc 4.8.5
编译的内核模块时,cleanup_module
符号重定向到了偏移量为792
的位置,这个位置在内核struct module
中为struct list_head target_list;
:
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 struct module { enum module_state state ; struct list_head list ; char name[MODULE_NAME_LEN]; ...... int (*init)(void ); #ifdef CONFIG_MODULE_UNLOAD struct list_head source_list ; struct list_head target_list ; void (*exit )(void ); atomic_t refcnt; #endif ...... }
而struct list
的定义(include/linux/types.h
)为:
1 2 3 struct list_head { struct list_head *next , *prev ; };
因而符号重定向覆盖的是target_list.prev
指针。source_list
和target_list
成员是用来表示模块依赖关系的结构。
对加载模块源码再次梳理后发现,符号重定向是发生在建立模块依赖关系之后:
1 2 err = add_unformed_module(mod);
并且内核加载过程中都是正向遍历, 使用target_list.next
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static int add_usage_links (struct module *mod) { int ret = 0 ; #ifdef CONFIG_MODULE_UNLOAD struct module_use *use ; mutex_lock(&module_mutex); list_for_each_entry(use, &mod->target_list, target_list) { ret = sysfs_create_link(use->target->holders_dir, &mod->mkobj.kobj, mod->name); if (ret) break ; } mutex_unlock(&module_mutex); if (ret) del_usage_links(mod); #endif return ret; }
而我们的模块代码中没有其他结构在不同编译器环境下存在差异,因而可以正常运行,只是不能卸载。
但这里还存在疑惑的点是,为什么在gcc 4.8.5
的环境下少了两个成员变量,但编译的struct module
结构的大小却都是0x380
(十进制896
)呢?这是因为struct module
的定义带有____cacheline_aligned
属性。
它定义在include/linux/cache.h
中:
1 2 3 4 5 6 7 8 9 #ifndef SMP_CACHE_BYTES #define SMP_CACHE_BYTES L1_CACHE_BYTES #endif ...... #ifndef ____cacheline_aligned #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) #endif
而在arch/arm64/include/asm/cache.h
文件中,L1_CACHE_BYTES
定义为64
:
1 2 #define L1_CACHE_SHIFT (6) #define L1_CACHE_BYTES (1 << L1_CACHE_SHIFT)
这样编译器将这个结构的大小以64
对齐,因而减少那两个成员变量后大小依然为896
。
接下来考虑如何能够不重启服务器卸载掉这个模块。
当卸载模块时,从链表中删除元素时会使用到prev
指针。我们只要能够把被覆盖的target_list.prev
指针恢复到之前的值并且把exit
函数修改到相应的符号内存地址就可以实现卸载了。
实际上,因为target_list
本身是双向链接,只要把target_list.prev
指向最后一个结构的地址就可以实现修复,而kdemo_exit
的符号地址可以通过kallsyms_lookup_name()
获取。
按这个思路编写修复模块,exitfix.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 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #define pr_fmt(fmt) "[%s]: " fmt, KBUILD_MODNAME #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> #include <linux/kallsyms.h> static char module_name[256 ] = "kdemo" ;MODULE_PARM_DESC(module_name, "module name to fix exit()" ); module_param_string(module_name, module_name, sizeof (module_name), 0 ); static char module_exit_symbol[256 ] = "kdemo_exit" ;MODULE_PARM_DESC(module_exit_symbol, "module exit symbol to fix exit()" ); module_param_string(module_exit_symbol, module_exit_symbol, sizeof (module_exit_symbol), 0 ); static int __init exitfix_init (void ) { struct module *mod ; struct module_use *use , *last_use ; unsigned long sym; pr_info("start\n" ); mod = find_module(module_name); if (mod == NULL ) { pr_crit("module %s not found\n" , module_name); return -1 ; } if (mod->exit != NULL ) { pr_crit("module->exit is 0x%lx\n" , (unsigned long )mod->exit ); return -1 ; } pr_info("STATE: %d\n" , mod->state); pr_info("INIT: %lx\n" , (unsigned long )mod->init); pr_info("EXIT: %lx\n" , (unsigned long )mod->exit ); pr_info("REFCOUNT: %u\n" , atomic_read (&mod->refcnt)); sym = kallsyms_lookup_name(module_exit_symbol); if (sym == 0 ) { printk(KERN_ERR "lookup symbol failed" ); return -1 ; } pr_info("exit_symbol: %s 0x%lx\n" , module_exit_symbol, sym); pr_info("mod->target_list.next: 0x%lx\n" , (unsigned long )mod->target_list.next); pr_info("mod->target_list.prev: 0x%lx\n" , (unsigned long )mod->target_list.prev); mutex_lock(&module_mutex); last_use = NULL ; list_for_each_entry(use, &mod->target_list, target_list) { struct module *i = use->target; pr_info("%s using %s\n" , mod->name, i->name); last_use = use; } if (last_use != NULL ) { mod->target_list.prev = &last_use->target_list; } else { mod->target_list.prev = &mod->target_list; } mod->exit = (void (*)(void ))sym; mutex_unlock(&module_mutex); pr_info("restore mod->target_list.prev to: 0x%lx\n" , (unsigned long )mod->target_list.prev); pr_info("restore mod->exit to: 0x%lx\n" , (unsigned long )mod->exit ); pr_info("over\n" ); return -1 ; } module_init(exitfix_init); MODULE_LICENSE("GPL" );
最初的示例模块没有任何代码,现在修改为依赖其他模块:
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 #define pr_fmt(fmt) "[%s]: " fmt, KBUILD_MODNAME #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> #include <linux/netfilter/ipset/ip_set.h> #include <linux/device-mapper.h> static int __init kdemo_init (void ) { void *p; p = ip_set_alloc(1 ); if (!p) { pr_crit("ip_set_alloc() failed\n" ); goto err; } ip_set_free(p); p = dm_vcalloc(1 , 1 ); if (!p) { pr_crit("dm_vcalloc() failed\n" ); goto err; } vfree(p); pr_info("loaded\n" ); return 0 ; err: return -1 ; } static void __exit kdemo_exit (void ) { pr_info("unloaded\n" ); } module_init(kdemo_init); module_exit(kdemo_exit); MODULE_LICENSE("GPL" );
使用gcc 4.8.5
编译后加载, 查看依赖关系, 可以看到kdemo
模块依赖了ip_set
和dm_mod
模块:
1 2 3 4 [root@localhost kdemo] kdemo 262144 0 ip_set 262144 1 kdemo dm_mod 327680 9 kdemo,dm_log,dm_mirror
执行rmmod
, 卸载失败:
1 2 3 [root@localhost kdemo] rmmod: ERROR: could not remove 'kdemo' : Device or resource busy rmmod: ERROR: could not remove module kdemo: Device or resource busy
使用gcc 8.3.1
编译修复模块exitfix.ko
并加载, 查看dmesg
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 [26734.237973] [exitfix]: start [26734.238187] [exitfix]: STATE: 0 [26734.238382] [exitfix]: INIT: ffff0000089f0000 [26734.238649] [exitfix]: EXIT: 0 [26734.238840] [exitfix]: REFCOUNT: 1 [26734.247782] [exitfix]: exit_symbol: kdemo_exit 0xffff00000a5a003c [26734.248170] [exitfix]: mod->target_list.next: 0xffff8001c80aed90 [26734.248540] [exitfix]: mod->target_list.prev: 0xffff00000a5a003c [26734.248920] [exitfix]: kdemo using dm_mod [26734.249168] [exitfix]: kdemo using ip_set [26734.249416] [exitfix]: restore mod->target_list.prev to: 0xffff8001c80ae010 [26734.249843] [exitfix]: restore mod->exit to: 0xffff00000a5a003c [26734.250206] [exitfix]: over
可以看到target_list.prev
和exit
地址都进行了修复。
此时再去执行rmmod
, 成功卸载:
1 [root@localhost kdemo]# rmmod kdemo
查看kdemo
依赖的ip_set
和dm_mod
模块, 可以看到模块的使用者中已经没有kdemo
, 引用计数也都减小了1
:
1 2 3 4 5 [root@localhost kdemo] ip_set 262144 0 nfnetlink 262144 1 ip_set [root@localhost kdemo] dm_mod 327680 8 dm_log,dm_mirror
至此,终于成功的在不重启服务器情况下完成了模块的卸载。总结来看,我们的内核模块能够修复是由于代码中并没有存在不同编译器环境下存在差异的其他结构。如果存在其他结构,可能在加载过程中就会出现崩溃的情况。
因而还是需要强调,必须使用和内核编译时一致的gcc
对模块进行编译。
参考: