A: BUG重现步骤
A.1
写一个空函数module_event,并且注册到module的通知事件链上:
1 2 3 4 5 6 7 8 9 10 | static int module_event(struct notifier_block *self, unsigned long val, void *data) { return 0; } static struct notifier_block module_load_nb = { .notifier_call = module_event, }; register_module_notifier(&gdb_module_load_nb); |
A.2:
A.2.1 connect gdb to kgdb:
(GDB was configured as "--host=i686-pc-linux-gnu --target=mips-linux-gnu".)
1 | (gdb) target remote udp:10.0.0.15:6443 |
A.2.2 set a break point at "module_event":
1 | (gdb) b module_event |
A.3 insert a module to target :
the "module_event" breakpoint will be hit.
A.4 Host send a "c" order to resume system:
1 | (gdb) c |
after do "c", the system will no response...
B: BUG现场分析
经过跟踪调试,发现系统并没有挂掉,只是陷入了一个kgdb踩中断点和响应断点事件的死循环里面.
其异常行为总结如下:
从触发一个断点进入do_trap_or_bp开始:
1 2 3 4 5 6 7 8 9 | void do_trap_or_bp() -> notify_die() -> notifier_call_chain() int notifier_call_chain() { struct notifier_block *nb, *next_nb; ... ret = nb->notifier_call(nb, val, v) ---> kgdb_handle_exception() ... } |
一直进行如上循环,看起来是在notifier_call_chain()函数中被设置了一个断点,由于kgdb会使用那块代码,所以导致kgdb不断的自己击中那个断点而陷入无限死循环..
C: BUG触发原因
首先; 我们并没有在notifier_call_chain()函数的任何地方设置过断点的. 但问题很明显,在击中module_event断点后,导致kgdb陷入死循环,所以就先从那下手.
act: 我们在击中那个断点后,然后在gdb端发个"continue"命令让系统继续运行.
Note gdb 如何响应 "continue" 命令:
在执行"continue" 命令时,由于需要将断点重置回去,gdb将
1 2 3 4 | 1:取消所有断点 2:在当前断点处,执行一个单步命令,跳过这个断点地址 3:将所有取消的断点设置回去 4:发送 continue 命令 |
因此先在断点"module_event"处,做个单步,跳过这个断点,然后再让系统恢复运行.
我们来分析下"module_event"处是如何实现单步的:
由于mips不知道硬单步,所以mips上的单步是通过设置断点来模拟的的,即由gdb计算出下一条运行指令的地址值,在那个地址上设置断点.
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 | ********************************************************************** int module_event(struct notifier_block *self, unsigned long val, void *data) { return 0; } 1 notifier_call_chain() kernel/notifier.c: 578 2 { 3 while (nb && nr_to_call) { 4 next_nb = rcu_dereference(nb->next); 5 ret = nb->notifier_call(nb, val, v); -----> call module_event() 6 7 if (nr_calls) 8 (*nr_calls)++; 9 nb = next_nb; 10 nr_to_call--; 11 } 12 } ********************************************************************** But for some compiler's reasons, the module_event() function be compiled as following: "module_event" at MIPS: 00000000 : 0: 03e00008 jr ra 4: 00001021 move v0,zero |
熟悉Mips体系结构的朋友应该了解,
在mips上,紧跟着任何跳转指令的指令(在延迟槽中)会被CPU执行,即跳转被执行.(Mips流水线,跳转延迟)
所以我们可以认为module_event函数只有一条指令,如果执行单步的话,必然断点将下在其下一个运行代码,
而从上面的分析来看,这个软单步断点将被下在
"5 ret = nb->notifier_call(nb, val, v); "的下一条指令:
1 | 7 if (nr_calls) |
上,原因终于被发现了.
D: BUG解决方法
原因虽然是找到了,但要解决确不简单.
我最先做了一个workaround的patch,即在module_event函数里插入空指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ********************************************************************** #include <asm/system.h> #ifndef nop #define nop() __asm__ __volatile__ ("nop") #endif int module_event(struct notifier_block *self, unsigned long val, void *data) { /* * add an "nop" instruction to avoid kgdb trap a die loop * when gdb do an software single step to skip the * "module_event" breakpoint. */ nop(); return 0; } ********************************************************************* |
上面的patch只是治标,并没有治本,其本质原因是, kgdb依赖了notifier_call_chain()来捕获断点,
所以只要往notifier_call_chain()下了断点就会让kgdb陷入死循环的问题.
所以从本质上解决问题的方法有两个:
1: 不让kgdb依赖notifier_call_chain().
2: 禁止把断点下类似notifier_call_chain()这样的kgdb依赖的函数里面.
对于方法1的解决方法很简单,只要在do_trap_or_bp()那里做个hook,让kgdb直接从hook里获取断点,而不再通过notify_die()来响应事件.
Jason已经将其实现并提交到内核里. 大家可以下载一份最新的kernel代码使用下面命令查看这个patch:
1 | git show 5dd11d5d47d248850c58292513f0e164ba98b01e |
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 | commit 5dd11d5d47d248850c58292513f0e164ba98b01e Author: Jason Wessel <jason.wessel@windriver.com> Date: Thu May 20 21:04:26 2010 -0500 mips,kgdb: kdb low level trap catch and stack trace The only way the debugger can handle a trap in inside rcu_lock, notify_die, or atomic_notifier_call_chain without a recursive fault is to have a low level "first opportunity handler" do_trap_or_bp() handler. Generally this will be something the vast majority of folks will not need, but for those who need it, it is added as a kernel .config option called KGDB_LOW_LEVEL_TRAP. Also added was a die notification for oops such that kdb can catch an oops for analysis. There appeared to be no obvious way to pass the struct pt_regs from the original exception back to the stack back tracer, so a special case was added to show_stack() for when kdb is active because you generally desire to generally look at the back trace of the original exception. Signed-off-by: Jason Wessel <jason.wessel@windriver.com> Acked-by: Ralf Baechle <[email protected]-mips.org> |
对于方法2: 禁止把断点下类似notifier_call_chain()这样的kgdb依赖的函数里面.
这个实现可以将kgdb依赖的函数放在一个特殊代码段里,然后在设置断点的时候检测断点地址是否属于那个特殊代码段,
如果属于,则通知gdb,设置断点失败. 目前我有个原型patch,但还得继续完善,因为有些函数已经被kprobes给划走了,所以得找个办法与kprobes统一.
[…] 关于KGDB_LOW_LEVEL_TRAP,详情可参考这里。 ?View Code DEBUG_INFOCONFIG_DEBUG_INFO = y 该选项可以使得编译的内核包含一些调试信息,使得调试更容易。 Location: -> Kernel hacking ?View Code FRAME_POINTERCONFIG_FRAME_POINTER = y 该选项将使得内核使用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯,即函数调用栈信息。 Location: -> Kernel hacking ?View Code MAGIC_SYSRQCONFIG_MAGIC_SYSRQ = y (如果你选择了KGDB_SERIAL_CONSOLE,这个选项将自动被选上) 激活"魔术 SysRq"键. 该选项对kgdboc调试非常有用,kgdb向其注册了‘g’魔术键来激活kgdb 。 Location: -> Kernel hacking […]