GDB支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调试手段。
断点是我们在调试中经常用的一个功能,我们在指定位置设置断点之后,程序运行到该位置将会暂停
,这个时候我们就可以对程序进行更多的操作,比如查看变量内容,堆栈情况
等等,以帮助我们调试程序。
以设置断点的命令分为以下几类:
breakpoint
可以根据行号、函数、条件生成断点,下面是相关命令以及对应的作用说明:
命令 | 作用 |
---|---|
break [file]:function | 在文件file的function函数入口设置断点 |
break [file]:line | 在文件file的第line行设置断点 |
info breakpoints | 查看断点列表 |
clear | 删除所有断点 |
watchpoint
watchpoint是一种特殊类型的断点,类似于正常断点,是要求GDB暂停程序执行的命令。
watchpoint分为硬件实现和软件实现
两种。前者需要硬件系统的支持;后者的原理就是每步执行后都检查变量的值是否改变。GDB在新建数据断点时会优先尝试硬件方式,如果失败再尝试软件实现。
使用数据断点时,需要注意:
p
,则watch *p
监控的是p
所指内存数据的变化情况,而watch p
监控的是p
指针本身有没有改变指向;最常见的数据断点应用场景:「定位堆上的结构体内部成员何时被修改」。由于指针一般为局部变量,为了解决断点失效,一般有两种方法。
命令 | 作用 |
---|---|
print &variable | 查看变量的内存地址 |
watch *(type *)address | 通过内存地址间接设置断点 |
watch -l variable | 指定location参数 |
watch variable thread 1 | 仅编号为1的线程修改变量var值时会中断 |
catchpoint
从字面意思理解,是捕获断点,其主要监测信号的产生。
命令 | 含义 |
---|---|
catch fork | 程序调用fork时中断 |
tcatch fork | 设置的断点只触发一次,之后被自动删除 |
catch syscall ptrace | 为ptrace系统调用设置断点 |
命令 | 作用 |
---|---|
run arglist | 以arglist为参数列表运行程序 |
set args arglist | 指定启动命令行参数 |
命令 | 作用 |
---|---|
backtrace [n] | 打印栈帧 |
frame [n] | 选择第n个栈帧,如果不存在,则打印当前栈帧 |
info args | 当前栈帧的参数列表 |
info locals | 当前栈帧的局部变量 |
GDB在调试多进程程序(程序含fork
调用)时,默认只追踪父进程。可以通过命令设置,实现只追踪父进程或子进程,或者同时调试父进程和子进程。
命令 | 作用 |
---|---|
info inferiors | 查看进程列表 |
attach pid | 绑定进程id |
inferior num | 切换到指定进程上进行调试 |
set follow-fork-mode child | 追踪子进程 |
set follow-fork-mode parent | 追踪父进程 |
set detach-on-fork on | fork调用时只追踪其中一个进程 |
set detach-on-fork off | fork调用时会同时追踪父子进程 |
在调试多进程程序时候,默认情况下,除了当前调试的进程,其他进程都处于挂起状态,所以,如果需要在调试当前进程的时候,其他进程也能正常执行,那么通过设置set schedule-multiple on
即可。
多线程开发在日常开发工作中很常见,所以多线程的调试技巧非常有必要掌握。
默认调试多线程时,一旦程序中断,所有线程都将暂停。如果此时再继续执行当前线程,其他线程也会同时执行。
命令 | 作用 |
---|---|
info threads | 查看线程列表 |
print $_thread | 显示当前正在调试的线程编号 |
set scheduler-locking on | 调试一个线程时,其他线程暂停执行 |
set scheduler-locking off | 调试一个线程时,其他线程同步执行 |
set scheduler-locking step | 仅用step调试线程时其他线程不执行,用其他命令如next调试时仍执行 |
如果只关心当前线程,建议临时设置 scheduler-locking
为 on
,避免其他线程同时运行,导致命中其他断点分散注意力。
命令 | 作用 |
---|---|
x/s str | 打印字符串 |
作用 |
---|
打印从数组开头连续10个元素的值 |
打印array数组下标从60开始的10个元素,即第60~69个元素 |
命令 | 作用 |
---|---|
print ptr | 查看该指针指向的类型及指针地址 |
print *(struct xxx *)ptr | 查看指向的结构体的内容 |
命令 | 含义 |
---|---|
core core_file | 加载core-dump文件 |
gcore core_file | 生成core-dump文件,记录当前进程的状态 |
使用gdb调试,一般有以下几种启动方式:
本节,我们大概讲下GDB调试的原理。
gdb 通过系统调用 ptrace
来接管一个进程的执行。ptrace 系统调用提供了一种方法使得父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器。它主要用来实现断点调试和系统调用跟踪。
ptrace系统调用定义如下:
#include
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)
pid_t pid
:指示 ptrace 要跟踪的进程。
void *addr:
指示要监控的内存地址。
enum __ptrace_request request:
决定了系统调用的功能,几个主要的选项:
PTRACE_TRACEME:
表示此进程将被父进程跟踪。PTRACE_ATTACH
:attach 到一个指定的进程,使其成为当前进程跟踪的子进程。PTRACE_CONT
:继续运行之前停止的子进程。运行并调试新进程,步骤如下:
运行gdb exe
输入run命令,gdb执行以下操作:
ptrace(PTRACE_TRACEME, 0, 0, 0)
操作。可以通过gdb attach pid来调试一个运行的进程,gdb将对指定进程执行ptrace(PTRACE_ATTACH, pid, 0, 0)操作。
需要注意的是,当我们attach一个进程id时候,可能会报如下错误:
Attaching to process 28849
ptrace: Operation not permitted.
这是因为没有权限进行操作,可以根据启动该进程用户下或者root下进行操作。
当我们通过b或者break设置断点时候,就是在指定位置插入断点指令,当被调试的程序运行到断点的时候,产生SIGTRAP信号。该信号被gdb捕获并 进行断点命中判断。
在程序中设置断点,就是先在该位置保存原指令,然后在该位置写入int 3。当执行到int 3时,发生软中断,内核会向子进程发送SIGTRAP信号。当然,这个信号会转发给父进程。然后用保存的指令替换int 3并等待操作恢复。
gdb将所有断点位置存储在一个链表中。命中判定将被调试程序的当前停止位置与链表中的断点位置进行比较,以查看断点产生的信号。
这个ptrace函数本身就支持,可以通过ptrace(PTRACE_SINGLESTEP, pid,...)
调用来实现单步。