kgdb抓虫日记 – set breakpoint at mips

/ 1评 / 0

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抓虫日记 – set breakpoint at mips”

  1. […] 关于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 […]

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据