一、常用普通调试命令
1.简单介绍GDB
介绍: gdb是Linux环境下的代码调试⼯具。
使⽤:需要在源代码⽣成的时候加上 -g 选项。
开始使⽤: gdb binFile
退出: ctrl + d 或 quit
2.调试过程
(1)list命令
- list linenum 显⽰binFile第linenum行周围的源代码,接着上次的位置往下列,每次列10⾏。
- list function 显示函数名为function的函数的源程序
- list 显示当前行后面的源程序
- list - 显示当前行前面的源程序
(2)run或r
运行程序。-
run args run命令可以直接接命令行参数值,也可以在执行run之前通过 set args + 参数值实现。
(3)break(b)
打断点,使用方法:- b linenum 在某行打断点
- b +offset/-offset 在当前行号的前面或后面offset停住
- b filename:linenum 在某文件的某行打断点
- b filename:function 在某文件某个函数入口停住
- b *address 在程序的运行地址处停住
- b 没有参数在下一句停住
- b where if condition 当某个条件满足时,在某一行停住(这个很有用,比如b 10 if ret == 5)
- breaktrace(或bt) 查看各级函数调⽤及参数
- delete breakpoints 删除所有断点
- delete breakpoints n 删除序号为n的断点
- disable breakpoints 禁⽤断点
- enable breakpoints 启⽤断点
(4)单步命令
普通用法就不说了。- step count 一次性执行count步,如果有函数会进入函数
- next count 一次执行count,不进入函数
- finish 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值以及参数信息
- until 退出循环体(尤其是针对for循环这种,很烦的)
(5)continue命令
continue(或c):从当前位置开始连续⽽⾮单步执⾏程序
(6)print(p)命令
这个命令比较常用,用来查看我们想看的内容。比如有关数组可以看全部,也可以看从左到右某一部分:- x 按十六进制格式显示变量
- d 按十进制格式显示变量
- u 按十六进制格式显示无符号整型
- o 按八进制格式显示变量
- t 按二进制格式显示变量t 按二进制格式显示变量
- a 按十六进制格式显示变量
- c 按字符格式显示变量
- f 按浮点数格式显示变量
(7)watch命令
这个命令比较有用。watch一般用来观察某个表达式(变量也是一种表达式)的值是否有变化,如果有变化,马上停住程序。我们有一下几种方法设置观察点:- watch expr 为表达式expr设置一个观察点,一旦表达式值有变化,马上停住程序
- rwatch expr 当表达式expr被读时,停住程序
- awatch expr 当表达式的值被读或被写时,停住程序。
- info watchpoints 列出所有观察点(info指令通常可以去套 举例如下,演示观测*i的值,一旦变化停下来:
(8)examine命令
使用该命令来查看内存地址中的值。语法是:x/u addr addr表示一个内存地址。“x/”后的n、f、u都是可选的参数,n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容;f 表示显示的格式,如果地址所指的是字符串,那么格式可以是s,如果地址是指令地址,那么格式可以是i;u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4字节。u参数可以被一些字符代替:b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指定的内存地址开始,读写指定字节,并把其当作一个值取出来。n、f、u这3个参数可以一起使用,例如命令“x/3uh 0x54320”表示从内存地址0x54320开始以双字节为1个单位(h)、16进制方式(u)显示3个单位(3)的内存。
(9)jump命令
jump命令不会改变程序栈的内容,一般只在同一函数内跳转。- jump linespec 指定下一条语句的运行点,linespec可以是linenum,filename+linenum,+offset这几种形式
- jump address 跳到代码行的地址
(10)signal命令
使用signal 信号名(如SIGINT)这种方式把信号发送给程序,如果程序注册了signal_handler函数,还可以进行相应的处理,帮助调试程序。- set args 设置命令行参数
- set env environmentVarname=value 设置环境变量。如:set env USER=benben
- call function 强制调用某函数
(14)其他命令
-
info(i) locals: 查看当前栈帧局部变量的值
-
info break : 查看断点信息
-
info(或i) breakpoints: 参看当前设置了哪些断点
-
finish: 执⾏到当前函数返回,然后挺下来等待命令
-
set var: 修改变量的值
-
display 变量名: 跟踪查看⼀个变量,每次停下来都显⽰它的值
-
undisplay: 取消对先前设置的那些变量的跟踪
- until X⾏号: 跳⾄X⾏ 直接回
- n 或 next: 单条执⾏
- p 变量: 打印变量值。
二、使用gdb调试多进程多线程程序
1.设置
默认设置下,在调试多进程程序时GDB只会调试主进程。但是GDB(>V7.0)支持多进程的分别以及同时调试,换句话说,GDB可以同时调试多个程序。只需要设置follow-fork-mode(默认值:parent)和detach-on-fork(默认值:on)即可。两者结合起来构成了GDB的调试模式。follow-fork-mode detach-on-fork 说明
parent on 只调试主进程(GDB默认)
child on 只调试子进程
parent off 同时调试两个进程,gdb跟主进程,子进程block在fork位置
child off 同时调试两个进程,gdb跟子进程,主进程block在fork位置
设置方法:set follow-fork-mode [parent|child] set detach-on-fork [on|off]
查询正在调试的进程:info inferiors
切换调试的进程: inferior <infer number>
添加新的调试进程: add-inferior [-copies n] [-exec executable] ,可以用file executable来分配给inferior可执行文件。
其他:remove-inferiors infno, detach inferior查看gdb默认的参数设置:
2.演示代码
下面这段代码的主要流程就是在main函数中fork创建一个子进程,然后在父进程中又创建一个线程,接着就使用gdb进行调试(block子进程)。
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <sys/types.h>
4 #include <unistd.h>
5
6 int main(int argc, const char **argv)
7 {
8 int pid;
9 pid = fork();
10 if (pid != 0) //add the first breakpoint.
11 Parent();
12 else
13 Child();
14 return 0;
15 }
16
17 //Parent process handle.
18 void Parent()
19 {
20 pid_t pid = getpid();
21 char cParent[] = "Parent";
22 char cThread[] = "Thread";
23 pthread_t pt;
24
25 printf("[%s]: [%d] [%s]\n", cParent, pid, "step1");
26
27 if (pthread_create(&pt, NULL, (void *)*ParentDo, cThread))
28 {
29 printf("[%s]: Can not create a thread.\n", cParent);
30 }
31
32 ParentDo(cParent);
33 sleep(1);
34 }
35 //Parent process handle after generate a thread.
36 void * ParentDo(char *argv)
37 {
38 pid_t pid = getpid();
39 pthread_t tid = pthread_self(); //Get the thread-id selfly.
40 char tprefix[] = "thread";
41
42 printf("[%s]: [%d] [%s] [%lu] [%s]\n", argv, pid, tprefix, tid, "step2"); //add the second breakpoint.
43 printf("[%s]: [%d] [%s] [%lu] [%s]\n", argv, pid, tprefix, tid, "step3");
44
45 return NULL;
46 }
47 //Child process handle.
48 void Child()
49 {
50 pid_t pid = getpid();
51 char prefix[] = "Child";
52 printf("[%s]: [%d] [%s]\n", prefix, pid, "step1");
53 return;
54 }
如果直接运行程序,那么输出的结果如下:
3.gdb调试
3.1设置调试模式和Catchpoint
设置调试父子进程,gdb跟主进程,子进程block在fork位置。
这时可以另开一个终端,使用如下命令查看当前CentOS系统所有进程的状态:发现父进程PID为10062,通过fork产生的子进程为10065:
同时,可以使用命令cat /proc/10062/status
查看当前进程的详细信息:进程PID为10060,它的父进程(即GDB进程)为10062,同时这也是追踪进程ID,线程数Threads为1(共享使用该信号描述符的线程数,在POSIX多线程序应用程序中,线程组中的所有线程使用同一个信号描述符)。
3.2 设置第一个断点
在程序的第46行设置断点,并运行到断点处:这时再次使用命令pstree -pul查看当前系统进程的状态:发现此时仍然只有父进程13162和子进程13159。
3.3 执行到第一个断点此时如果切换到子进程13162
重新切换到父进程13159
3.4 设置第二个断点并调试
在第50行设置断点继续调试主进程(使父进程产生线程),其中父进程和线程到底是谁先执行是由内核调度控制的。这时使用命令pstree -pul查看当前系统进程的状态:存在父进程13159和子进程13162以及父进程创建的一个线程14208(线程用大括号{}表示)。同时,使用命令cat /proc/13159/status
查看当前进程的详细信息:进程PID为13159,它的父进程(即GDB进程)为13154,同时这也是追踪进程ID,线程数Threads为2(当前父进程13159+线程14208)。
3.5 查看第二个断点处的调试信息
3.6手动切换到线程
3.7 开始执行第二个断点处的代码
这时使用命令查看当前系统进程的状态:存在父进程13159和子进程13162,其中线程14208已经结束了。
本文部分参考:https://typecodes.com/cseries/multilprocessthreadgdb.html