GDB是一个工具,全称the GNU Project debugger,可以让你看到程序内部信息当程序执行时或者crash时。因此GDB可以称之为C/C++(也可以支持其它语言如Ada,Pascal等)应用的医生。官方网址:http://www./s/gdb/ GDB对应用进行调试分两种情况,一种是远程调试,一种是本地调试。但GDB是一个与系统无关的跨平台调试器,它是基于ptrace来实现的,ptrace实现了底层的调试原语,但是对多线程和并行支持不好,如果要调试这两种场景,可以尝试使用TotalView(http://www.) 这种专用调试器,它也是免费的。 ptrace 是一个系统调用,提供了一种方法来让父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器。 ptrace的原型如下: #include <sys/ptrace.h> long int ptrace(enum __ptrace_request request, pid_t pid, void * addr, void * data)我们可以看到,ptrace有4个参数,其中request决定ptrace做什么,pid是被跟踪进程的ID,data存储从进程空间偏移量为addr的地方开始将被读取/写入的数据。 不管ptrace是什么时候被调用的,它首先做的就是锁住内核.在ptrace返回前,内核会被解锁.在这个过程中,ptrace通过将request选项来控制子进程。 常见的request选项有: PTRACE_TRACEME: 表示本进程将被其父进程跟踪,正如前面所说的,任何信号(除了SIGKILL),不管是从外来的还是由exec系统调用产生的,都将使得子进程被暂停,由父进程决定子进程的行为.在request为PTRACE_TRACEME情况下,ptrace()只干一件事,它检查当前进程的ptrace标志是否已经被设置,没有的话就设置ptrace标志,除了request的任何参数(pid,addr,data)都将被忽略. PTRACE_ATTACH: request为PTRACE_ATTACH也就意味着,一个进程想要控制另外一个进程.需要注意的是,任何进程都不能跟踪控制起始进程init,一个进程也不能跟踪自己.某种意义上,调用ptrace的进程就成为了ID为pid的进程的’父’进程.但是,被跟踪进程的真正父进程是ID为getpid()的进程. PTRACE_DETACH: 用来停止跟踪一个进程.跟踪进程决定被跟踪进程的生死.PTRACE_DETACH会恢复PTRACE_ATTACH和PTRACE_TRACEME的所有改变.父进程通过data参数设置子进程的退出状态(exit code).子进程的ptrace标志就被复位,然后子进程被移到它原来所在的任务队列中.这时候,子进程的父进程的ID被重新写回子进程的父进程标志位.可能被修改了的single-step标志位也会被复位.最后,子进程被唤醒,貌似神马都没有发生过;参数addr会被忽略. PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER: 这些宏用来读取子进程的内存和用户态空间(user space).PTRACE_PEEKTEXT和PTRACE_PEEKDATA从子进程内存读取数据,两者功能是相同的.PTRACE_PEEKUSER从子进程的user space读取数据.它们读一个字节的数据,保存在临时的数据结构中,然后使用put_user()(它从内核态空间读一个字符串到用户态空间)将需要的数据写入参数data,返回0表示成功. 对PTRACE_PEEKTEXT和PTRACE_PEEKDATA而言,参数addr是子进程内存中将被读取的数据的地址.对PTRACE_PEEKUSER来说,参数addr是子进程用户态空间的偏移量,此时data被无视. PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER: 这些宏行为与上面的几个是类似的.唯一的不同是它们用来写入data.(译注: 这段文字与上面的描述差不多,为免繁复,不译.) PTRACE_SYSCALL, PTRACE_CONT: 这些宏用来唤醒暂停的子进程.在每次系统调用之后,PTRACE_SYSCALL使子进程暂停,而PTRACE_CONT让子进程继续运行.子进程的返回状态都是由ptrace()参数data设置的.但是,这只限于返回状态是有效的情况.ptrace()重置子进程的single-step位,设置/复位syscall-trace位,然后唤醒子进程;参数addr被无视. PTRACE_SINGLESTEP: PTRACE_SINGLESTEP的行为与PTRACE_SYSCALL无异,除了子进程在每次机器指令后都被暂停(PTRACE_SYSCALL是使子进程每次在系统调用后被暂停).single-step会被设置,跟PTRACE_SYSCALL一样,参数data包含返回状态,参数addr被无视. PTRACE_KILL: PTRACE_KILL被用来终止子进程.”谋杀”是这样进行的: 首先ptrace() 查看子进程是不是已经死了.如果不是, 子进程的返回码被设置为sigkill. single-step位被复位.然后子进程被唤醒,运行到返回码时子进程就死掉了. PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETFPXREGS: 这些宏用来读子进程的寄存器.寄存器的值通过getreg()和__put_user()被读入data中;参数addr被无视. PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETFPXREGS: 跟上面的描述相反,这些宏被用来设置寄存器.
在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系之后,交付给目标程序的任何信号(除SIGKILL之外)都将被gdb先行截获,或在远程调试中被gdbserver截获并通知gdb。 gdb因此有机会对信号进行相应处理,并根据信号的属性决定在继续目标程序运行时是否将之前截获的信号实际交付给目标程序。 信号是实现断点功能的基础。以x86为例,向某个地址打入断点,实际上就是往该地址写入断点指令INT 3,即0xCC。目标程序运行到这条指令之后就会触发SIGTRAP信号,gdb捕获到这个信号,根据目标程序当前停止位置查询gdb维护的断点链表,若发现在该地址确实存在断点,则可判定为断点命中。gdb暂停目标程序运行的方法是向其发送SIGSTOP信号。 kill_lwp(process->head.id, SIGSTOP); 因此GDB的运转模式完全由外部事件来激励的。通常有两种外部事件,一种来自于标准输入(用户输入的调试命令,如cli的continue、next、step、breakpoint或者mi的exec-continue、exec-next、exec-step、break-insert等等,另外一种来自目标程序可能遇到的调试事件,如断点、随机信号、单步结束、线程退出等等。 GDB对这两种事件的响应可以有同步方式,也可以有异步方式。 1)同步模式 – gdb将以同步方式等待目标程序发生停止事件,可称之为“死等”。因此,在目标程序运行期间,gdb不再扫描标准输入,用户也无法输入任何调试命令,要么等待目标程序发生调试事件而停止,要么通过“Ctrl c”来暂停目标程序的运行。 2)异步模式 – gdb不会同步等待目标程序发生停止事件,此类事件将通过异步上报的方式告知gdb。在目标程序运行期间,gdb仍将扫描标准输入,用户可以输入调试命令。
GDB的四种调试方式: 1)attach 并调式一个已经运行的进程 a) 用户指定需要进行调试的进程ID b)运行GDB,输入attach pid,GDB对指定进程执行如下操作: ptrace(PTRACE_ATTACH,pid,0,0) root@2003:# gdb
2)运行并调试一个应用的进程 a)运行gdb,可以直接使用命令行,如gdb Test 或者运行GDB后使用file 命令 (gdb) file /usr/app/bin/SecurityMgr
b)运行run命令,run 命令可以带参数,GDB接受到run命令后,通过fork系统调用并创建一个子进程,子进程调用ptrace(PTRACE_TRACEME,0,0,0)设置ptrace标志,并调用execv()系统调用加载用户可执行文件。 3)远程调试目标机上新创建的进程 GDB运行在调试机上,gdbserver运行在目标机上,两者之间使用gdb远程串行协议 4)调试进程产生的CORE文件 GDB 支持单步调试和断点,单步又分为指令级单步和C代码级单步。 所谓指令级单步就是指gdb控制目标程序只运行一条指令之后即停止。指令级单步是next、step、nexti、stepi等运行类调试命令的基础。 指令级单步有硬件单步和软件单步之分。所谓硬件单步是指cpu架构本身就支持指令级单步,目标程序可以在运行一条指令之后自动停止。所谓软件单步是指cpu架构不支持指令级单步,需要gdb用软件方法来实现指令级单步。 支持硬件单步的架构如x86和ppc。对于x86,可通过设置EFLAGS寄存器中的TF标志来将cpu置于单步模式。对于ppc,则可通过设置MSR寄存器中的SE标志来将cpu置于单步模式。在单步模式中,cpu每执行一条指令,就会产生一个单步异常,通知gdb进行处理。不支持硬件单步的架构如arm和mips。对于此类架构,gdb采用的是用临时的软件断点来模拟单步的方法。即在需执行指令的下一条指令处临时插入一个断点,然后让目标程序继续运行,它会在执行完当前指令之后遇到下一条指令处的临时断点,于是目标程序停止,通知gdb命中断点,gdb再将此断点删除,由此来完成指令级单步的过程。(插入临时断点需要gdb实现代码分支预测的功能) next命令实现c代码级的单步。 断点功能的实现就是在指定位置插入断点指令,使目标程序运行至该处时产生SIGTRAP信号,该信号被gdb捕获,通过断点地址的匹配确定是否命中断点。 断点的属性: a)是否有条件(由condition命令修改); b)是否有忽略次数 (由ignore命令修改); c)是否只针对某个线程有效(由break命令的thread参数指定); d)是否是临时断点(由tbreak命令插入)。
下面以一个例子来演示GDB的常用操作:
#include <stdio.h>
void doloop(int n)
{
int i=0;
printf("loop is going to start>>>\n");
for(i=0;i<n;i++){
printf("i=%d\n",i);
}
printf("loop is done<<<\n");
}
int main(int argc, char*argv[]){
int n=10;
char *str="hello world";
if(argc>1)
{
printf("args:%s\n",argv[1]);
}
printf("say hello:%s\n",str);
doloop(n);
printf("exit now!\n");
return 0;
}
通常为了方便调试,想要在调试时能直接看到源码相关变量、源码行值等信息,
我们需要在对C/C++源码编译时加上编译参数-g,
1) 启动 gdb, 运行 run(+参数)
[windriver@windriver-machine ltest]$
[windriver@windriver-machine ltest]$ gdb showgdb
Program exited normally.
Program exited normally.
2)使用ctrl+c 发出一个SIGINT 信号使程序中断,然后打印出当前堆信息 3)kill退出一个运行进程,quit退出gdb 4)断点操作,断点可以设置在源码的行号,函数名,内存地址。info breakpoints 列出所有断点 Breakpoints Fun 或者breakpoints *0x80483da来设置断点。断点有编号,可以进行enable,disable delete 5)单步操作, Stepi, nexti, continue, finish. 其中stepi需要进入到函数内部执行,而nexti不需要。 进行单步操作时,需要停在不同的指令地址时,这里需要借助objdump –d 工具来打印出汇编代码。通过汇编代码来显示指令地址。 6)查看数据,当程序运行到某个断点时,我们所要做的工作就是要知道当前的变量、内存、寄存器中的数据。 查看变量可以使用print命令(简写命令为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式是: print /<f> <expr>
<expr>是表达式或者变量名称, <f>是输出的格式,缺省会根据变量类型来输出。 x 按十六进制格式显示变量。
查看内存可以用info registers(i r) 或者x[n][d/s] $esp[][0x804850] x/<n/f/u> <addr> n、f、u是可选的参数。
n/f/u三个参数可以一起使用。例如:
7)当前位置和堆序列 where和backtrace,对多个frame信息,可以用frame n或者up down改变当前的frame 8)常用的快捷方式,按TAB键进行补全,回车重复执行上一次命令,使用一些显示选项进行查看,如set print address on, 使用L(list )可以查看源代码, dessemble可以查看汇编码 附录:IA32常用内存结构 如果要进行GDB调试,一定要理解IA32寄存器和一些简单的汇编写法,如下图所示,前面六个寄存器都是通用寄存器,最后两个是专门用来针对函数调用的栈寄存器。 IA32程序是用程序栈来支持函数调用,栈用来传递函数参数,存储返回信息、保存寄存器供以后恢复之用。为单个函数分配的那部分栈称之为栈帧(Stack frame),栈帧的最顶端是以两个指针定界,寄存器%ebp作为帧指针,而寄存器%esp作为栈指针。 常见操作数格式:
|
|