一篇文章学会GDB调试工具的原理以及使用

参考文章:https://blog.csdn.net/weixin_55953651/article/details/126908125

一、GDB调试工具的原理

1.1 未执行进程调试

启用gdb调试运行gdb ./test的时候,在操作系统里发生了很多复杂的事情,系统首先会启动gdb进程,这个进程会调用系统函数fork()来创建一个子进程,这个子进程做两件事情:

  • 调用系统函数ptrace(PTRACE_TRACEME,[其他参数]);
  • 通过exec来加载、执行可执行程序test,那么test程序就在这个子进程中开始执行了。

请添加图片描述

1.2 执行中进程调试

如果想对一个已经执行的进程进行调试,那么就要在gdb这个父进程中调用ptrace(PTRACE_ATTACH,[其他参数]),此时,gdb进程会attach(绑定)到已经执行的进程B,gdb把进程B收养成为自己的子进程,而子进程B的行为等同于它进行了一次 PTRACE_TRACEME操作。此时gdb进程会发送SIGSTO信号给子进程B,子进程B接收到SIGSTOP信号后,就会暂停执行进入TASK_STOPED状态,表示自己准备好被调试了。

请添加图片描述

1.3 gdb系统调用原型介绍

#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

ptrace系统函数是Linux内核提供的一个用于进程跟踪的系统调用,通过它,一个进程(gdb)可以读写另外一个进程(test)的指令空间、数据空间、堆栈和寄存器的值。而且gdb进程接管了test进程的所有信号,也就是说系统向test进程发送的所有信号,都被gdb进程接收到,这样一来,test进程的执行就被gdb控制了,从而达到调试的目的。

下面对各个参数进行解释:

  1. enum __ptrace_request request:是一个枚举类型,用于指定要执行的操作类型。这个参数告诉 ptrace 函数将要对进程进行何种跟踪操作,例如读取寄存器、写型,其定义了一系列跟踪请求类型的常量。例如,PTRACE_ATTACH 表示附加到一个新进程,PTRACE_GETREGS 表示获取寄存器值。request的主要类型如下:
PTRACE_TRACEME:用于将当前进程标记为被跟踪的目标。调用进程使用这个类型请求后,它的父进程可以使用 PTRACE_ATTACH 来附加到它,对其进行调试和跟踪。
PTRACE_ATTACH:用于将一个进程附加到另一个进程上进行调试和跟踪。调试器进程可以使用这个类型请求,通过指定目标进程ID来附加到目标进程。
PTRACE_DETACH:用于从一个已经被附加和调试的进程上分离调试器。这个请求会停止对目标进程的跟踪,并将其恢复为正常运行状态。
PTRACE_PEEKDATA:用于从目标进程的内存中读取数据。可以使用该请求来读取目标进程的内存值,例如寄存器、栈帧等。
PTRACE_POKEDATA:用于向目标进程的内存中写入数据。可以使用该请求来修改目标进程的内存值,例如修改寄存器、改变变量值等。
PTRACE_GETREGS:用于获取目标进程的寄存器值。通过这个请求,可以获得目标进程的 CPU 寄存器的当前值,用于调试和跟踪。
PTRACE_SETREGS:用于设置目标进程的寄存器值。通过这个请求,可以将特定的寄存器值设置为目标进程中的特定值。
PTRACE_CONT:用于继续执行已附加的目标进程。调试器进程可以使用这个请求来继续目标进程的执行,直到下一个断点或者其他事件触发。
  1. pid_t pid:是一个整数类型,表示要操作的目标进程的进程ID(PID)。pid 指定了要对哪个进程进行跟踪操作,可以是当前进程、正在运行的其他进程或子进程等。
  2. void *addr:是一个指针类型,用于指定内存地址,具体用途根据不同的 request 参数而定。例如,对于一些读写内存的请求,addr 指定了要读取或写入的内存地址。
  3. void *data:是一个指针类型,用于传递数据,具体用途也根据不同的 request 参数而定。对于一些读写内存或寄存器的请求,data 指定了要读取或写入的数据存储位置。

ptrace 函数返回一个 long 类型值,表示操作的结果或错误码。通常情况下,返回值大于等于 0 表示成功,小于 0 表示发生错误。

如果没有gdb调试,操作系统与目标进程之间是直接交互的;如果使用gdb来调试程序,那么操作系统发送给目标进程的信号就会被gdb截获,gdb根据信号的属性来决定:在继续运行目标程序时是否把当前截获的信号转交给目标程序,如此一来,目标程序就在gdb发来的信号指挥下进行相应的动作。

image-20230726111320295

二、GDB调试工具的使用

2.1 启动调试

在程序开始调试之前,要确保程序在gcc、g++编译时,有如下-g的添加调试信息的选项

gcc -g test.c -o test

1、启动未运行的程序

启动未运行的程序,只需要在对应程序目录中使用下面的命令

gdb test

2、调试已经开始运行的程序进程

调试已经开始运行的程序进程,首先先用top命令查看运行的程序进程的pid如下:

image-20230730231037766

比如我要加载的程序qemu-system-x86的pid为269427,则用下面的命令将进程附加到GDB调试

gdb attach 269427#gdb attach [进程号]

或者先打开gdb,在gdb中输入attach 269427也可以。效果如下

image-20230726180838421

此时程序是处于暂停状态的,用下面的命令让程序继续运行

#前面是缩写,#后是全写,两者皆可
(gdb) c#continue

效果如下:

image-20230726181136712

2.2 断点管理

添加断点

  • 方法一
#源程序只有一个文件
b [行号]#break [行号]
  • 方法二
#源程序多个个文件
b [文件名.c]:[行号]#break [文件名.c]:[行号]

添加断点后如下所示:

image-20230726181712075

查看所有断点

info b#info break

效果如下:

image-20230726181824992

我们可以看到所有的断点还有断点的编号

删除断点

delete#删除所有断点
delete [断点编号]#删除指定断点

2.3 中断调试执行

s#step 单步执行,进入函数调用
n#next 逐行执行当前线程的代码,不进入函数调用
c#continue 执行代码到下一个断点
f#finish 执行完当前函数并跳出
ignore [断点号] [次数]#设置或修改运行时断点的忽略计数

2.4 运行参数监控

p [变量名]#print [变量名] 输出该变量的值
display [变量名]#调试运行的每一步自动输出该变量的值
bt#显示当前的函数调用堆栈情况
list [行数]#显示当前执行的代码,默认10行
watch [变量名]#设置变量监视点,在变量值改变时暂停程序。

info breakpoints#显示当前已设置的断点列表。
info watchpoints#显示当前已设置的监视点(观察点)列表。
info functions#显示程序中定义的所有函数列表。
info variables#显示程序中定义的所有全局变量和静态变量列表。
info locals#显示当前函数的局部变量信息。
info args#显示当前函数的参数信息。
info threads#显示当前正在运行的所有线程信息。
info registers#显示当前线程的寄存器值。
info frame#显示当前的调用帧信息。
info sharedlibrary#显示加载的共享库信息。
info inferior#显示当前程序的执行状态信息。
info record#显示反复执行的命令数量。
info breakpoints location <address>#显示指定地址上设置的断点信息。
info break <breakpoint_number>#显示指定编号的断点信息。
info line <filename>:<line>#显示指定文件和行号的源代码信息。

2.5 多线程调试

info thread#命令查看当前线程的信息
info threads#命令查看当前所有线程
frame [栈帧号] #命令切换到指定的栈帧
thread [线程号]#切换到指定线程

猜你喜欢

转载自blog.csdn.net/qq_40959462/article/details/131995352