Keep learning, keep living...

0%

Linux内核模块无法卸载分析与修复

近期遇到一个服务器上内核模块无法卸载的问题。执行rmmod命令返回错误信息:

1
Device or resource busy

通过Google搜索,发现其他人也遇到过类似的问题,原因基本指向是编译内核模块的gcc版本和编译内核的gcc版本不一致。

于是开始检查我们的环境。

我们的服务器系统是CentOS 7.8 ARM:

1
2
3
4
[root@localhost ~]# cat /etc/redhat-release
CentOS Linux release 7.8.2003 (AltArch)
[root@localhost ~]# uname -a
Linux localhost.localdomain 4.18.0-147.8.1.el7.aarch64 #1 SMP Wed Apr 15 18:13:44 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux

查看编译内核的gcc版本,使用的是gcc 8.3.1:

1
2
[root@localhost ~]# cat /proc/version
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)) #1 SMP Wed Apr 15 18:13:44 UTC 2020

查看内核模块的gcc版本, 使用的是gcc 4.8.5, 确实不一致:

1
2
3
4
5
[root@localhost ~]# readelf -p .comment xxx.ko

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 ~]# rpm -qa |grep gcc
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 --version
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
/* Doing init or already dying? */
if (mod->state != MODULE_STATE_LIVE) {
/* FIXME: if (force), slam module count damn the torpedoes */
pr_debug("%s already dying\n", mod->name);
ret = -EBUSY;
goto out;
}

/* If it has an init func, it must have an exit func to unload */
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
/* This module can't be removed */
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;

/* Member of list of modules */
struct list_head list;

/* Unique handle for this module */
char name[MODULE_NAME_LEN];

......

/* Startup function. */
int (*init)(void);

#ifdef CONFIG_MODULE_UNLOAD
/* What modules depend on me? */
struct list_head source_list;
/* What modules do I depend on? */
struct list_head target_list;

/* Destruction function. */
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_modulecleanup_module函数。这和我们代码里的kdemo_initkdemo_exit的关系是什么呢?

module_initmodule_exit定义在include/linux/module.h中:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));

/* This is only required if you want to be unloadable. */
#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_modulecleanup_module作为kdemo_initkdemo_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是如何初始化的。

内核模块的加载过程非常复杂,这两篇资料比较详细:

过程可以简化为如下步骤:

  1. 用户态进程把ko文件加载到内存,然后调用init_module系统调用。内核会动态分配内存并把ko文件内容复制到内核空间。
  2. 内核校验ELF头以及.modinfo__version节中的版本信息。
  3. 版本校验通过后,内核调用自己的链接器解析所有符号重定位,把模块内的所有符号引用替换为这些符号在内存中的实际地址。
  4. 内核把模块的struct module结构加入到内核维护的所有模块的链表中,然后调用struct module.init函数。

内核实现了自己的链接器,源码实现在文件kernel/module.cload_module函数:

1
2
3
4
5
6
7
8
/* Fix up syms, so that st_value is a pointer to location. */
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;

/* Now do relocations. */
for (i = 1; i < info->hdr->e_shnum; i++) {
unsigned int infosec = info->sechdrs[i].sh_info;

/* Not a valid relocation section? */
if (infosec >= info->hdr->e_shnum)
continue;

/* Don't bother with non-allocated sections */
if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC))
continue;

/* Livepatch relocation sections are applied by livepatch */
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_modulecleanup_module

为了观测加载过程中.exit的变化,可以使用kprobe机制来定位。这里我设计了两个观测点:

  1. 符号重定向过程中
  2. 符号重定向完成后

为了找到具体的指令位置,需要对内核进行反汇编。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的输出中的3section中的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_offset0x318cleanup_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.hstruct 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
# check for 'asm goto'
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 ~]#

.initstruct 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;

/* Member of list of modules */
struct list_head list;

/* Unique handle for this module */
char name[MODULE_NAME_LEN];

......

/* Startup function. */
int (*init)(void);

#ifdef CONFIG_MODULE_UNLOAD
/* What modules depend on me? */
struct list_head source_list;
/* What modules do I depend on? */
struct list_head target_list;

/* Destruction function. */
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_listtarget_list成员是用来表示模块依赖关系的结构。

对加载模块源码再次梳理后发现,符号重定向是发生在建立模块依赖关系之后:

1
2
/* Reserve our place in the list. */
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);

/* find last struct module_use */
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");
/* just run once */
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;

/* use module: ip_set */
p = ip_set_alloc(1);
if (!p) {
pr_crit("ip_set_alloc() failed\n");
goto err;
}
ip_set_free(p);

/* use module: dm_mod */
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_setdm_mod模块:

1
2
3
4
[root@localhost kdemo]# lsmod |grep 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 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.prevexit地址都进行了修复。

此时再去执行rmmod, 成功卸载:

1
[root@localhost kdemo]# rmmod kdemo

查看kdemo依赖的ip_setdm_mod模块, 可以看到模块的使用者中已经没有kdemo, 引用计数也都减小了1:

1
2
3
4
5
[root@localhost kdemo]# lsmod |grep ip_set
ip_set 262144 0
nfnetlink 262144 1 ip_set
[root@localhost kdemo]# lsmod |grep dm_mod
dm_mod 327680 8 dm_log,dm_mirror

至此,终于成功的在不重启服务器情况下完成了模块的卸载。总结来看,我们的内核模块能够修复是由于代码中并没有存在不同编译器环境下存在差异的其他结构。如果存在其他结构,可能在加载过程中就会出现崩溃的情况。

因而还是需要强调,必须使用和内核编译时一致的gcc对模块进行编译。

参考: