9. C++ GDB调试原理(简洁版)

背景

GDB支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调试手段

断点

断点是我们在调试中经常用的一个功能,我们在指定位置设置断点之后,程序运行到该位置将会暂停,这个时候我们就可以对程序进行更多的操作,比如查看变量内容,堆栈情况等等,以帮助我们调试程序。

以设置断点的命令分为以下几类:

  • breakpoint;
  • watchpoint;
  • catchpoint;

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-lockingon,避免其他线程同时运行,导致命中其他断点分散注意力。

打印字符串
命令 作用
x/s str 打印字符串
打印数组
作用
打印从数组开头连续10个元素的值
打印array数组下标从60开始的10个元素,即第60~69个元素
打印指针
命令 作用
print ptr 查看该指针指向的类型及指针地址
print *(struct xxx *)ptr 查看指向的结构体的内容
调试和保存core文件
命令 含义
core core_file 加载core-dump文件
gcore core_file 生成core-dump文件,记录当前进程的状态
启动方式

使用gdb调试,一般有以下几种启动方式:

  • gdb filename: 调试可执行程序
  • gdb attach pid: 通过”绑定“进程ID来调试正在运行的进程
  • gdb filename -c coredump_file: 调试可执行文件
原理

本节,我们大概讲下GDB调试的原理。

gdb 通过系统调用 ptrace 来接管一个进程的执行。ptrace 系统调用提供了一种方法使得父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器。它主要用来实现断点调试和系统调用跟踪。

9. C++ GDB调试原理(简洁版)_第1张图片

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执行以下操作:

    • 通过fork()系统调用创建一个新进程。
    • 在新创建的子进程中执行ptrace(PTRACE_TRACEME, 0, 0, 0)操作。
    • 在子进程中通过execv()系统调用加载指定的可执行文件。
attach运行的进程

可以通过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,...)调用来实现单步。

你可能感兴趣的:(C++知识,c++,java,linux)