《电子技术应用》
您所在的位置:首页 > 嵌入式技术 > 业界动态 > GDBstub的剖析与改进

GDBstub的剖析与改进

2008-05-27
作者:黄红燕,史 烈

  摘 要: 讨论了GDB远程调试" title="远程调试">远程调试技术在调试内核、嵌入式系统" title="嵌入式系统">嵌入式系统中的实现,简要阐述GDB宿主机和GDB远程串行协议,详细分析GDB调试代理在内核层、应用层的各种实现方法,并提出了一种在不修改操作系统内核前提下调试应用程序" title="应用程序">应用程序的方法。这种方法可移植性强,而且消除了修改系统内核可能带来的隐患,减少了因修改内核而带来的工作量,在调试微内核操作系统服务的应用中非常有效。
  关键词: 远程调试 stub GDBserver KGDB 嵌入式系统调试


  调试是开发过程中必不可少的环节,然而内核、嵌入式系统的调试不同于传统的调试系统。通常嵌入式系统不具备使用本地调试器" title="调试器">调试器的能力,原因如下:
  (1)系统自身的资源有限,内存小,输入输出设备不能用于调试。
  (2)传统的调试系统需要文件系统,而嵌入式系统通常无文件系统,内核调试时还不支持文件系统。
  (3)调试器的运行本身需要操作系统的支持,因此无法实现操作系统内核的调试。
  最有效的解决方法是采用远程调试技术。远程调试是指调试器运行的环境(主机)与被调试的系统(目标机)在物理上是分离的、通过串口或者网络进行连接的调试技术。
  GNU免费提供的GDB拥有强大的远程调试功能,它能够使开发人员以远程调试的方式单步执行目标平台上的程序代码、设置断点、查看内存,并与目标平台交换信息。GDB远程调试的实时、动态、方便、免费等优点,使它逐渐成为嵌入式开发首选的调试方案。
  远程调试系统由三部分组成:主机上的本地调试器、目标机上的调试代理、远程调试协议,如图1所示。对应GDB远程调试系统的三部分为:GDB、GDBstub、GDB远程串行协议。下面就这三部分进行分析。


1 RSP协议
  GDB RSP(Remote Serial Protocol)定义了GDB宿主机与被调试目标机进行通信时数据包的格式。信息的格式是:$数据#校验码。多数信息使用ASCII码,数据由一系列的ASCII码组成,校验码是由两个16进制数组成的单字节校验码。接收方接收数据并校验,若正确则回应“+”,否则回应“-”。通信的内容包括读写数据、控制程序运行、报告程序状态等命令。RSP的基本命令从通信对话角度可以分为两种:
  (1)请求
  ?:读当前系统状态
  g:读所有寄存器
  G〈register_data〉:写所有寄存器
  m〈address〉,〈length〉:读内存
  M〈address〉,〈length〉:〈memory_data〉:写内存
  c:继续执行
  s:单步执行
  k:终止进程
  (2)答复
  “”:告诉GDB上次请求命令不支持。
  E:告诉GDB出错
  OK:上次请求正确
  W〈exit_status〉:系统在“exit_status”状态下退出。
  X〈signal〉:系统在signal信号下终止。
  S〈signal〉:系统在signal信号下停止。
  O:告诉GDB控制台输出,这也是惟一向GDB发出的命令。
2 GDB远程调试功能
  调试内核时通常还没有文件系统,而且多数嵌入式系统由于自身资源的限制不具备文件系统,因此将与文件系统有关的源文件、目标文件及符号表都存放在主机上,由主机上的调试器处理。同样,调试用的输入输出设备也是由主机提供。主机上的调试器接收用户输入的调试命令并进行预处理,对于某些命令(如breakpoint)的处理在主机GDB上实现,不需要与目标机通信。当然,更多的指令需要在目标机调试代理上实现。主机根据RSP对预处理之后的命令进行封装,发送给目标机上的调试代理,调试代理接收命令后作相应的处理,并返回信息给主机上的调试器。
3 目标机上stub的实现
  目标机上stub的基本功能是与主机GDB通信,实现读写内存、寄存器,stop、continue指令。主机GDB与目标机上stub通信的通用模型如图2。


  目标机与主机通过硬件连接,被调试部分插入stub,GDB与被调试部分通过RSP通信。根据stub所处层的不同实现不同层的调试,包括内核层、应用层的调试。
3.1 内核层调试模型
  内核层调试模型如图3,将stub插入到内核可以实现内核的调试。Linux内核调试机制KGDB就是使用这种模式。KGDB可以分为初始化模块和控制模块。


3.1.1 初始化模块
  修改异常处理" title="异常处理">异常处理函数,使得异常发生时进入函数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;\
    }\
  }
  (1)改变程序的流程,以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( )。
  (2)不改变程序的流程,以异常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内核在非调试状态下的异常处理函数已经输出必要的状态信息、出错信息。
3.1.2 控制模块
  控制模块与主机GDB通信的具体流程如图4。KGDB只调试内核态程序。handle_exception函数首先判断CPU是否处于VM86模式或用户态,若是则返回;然后接收GDB发来的信息,根据接收的信息作出相应的操作和回复。流程图内的虚线框是所有GDBstub中handle_exception函数的通用流程。


3.2 应用程序调试模型
  在嵌入式Linux开发领域中调试应用程序常用调试代理工具GDBserver。其工作原理不是在被调试应用程序内编译stub,而是把被调试程序作为GDBserver的子进程,这样GDBserver可以利用内核提供的代码跟踪机制(ptrace)监控被调试进程的运行,从而完成调试任务。此工作原理与GDB本地调试相似。其调试模型如图5。GDBserver的工作流程是:GDBserver创建子进程-〉绑定跟踪ptrace(ptrace_traceme,,)-〉从主机传来的各种调试命令通过GDBserver转化为各种操作需求的ptrace。如果用GDBserver进行远程调试,需要内核操作系统的支持,包括子进程、代码跟踪机制,这样其他嵌入式系统内核工作量会较大。而且ptrace也有其局限性,例如只能跟踪子进程,在调试进程与被调试进程之间传送一个长字的数据。使用通用的调试模式工作量会更小。如图6,在应用程序中编译stub,并在应用程序入口处插入断点,程序开始将控制权交给GDB,之后的流程与内核层调试类似。


4 不修改内核前提下调试应用程序
  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。
  这种方法理论上在内核调试和应用程序调试中都可以使用,但在应用程序的调试中优点更明显。在写stub时不涉及内核,在调试应用程序时不需切换到内核模式下,直接在用户模式中即可完成。此方法也存在不足之处。为了实现现场保护,要求用户了解系统内的寄存器。随着stub本身复杂度的增加,其正确性需要更多的检验。
  加stub的远程调试方法方便而有效,而且可以降低项目成本,在实际工作中得到广泛的应用。在不修改内核前提下调试应用程序的方法已成功应用于笔者开发的微内核结构的操作系统,为系统的开发应用提供了良好的调试手段。当然加stub的远程调试方法也存在一些不足。由于stub的应用是在串口通信的基础上,因此串口处理函数以及stub自身处理函数的正确性是确保stub安全调试的前提。
参考文献
1 李红卫,李翠萍.kgdb调试Linux内核的剖析与改进.微型机与应用,2004;23(10)
2 郭胜超.GDB远程调试及其在嵌入式Linux系统中的应用.计算机工程与应用,2004;26(10)
3 彭进展.GRDBS:一种针对嵌入式系统的通用远程调试系统.计算机工程,2003;29(2)
4 Gatliff,Bill.Embedding with GNU:the gdb Remote Serial Protocol.Embedded Systems Programming,1999;(9):109
5 Gilmore J,Shebs S.GDB Internals:A Guild to the Internals of the GNU Debugger.Free Software Foundation Inc,1999

本站内容除特别声明的原创文章之外,转载内容只为传递更多信息,并不代表本网站赞同其观点。转载的所有的文章、图片、音/视频文件等资料的版权归版权所有权人所有。本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如涉及作品内容、版权和其它问题,请及时通过电子邮件或电话通知我们,以便迅速采取适当措施,避免给双方造成不必要的经济损失。联系电话:010-82306118;邮箱:aet@chinaaet.com。