c++gdb

大纲

  • 概述
  • gdb的安装
  • 调试原理
  • 示例
  • 符号表的生成

概述

gdb是什么之类的,这里就不说了,看这个文章的,多多少少都知道的,简而言之就是调试的!与之差不多的还有很多,eg:llvm

gdb的安装

本文是在Msys2的环境中进行的,所以这里说的是在Msys2中如何安装gdb

使用pacman安装

pacman -S gdb

安装完成后,直接敲入gdb指令回车后可以看到类似以下的样子

png

调试原理

这里就以gdb为例简述gdb的底层调试原理。使用一下指令生成gdb调试用的程序(debugrelease文件的差异或在后续的文档中记录)

g++ -g hello.cpp -o hello

g++中的-g是调试时使用的参数,通过-g生成gdb需要的调试文件,如果通过-g生成的是汇编文件或者是机器码文件,则可以通过strip去直接剔除debug信息直接生成release版本的执行程序!后面会详细说明!

说完debug程序的生成后,我们看看gdb的一些基本原理!

GDB调试包括2个程序:gdb程序被调试程序。根据这2个程序是否运行在同一台电脑中,可以把GDB的调试模型分为2种:

  1. 本地调试
  2. 远程调试
  • 本地调试

gdb调试程序被调试程序运行在同一台电脑中。

png

  • 远程调试

调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。

png

RSP协议,全称是: GDB Remote Serial Protocol(GDB远程通信协议)。 它们都是字符串,有固定的开始字符(‘$’)和结束字符(‘#’),最后还有两个十六进制的ASCII字符作为校验码

$: 开始符
s:单步指令
#:结束符
2个16进制ASCII符:校验码  ->(73)

gdb常见的指令:

指令 说明
run 从头开始连续而非单步执行程序
next 执行下一行语句
step 执行下一行语句,如果有函数调用则进入函数
qiut 退出
break n 在n行设置断点
delete n 删除n行的断点
enable n 启动n行的断点
disable n 禁用n行的断点
clear 清理所有的断点
bt 查看各级函数调用及参数
set args 设置程序中变量的值
show args 查看程序中变量的值
watch 设置观察点
print 输出变量值
continue 执行到下一个断点
多线程指令 说明
info threads 显示当前可调试的所有线程
thread ID(1,2,3…) 切换当前调试的线程为指定ID的线程
break thread_test.c:123 thread all 在所有线程中相应的行上设置断点
thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command
thread apply all command 让所有被调试线程执行GDB命令command
set scheduler-locking 选项 command 设置线程是以什么方式来执行命令
set scheduler-locking off 不锁定任何线程,也就是所有线程都执行,这是默认值
set scheduler-locking on 只有当前被调试程序会执行
set scheduler-locking on step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行

以上仅仅是一部分,需要的可以自行google完整的!不用担心记不住,知道有这些指令就行,用的时候找找就好,用多了就熟悉了!

示例

我们以一个简单的hello程序为例:

#include <stdio.h>

int main(void) {
	int num1 = 1;
    int num2 = 2;
    int sum = num1 + num2;
    printf("sum = %d \n", sum);
	
    return 0;
}

通过以下指令可以生成这个程序的debug程序

g++ -g hello.cpp -o hello_debug

在正确的坏境中会生成一个hello_debug.exe的文件,对可执行程序 hello_debug 进行调试,

启动gdb

输入命令:

gdb ./hello_debug

png

系统首先会启动gdb进程,这个进程会调用系统函数fork()来创建一个子进程,这个子进程做两件事情: 1. 调用系统函数ptrace(PTRACE_TRACEME,[其他参数]); 2. 通过execc来加载、执行可执行程序hello_debug,那么hello_debug程序就在这个子进程中开始执行了。

函数ptrace:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

png

ptrace的说明在文章后面,这里只需要知道有这么一回事就可以了~

前文已经对hello_debug进行了编译!展开其汇编码:

png

在执行了gdb ./hello_debug后,hello_debug其实是还没有启动的!

png

需要开始调试,还需要在gdb下输入start;

png

到了这里, 就能看到命令里提示的int num1 = 1;

这也说明调试程序已经执行到了该行(但是还没有执行, 仅仅只是执行到!)

png

通过打印,可以看到,num10并不是我们初始申明的1

p(也是print)指令是用来打印的,后面可以跟随变量名称!

断点和继续

  • 下一步

next指令可以使gdb执行下一条指令(这里要注意的是,所谓的下一条指令,并不是汇编级别的指令)

png

  • 断点

回到start指令的状态(可以通过重新启动gdb)

start的时候,gdb是停留在line 4的,即main()之后的第一个行代码处;假设我们现在line 6(即是int sum = num1 + num2;处打上断点)

break 6

png

标记上断点后,使用c(continue)指令就能直接执行到断点处!

png

  • 其他

gdb的其他指令这里就不一一说了,可以自行探索!

符号表的生成

这里就补充一下符号表相关的部分说明!

首先前文已经说了debug程序生成的时候使用了-g生成;(前文的hello_debug)。

系统提供了objcopy供我们单独拷贝符号信息,通过参数--only-keep-debug使得copy的对象仅保留了debug信息,即符号表。

 objcopy --only-keep-debug hello_debug hello.debug

说到这里,不得不提一下release版本程序生成的另一种方式!

strip --strip-all hello_debug
strip --strip-debug hello_debug

以上指令是剥离在hello_debug的信息,使得hello_debug保留下来的执行程序和release一样!(--strip-all--strip-debug还是有区别的,这里就不细说明!)

按照前文的,在剥离符号表后,再次使用gdb hello_debug(已经执行strip后或者是release),

得到的结果如下:

png

没有找到符号表,这样就导致我们需要debug的时候就无法进行理想的断点了!这个时候,我们需要把已经剥离的符号表link到程序中(将原本已经striphello_debug同为hello_release):

objcopy --add-gnu-debuglink=./hello.debug hello_release

执行完成以上指令后,再次进行gdb,就能正确加载符号表了:

png

添加符号表还可以通过以下几种方式去绑定到程序中:

  • 启动gdb时,参数-s指定

gdb -e ./hello_release -s ./hello.debug

  • 启动gdb后,通过set debug-file-directory指定符号表目录

set debug-file-directory ./

  • 启动gdb后,通过add-symbol-file单独挂载符号表

add-symbol-file ./hello.debug

本来还想细说ptrace的,但是内容有点多,到时再单独开一章!