Linux下GDB调试程序

1. 什么是GDB

GDB(全称:GNU Debugger)是GNU工程师为GNU操作系统开发的调试器。它可以用于调试C、C++、Objective-C、Pascal、Ada等语言编写的程序。

2. GDB的使用条件

在程序编译的时候,添加响应的调试信息,才能使程序使用GDB进行调试,以CMake为例,示范添加调试信息的方法:

SET(CMAKE_BUILD_TYPE "Debug")    # 使得生成的程序包含调试信息

SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")

SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

设置的具体含义可参考《CMAKE学习笔记

注:

  • 通常在为调试而编译程序时,必须关掉编译器的优化现象(-0n),并打开调试选项 -g,另外,’-Wall‘在尽量不影响程序行为的情况下打开,提示所有的warning,优化选项的具体含义可参考 《CMAKE学习笔记
  • -g选项的作用是在可执行文件中加入调试需要的信息GDB中查看的时源文件的代码)

3. GDB启动方法

3.1 GDB启动方法

gdb xxx     // xxx表示需要调试的程序名

3.2 GDB常用命令

GDB常用命令
命令简写功能
file载入需要调试的可执行文件
kill终止正在调试的程序
listl

列出部分源代码,可列出的是正在执行的位置附近的源代码。

1. 输入list后,每次列出大概10行左右的代码,重复enter可不断

    列出后续的源代码

2. list linenum,可列出特定行linenum附近的源代码

3. list filename:linenum  列出特定文件的特定行的源代码

nextn执行一行源代码,但是不进入函数内部
step执行一行源代码,且可以进入函数内部
runr开始执行当前被调试的程序,遇到第一个断点的时候停下来,如果没有断点,会直接往下运行。
continuec继续运行程序到下一个断点的位置
start运行程序并停在主函数开始的地方(即使没有断点)
breakb

设置断点

1. b linenum  在当前文件的linenum行设置断点

2. b filename:linename 在特定文件的特定行设置断点

3. b func  在函数func处设置断点

4. b filename:func  在特定文件func函数处设置断点

5. b linenum if i==x  在某行设置条件断点

info b查看算有断点
watch监视一个变量的值,在调试过程中,变量的值发生变化的时候程序会停在变量值发生变化的位置,watch的优点是不需要提前预知到变量的值在哪里会发生变化而去打断点,在变量被watch之后,在那个地方变量的值发生了改变,程序就会停在这个地方(相当于到了断点)
rwatch只要程序中出现读取目标变量的值,则程序就会停在读取的位置处
awatch只要程序中出现读取目标变量的值或者修改目标变量的值,则程序就会停在读取或者修改值的位置处
printp查看一个变量的值
display与print类似,也是用于在调试过程中查看变量或者表达式的值,但使用display不仅在执行该命令的同时会看到目标变量的值,后续每次程序停止执行时(停在断点处),GDB 调试器都会将目标变量的值打印出来。
whatis 显示变量或者函数的类型
ptype显示结构的定义,如结构体类型的具体定义
make不退出gdb,重新生成可执行文件
shell再不退出GDB的情况下,可执行Linux shell命令
info b打印输出所有设置的断点
info watchpoints打印输出所有的观察点
info files显示被调试文件的详细信息
info args查看传入当前函数的参数值
info func显示所有的函数名称
info prog显示被调试程序的执行状态
info locals打印函数内所有的变量值
info inferiors

显示当前调试程序的所有进程 (父进程和子进程)

inferior n切换到进程n(多进程程序调试)
info frame查看所有的栈帧信息
frame n查看栈信息, n为栈帧的编号
up n

在当前栈帧编号(假设为t)的基础上,将编号t+n的栈帧作为新的栈帧,n的默认值为1

(可理解为在栈中进行上下移动)

down n

在当前栈帧编号(假设为t)的基础上,将编号t-n的栈帧作为新的栈帧,n的默认值为1

(可理解为在栈中进行上下移动)

backtracebt

查看栈信息:

backtrace n :打印最里层的n的栈帧的信息

backtrace -n: 打印最外层的n个栈帧的信息

backtrace -full 打印栈帧信息的同时打印局部变量的值

where显示当前程序运行到哪一个文件的哪一行
enable n使能断点n
disabke n禁用断点n
del n

删除断点n

del m n t 删除多个断点

finish终止当前函数并返回到函数调用点
set variable

设置变量的值,当程序运行到某个地方停住,如果想改变这个位置前某一个变量的值,则可以使用set variable来实现修改:

set variable data=1

set variable buffer="testcon"

call name(args)调用并执行函数name,传递的参数是args
return val停止当前函数,并将值val返回给函数调用者
quitq退出GDB

关于watch命令的补充:

watch命令实现变量监视机制的方式有两种

  • 为变量设置硬件观察点  Hardware watchpoint
  • 为变量设置软件观察点  Software  watchpoint

软件观察点:watch命令监视目标变量或者表达式之后,GDB调试器会以单步执行的方式运行程序,在运行完每一行代码之后,都会区检测目标变量或者表达式的值是否发生了变化,如果改变,则程序会停止在值发生变化的位置。这种机制会降低程序的调试效率,但是调试程序的目的是为了查找到其中的bug,所以一定程度的效率降低并不是关注的重点。

硬件观察点:系统会为GDB提供少量的寄存器(Intel x86 提供4个调试寄存器),每个寄存器可以作为一个观察点,协助GDB完成变量监视,这种机制在同样实现变量监视的同时,不会影响程序的调试效率。

因为系统提供的调试寄存器数量有限,因此如果在程序中设置过多的硬件观察点,则可能会导致观察点失效,此时GDB会提示:

Hardware watchpoint num: Could not insert watchpoint

此时需要删除或者禁用一些观察点。

此外,调试寄存器的大小固定,因此不能用硬件观察点来监视占用字节数较多的变量(比如一些操作系统中,GDB只能监视4字节长度的数据,如 long 类型监视不了,可以尝试转换为 int 类型)。目前大多数系统都支持建立硬件观察点,所以GDB调试在建立观察点的时候,会优先建立硬件观察点,只有当系统不支持硬件观察点的时候,才会去建立软件观察点。使用如下命令,可强制GDB只建立软件观察点:

set can-use-hw-watchpoints 0

注:awatch 和 rwatch 命令只能设置硬件观察点,当系统不支持硬件观察点的时候,GDB会打印输出如下信息:

Expression cannot be implemented with read/access watchpoint.

关于display命令的补充:

display命令还支持将变量值通过特定的格式进行输出:

display/fmt variable
/fmt描述
/d以有符号、十进制的形式打印出整数。
/x以十六进制的形式打印出整数。
/u以无符号、十进制的形式打印出整数。
/t以二进制的形式打印出整数。
/o以八进制的形式打印出整数。
/f以浮点数的形式打印变量或表达式的值。
/c以字符形式打印变量或表达式的值。

通过display显示的变量或者表达式,都会被记录在自动显示列表中,可通过执行如下命令,查看列表中记录的所有变量或者表达式:

info dispaly

 Num: GDB为列表中的变量或者表达式提供的唯一编号

 Enb: 列表中的变量是处于激活状态还是禁止状态(y/n)

Expression: 列表中的变量或者表达式

可使用如下命令删除自动显示列表中的某个变量:

undisplay n
delete display n

可使用如下命令使能或者禁用自动显示列表中的某个变量:

enable display n       // 使能
disable display n      // 禁止

关于frame命令的补充

在程序中每个被调用的函数在执行的时候,都会生成与此函数相关的一些基本信息,这些基本信息包括:

  • 当前函数在程序中什么位置被调用
  • 函数被调用时传入的具体的参数值
  • 函数体中各局部变量的值

这些基础信息会存储在一块称栈帧的内存空间中,即程序运行时,每调用一个函数,就会生成一个对应的栈帧,程序调用结束的时候,栈帧会自动销毁。而这些栈帧的存储位置集中在一块特定的内存区域,称之为栈或者栈区。(在程序执行的时候都会占用一整块内存空间,且这块内存空间会被细分为多个不同的区域,例如栈区、堆区、全局数据区、常量区等,用以存储程序中不同的资源)。

因此当程序因在某个函数中存在某种错误而停止执行的时候,可以通过程序的栈帧记录的信息,查找程序异常停止的原因(C,C++程序中至少存在一个函数,即main函数,因此也会至少生成一个栈帧)。

frame命令的用法:

frame spec

通过上述命令,可以将指定的栈帧选定为当前的栈帧,spec参数可以指定为:

  • 栈帧编号,0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
  • 栈帧的地址
  • 通过函数的函数名指定。如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。

栈帧的编号以及栈帧的地址,都可以通过如下命令进行查询:

info frame

 通过info frame可以查看到栈帧的如下信息:

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

-----------------------------------------------分割线------------------------------------------------

 4. GDB调试多进程程序

多进程程序调试,首先启动GDB调试,接着需要做两个设置:

set follow-fork-mode child
set detach-on-fork off

follow-fork-mode: 可取值为:child , parent, 用于设置GDB跟踪子进程还是父进程,在进行多进程程序调试的时候,可设置为跟踪子进程。

detach-on-fork: 可取值为off 或者 on, 表示调试当前进程的时候,其他进程是否继续运行,当设置为off的时候,调试当前进程,其他进程会被GDB挂起。当设置为on,调试当前进程的时候,其他进程会继续运行,

可以通过如下语句查看设置值:

show follow-fork-mode
show detach-on-fork

在设置上述的两个选项之后,即可开始调试多进程程序,在遇到fork()进程之后,GDB会自动切换新fork出的进程里面,原来的进程则被GDB挂起,可通过如下语句查看目前程序的所有进程:

info inferiors

可看到当前程序共有两个进程,可通过如下命令在不同进程之间进行切换:

inferior n

其中n表示info输出的进程的Num号,而不是进程号

使用如下命令可使进程脱离GDB调试:

detach inferiors n

5. GDB调试多线程程序

Linux环境下的线程本质上依然是进程,称之为轻量级进程(Light Weight Process, LWP),计算机是以进程作为资源分配的最小单位。而线程是操作系统调度执行的最小单位。

测试代码如下所示:

#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

// 编写多线程测试程序
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void* worker1(void* args)
{

    pthread_mutex_lock(&mutex);

    int* arrs = (int*)args;

     pthread_t tid = pthread_self();     // 获取线程ID

    for (size_t i = 0; i < 10; i++)
    {
        /* code */
        arrs[0]++;

        sleep(1);

        printf("Thread %d current cnt value is %d\n", tid, arrs[0]);
    }
    

    pthread_mutex_unlock(&mutex);

    return NULL;
}

void* worker2(void* args)
{
    pthread_mutex_lock(&mutex1);

    int* arrs = (int*)args;

    pthread_t tid = pthread_self();

    for (size_t i = 0; i < 10; i++)
    {
        /* code */
        arrs[1]++;

        sleep(1);

        printf("Thread %d current cnt value is %d\n", tid, arrs[1]);
    }
    
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

int main(int argc, char* argv[])
{
    int array[2] = {0};

    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, worker1, array);
    pthread_create(&thread2, NULL, worker2, array);

    pthread_detach(thread1);
    pthread_detach(thread2);

    //pthread_join(thread1, NULL);
    //pthread_join(thread2, NULL);

    while (true)
    {
        // 等待两个子线程运行结束,主线程才能结束
        // 否则会由于主线程的提前推出而导致子线程执行失败
        // printf("Waiting for child thread to terminate...\n");
        pthread_mutex_lock(&mutex2);
        
        if (array[0] >= 10 && array[1] >=10)
        {
            break;
        }

        pthread_mutex_unlock(&mutex2);
    }
    
    printf("The Main thread terminate!\n");

    return 0;
}

运行结果:

 开始调试:

在进程多线程调试的时候,我们需要设置,让调试当前线程的时候,其他的线程能够被GDB挂起,可通过如下命令设置命令设置线程锁:

set scheduler-locking off

scheduler-locking 可取值为:

  • on   锁定线程,调试当前线程的时候,其他的线程会暂时被GDB挂起
  • off   不锁定线程,在调试当前线程的时候,其他的线程都会继续运行
  • step 当单步执行当前线程时,其它线程不会执行。但如果该模式下执行 continue、until、                finish 命令,则其它线程也会继续执行,并且如果某一线程执行过程遇到断点,则 GDB            调试器会将该线程作为当前线程。

可通过如下命令查看线程锁的设置值:

show scheduler-locking

注:set scheduler-locking要处于线程运行环境下才能生效,也就是程序已经运行并且暂停在某个断点处,否则会出现 “Target 'exec' cannot support this command.” 这样的错误;而且设置后的scheduler-locking值在整个进程内有效,不属于某个线程。

 运行至创建线程之后,可通过如下的方式查看所有的线程(主线程和子线程)

 可看到线程ID前面带有星号,表示此线程是当前正在被调试的线程,可通过thread id去切换到不同的线程进行调试。

thread n

 可在循环中设置条件断点,来调试程序:

线程2中,当循环进行到i=6的时候,会触发断点

 此时切换到线程3进行,按照相同的方式进行调试,此外在调试过程中,可指定某个或者所有的线程执行GDB命令

thread apply id GDB_CMD
thread apply all GDB_CMD

tread apply all detach 所有被挂起的线程进行释放,开始运行

-------------------------------------to be continued-----------------------------------------

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值