gdbStub
2.0的调试器使用的是GNU的GDB,GDB本身不但可以作为本地调试器,也可以远程调试。在远程调试的时候,必须在远程系统上嵌入stub,这个stub代码量不多,本文所说的KDBG就是这个stub,也是2.0内核态的stub。
总体设计:
GDB和stub之间的通信是通过远程通讯协议实现的。GDB远程串行协议RSP(Remote Serial Protocol)是一种基于消息的ASCII码协议,包含了诸如读写内存、查询寄存器、运行程序等命令.
RSP包格式: $ <data> # CSUM1 CSUM2
<data>是ASCLL串,但不包含$和#. CSUM1和CSUM2分别是一个ASCLL十六进制的对<data>的校验数。
接收方以+和-响应RSP包的发送方。
+ :如果校验数字正确,接收方准备发送下一个RSP包。
- :校验数错误
Stub在kernel初始化的时候就初始化了,一旦kernel运行出现了异常,kernel就会进入异常处理函数,异常处理函数会将kernel的异常信息通过调试端口发送给主机,如果在主机端启动GDB,GDB就会和stub建立连接,这时异常处理函数就会进入一个GDB和stub的会话循环,通过这个循环,我们就可通过GDB来调试kernel。
具体实现:
stub实现分为三个部分:通讯命令处理,底层通讯,上下文(contect)接口。其关系如下:
上下文(contect)接口:stub和GDB对寄存器信息的交流是通过指定寄存器号来实现的(寄存器编号是GDB定义的),应此KDBG定义了一个KDBCONTEXT:
struct KDBCONTEXT {
uint32_t EAX,ECX;
uint32_t EDX,EBX;
uint32_t ESP,EBP,ESI,EDI;
uint32_t EIP,EFLA
GS,CS;
uint32_t SS,DS;car
}
那么EAX寄存器的编号就是0,ECX就是1,依次类推。另外定义了几个上下文接口,stub的命令处理部分在对上下文的读写时会调用这些接口。
对ESP,EBP,EIP的操作:
GetESP(),GetEBP(),GetEIP() :分别得到三个寄存器的值
SetEIP() :设置EIP的值
GetESPIdx(),GetEBPIdx(),GetEIPIdx() :分别得到三个寄存器的编号
KDBCONTEXT结构和系统上下文的转化:
ExpCxtToKdbCxt () :系统上下文的值写入KDBCONTEXT
KdbCxtToExpCxt () :KDBCONTEXT的值写入系统上下文
对陷阱标志位的控制:
DisableTrace()
EnableTrace()
通讯命令处理:stub和GDB之间只是交流一些基本的信息,具体是:内存的读写,寄存器的读写,被调试程序状态信息,断点维护以及一些基本的程序控制信息(程序单步执行,继续执行,退出);至于GDB的许多并且有点复杂的命令,除了stub对这些基本信息的维护,GDB都自己处理了(有关GDB命令的说明,现在还没有文档,不过可以参看GDB的help)。
命令处理是由一个KdbLoop函数实现的,这个函数里有个循环是为stub和GDB的会话而设置的。每次进入KdbLoop函数,stub首先会将当前被调试的kernel的状态告诉GDB,具体是stub将当前的ESP,EBP和EIP的值发给GDB,之后stub就等待GDB的响应,如果GDB给出了回应,stub就会进入会话循环,也就是对通讯命令的处理。如下图的会话循环:
“?”命令:GDB用此命令来找出目标机的当前状态。
当stub收到这个命令的时候,stub返回当前的ESP,EBP,EIP三个寄存器的值给GDB,让GDB知道当前的被调试的kernel的状态。
“g”命令:读所有寄存器的值,stub返回KDBCONTEXT结构所有内容给GDB。
“q”命令:RSP包格式:$qOffsets#xx GDB发送此命令来决定重定位。不用于嵌入式系统中,在kernel中保留此命令,只是为了提高GDB的启动速度。 stub返回给GDB代码,数据以及BSS的偏移,三者都为0(不解)。
“H”命令:RSP包格式:$H<线程号>#xx 此命令的功能是将所有的命令都限制于此命令参数中指定的线程,这对于运行在实时操作系统上多线程应用程序是非常重要的。线程号由用户指定,如果线程号为-1,说明GDB命令用于所有的线程。 stub给出一个OK响应。
“G”命令:写所有寄存器。RSP包格式:$G<data1><data2>…#xx
(设置第 0个寄存器(EAX)内容为: 0x12345678,第 1个寄存器(ECX)内容为:0x9abcdef0, 依次类推。)
GDB把按照寄存器编号顺序的寄存器的值发给stub,stub按照寄存器编号顺序一一将值写入寄存器(通过上下文接口KdbCxtToExpCxt()),如果成功给出ok应答,否则给出错误应答。
“P”命令:写单个寄存器。RSP包格式:$P<寄存器编号>=<data>#xx 。GDB发给stub某个寄存器编号和要写入的值,stub根据寄存器编号找到KDBCONTEXT中相应的寄存器并写入值,然后通过上下文接口KdbCxtToExpCxt()完成对相应寄存器的写入。如果成功给出ok应答,否则给出错误应答。
“m”命令:读内存。RSP包格式:$m<address>,<bytecount>#xx GDB发给stub要读的内存地址以及要读的字节数,由于内核的可执行映像是1:1的映射,故stub根据此内存地址读出请求的字节数内容并返回给GDB。
“M”命令:写内存。RSP包格式:$m<address>,<bytecount>:<data>#xx GDB发给stub要写的内存地址,要写入的字节数和要写入的值,stub根据这些要求擦写相应的内存,成功会给GDB一个OK应答。
“Z”命令和“z”命令:GDB允许用户在调试程序时设置断点,GDB会纪录端点信息。当GDB把控制权交给被调试程序的时候,在断点处程序会停止。这个过程的实现原理是:stub维护一个 断点列表
struct KdbBreakpoint : public DLinkNode {
unsigned char *pAddr; //保存断点所在地址
char buf[2]; //保存断点所在地址的内容。
};
当GDB把控制权交给被调试程序的时候,如:用户输入了continue命令,GDB首先用“Z”命令通知stub,“Z”命令的RSP包如下:
$ Z<type>,<address>,<len>#xx
type是断点指令类型为0,len是断点指令长度为1,address是用户设置的断点所在地址。Stub首先会在断点列表里查找是否有相同的断点地址(用户可能在同一处设置断点),如果没有,stub保存了address以及address的内容,把这个断点信息加入断点列表,并且stub设置address的内容为一条断点指令cc(int $3的机器码),stub处理完“Z”命令后给GDB一个成功响应,GDB向stub发送continue命令,这时GDB把控制权交给被调试程序,但是由于address被stub设置为断点指令,程序在address处会进入breakpoint异常,GDB会向stub发送“z”命令, “z”命令的RSP包格式和“Z”命令一样,stub会根据address查找断点列表,如果有address,那么会把这个断点信息从断点列表中摘掉,并把保存的address原来的内容buf回写到address。
“c”命令:GDB发出此命令让被调试的目标继续往下执行,直到遇到新的断点或者运行过程中产生异常,程序就停止,否则程序会一直运行到结束。 stub在处理此命令的时候只是将EFLAGS寄存器的trap标志位禁止。
“s”命令:单步运行命令。 stub置位EFLAGS寄存器的trap标志位,这样被调试目标执行一条指令后又会产生一个调试异常,以此来完成单步调试任务。
“k”命令和“D”命令:杀死调试目标,stub处理此命令是通过调用kernel的Reboot()函数实现的。
底层通讯:这个模块的功能就是完成对RSP包的解包和打包。为此它提供了两个接口:
void AtomicGetDebugPacket (char * buffer) :完成接收RSP包并解包
void AtomicPutDebugPacket (const char * buffer) :对命令打包成RSP包并完成发送。
这两个函数的实现是根据RSP包的格式来实现的(RSP格式在本文开头有说明),实现比较简单,在此不再说明。