模块接口分析
模块编译
在代码编译前宏展开时,需要进行条件编译,这需要gcc指定参数,而指定什么参数由Makefile来控制.
trace.c在编译时gcc中的参数中会带有 -D__KERNEL__, -DMODULE, -D__KBUILD_MODNAME=kmod_trace, 可以参考linux-likely学习,这用在之后的宏展开。
模块入口
不同的模块根据要求和实现有着不同的初始化顺序,内核模块的入口在include/linux/init.h
中定义
内核模块入口
1 |
动态模块的入口为include/linux/module.h
中定义,因为在编译模块时会使用gcc指令,其中会包含-DMODULE,因此相当于走#ifdef MODULE
动态模块入口
1 |
|
入口扩展
内核模块入口扩展开是什么样的,比如fs_initcall(fn)
1 |
fs_initcall(tracer_init_tracefs)最后通过宏扩展为
1 | static initcall_t __initcall__kmod_trace__397_9768_tracer_init_tracefs5 __attribute__((__used__)) __attribute__((__section__(".initcall5.init"))) = tracer_init_tracefs; |
扩展过程如下:
1 | fs_initcall(tracer_init_tracefs); |
宏扩展规则:
- 如果宏定义中有宏,则会先进行一次扩展,即参数的宏进行展开,之后进行替换 比如 #define Tag test; #define EXP(a, b) _EXP(a,b); 则EXP(Tag, First)首先扩展为_EXP(test, First)
- 如果宏定义中没有宏,则直接替换 #define _EXP(a,b) a##b, 则_EXP(test, First) 扩展为testFirst
注意如下, 扩展出来类似为.initcall5.init
, 不是.initcall5 .init
1 |
|
关于COUNTER ,见Common Predefined Macros,相当于计数器
关于attribute((used)),见 gcc 对used变量描述和gcc 对used函数描述
1 | unused >>可能不使用,不用警告 |
unused: 实践发现在静态变量中,如果一个变量没有使用,会进行未使用提示,但是普通的全局变量则不会,可以通过attribute((unused))来消除警告,_attribute__((used))也可以消除
used:如果静态变量如果没有使用,但是目标文件依旧做保留,可以通过section查看
unused测试
1 | static int test_var = 1; |
used测试
1 | static int test_var = 1; >>正常为data section下的,但是因为没有被使用过,所以目标文件中不存在 |
__COUNTER__测试
1 |
|
执行结果为:
1 | test is 0 |
扩展分析
扩展结果为
1 | static initcall_t __initcall__kmod_trace__397_9768_tracer_init_tracefs5 __attribute__((__used__)) __attribute__((__section__(".initcall5.init"))) = tracer_init_tracefs; |
initcall_t 定义为一个普通的函数指针类型,这里申明了__initcall__kmod_trace__397_9768_tracer_init_tracefs5
这个函数指针变量,指向函数tracer_init_tracefs,section为.initcall5.init, 即使没有被引用过,也在目标文件做保留
1 | ➜ linux git:(master) ✗ objdump -t kernel/trace/trace.o | grep -n "tracer_init_tracefs" |
section可以理解为内存中一块连续的区域,因此section 为.initcall5.init中的函数在内核中地址是连续的,可以查看symbol,比如System.map
1 | ffffffff82ebb71c T __initcall5_start >>section .initcall5 内存开始 |
也可以通过gdb 倒入vmlinux查看符号表
1 | (gdb) info address __initcall__kmod_trace__397_9768_tracer_init_tracefs5 |
发现一个问题,即vmlinux中的__initcall__kmod_trace__397_9768_tracer_init_tracefs5
section 变为.init.data
检查vmlinux,既然作为一个not stripped 的ELF文件,肯定是带有section的,通过ojbdump解析
1 | ➜ linux git:(master) ✗ objdump -h vmlinux |
发现并没有找到.initcall5.init section,思考:肯定是先编译出来的kernel/trace/trace.o,再整合出来的vmlinux,那么从单个.o到vmlinux,
函数的symbol肯定进行一个合并,而且section也会发现变化,这个整合section的规则是什么?因此开始研究第二部分
section整合规则
在之前Kbuild-Makefile的学习中了解到
- ld有自己的默认链接方式,也可以指定链接脚本进行链接
- 在链接vmlinux时会先链接.tmp_vmlinux.kallsyms1,
链接参数为--script=./arch/x86/kernel/vmlinux.lds
- vmlinux.lds是vmlinux.lds.S通过gcc预处理而来,-E选项代表只进行预处理,不进行编译
1
"gcc -E -Wp,-MMD,arch/x86/kernel/.vmlinux.lds.d -nostdinc -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -Ux86_64 -P -Ux86 -D__ASSEMBLY__ -DLINKER_SCRIPT -o arch/x86/kernel/vmlinux.lds arch/x86/kernel/vmlinux.lds.S"
因此重点关注
vmlinux.lds.S如何进行预处理的? 预处理时针对内核进行了哪些动态的设置?
预处理应该是进行一些注释的删除,文件包含,宏定义,条件编译
动态设置包括:输出格式,函数入口,section的内存存放地址和整合规则
下面是.init.data section的封装设置.init.data section的封装
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//vmlinux.lds.S
INIT_DATA_SECTION(16)
//include/asm-generic/vmlinux.lds.h
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
INIT_DATA \
INIT_SETUP(initsetup_align) \
INIT_CALLS \
CON_INITCALL \
INIT_RAM_FS \
}
__initcall#
KEEP(*(.initcall#
KEEP(*(.initcall#
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
__initcall_end = .;section的调整规则?
在vmlinux.lds的SECTIONS部分,针对.init.data section,通过以上宏扩展,
部分扩展如下__initcall5_start = .; KEEP(*(.initcall5.init))
,
强制将.initcall5.init
section保留并合并到.init.data section
分区
启动调用分析
我们将各级的.initcall都整合到.init.data
section之后链接到vmlinux。此时各对象(比如trace.o)下.initcall5.init
的symbol都链接到.init.data
的同一块内存区域,
组成了一块从__initcall5_start地址开始的一个类型为initcall_t的数组, 可以理解为 initcall_t __initcall5_start[];
在init.c中就可以通过__initcall5_start
这个数组去调用每一个
通过fs_initcall
调用的模块接口
下面是具体的调用实现
__initcall5_start调用
1 | //init/main.c |
启动调用gdb分析
大坑疑问
在设置gdb断点时碰到了大坑,代码部分如下:
1 | 1359 static void __init do_initcall_level(int level, char *command_line) |
下面是疑问?
b do_initcall_level
可以设置成功,但是symbol里应该没有这个函数,但是为什么可以设置成功1
2(gdb) b do_initcall_level
Breakpoint 3 at 0xffffffff82d3197d: file init/main.c, line 1365.b do_initcall_level
既然可以设置成功,但是为什么断点只触发一次?为什么设置的断点在line 1365.
1
2
3(gdb) info symbol 0xffffffff82d3197d
kernel_init_freeable + 367 in section .init.text设置
b init/main.c:1359
为什么也只触发一次?1
2(gdb) b init/main.c:1359
Breakpoint 1 at 0xffffffff82d31984: file init/main.c, line 1363.设置
b __initcall__kmod_nmi__312_102_nmi_warning_debugfs5
无效?但是symbol对应的地址都是正确的。 –>因为symbol的类型不是function1
2
3
4(gdb) info address __initcall__kmod_nmi__312_102_nmi_warning_debugfs5
Symbol "__initcall__kmod_nmi__312_102_nmi_warning_debugfs5" is at 0xffffffff82ebb71c in a file compiled without debugging.
(gdb) b __initcall__kmod_nmi__312_102_nmi_warning_debugfs5
Function "__initcall__kmod_nmi__312_102_nmi_warning_debugfs5" not defined.
分析CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y情况
为了分析.init.data section中的.initcall5.init
内存地址,设置如下断点
init/main.c:1369或者trace_initcall_level(更好)
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//设置断点init/main.c:1369时的触发打印
(gdb) info b
Num Type Disp Enb Address What
breakpoint keep y 0xffffffff82d319f1 in do_initcall_level at init/main.c:1369
Continuing.
Thread 1 hit Breakpoint 1, do_initcall_level (command_line=0xffff888003783100 "root",
level=<optimized out>) at init/main.c:1369
1369 trace_initcall_level(initcall_level_names[level]);
(gdb) p level
$1 = <optimized out> >>编译器优化无法查看level,无法判定走到哪个级别的内存处理
//设置断点trace_initcall_level时的触发打印
(gdb) b trace_initcall_level
Breakpoint 1 at 0xffffffff81b07600: trace_initcall_level. (2 locations)
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y <MULTIPLE>
1.1 y 0xffffffff81b07600 in trace_initcall_level
at ./arch/x86/include/asm/jump_label.h:27
1.2 y 0xffffffff82d591ba in trace_initcall_level
at ./arch/x86/include/asm/jump_label.h:27
(gdb) c
Continuing.
[Switching to Thread 2]
Thread 2 hit Breakpoint 1, trace_initcall_level (level=0xffffffff8237c4a7 "fs") >>此时可以看到initcall_level_names
at ./include/trace/events/initcall.h:10
10 TRACE_EVENT(initcall_level,
init/main.c:1370
和do_one_initcall
到了流程跑到级别5的时候再去设置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21trace_initcall_level (level=0xffffffff8237c4a7 "fs") >>级别5
(gdb) b init/main.c:1370
Breakpoint 2 at 0xffffffff82d319fe: file init/main.c, line 1370.
(gdb) b do_one_initcall
Breakpoint 3 at 0xffffffff81000f90: file init/main.c, line 1289.
//do_one_initcall是有symbol的
ffffffff81000f90 g F .text 00000000000001d6 do_one_initcall
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, do_initcall_level (command_line=0xffff888003783100 "root",
level=<optimized out>) at init/main.c:1370
1370 for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
(gdb) p fn
$1 = <optimized out> >>fn也被编译器优化了
(gdb) p level
$2 = <optimized out>
此时开始检查内存中存放.initcall5.init
section的内存存储的symbol
1 | (gdb) c |
能发现.initcall5.init
section的内存地址的存储的symbol地址如下
1 | ffffffff82d3dd47 l F .init.text 0000000000000027 nmi_warning_debugfs <-- |
fn是initcall_entry_t *
类型,fn++是地址偏移 指针指向类型 的大小,从symbol来看,.initcall5.init
section下的symbol都是地址都是偏移4个字节,此时发现.config配置为CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y
1 | ffffffff82ebb71c l .init.data 0000000000000000 __initcall__kmod_nmi__312_102_nmi_warning_debugfs5 |
地址类型如下
1 | (gdb) p sizeof(initcall_entry_t *) |
可以发现问题, 从.initcall5.init
section下的内存取出来的symbol 地址不对
1
2
3
4
5
6
7
(gdb) x/x 0xffffffff82ebb71c
0xffffffff82ebb71c: 0xffe8ae5dffe8262b
(gdb) x/x 0xffffffff82ebb720
0xffffffff82ebb720: 0xffe93272ffe8ae5d
(gdb) x/x 0xffffffff82ebb724
0xffffffff82ebb724: 0xffe935d2ffe93272
怀疑问题和CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y
有关
禁止编译优化分析
禁止编译优化的几种尝试
内核设置编译优化等级为O0编译失败,内核支持-O2和-Os的编译优化
1
2ifdef CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE >>默认
KBUILD_CFLAGS += -O0内核设置编译等级为O1,但是启动panic
init部分优化, 修改init/Makefile 如下,编译失败
1
2- ccflags-y := -fno-function-sections -fno-data-sections
+ ccflags-y := -fno-function-sections -fno-data-sections -O2修改为-O1,依旧为optimized out
增加如下,打印log为
print fn is (____ptrval____),size is 8:4
1
2
3
4
51369 trace_initcall_level(initcall_level_names[level]);
1370 for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++){
1371 pr_info("print fn is %p,size is %ld:%ld/n", fn, sizeof(fn), sizeof(initcall_entry_t));
1372 do_one_initcall(initcall_from_entry(fn));
1373 }重新调整打印指针为%px,打印如下
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(gdb) c
Continuing.
Thread 1 hit Breakpoint 3, do_one_initcall (fn=0xffffffff82d31cf6 <nmi_warning_debugfs>)
at init/main.c:1289
1289 {
(gdb) c
Continuing.
Thread 1 hit Breakpoint 3, do_one_initcall (fn=0xffffffff82d3a502 <save_microcode_in_initrd>)
at init/main.c:1289
1289 {
(gdb) p initcall_levels
$1 = {0xffffffff82ea0190, 0xffffffff82ea01a4, 0xffffffff82ea0290, 0xffffffff82ea0314,
0xffffffff82ea0378, 0xffffffff82ea05e8, 0xffffffff82ea070c, 0xffffffff82ea0ae8, 0xffffffff82ea0c38}
[ 1.175606] print fn is ffffffff82ea05e8,size is 8:4 >>逻辑正确,sizeof大小也正确
[ 1.211950] print fn is ffffffff82ea05ec,size is 8:4
(gdb) x/x 0xffffffff82ea05e8
0xffffffff82ea05e8: 0xffe9170e >>这个怎么找到0xffffffff82d31cf6 <nmi_warning_debugfs>?
(gdb) x/x 0xffffffff82ea05ec
0xffffffff82ea05ec: 0xffe99f16
最终发现,0xffe9170e是一个偏移,见offset_to_ptr
, 0xffffffff82d31cf6 = 0xffffffff82ea05e8 + 0xffe9170e,最终找到nmi_warning_debugfs
的symbol
由此可见,CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y的情况下function symbol addr = fn addr + *fn
, .initcall5.init
section下存储的是偏移(也可以理解为相对地址)
以下两种是没有symbol的(-O2)
a. static 标记的并且只被调用过一次的可能被编译器优化为inline, 比如`do_initcall_level` b. static inline标记的函数,比如`initcall_from_entry`
因此可能编译优化导致参数gdb打印为
optimized out
修改为noinline如下
1
2
3
4
5
6
7
8
9-static void __init do_initcall_level(int level, char *command_line)
+static noinline void __init do_initcall_level(int level, char *command_line)
-static inline initcall_t initcall_from_entry(initcall_entry_t *entry)
+static noinline initcall_t initcall_from_entry(initcall_entry_t *entry)
-static inline void *offset_to_ptr(const int *off)
+static noinline void *offset_to_ptr(const int *off)注意,因为init.h是被很多模块都包含的,但是如果没有调用
initcall_from_entry
,就会在编译时提示如下,可以增加__maybe_unused
或者attribute((unused))
消除警告提示1
2
3
4
./include/linux/init.h:122:28: 警告:‘initcall_from_entry’ defined but not used [-Wunused-function]
static noinline initcall_t initcall_from_entry(initcall_entry_t *entry)新的符号表如下:
1
2
3
4
5//符号表
ffffffff82d2578f l F .init.text 000000000000008e do_initcall_level
...
ffffffff81aee60c l F .text 0000000000000007 initcall_from_entry >>这里出现两次,应该是被两个目标文件生成的 init/main.c
ffffffff81af001d l F .text 0000000000000007 initcall_from_entry >> kernel/printk/printk.c调试如下: 0xffffffff82d272f2 =0xffffffff82ea074c + 0xffe86ba6
noinline测试
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
87
88
89
90
91
92
(gdb) b do_initcall_level
Breakpoint 1 at 0xffffffff82d2578f: file init/main.c, line 1360.
(gdb) c
Continuing.
Thread 1 hit Breakpoint 1, do_initcall_level (level=level@entry=6,
command_line=command_line@entry=0xffff888003c18100 "root=/dev/sda rdinit=init crashkernel=128M console=ttyS0 rw nokaslr") at init/main.c:1360
1360 {
(gdb) p level
$1 = 6
(gdb) b initcall_from_entry
Breakpoint 2 at 0xffffffff81aee60c: initcall_from_entry. (2 locations)
(gdb) b do_one_initcall
Breakpoint 3 at 0xffffffff81000f90: file init/main.c, line 1289.
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, initcall_from_entry (entry=entry@entry=0xffffffff82ea074c)
at ./include/linux/init.h:123
123 {
(gdb) p initcall_levels
$2 = {0xffffffff82ea01d0, 0xffffffff82ea01e4, 0xffffffff82ea02d0, 0xffffffff82ea0354,
0xffffffff82ea03b8, 0xffffffff82ea0628, 0xffffffff82ea074c, 0xffffffff82ea0b28, 0xffffffff82ea0c78}
(gdb) b offset_to_ptr
Breakpoint 4 at 0xffffffff8114a7b0: offset_to_ptr. (5 locations)
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0xffffffff82d2578f in do_initcall_level at init/main.c:1360
breakpoint already hit 7 times
2 breakpoint keep y <MULTIPLE>
breakpoint already hit 1 time
2.1 y 0xffffffff81aee60c in initcall_from_entry at ./include/linux/init.h:123
2.2 y 0xffffffff81af001d in initcall_from_entry at ./include/linux/init.h:123
3 breakpoint keep y 0xffffffff81000f90 in do_one_initcall at init/main.c:1289
4 breakpoint keep y <MULTIPLE>
4.1 y 0xffffffff8114a7b0 in offset_to_ptr at ./include/linux/compiler.h:233
4.2 y 0xffffffff8118f220 in offset_to_ptr at ./include/linux/compiler.h:233
4.3 y 0xffffffff815f6370 in offset_to_ptr at ./include/linux/compiler.h:233
4.4 y 0xffffffff81aee600 in offset_to_ptr at ./include/linux/compiler.h:233
4.5 y 0xffffffff81af0011 in offset_to_ptr at ./include/linux/compiler.h:233
(gdb) c
Continuing.
Thread 1 hit Breakpoint 4, offset_to_ptr (off=off@entry=0xffffffff82ea074c)
at ./include/linux/compiler.h:233
233 {
(gdb) b include/linux/compiler.h:235
Breakpoint 5 at 0xffffffff8114a7bb: include/linux/compiler.h:235. (5 locations)
(gdb) c
Continuing.
Thread 1 hit Breakpoint 5, offset_to_ptr (off=off@entry=0xffffffff82ea074c)
at ./include/linux/compiler.h:235
235 }
(gdb) p off
$3 = (const int *) 0xffffffff82ea074c
(gdb) x/x *off
0xffffffffffe86ba6: Cannot access memory at address 0xffffffffffe86ba6
(gdb) x/x 0xffffffff82ea074c
0xffffffff82ea074c: 0xffe86ba6
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0xffffffff82d2578f in do_initcall_level at init/main.c:1360
breakpoint already hit 7 times
2 breakpoint keep y <MULTIPLE>
breakpoint already hit 1 time
2.1 y 0xffffffff81aee60c in initcall_from_entry at ./include/linux/init.h:123
2.2 y 0xffffffff81af001d in initcall_from_entry at ./include/linux/init.h:123
3 breakpoint keep y 0xffffffff81000f90 in do_one_initcall at init/main.c:1289
4 breakpoint keep y <MULTIPLE>
breakpoint already hit 1 time
4.1 y 0xffffffff8114a7b0 in offset_to_ptr at ./include/linux/compiler.h:233
4.2 y 0xffffffff8118f220 in offset_to_ptr at ./include/linux/compiler.h:233
4.3 y 0xffffffff815f6370 in offset_to_ptr at ./include/linux/compiler.h:233
4.4 y 0xffffffff81aee600 in offset_to_ptr at ./include/linux/compiler.h:233
4.5 y 0xffffffff81af0011 in offset_to_ptr at ./include/linux/compiler.h:233
5 breakpoint keep y <MULTIPLE>
breakpoint already hit 1 time
5.1 y 0xffffffff8114a7bb in offset_to_ptr at ./include/linux/compiler.h:235
5.2 y 0xffffffff8118f22b in offset_to_ptr at ./include/linux/compiler.h:235
5.3 y 0xffffffff815f637b in offset_to_ptr at ./include/linux/compiler.h:235
5.4 y 0xffffffff81aee60b in offset_to_ptr at ./include/linux/compiler.h:235
5.5 y 0xffffffff81af001c in offset_to_ptr at ./include/linux/compiler.h:235
(gdb) c
Continuing.
Thread 1 hit Breakpoint 3, do_one_initcall (fn=0xffffffff82d272f2 <ia32_binfmt_init>) at init/main.c:1289
1289 {
分析CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n情况
调整CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n
时需要将select HAVE_ARCH_PREL32_RELOCATIONS
从arch/x86/Kconfig中注释掉,因为这里将其默认使能def_bool y
此时typedef initcall_t initcall_entry_t;
且initcall_from_entry
直接return *entry;
,此时应该.initcall5.init
section下存储的是symbol地址
从vmlinux解析出来的符号表就可以验证:
1 | ffffffff82ea0ab8 g .init.data 0000000000000000 __initcall5_start |
从内存存储空间来看,直接显示内存存储的指针指向funtion空间
1 | //这块内存需要在init没有释放前去打印 |