本文讨论了GDB远程调试技术在调试内核、嵌入式系统中的实现,简要阐述GDB宿主机和GDB远程串行协议,详细分析GDB调试代理在内核层、应用层的各种实现方法。并提出了一种在不修改操作系统内核前提下调试应用程序的方法。这种方法可移植性强,而且消除了修改系统内核可能带来的隐患,减少了因修改内核而带来的工作量。在调试微内核操作系统服务的应用中表明,此方法非常有效。
关键词:
远程调试;stub;GDBserver; KGDB;嵌入式系统调试
Abstract:
This thesis discusses the realization of GDB remote debugging technology in kernel and embedded system. Firstly it describes the GDB host and GDB Remote Serial Protocol, then it analyses in detail the realization of GDBstub on kernel layer and application layer, at last the authors give a new method for debugging application while the OS kernel doesn’t be modified. This method has strong portability, and it eliminates the hidden trouble of OS kernel modification, also it reduces the workload subject to OS kernel modification. The application in debugging OS service of micro-kernel system shows that this method is reasonably efficient.
Key words:
Remote debugging; stub; GDBserver; KGDB; embedded system debugging
1、引言
调试是开发过程中必不可少的环节,然而内核、嵌入式系统的调试不同于传统的调试系统。通常嵌入式系统不具备使用本地调试器的能力,由于:
系统自身的资源有限。内存小,输入输出设备不能用于调试。
传统的调试系统需要文件系统,嵌入式系统通常无文件系统,内核调试时还不支持文件系统。
调试器的运行本身需要操作系统的支持,因此无法实现操作系统内核的调试。
最有效的解决方法是采用远程调试技术。远程调试是指调试器运行的环境(主机)和被调试的系统(目标机)在物理上是分离的,通过串口或者网络进行连接的调试技术。
GNU免费提供的GDB就拥有强大的远程调试功能,它能够使开发人员以远程调试的方式单步执行目标平台上的程序代码、设置断点、查看内存,并同目标平台交换信息。GDB远程调试的实时、动态、方便、免费等优点使它逐渐成为嵌入式开发首选的调试方案。
远程调试系统由三部分组成:主机上的本地调试器,目标机上的调试代理,远程调试协议。如图1。对应于GDB远程调试系统的三部分:GDB,GDBstub, GDB远程串行协议。下面就这三部分进行分析。
图1. 远程调试系统
2、RSP协议
GDB RSP(Remote Serial Protocol)定义了GDB宿主机与被调试目标机进行通信时数据包的格式。信息的格式是:$数据#校验码。多数的信息都使用ASCII码,数据由一系列的ASCII码组成,校验码是由两个16进制数组成的单字节校验码。接受方接受数据并校验,若正确则回应“+”,错误则回应“-”。通信的内容包括读写数据、控制程序运行、报告程序状态等命令。RSP的基本命令从通信对话角度可以分为两种:
1) 请求
?:读当前系统状态
g:读所有寄存器
G:写所有寄存器
m:读内存
M:写内存
c:继续执行
s: 单步执行
k:终止进程
2) 答复
“”:告诉GDB上次请求命令不支持。
E:告诉GDB出错
OK:上次请求正确
W:系统在exit_status状态下退出。
X:系统在signal信号下终止。
S:系统在signal信号下停止。
O:告诉GDB控制台输出,这也是唯一向GDB发出的命令
3、GDB远程调试功能
调试内核时通常还没有文件系统,而且多数嵌入式由于自身资源的限制不具备文件系统,因此将与文件系统有关的源文件、目标文件及符号表都存放在主机上,由主机上的调试器处理。同样,调试用的输入输出设备也是由主机提供。主机上的调试器接受用户输入的调试命令并进行预处理,对于有些命令(如breakpoint)的处理就在主机GDB上实现,不需要同目标机进行通信。当然,更多的指令需要在目标机上调试代理上实现的。主机将预处理完之后的命令根据RSP进行封装,发送给目标机上的调试代理,调试代理接受命令后作相应的处理,并返回信息给主机上的调试器。
4、目标机上stub的实现
目标机上stub的基本功能是与主机GDB进行通信,实现读写内存、寄存器,stop,continue。主机GDB同目标机上stub进行通信的通用模型如图2:
图2. GDB同目标机上stub通信的通用模型
目标机与主机通过硬件连接,被调试部分插入stub,GDB与被调试部分通过RSP进行通信。根据stub所处层的不同来实现不同层的调试,包括内核层、应用层的调试。
4.1 内核层调试模型
图3. 使用stub对内核进行调试
如图3,将stub插入到内核里就可以实现内核的调试了。Linux内核调试机制KGDB就是使用这种模式。KGDB可以分为初始化模块和控制模块。
4.1.1初始化模块
修改异常处理函数,使得在异常发生时都进入函数handle_exception(),这样GDB就能够捕获这些异常。初始化之后使用breakpoint()函数将系统控制权直接交给GDB。KGDB对异常处理函数的修改基本上可以分为二种。
定义宏CHK_REMOTE_DEBUG
#define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after) { if (linux_debug_hook != (gdb_debug_hook *) NULL && !user_mode(regs)) { (*linux_debug_hook)(trapnr, signr, error_code, regs) ; after; } }
改变程序的流程,以int3的处理函数为例
#define DO_VM86_ERROR(trapnr, signr, str, name) asmlinkage void do_##name(struct pt_regs * regs, long error_code) { CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,goto skip_trap) do_trap(trapnr, signr, str, 1, regs, error_code, NULL); skip_trap: return; }
展开DO_VM86_ERROR (3,SIGTRAP,"int3",int3)
asmlinkage void do_int3(struct pt_regs *regs, long error_code)
{ if (linux_debug_hook != ( gdb_debug_hook *)NULL&&! user_mode(regs))
{ (*linux_debug_hook)(3, SIGTRAP, errorcode, regs);
goto skip_trap;
}
do_trap(3, SIGTRAP, "int3", 1, regs, error_code, NULL);
skip_trap:
return;
}
从以上代码可见,进入内核调试状态之后,异常处理函数就是handle_exception(),程序流程跳过了非调试状态时的处理函数do_trap。
不改变程序的流程,以异常divide_error 的处理函数为例
#define DO_VM86_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) asmlinkage void do_##name(struct pt_regs * regs, long error_code) { …… do_trap(trapnr, signr, str, 1, regs, error_code, &info); }
展开DO_VM86_ERROR_INFO( 0, SIGFPE, "divide error", divide_error, FPE_INTDIV, regs->eip)
asmlinkage void do_divide_error (struct pt_regs *regs, long error_code)
{ if (linux_debug_hook != ( gdb_debug_hook *)NULL&&! user_mode(regs))
{ (*linux_debug_hook)(3, SIGTRAP, errorcode, regs);
}
do_trap(0, SIGTRAP, "divide erro", 1, regs, error_code, &info);
}
从以上代码中看不出调试状态跟非调试状态的区别,然而我们看一下do_trap函数中可能会调用的函数die()。
void die(const char * str, struct pt_regs * regs, long err)
{
CHK_REMOTE_DEBUG(1,SIGTRAP,err,regs,)
do_exit(SIGSEGV);
}
由此可见,调试状态下的异常处理函数还是进入了handle_exception函数。不过与上面一种异常不同之处在于:异常处理函数在调试与非调试状态下的程序流程是相同的,handle_exception只提供获取系统当时的状态,继续运行的结果还是do_exit。
虽然不是所有异常函数都是按上述两种方法定义的,但本质上都可以归划为其一,显然绝大多数处理函数的修改属于第二种,因为第一种异常就是为调试准备的。因此在目标机具有调试用的输出设备的情况下,完全可以不修改第二种异常处理函数,因为linux内核在非调试状态下的异常处理函数已经输出必要的状态信息、出错信息。
4.1.2控制模块
在控制模块完成与主机GDB的通信,具体流程如图4,handle_exception函数首先判断CPU是否处于VM86模式或用户态,若是则返回,可见KGDB只调试内核态程序。然后接受GDB发来的信息,根据接受的信息作出相应的操作和回复。流程图的虚线框内是所有GDBstub中handle_exception函数的通用流程。
4.2 应用程序调试模型
在嵌入式Linux开发领域里调试应用程序常用调试代理工具GDBserver,其工作原理并不是将stub编译在被调试应用程序内,而是把被调试程序作为GDBserver的子进程,这样GDBserver就可以利用内核提供的代码跟踪机制(ptrace)监控被调试进程的运行,从而来完成调试任务。此工作原理同GDB本地调试相似。其调试模型如图5。GDBserver的工作流程是:GDBserver创建子进程->绑定跟踪ptrace(ptrace_traceme,,)->从主机传来的各种调试命令通过GDBserver转化为各种操作需求的ptrace。显然,如果要用GDBserver来进行远程调试的话,就需要内核操作系统的支持,包括子进程、代码跟踪机制,这样对于其他嵌入式系统内核工作量会比较大。而且ptrace也有其局限性,比如只能跟踪它的子进程,在调试进程和被调试进程之间传送一个长字的数据。使用通用的调试模式工作量会更小。如图6,将stub编译在应用程序中,并在应用程序入口处就插入断点,程序开始就上控制权交给GDB,之后的流程跟内核层调试类似。
图4. GDBKGDB中handle_exception函数流程
图5. 使用GDBserver对应用程序进行调试
图6. 使用stub对应用程序进行调试
5. 不修改内核前提下调试应用程序
GDB实现设置断点的方式是使用内存的读写,即将原指令用一个trap指令代替,使得程序执行到该指令时产生单步调试中断,然后就进入异常处理函数,针对调试器的各种操作处理函数需要作出相应的操作。不同的系统提供不同的调试异常指令,如int3,trap2等,显然对于使用这些硬件平台提供的断点指令为了实现GDBstub调试功能需要改写这些指令异常处理函数。因此一般的调试系统器或调试代理都需要涉及单步调试指令的处理函数,需要系统内核的支持。上面提到的KGDB修改了异常处理函数,GDBserver需要系统内核提供ptrace函数。这种方法存在一些不足之处:修改内核工作量大,移植性差。针对这些情况我们可以采用另一种断点实现方案:在stub中定义一个设置断点函数。
断点函数模拟调试异常指令,实现保护现场、调用异常处理函数、恢复现场并将控制权交给被调试程序。断点函数的基本流程如下。
#define BREAKPOINT __asm__ __volatile__(" bl ent_exception\n)
void debug_trap()
{ __asm__ __volatile__(
" ent_exception: \n"
保存现场
" bl handle_exception \n"
" out_exception: \n"
恢复现场
);
}
handle_exception()函数流程类似图4中的虚线框部分。其中有一点,也是这种方法实现的关键部分是:断点指令的替换。断点设置时从GDB传过来的硬件平台提供的断点异常指令的二进制码,必须将此二进制码替换成在stub中新定义的BREAKPOINT二进制码,这样才能进入调试异常处理函数。因此在handle_exception()函数中,如果收到的请求是“M”,则需要作些处理,流称如图7:
图7. 替换指令
这种方法理论上在内核调试和应用程序调试中都可以使用,但在应用程序的调试中其优点更明显。这种方法在写stub时候不涉及内核,在调试应用程序时不需要切换到内核模式下,直接在用户模式中就可以完成。这种方法也存在些不足之处。为了实现现场保护,要求用户了解系统内的寄存器。随着stub本身复杂度的增加,它的正确性需要更多的检验。
6、结束语
加stub的远程调试方法方便而有效,而且可以降低项目成本,在实际工作中得到广泛的研究和应用。本文提到在不修改内核前提下调试应用程序的方法已成功应用于我们自己开发的微内核结构的操作系统里,为该系统的开发应用提供良好的调试手段。当然加stub的远程调试方法也存在一些不足。显然stub的应用是在串口通信的基础上,因此串口处理函数以及stub自身处理函数的正确性是确保stub安全调试的前提。
参考文献
[1].李红卫李翠萍,kgdb调试Linux内核肋剖析与改进,微型机与应用,2004年第10期
[2].郭胜超,GDB远程调试及其在嵌入式Linux系统中的应用,计算机工程与应用,2004年第26卷第10期.
[3].彭进展,GRDBS:一种针对嵌入式系统的通用远程调试系统,计算机工程,2003年2月第29卷第2期.
[4] .Gatliff, Bill, Embedding with GNU: the gdb Remote Serial Protocol, Embedded Systems Programming, September 1999, p. 109.
[5].Gilmore J, Shebs S, GDB Internals: A Guild to the Internals of the GNU Debugger, Free Software Foundation Inc.,1999.
作者单位:浙江大学计算机系
地址:浙江大学玉泉校区4舍230 310027
Email:liulin@zju.edu.cn