GDB 高级调试-多线程、后台调试、多进程、反向调试

本文详细介绍了GDB调试多线程程序的常用命令,包括查看所有线程、调整当前线程、设置断点和线程锁。还探讨了GDB的non-stop模式,以及如何在后台执行调试命令。此外,内容涵盖了多进程调试,如attach命令、显式指定调试进程和detach-on-fork选项。最后,文章讲解了反向调试的概念和GDB的相关命令,以及信号处理和查看栈信息的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

调试多线程

GDB调试多线程程序常用命令

调试命令功 能
info threads查看当前调试环境中包含多少个线程,并打印出各个线程的相关信息,包括线程编号(ID)、线程名称等。
thread id将线程编号为 id 的线程设置为当前线程。
thread apply id… commandid… 表示线程的编号;command 代指 GDB 命令,如 next、continue 等。整个命令的功能是将 command 命令作用于指定编号的线程。当然,如果想将 command 命令作用于所有线程,id… 可以用 all 代替。
break location thread id在 location 指定的位置建立普通断点,并且该断点仅用于暂停编号为 id 的线程。
set scheduler-locking off|on|step默认情况下,当程序中某一线程暂停执行时,所有执行的线程都会暂停;同样,当执行 continue 命令时,默认所有暂停的程序都会继续执行。该命令可以打破此默认设置,即只继续执行当前线程,其它线程仍停止执行。

调试的代码:

 #include <stdio.h>
#include <pthread.h>
void* thread_job(void*name)
{
    char * thread_name = (char*)name;
    printf("this is %s\n",thread_name);
    sleep(2);
    printf("http://c.biancheng.net\n");
}
int main()
{
    pthread_t tid1,tid2;
    pthread_create(&tid1, NULL, thread_job, "thread1_job");
    pthread_create(&tid2, NULL, thread_job, "thread2_job");
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("this is main\n");
    return 0;
}

编译命令:

gcc main.c -o main -g -lpthread

查看所有线程

info threads 命令的功能有 2 个,既可以查看当前调试环境下存在的线程数以及各线程的具体信息,也可以通过指定线程的编号查看某个线程的具体信息。

(gdb) info threads [id...]

其中,参数 id… 作为可选参数,表示要查看的线程编号,编号个数可以是多个。

(gdb) b 6
Breakpoint 5 at 0x400699: file main.c, line 6.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/learn/c++11/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff77f0700 (LWP 16854)]			 <--新线程
[Switching to Thread 0x7ffff77f0700 (LWP 16854)]   <--该线程作为当前线程,因为它最先碰到断点

Breakpoint 5, thread_job (name=0x4007f3) at main.c:7
7           char * thread_name = (char*)name;
(gdb) 
(gdb) info threads;
[New Thread 0x7ffff6fef700 (LWP 16855)]
Args must be numbers or '$' variables.
(gdb) info threads
  Id   Target Id         Frame 
  3    Thread 0x7ffff6fef700 (LWP 16855) "main" 0x00007ffff78ef9c1 in clone () from /lib64/libc.so.6
* 2    Thread 0x7ffff77f0700 (LWP 16854) "main" thread_job (name=0x4007f3) at main.c:7		<--Id 列前标有 * 号的线程即为当前线程
  1    Thread 0x7ffff7fe6740 (LWP 16853) "main" 0x00007ffff78ef9c1 in clone () from /lib64/libc.so.6

其中 Id 列表示各个线程的编号(ID 号);Target Id 列表示各个线程的标识符;Frame 列打印各个线程执行的有关信息,例如线程名称,线程暂停的具体位置等。
我们输入的调试命令并不仅仅作用于当前线程,例如 continue、next 等,默认情况下它们作用于所有线程。

调整当前线程

将编号为 id 的线程设定为当前线程

(gdb) thead id 

为特定线程设置断点

(gdb) break location thread id
(gdb) break location thread id if...

location 表示设置断点的具体位置;id 表示断点要作用的线程的编号;if… 参数作用指定断点激活的条件,即只有条件符合时,断点才会发挥作用。

设置线程锁

使用 GDB 调试多线程程序时,默认的调试模式为:一个线程暂停运行,其它线程也随即暂停;一个线程启动运行,其它线程也随即启动。要知道,这种调试机制确实能帮我们更好地监控各个线程的“一举一动”,但并非适用于所有场景
一些场景中,我们可能只想让某一特定线程运行,其它线程仍维持暂停状态。要想达到这样的效果,就需要借助 set scheduler-locking 命令。 此命令可以帮我们将其它线程都“锁起来”,使后续执行的命令只对当前线程或者指定线程有效,而对其它线程无效

(gdb) set scheduler-locking mode

参数 mode 的值有 3 个,分别为 off、on 和 step,它们的含义分别是:

  • off:不锁定线程,任何线程都可以随时执行;
  • on:锁定线程,只有当前线程或指定线程可以运行;
  • step:当单步执行某一线程时,其它线程不会执行,同时保证在调试过程中当前线程不会发生改变。但如果该模式下执行 continue、until、finish 命令,则其它线程也会执行,并且如果某一线程执行过程遇到断点,则 GDB 调试器会将该线程作为当前线程。

查看各个线程锁定的状态

(gdb) show scheduler-locking
Mode for locking scheduler during execution is "on".

GDB non-stop 模式

对于调试多线程程序,GDB 默认采用的是 all-stop 模式,即只要有一个线程暂停执行,所有线程都随即暂停。这种调试模式可以适用于大部分场景的需要,借助适当数量的断点,我们可以清楚地监控到各个线程的具体执行过程。
在某些场景中,我们可能需要调试个别的线程,并且不想在调试过程中,影响其它线程的运行。这种情况下,可以将 GDB 的调试模式由 all-stop 模式更改为 non-stop 模式,该模式下调试多线程程序,当某一线程暂停运行时,其它线程仍可以继续执行
只有 7.0 版本以上的 GDB 调试器,才支持 non-stop 模式。

  1. non-stop 模式下可以进行 all-stop 模式无法做到的调试工作,例如:
  • 保持其它线程继续执行的状态下,单独调试某个线程;
  • 在所有线程都暂停执行的状态下,单步调试某个线程;
  • 单独执行多个线程等等。

all-stop 模式下,continue、next、step 命令的作用对象并不是当前线程,而是所有的线程;但在 non-stop 模式下,continue、next、step 命令只作用于当前线程。
在 non-stop 模式下,如果想要 continue 命令作用于所有线程,可以为 continue 命令添加一个 -a 选项,即执行 continue -a 或者 c -a 命令,即可实现令所有线程继续执行的目的。

由 all-stop 模式转换到 non-stop 模式:未启动程序前执行如下命令即可

(gdb) set non-stop mode

其中,mode 参数的值有 2 种,分别是 on 和 off,on 表示启用 non-stop 模式;off 表示禁用 non-stop 模式。

show non-stop 命令,可以查看 non-stop 模式是否开启

后台执行调试命令

对于某些调试命令,GDB 调试器提供有 2 种执行方式:

  • 同步执行:“一个一个”的执行,即必须等待前一个命令执行完毕,才能执行下一个调试命令。
  • 后台执行:又称“异步执行”,即当某个调试命令开始执行时,(gdb) 命令提示符会立即出现,我们无需等待前一个命令执行完毕就可以继续执行下一个调试命令。
    以后台(异步)的方式执行一个调试命令,其语法格式如下:
(gdb) command&

其中,command 表示就是要执行的调试命令。command 和 & 之间不需要添加空格。

GDB支持后台执行的调试命令:

调试命令含 义
run(r)启动被调试的程序
attach调试处于运行着的的程序。
step单步调试程序
stepi执行一条机器指令。
next(n)单步调试程序
nexti执行一条机器指令,其与 stepi 命令的区别,类似于 step 和 next 命令的区别。
continue继续执行程序。
finish结束当前正在执行的函数
until(u)快速执行完当前的循环体

后台执行命令异步调试程序的方法,多用于 non-stop 模式中。虽然 all-stop 模式中也可以使用,但在前一个异步命令未执行完毕前,仍旧不能执行其它命令。

暂停后台线程执行

在后台处于执行状态的线程,可以使用 interrupt 命令将其中断

注意,在 all-stop 模式下,interrupt 命令作用于所有线程,即该命令可以令整个程序暂停执行;而在 non-stop 模式下,interrupt 命令仅作用于当前线程。 如果想另其作用于所有线程,可以执行 interrupt -a 命令。

多进程调试

GDB多进程调试常用命令

命令语法格式功 能
(gdb)show detach-on-fork查看当前调试环境中 detach-on-fork 选项的值。
(gdb) info inferiors查看当前调试环境中有多少个进程。其中,进程 id 号前带有 * 号的为当前正在调试的进程。
(gdb) inferiors id切换到指定 ID 编号的进程对其进行调试。
(gdb) detach inferior id断开 GDB 与指定 id 编号进程之间的联系,使该进程可以独立运行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 <null>,并且借助 run 仍可以重新启用。
(gdb) kill inferior id断开 GDB 与指定 id 编号进程之间的联系,并中断该进程的执行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 <null>,并且借助 run 仍可以重新启用。
remove-inferior id彻底删除指令 id 编号的进程(从 info inferiors 打印的列表中消除),不过在执行此操作之前,需先使用 detach inferior id 或者 kill inferior id 命令将该进程与 GDB 分离,同时确认其不是当前进程。

GDB attach命令调试进程

#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t pid = fork();
    if(pid == 0)
    {
         int num =10;
	    while(num==10){
	        sleep(10);
	    }
	    printf("this is child,pid = %d\n",getpid());
    }
    else
    {
        printf("this is parent,pid = %d\n",getpid());
    }
    return 0;
}
[root@bogon demo]# gdb myfork.exe -q
Reading symbols from ~/demo/myfork.exe...done.
(gdb) r
Starting program: ~/demo/myfork.exe
Detaching after fork from child process 5316.  <-- 子进程的 ID 号为 5316
this is parent,pid = 5313               <-- 父进程执行完毕

Program exited normally.
(gdb) attach 5316                          <-- 跳转调试 ID 号为 5316 的子进程
......
(gdb) n                                           <-- 程序正在运行,所有直接使用 next 命令就可以进行单步调试
Single stepping until exit from function __nanosleep_nocancel,
which has no line number information.
0x00000037ee2acb50 in sleep () from /lib64/libc.so.6
(gdb) n
Single stepping until exit from function sleep,
which has no line number information.
main () at myfork.c:10
10  while(num==10){
(gdb) p num=1
$1 = 1
(gdb) n                                           <-- 跳出循环
13         printf("this is child,pid = %d\n",getpid());
(gdb) c
Continuing.
this is child,pid = 5316

Program exited normally.
(gdb) 

显式指定要调试的进程

GDB 调试多进程程序时默认只调试父进程。对于内核版本为 2.5.46 甚至更高的 Linux 发行版系统来说,可以通过修改 follow-fork-mode 或者 detach-on-fork 选项的值来调整这一默认设置。

(gdb) set follow-fork-mode mode

参数 mode 的可选值有 2 个:

  • parent:选项的默认值,表示 GDB 调试器默认只调试父进程;
  • child:和 parent 完全相反,它使的 GDB 只调试子进程。且当程序中包含多个子进程时,我们可以逐一对它们进行调试。
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "child".

detach-on-fork选项

借助 follow-fork-mode 选项,我们只能选择调试子进程还是父进程,且一经选定,调试过程中将无法改变。如果既想调试父进程,又想随时切换并调试某个子进程,就需要借助 detach-on-fork 选项。

(gdb) set detach-on-fork mode

其中,mode 参数的可选值有 2 个:

  • on:默认值,表明 GDB 只调试一个进程,可以是父进程,或者某个子进程;
  • off:程序中出现的每个进程都会被 GDB 记录,我们可以随时切换到任意一个进程进行调试。

反向调试

所谓反向调试,指的是临时改变程序的执行方向,反向执行指定行数的代码,此过程中 GDB 调试器可以消除这些代码所做的工作,将调试环境还原到这些代码未执行前的状态。

GDB反向调试的常用命令

命 令功 能
(gdb) record/(gdb) record btrace让程序开始记录反向调试所必要的信息,其中包括保存程序每一步运行的结果等等信息。进行反向调试之前(启动程序之后),需执行此命令,否则是无法进行反向调试的。
(gdb) reverse-continue/(gdb) rc反向运行程序,直到遇到使程序中断的事件,比如断点或者已经退回到 record 命令开启时程序执行到的位置。
(gdb) reverse-step反向执行一行代码,并在上一行代码的开头处暂停。和 step 命令类似,当反向遇到函数时,该命令会回退到函数内部,并在函数最后一行代码的开头处(通常为 return 0; )暂停执行。
(gdb) reverse-next反向执行一行代码,并在上一行代码的开头处暂停。和 reverse-step 命令不同,该命令不会进入函数内部,而仅将被调用函数视为一行代码。
(gdb) reverse-finish当在函数内部进行反向调试时,该命令可以回退到调用当前函数的代码处。
(gdb) set exec-direction modemode 参数值可以为 forward (默认值)和 reverse:forward 表示 GDB 以正常的方式执行所有命令;reverse 表示 GDB 将反向执行所有命令,由此我们直接只用step、next、continue、finish 命令来反向调试程序。注意,return 命令不能在 reverse 模式中使用。

handle命令:信号处理

GDB 调试器提供了info handles指令,用于查看 GDB 可以处理的信号种类,以及各个信号的具体处理方式

(gdb) info signals
Signal        Stop Print  Pass to program  Description

SIGHUP        Yes  Yes    Yes              Hangup
SIGINT        Yes  Yes    No               Interrupt
SIGQUIT       Yes  Yes    Yes              Quit
SIGILL        Yes  Yes    Yes              Illegal instruction
SIGTRAP       Yes  Yes    No               Trace/breakpoint trap
SIGABRT       Yes  Yes    Yes              Aborted
SIGEMT        Yes  Yes    Yes              Emulation trap
SIGFPE        Yes  Yes    Yes              Arithmetic exception
SIGKILL       Yes  Yes    Yes              Killed
......

其中各列的含义分别为:
Signal:各个信号的名称;
Stop:当信号发生时,是否终止程序执行。Yes 表示终止,No 表示当信号发生时程序认可继续执行;
Print:当信号发生时,是否要求 GDB 打印出一条提示信息。Yes 表示打印,No 表示不打印;
Pass:当信号发生时,该信号是否对程序可见。Yes 表示程序可以捕捉到该信息,No 表示程序不会捕捉到该信息;
Description:对信号所表示含义的简单描述。

(gdb) handle signal mode

其中,signal 参数表示要设定的目标信号,它通常为某个信号的全名(SIGINT)或者简称(去除‘SIG’后的部分,如 INT);如果要指定所有信号,可以用 all 表示。
mode 参数用于明确 GDB 处理该目标信息的方式,其值可以是如下几个:
nostop:当信号发生时,GDB 不会暂停程序,其可以继续执行,但会打印出一条提示信息,告诉我们信号已经发生;
stop:当信号发生时,GDB 会暂停程序执行。
noprint:当信号发生时,GDB 不会打印出任何提示信息;
print:当信号发生时,GDB 会打印出必要的提示信息;
nopass(或者 ignore):GDB 捕获目标信号的同时,不允许程序自行处理该信号;
pass(或者 noignore):GDB 调试在捕获目标信号的同时,也允许程序自动处理该信号。

注意,当 GDB 捕获到信号并暂停程序执行的那一刻,程序是捕获不到信号的,只有等到程序继续执行时,信号才能被程序捕获。

查看栈信息

frame 命令的常用形式有 2 个:

frame命令

选定要查看的栈帧

(gdb) frame spec

该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:

  • 通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
  • 借助栈帧的地址指定。栈帧地址可以通过 info frame 命令(后续会讲)打印出的信息中看到;
  • 通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。

除此之外,对于选定一个栈帧作为当前栈帧,GDB 调试器还提供有 up 和 down 两个命令。其中,up 、down命令的语法格式为:

(gdb) up n
(gdb) down n

up—>其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m+n 为编号的栈帧作为新的当前栈帧。
down—>其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m-n 为编号的栈帧作为新的当前栈帧。

查看当前栈帧中存储的信息

(gdb) info frame

该命令会依次打印出当前栈帧的如下信息:

  • 当前栈帧的编号,以及栈帧的地址;
  • 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
  • 当前函数的调用者,对应的栈帧的地址;
  • 编写此栈帧所用的编程语言;
  • 函数参数的存储地址以及值;
  • 函数中局部变量的存储地址;
  • 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。

除此之外,还可以使用info args命令查看当前函数各个参数的值;使用info locals命令查看当前函数中各局部变量的值

backtrace命令

backtrace 命令用于打印当前调试环境中所有栈帧的信息

(gdb) backtrace [-full] [n]

其中,用 [ ] 括起来的参数为可选项,它们的含义分别为:

  • n:一个整数值,当为正整数时,表示打印最里层的 n 个栈帧的信息;n 为负整数时,那么表示打印最外层 n 个栈帧的信息;
  • full:打印栈帧信息的同时,打印出局部变量的值。

注意,当调试多线程程序时,该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息,应执行thread apply all backtrace命令。

#include <stdio.h>
int func(int num){
    if(num==1){
        return 1;
    }else{
        return num*func(num-1);
    }
}
int main ()
{
    int n = 5;
    int result = func(n);
    printf("%d! = %d",n,result);
    return 0;
}
(gdb) b 3
Breakpoint 1 at 0x4004cf: file main.c, line 3.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, func (num=5) at main.c:3
3     if(num==1){
(gdb) c
Continuing.

Breakpoint 1, func (num=4) at main.c:3
3     if(num==1){
(gdb) p num
$1 = 4
(gdb) backtrace        <-- 打印所有的栈帧信息
#0  func (num=4) at main.c:3
#1  0x00000000004004e9 in func (num=5) at main.c:6
#2  0x0000000000400508 in main () at main.c:12
(gdb) info frame       <-- 打印当前栈帧的详细信息
Stack level 0, frame at 0x7fffffffe240:            <-- 栈帧编号 0,地址 0x7fffffffe240
rip = 0x4004cf in func (main.c:3); saved rip 0x4004e9   <-- 函数的存储地址 0x4004cf,调用它的函数地址为 0x4004e9
called by frame at 0x7fffffffe260        <-- 当前栈帧的上一级栈帧(编号 1 的栈帧)的地址为 0x7fffffffe260
source language c.
Arglist at 0x7fffffffe230, args: num=4  <-- 函数参数的地址和值
Locals at 0x7fffffffe230, Previous frame's sp is 0x7fffffffe240  <--函数内部局部变量的存储地址
Saved registers:    <-- 栈帧内部存储的寄存器
  rbp at 0x7fffffffe230, rip at 0x7fffffffe238
(gdb) info args          <-- 打印当前函数参数的值
num = 4
(gdb) info locals        <-- 打印当前函数内部局部变量的信息(这里没有)
No locals.
(gdb) up   <-- 查看编号为 1 的栈帧
#1  0x00000000004004e9 in func (num=5) at main.c:6
6         return num*func(num-1);
(gdb) frame 1           <-- 当编号为 1 的栈帧作为当前栈帧
#1  0x00000000004004e9 in func (num=5) at main.c:6
6         return num*func(num-1);
(gdb) info frame      <-- 打印 1 号栈帧的详细信息
Stack level 1, frame at 0x7fffffffe260:
rip = 0x4004e9 in func (main.c:6); saved rip 0x400508
called by frame at 0x7fffffffe280, caller of frame at 0x7fffffffe240  <--上一级栈帧地址为 0x7fffffffe280,下一级栈帧地址为 0x7fffffffe240
source language c.
Arglist at 0x7fffffffe250, args: num=5
Locals at 0x7fffffffe250, Previous frame's sp is 0x7fffffffe260
Saved registers:
  rbp at 0x7fffffffe250, rip at 0x7fffffffe258
(gdb)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值