KGDB 与 指令级单步调试

DDD  2010年06月21日 星期一 16:49 | 3894次浏览 | 0条评论

本文主要是介绍下kgdb和其单步条调试与实现的一些细节和历史..

半年前就想写这篇文章了,因种种原因迟迟没有下笔,如今再也没给自己找到耽搁的理由,就开始动手了。但在这半年间,我对kgdb有了进一步的了解,这就算是耽搁的福利吧。 :-)

1 KGDB 与 GDB:
KGDB 相当于一个 gdb server, 只是这个server是跑在内核里面,对内核进行一些破坏-恢复和探测性活动的。通过KGDB,我们可以象调试应用程序那样,对内核进行一些有限制的操作,如设置断点、检查变量值、单步跟踪等。

目前KGDB与GDB之间的通信协议是遵循GDB远程通信协议,该协议是基于早先的串口直联通信的背景下设计的,所以该协议只是对每个协议包的数据的有效性进行校验,并没有对收发数据的顺序进行校验,这在串口直联通信下是没问题的,但在复杂的网络环境下则可能就会出现问题,这也是为什么kgdboe没被内核接纳原因之一,很期待某天kgdb和gdb能达成一致,对协议增加个小小扩展来解决收发数据顺序问题,从而可以放心的通过网络进行对内核进行调试了。

GDB远程通信协议文档:
http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/gdb/remote-protocol.html

2 KGDB单步调试:

KGDB单步调试的触发可以简单的在gdb那边输入一个 's' 命令:
(gdb) s

对于输入的s指令有软单步和硬单步两种处理方法,其具体实现依赖体系结构和gdb实现。

2.1 硬单步:
顾名思义,即由cpu硬件支持的指令单步调试,即CPU每执行一条指令后,就产生一个调试异常。

目前我知道的只有PPC和X86支持硬单步,KGDB支持硬单步很简单:

在X86上只需要设置EFLAGS寄存器的TF位来预设cpu为单步模式。
linux_regs->flags |= X86_EFLAGS_TF;


PPC上 如果是BOOKE或40x处理器则是设置MSR寄存器中的DE位,其它PPC处理器则是设置SE位,来预设cpu为单步模式
#if defined(CONFIG_40x) || defined(CONFIG_BOOKE)
            mtspr(SPRN_DBCR0,
                  mfspr(SPRN_DBCR0) | DBCR0_IC | DBCR0_IDM);
            linux_regs->msr |= MSR_DE;
#else
            linux_regs->msr |= MSR_SE;
#endif


当kgdb退出调试异常时,会把linux_regs(就是struct pt_regs)弹出,这样
被恢复了的内核代码就会处于单步调试的状态下了,即cpu进入单步模式。


2.2 软单步(也有称做伪单步):

软单步是在目标系统不支持硬件单步的情况下不得以而实现的,目前主要在ARM和MIPS上被使用。

软单步实现方式是先预测分析出系统运行的下一条指令的地址,然后在那个地址上设置断点,让系统继续运行。当系统运行到那个指令地址时,将触发断点并产生异常,这样就软模拟了一个单步运行的过程.

软单步通常是放在GDB端实现,但也有放在kgdb端实现的....


3 KGDB 单步调试流程(X86,硬单步):

A: KGDB进入调试状态,等待gdb的指令.(陷入调式异常的处理函数,使用轮询方式来检测gdb端的输入数据)
B: gdb 发送 "s" 指令给 KGDB
C: KGDB 收到"s" 指令后,设置EFLAGS寄存器的TF位,使得当前CPU进入单步调试模式.
D: KGDB退出调式异常处理函数,让系统恢复正常运行
E: CPU执行一条指令后,就会产生单步异常。
F: KGDB陷入调式异常, 发信息通知gdb。
G: KGDB回到 A 状态, 继续等待gdb指令.



*********************************************************************************
以下内容过于代码细节,比较枯燥无趣,大家可以选读咯
*********************************************************************************


4 KGDB硬单步 on X86 SMP:

4.1 引子
在说这个之前,我们先来看个patch

commit 8097551d9ab9b9e3630694ad1bc6e12c597c515e
Author: Jason Wessel <jason.wessel@windriver.com>
Date:   Fri Dec 11 08:43:18 2009 -0600

    kgdb,x86: do not set kgdb_single_step on x86
  
    On an SMP system the kgdb_single_step flag has the possibility to
    indefinitely hang the system in the case.  Consider the case where,
    CPU 1 has the schedule lock and CPU 0 is set to single step, there is
    no way for CPU 0 to run another task.  
    ...
    ...
    CC: Ingo Molnar <mingo@elte.hu>
    Signed-off-by: Jason Wessel <jason.wessel@windriver.com>

diff --git a/arch/x86/kernel/kgdb.c b/arch/x86/kernel/kgdb.c
index aefae46..dd74fe7 100644
--- a/arch/x86/kernel/kgdb.c
+++ b/arch/x86/kernel/kgdb.c
@@ -400,7 +400,6 @@ int kgdb_arch_handle_exception(int e_vector, int signo, int err_code,
                /* set the trace bit if we're stepping */
                if (remcomInBuffer[0] == 's') {
                        linux_regs->flags |= X86_EFLAGS_TF;
-                       kgdb_single_step = 1;
                        atomic_set(&kgdb_cpu_doing_single_step,
                                   raw_smp_processor_id());
                }


这个patch很简单,就一行,但解决了一个历史悠久的BUG,悠久到得从到kgdb进入内核起就存在。说不定某次你在SMP的机器上使用kgdb执行单步调试时把系统给hang住,很有可能就是这个臭虫干的。

这个patch改变了kgdb在多核系统上单步调试时的对其它CPU控制的策略,而kgdb_single_step则是被用来决定其它CPU运行还是暂停的变量.

以前的实现是在某一个CPU做单步调试的时候,其它的CPU仍然被暂停运行,即设置kgdb_single_step为1,
现在则改变为其它的CPU也将恢复正常运行,等单步调试异常触发后,再将他们暂停运行。


4.2 X86的硬单步灵魂 --- TF标志:
TF标志位在Intel的文档上解释为:
TF Trap (bit 8) — Set to enable single-step mode for debugging; clear to
   disable single-step mode. In single-step mode, the processor generates a
   debug exception after each instruction. This allows the execution state of a
   program to be inspected after each instruction. If an application program
   sets the TF flag using a POPF, POPFD, or IRET instruction, a debug exception
   is generated after the instruction that follows the POPF, POPFD, or IRET.

请注意最后的一句注明:
如果TF是被POPF,POPFD或IRET指令所设置,则debug异常则是在等POPF,POPFD或IRET指令后的下一条指令执行完才产生。


4.3 KGDB X86 硬单步实现:
回过头来看KGDB X86 硬单步的实现,正如第2章里面所描述的,
KGDB收到"s"指令后,去设置EFLAGS寄存器的TF位来使CPU进入单步调试模式.

其代码指令流为:
1: exception hanlde function(int3, or others) ->
2:  -> kgdb 's': set the trace bit(TF) for single step,
       but didn't clear interrupt bit(IF).
3: <- IRET (quit exception hanlde function)
  -- it is possbile that the system could be interrupted here. --
4: execute an instruction
5: debug trap -- here will be generated a debug exception
6: -> do_debug
...

kgdb退出异常处理函数后,使用iret来恢复被中断前的CPU寄存器,所以正好符合3.1里面注明的条件,所以异常将在iret的下一条指令执行后才被产生.即在将“5”处产生debug exception.

4.4 分裂:

由于interrupt位(IF)并没有被显示给清理,在执行3-4之间,如果有中断发生,并且调试异常打断的上下文处于开中断状态,系统就会被中断打断,进入中断处理函数。此时将出现了不可控的状况,而导致系统挂掉.如:


CPU1被暂停的时候拿了一把锁,这时候CPU0在做单步,然后被中断打断,而中断处理函数里很可能也会去拿那把已经被CPU1拿的锁,
然后死锁就这样发生了...

others: 为什么kgdb在前期运行的时候并没有被中断打断
在x86上,int3和debug异常都是作为中断门给初始化的.可以参考代码:
linux/arch/x86/kernel/traps_32.c : early_trap_init()
linux/arch/x86/kernel/traps_64.c : trap_init()


4.5 解决分裂:

4.5.1 解决方法一 单步时关中断,单步调试完后恢复原来的中断位

/* Clean the interrupt bit if we're stepping */
   linux_regs->flags &= ~X86_EFLAGS_IF;
/* Set the trace bit if we're stepping */
   linux_regs->flags |= X86_EFLAGS_TF;

这个最初是我提出来的,它应该是从"根"上解决分裂问题,虽然就一个清理中断位(IF)的操作,
但由于kgdb的特殊性,实际上还需要考虑很多问题,而且有些问题看起来是无法解决的...

单步时关中断需要考虑的问题如下:
问题A:
如果单步调试的对象是 -- 修改中断位的指令
这些指令(cli,sti,iret/iretd,popf/popfd)可能会改变中断位的值,如果kgdb还是恢复原来的中断位,就会修改程序的行为,所以遇到这些指令的话,需要特殊处理。

问题B:
如果单步调试的对象是 -- 可能引起内核态切换到用户态的指令
这些指令(retn Iw(f64), retn, retf Iw(f64), retf, iret/iretd)可能引起内核态切换到用户态,
一旦切换到用户态,那前面中断位的修改,将不可能再被恢复... 这就导致调试器修改了系统的行为,是绝对不能被接受的.

正由于问题B的存在,导致“解决方法二 单步时关中断,单步调试完后恢复原来的中断位”这个方法无法彻底实现...

4.5.2 解决方法二 让其它CPU也跑起来

这是个简洁的方法,目前内核的那个patch就是采用这个方式。这个方式有个小小的缺点,
为了避免出现其它非单步CPU踩中断点的而引起gdb调试器的误解,在单步调试的时候并没有将断点使能,所以有可能会丢失断点。
但无论如何,正如patch所说,丢失断点总比让系统hang要好吧..


5 特别感谢:
我需要特别感谢下我同事jia.zhang,他作为本文的第一读者,给了我很好的建议和修改。
尤其是他在我尝试做 ”4.5.1 解决方法一“ 时给予我极大的帮助,和我一起讨论研究,找资料,帮我解决问题。

评论

我的评论:

发表评论

请 登录 后发表评论。还没有在Zeuux哲思注册吗?现在 注册 !

暂时没有评论

Zeuux © 2024

京ICP备05028076号