工控网首页
>

应用设计

>

GDBstub的剖析与改进

GDBstub的剖析与改进

2005/11/14 10:27:00
摘要:   本文讨论了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.图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.图2. GDB同目标机上stub通信的通用模型
  目标机与主机通过硬件连接,被调试部分插入stub,GDB与被调试部分通过RSP进行通信。根据stub所处层的不同来实现不同层的调试,包括内核层、应用层的调试。 4.1 内核层调试模型
图3.图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)监控被调试进程的运行,从而来完成调
投诉建议

提交

查看更多评论