1. 概论
我们将学习使用gdb来调试通过一个通过串行线同PC相连的嵌入式系统.
Gdb可以调试各种程序,包括C、C++、JAVA、PASCAL、FORAN和一些其它的语言。包括GNU所支持的所有微处理器的汇编语言。
在gdb的所有可圈可点的特性中,有一点值得注意,就是当运行gdb的平台(宿主机)通过串行端口(或网络连接,或是其他别的方式)连接到目标板时(应用程序在板上运行),gdb 可以调试对应用程序进行调试。这个特性不光在将GNU工具移植到一个新的操作系统或微处理器时侯很有用,对于那些使用GNU已经支持的芯片的嵌入式系统进行开发的设计人员来讲,也是非常有用的。
当gdb被适当的集成到某个嵌入式系统中的时候,它的远程调试功能允许设计人员一步一步的调试程序代码、设置断点、检验内存,并且同目标交换信息。Gdb同目标板交换信息的能力相当强,胜过绝大多数的商业调试内核,甚至功能相当于某些低端仿真器。
2.Gdb在嵌入式领域的功能实现
当调试一个远端目标设备时,gdb依靠了一个调试stub来完成其功能。调试stub即是嵌入式系统中一小段代码,它提供了运行gdb的宿主机和所调试的应用程序间的一个媒介。
Gdb和调试stub通过GDB串行协议进行通信。GDB串行协议是一种基于消息的ASCII码协议,包含了诸如读写内存、查询寄存器、运行程序等命令。由于绝大多数嵌入式系统设计人员为了最好的利用他们手中特定的硬件的特征,总是自己编写自己的stub。所以我们有必要清楚的了解一下gdb的串行通信协议。在后面我们会详细介绍。
为了设置断点,gdb使用内存读写命令,来无损害地将原指令用一个TRAP命令或其它类似的操作码(在此假定,被调试的应用程序是处在RAM中的,当然,如果stub有足够好的性能,硬件也不错的话,这个条件也不是必须的)代替,使得执行该命令时,可以使得控制权转移到调试stub手中去。在此时,调试stub的任务就是将当前场景传送给gdb(通过远程串行通信协议),然后从gdb处接收命令,该命令告诉了stub下一步该做什么。
为了说明,下面的代码是Hitachi SH-2处理器的一个TRAP异常处理程序:
/*将当前寄存器的值存储到堆栈中*/
/* 然后调用gdb_exception. */
asm("
.global _gdb_exception_32
_gdb_exception_32:
/* 将堆栈指针和r14压入堆栈*/
mov.l r15, @-r15
mov.l r14, @-r15
/*当执行一个陷阱异常时,sh2 自动的将pc 和sr 放入堆栈 */
/*所以我们必须调整我们给gdb的堆栈指针值,以此来说明这个特别的数据 */
/* 换言之,在该陷阱被执行前,gdb想看看堆栈指针的值,*/
/* 而不是陷阱被执行当前时的值。*/
/*所以,从我们刚压入堆栈的sp值中减去8*/
/*(pc和sr都是4个字节的 )*/
mov.l @(4,r15), r14
add #8, r14
mov.l r14, @(4,r15)
/*将其它寄存器值压入堆栈 */
mov.l r13, @-r15
mov.l r12, @-r15
mov.l r11, @-r15
mov.l r10, @-r15
mov.l r9, @-r15
mov.l r8, @-r15
mov.l r7, @-r15
mov.l r6, @-r15
mov.l r5, @-r15
mov.l r4, @-r15
mov.l r3, @-r15
mov.l r2, @-r15
mov.l r1, @-r15
mov.l r0, @-r15
sts.l macl, @-r15
sts.l mach, @-r15
stc vbr, r7
stc gbr, r6
sts pr, r5
/* 调用gdb_exception, 令其异常值=32 */
mov.l _gdb_exception_target, r1
jmp @r1
mov #32, r4
.align 2
_gdb_exception_target: .long _gdb_exception
");
/* 下面是一个从调试stub返回对某个应用程序的控制的样例(针对Hitachi SH2)*/
/*如果用C语言写,那么该语句的原型为:*/
/* void gdb_return_from_exception( gdb_sh2_registers_T registers );*/
/* 总而言之,我们可以用同gdb_exception_nn把寄存器压入堆栈同样的方式*/
/* 将其从堆栈中弹出。然而,通常返回指针同我们的返回堆栈指针不一样。*/
/*所以如果我们在拷贝pc和sr到返回指针之前将r15弹出的话,我们就回*/
丢失掉pc和sr。
*/
asm("
.global _gdb_return_from_exception
_gdb_return_from_exception:
/*恢复某些寄存器*/
lds r4, pr
ldc r5, gbr
ldc r6, vbr
lds r7, mach
lds.l @r15+, macl
mov.l @r15+, r0
mov.l @r15+, r1
mov.l @r15+, r2
mov.l @r15+, r3
mov.l @r15+, r4
mov.l @r15+, r5
mov.l @r15+, r6
mov.l @r15+, r7
mov.l @r15+, r8
mov.l @r15+, r9
mov.l @r15+, r10
mov.l @r15+, r11
mov.l @r15+, r12
/* 将pc和 sr弹出到应用程序的堆栈*/
mov.l @(8,r15), r14
mov.l @(16,r15), r13
mov.l r13, @-r14
mov.l @(12,r15), r13
mov.l r13, @-r14
/* 完成恢复寄存器的工作*/
mov.l @r15+, r13
mov.l @r15+, r14
mov.l @r15, r15
/*调整应用程序的堆栈,来说明pc, sr */
add #-8, r15
/* ...返回到应用程序*/
rte
nop
");
当处理器遇到了一个TRAP指令(该指令是由gdb 设置的,做断点用)时,该指令使得处理器的当前场景转向一个名为gdb_exception()的函数。最终,目标调用了gdb_return_from_exception()函数,该函数恢复了处理器的场景并将控制权交给应用程序。
远程串行协议的步进命令稍微更有挑战性些,特别当目标处理器不提供一个"跟踪位"或类似的功能时。在这些情况下,唯一的替代办法就是让stub把将要执行的指令反汇编。这样它就会知道程序下一步要执行到何处。
幸运的是,在gdb的源代码中也提供了关于如何一些实现这些步近命令的建议。对于Hitachi SH-2芯片而言,在gdb/sh-stub.c文件中说明了函数doSStep()的使用,对于其它种类的芯片,函数的名字也差不多,请看文件gdb/i386-stub.c和gdb/m68k-stub.c
3.gdb的其它功能
Gdb还可以求解在控制台中输入的任意的C表达式的值,包括包含有对远端目标的函数功能调用的表达式。我们可以输入如下命令:
print foo( sh_sci[current_sci]->smr.brg )
gdb就会将mr.brg的值传送给foo(),并报告其返回值。
当然,gdb也可以反汇编代码。只要可能的话,它还可以很好的为所需的数据提供等价的符号信息。例如,gdb用下列输出:
jmp 0x401010 <main + 80>
告诉了我们,所显示的地址与从函数main()的起始地址起偏移80个字节的地址相等。
Gdb 可以显示其自身和所调试的目标间的远程串行调试信息,也可以将该信息记录到日志文件中去。这些特性对于我们调试一个新的stub,了解stub是如何使用远程串行协议来实现用户对数据、程序内存、系统调用等等的需求是十分有用的。
Gdb拥有脚本语言,允许对目标自动的设置和检测。该语言是对目标处理器独立的,所以应用程序从一个目标处理器移植到另外的处理器时,脚本可以重用。
最后,gdb还提供了跟踪点的功能,该功能可以记录某个运行程序的信息,而尽可能的不打断程序收集数据。跟踪点需要特别的调试stub来实现。
4.一个典型的gdb会话过程
现在我们已经探讨了gdb的通用功能,现在我们来看看gdb的执行。下面给出了一个典型的gdb 调试会话过程。在该过程中,gdb初始化了同一个运行调试stub的远端目标间的通信,然后下载程序,设置断点,并运行该程序。当遇到断点时,调试stub通知gdb,gdb然后就将其源代码行显示给用户。接着,用户显示了一个变量,步近执行一个指令,然后推出gdb 。
请注意,下面并未显示用户在使用gdb时所见到的内容。用户所见到的是一个终端,显示的内容都是用英文写成的源代码、要显示的变量等等。但是,下面显示的脚本说明了当用户键入命令时在幕后发生的内容。
典型的gdb会话过程的描述
上图中,左边一栏显示了gdb控制台的一部分。在此用户键入命令并监视数据。右边一栏显示了一些使用GDB远程串行协议在宿主机和嵌入式设备之间的通信消息。在方括号中是一些解释信息。如果想清楚的了解这些信息的含义,请见附录《GDB远程串行协议》部分。
5.Gdb调试stub的源代码
虽然远程软件调试具有依赖于目标的特性,但是还是可以创建一个有高度的可移植性的调试stub,在不同的嵌入式处理器芯片之间可以被重用,而所需的修改最小。
有人已经尝试了这方面的工作。如果各位感兴趣,可以去上网查阅相关的资料。例如http://sourceforge.net/projects/gdbstubs。
处理器特定的代码包含在与处理器相关的文件名中,例如gdb_sh2*.c。我们可以针对我们特定的处理器下载相关的文件(例如gdb_m68k*.c),然后在用其替代我们机器上的相关内容。
6.关于改造gdb来解决特定问题的考虑
gdb使用了一个模块化的体系结构来实现,那么对它某些不适合我们需要的特性就可以很直接的加以处理。例如,如果我们的产品仅仅有一个通信端口,而它使用的并不是gdb的通信协议的话,那么,可以修改gdb,使得调试器的信息同我们产品已经使用的信息包相匹配。
类似地,如果我们的产品没有串行端口,而有些别的通信接口(例如CAN端口),那么我们可以加强gdb的远程通信功能,来适应该端口。
我们也可以修改gdb的工作方式使其同我们嵌入式应用程序更加的相容。例如,如果我们正在使用TRAPA #32来做些同gdb无关的工作,我们就可以改变gdb为了设置断点而使用的操作码,或者我们可以使用gdb来产生一个新的消息告诉我们的目标板开启指令追踪的功能或使能芯片内的断点产生硬件。
文件gdb/remote.c包含了gdb的远程串行协议的实现过程。对于研究gdb的模块化的实现是如何允许我们快速的将其改造以适应特定的调试目标而言,该文件是个很好的起点。其它的文件,例如gdb/remote-hms.c 和gdb/remote-e7000.c,使用了该模块化的结构来为诸如Hitachi, Motorola等公司的芯片的调试器和仿真器提供支持。
7.总结
gdb对于调试目标(包括对其内存的使用,通信媒介等等方面)的可适应性使得它对于目标板的调试而言,常常是唯一的选择。考虑到单芯片高集成度、基于IP的嵌入式产品的普及,情况更是如此。在今天,嵌入式设备的复杂性与日俱增,在进行新的设计时,其供选择的技术的选择也越来越多,要找到一个商业的开发产品是越来越困难了。
而使用GNU工具将是个很好的选择。GNU工具对各种流行的嵌入式处理器的支持意味着,当我们正在使用的开发工具对我们将要在下一个设计中使用的处理器不支持时,我们可以减少寻找新的开发工具所带来的危险。