《电子技术应用》
您所在的位置:首页 > 嵌入式技术 > 设计应用 > kgdb调试Linux内核的剖析与改进
kgdb调试Linux内核的剖析与改进
李红卫1 李翠萍1 韩红宇2
1. 常州江苏技术师范学院(213001) ; 2. 阜新辽宁工程技术大学(123000)
摘要: 在Linux内核中加入kgdb,通过开发机上的gdb对目标机上的内核进行源代码级的调试技术。
Abstract:
Key words :

摘   要: 在Linux内核中加入kgdb,通过开发机上的gdb对目标机上的内核进行源代码级的调试技术。
关键词: 远程调试  kgdb  串口通信协议  异常处理

  操作系统的内核调试器除完成一般的调试功能外,还必须工作在内核中。因此,内核调试器与用户级的调试器有很大的区别。在Linux系统中,对内核的调试有很多种方法,例如:可在内核中插入printk( )函数,将调试信息输出,然后针对输出的信息进行分析;可以使用/proc文件系统对内核进行分析;可以利用strace命令对系统调用进行分析。但这些方法都不能直接对内核源代码级进行调试。因此,本文介绍一种在内核源代码中插入kgdb(Kernel GNU Debuger,GNU内核调试器),通过开发机上的gdb调试器对运行于目标机上的内核进行源代码级调试的技术。
1  kgdb调试内核的实现原理
1.1 通信协议
  kgdb提供了一种使用gdb调试Linux内核的机制。使用kgdb调试内核需要二台机器,一台作为开发机,另一台作为目标机,通过串口将它们连接起来。在将要调试的内核中插入kgdb,重新编译内核,使其运行在目标机上。而gdb在开发机上运行,gdb通过串口与要调试的内核进行通信,对目标机上的内核进行控制,从而实现远程调试内核的目的。
  要使gdb有效地控制调试目标机上的内核,必须与目标机上的kgdb约定相互的通信协议。而gdb本身带有一个远程串行通信协议,所以在kgdb中包含相同的协议即可实现开发机与目标机之间的通信。
  开发机上的gdb可以向目标机发送一些命令数据包,如果kgdb能够实现g、G、m、M、c和s等主要命令,则在使用gdb对目标机上的内核进行调试时就像在本机上调试程序一样。这六个命令的功能描述如下:
  g:查看CPU寄存器的值;
  G:设置CPU寄存器的值;
  maddr,count:从addr位置开始读count个字节的数据;
  Maddr,count:从addr位置开始写count个字节的数据;
  c/caddr:在当前位置上继续执行程序或从地址addr处开始执行;
  s/saddr:单步执行当前的指令,或者执行到指定的addr位置。
1.2 kgdb远程调试技术分析
  远程调试常采用插桩(stub)的方法实现。在目标操作系统内和开发机上的调试器内分别加入某些功能模块(如通信模块和异常处理模块等),二者互通信息来完成调试工作,这些功能模块统称为插桩。gdb本身带有这些功能模块,所以在调试内核时,只需要在内核中加入stub插桩模块即可实现用gdb远程调试内核的目的。gdb远程调试器的结构如图1所示。


  通过对kgdb源程序进行分析可知,kgdb主要有二大模块:一个是初始化模块,完成初始化过程,接管所有异常、设置串口通信等低层实现;另一个是控制模块,实现通信,对接收到的信息包进行解析并执行,对应答包进行打包发送。
1.2.1 kgdb初始化模块的实现
  在Linux启动时要调用start_kernel( )函数。该函数调用一系列初始化函数,完成系统初始化工作。kgdb通过对该函数进行修改,使其在内核初始化工作完成后将控制权交给插桩程序kgdb,由插桩程序完成调试内核的任务。
  (1)设置异常中断处理程序入口
  kgdb插桩模块要对目标机上被调试的程序进行控制,必须对目标机上所发生的所有异常进行统一管理。在上述Linux启动过程中,若要执行start_kernel( )函数,则应在该函数中调用trap_init( )函数设置各种入口地址,如异常事件处理程序入口、系统调用入口等。其中trap0~trap19为各种异常事件错误入口,如被0除、溢出、存储器越界等。为了使gdb能够捕获这些发生在目标机上的异常,kgdb要对各个需要捕获的异常处理函数进行修改。当发生异常时使异常处理事件进入异常处理函数handle_excepton( ),han-
dle_excepton( )就会将目标机上发生的异常以信号“Sxx”的方式通知主机上的gdb,主机上的gdb即可知道目标机上发生的异常。
  (2)串口初始化
  将所有异常入口地址进行修改后,应该对串口进行初始化,并设置linux_debug_traps指针变量的值,使其指向异常处理程序入口地址。此工作结束后,kgdb就接管所有发生在目标机上的异常事件。
1.2.2 kgdb控制模块的实现
  kgdb控制模块主要完成对协议的解析和执行,这些功能都在handle_exception( )函数中实现。通过该函数可实现在内核中设置断点、单步执行代码和监视变量的值。
  handle_exception( )函数首先判断CPU是否处于虚拟86模式或用户态,若是则返回。由此可见,kgdb只对内核态程序进行调试。其次判断是否发生读/写内存异常,若是则为读/写内存操作设置一个有效内存地址,返回重试,否则向主机发送信息“Sxx”,其中xx是发生异常的信号量值,即通知开发机上的gdb在目标机上发生了什么样的异常。接下来handle_exception( )接收来自主机的调试命令,对来自主机的命令进行解析并完成命令的执行,在kgdb中主要完成的调试命令有m、M、g、G、s、c。当这些命令执行完后还需要向开发机发送应答信息,报告完成命令的情况。此函数的流程如图2所示。


  (1)g读寄存器命令的实现。当系统发生异常时,系统首先将发生异常的进程的CPU状态保存在核心堆栈中。用g命令读寄存器时,实际上就是从核心栈中去取,但gdb所规定的寄存器的排列与Linux内核中对寄存器的排列顺序不一致,需要进行寄存器的转换。通过函数regs_to_gdb_regs(gdb_regs,&regs)实现转换,转换结束后将寄存器的值打包送回主机。
  (2)G写寄存器命令的实现。对接收到的数据包进行解包,把结果放入gdb_regs结构中,再通过gdb_regs_to_regs(gdb_regs,&regs)函数将接收到的寄存器的值进行转换,并放入核心栈中,通过对核心栈的修改得到修改寄存器的目标。
  (3)读/写内存命令的实现。对内存的读写有可能再次发生读写内存异常。例如对一个非法的地址进行读写的kgdb解决方法如下:
  kgdb通过函数int get_char(char?鄢char)读内存,通过函数void set_char(char?鄢addr,int val)写内存。kgdb在读/写不确定的内存时,通过二个变量来判断是否读/写内存发生异常。一个变量是kgdb_memerr_expected,标志是否读写不确定的内存;另一个变量是kgdb_memerr,标志在读写内存时是否发生了异常。这样,对不确定内存的读/写过程可分三步。
  第一步:对这二个变量赋值。将kgdb_memerr_expected赋值为1,表示对不确定内存进行读/写操作。对kgdb_me-merr赋值为0,表示在读/写内存时没有发生异常。
  第二步:对内存进行读写操作。若读写地址是非法地址,则进入异常处理程序,转第三步。若正确,则返回,并清变量kgdb_memerr_expected为0,表示读写内存结束。
  第三步:进入异常处理程序中。首先要判断kgdb_me-merr_expected是否为1。若是1,则说明对内存读/写时发生了异常。这时先清kgdb_ memerr_expected为0,置kgdb_memerr为1,标志在读/写内存时发生了异常,并为读/写内存设置一个有效地址。这个有效地址是一个可读/写的内存地址,可以对其进行操作,操作的结果是无效的。
  (4)s单步命令的实现。基于x86 CPU的体系结构支持单步执行,但它只是指令级的单步,而不是源代码级的单步。这里的s命令是实现指令级的单步,它的处理方法是:将产生异常的进程标志寄存器的陷阱标志位TF置1,异常中断处理程序结束返回到异常点继续执行。因TF标志位为1,所以程序执行一条指令后又进入异常处理程序。主机处的gdb利用此命令实现源代码级的单步跟踪。
  (5)c继续运行命令的实现。异常处理程序结束,恢复异常处的CPU状态,使程序继续执行。
2  kgdb插桩技术的不足与改进
  现有的kgdb不具有使目标程序进行调试模式和正常运行模式的切换功能。目前的处理方法是:当程序调试好后,将kgdb卸下,重新编译程序。当发生错误时再将kgdb插入目标程序中,编译后再在目标机上调试。由此可见操作十分不便。本文提出一种新的设计思路,使目标机上运行的程序可随时进行调试模式与正常运行模式的切换,这样更便于调试。下面讨论其实现方法。
   (1)在目标机方:通过对kgdb源代码分析可知,在进入各异常函数中,首先判断指向gdb_debug_hook类型的函数指针变量linux_debug_hook是否为空。若不为空则执行异常处理函数handle_exception( ),否则,执行原异常处理程序。这样通过修改这个变量的值,即可实现调试模式与正常运行模式的转换。当设置linux_debug_hook为(gdb_debug_hook?鄢)NULL时,目标机程序运行在正常运行状态;当设置linux_debug_hook为指向异常函数handle_exception( )时,则目标机程序运行在调试状态。
   (2)在主机方:调试状态到正常运行状态的转换:在kgdb中设置一个变量int stop_gdb。当主机对该变量进行赋值时,目标机上的kgdb获取信息后,使指向函数的指针变量linux_debug_hook指向(gdb_debug_hook?鄢)NULL,然后,kgdb构造c命令并执行,使目标机上的程序脱离调试运行模式进入正常运行模式。
  主机端发送命令:set stop_gdb=1
  这条命令经过主机端的gdb解析,实际上是执行协议命令M,即写内存命令。这样就需要在kgdb中对M命令进行修改。
  目标端kgdb 执行M命令的算法描述如下:
  (1)向主机返回应答信息“OK”;
  (2)如果所写地址与变量stop_gdb的地址相同,则转(4);
  (3)执行原M命令,转(8);
  (4)设置变量initialized为0;
  (5)设置指针变量linux_debug_hook为(gdb_debug_hook*)NULL;
  (6)构造命令c;
   (7)执行命令c,目标机上被调试的程序转为正常运行模式;
  (8)结束。
  正常运行状态到调试状态的转换:目标程序从正常运行状态切换到调试状态,就不像从调试状态到正常运行状态切换那样对一个变量进行设置而触发状态的切换。因为目标机已不在主机端gdb的控制下运行,主机端的gdb与目标机端的kgdb已失去了联系。所以要激活目标机进入调试状态,只能使用中断技术,利用串口中断来激活对目标机由正常运行状态切换到调试状态。
  在gdb与目标机建立连接时,gdb首先要向目标机发送一个数据包$Hc-1#09,它会触发目标机上串口中断处理程序的执行,并判断接收的数据包是否为“Hc-1”。若是则设置linux_debug_hook指向异常中断处理程序handle_exception( ),并执行breakpoint( )函数,使目标机进入调试状态。
  通过上述方法即可实现目标机上程序运行状态的切换,使程序调试更加灵活。
3  结束语
  通过对kgdb调试技术的分析与改进,增加了调试的方法和思路。此调试技术可以应用于嵌入式系统的设计和开发中,为嵌入式开发工具增加了一种廉价而强有力的调试工具。
参考文献
1   李善平,刘文峰.Linux内核2.4版源代码分析大全.北京: 机械工业出版社,2002
2   李红卫,李翠萍.嵌入式软件的调试技术.计算机时代,2002;(8)
 

此内容为AET网站原创,未经授权禁止转载。