详解gdb的使用技巧

01 概念

    GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。

    在实际应用中,有两种调试方法:在线调试和离线调试。

    离线调试适用于开发测试环境,可以自由启停进程,设置断点;在线调试一般用于现场问题分析,不能随便启停进程,对于技术要求较高。

02 前提条件

2.1 编译

    若想执行gdb调试,在Makefile文件中需要增加编译调试选项-g,例如:

    gdb dup_file.c –o dum_file_elf –g –lpthread

    说明:-g选项的作用是在可执行文件(ELF)中加入源代码的相关信息,比如ELF中第几条机器指令对应源代码的行数。但不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。

    -g完整格式是-glevel,其中,level中指定了调试信息中包含了调试信息的多少,默认的是2,level=1最少,level=3最多。

2.2 readelf查看段信息

    例如:

    readelf -S helloWorld|grep debug

    注:helloWorld为文件名,如果没有任何debug信息,则不能被调试。

2.3 file查看strip状况

    下面的情况也是不可调试的:

    file helloWorld

    helloWorld: (省略前面内容) stripped

    注:如果最后是stripped,则说明该文件的符号表信息和调试信息已被去除,不能使用gdb调试。但是not stripped的情况并不能说明能够被调试。

03 使用方法

3.1 启动调试

    在开发中可以将源码和可执行文件拷贝到某一目录下,使用gdb启动进程进行调试,也可以不拷贝源码和可执行文件,使用NFS挂载到编译环境执行调试;在现场环境中使用ps获取进程的pid,然后gdb –p pid执行在线调试。

    离线调试:

    gdb 进程名

    gdb –tui 进程名

    在线调试:

    ps –A | grep 进程名

    gdb –p pid/gdb attach pid

    说明:使用-tui参数可以将调试窗口分为两部分:上面是源码,下面是调试信息,使用Ctrl+n/Ctrl+p或者方向键进行翻页。

    带参数调试:

    1、启动的时候带上参数

    gdb --args xxx 参数

    2、启动之后 run 带上参数

    # gdb xxx

    (gdb)run 参数

    3、启动之后 set args 设置参数

    # gdb xxx

    (gdb)set args 参数

    core文件调试

    当程序core dump时,可能会产生core文件,它能够很大程序帮助我们定位问题。但前提是系统没有限制core文件的产生。可以使用命令limit -c查看:

    $ ulimit -c

    如果结果是0,即便程序core dump了也不会有core文件留下。我们需要让core文件能够产生:

    ulimit -c unlimied  #表示不限制core文件大小

ulimit -c 10  #设置最大大小,单位为块,一块默认为512字节

    上面两种方式可选其一。第一种无限制,第二种指定最大产生的大小。

    针对生成core文件进行调试,可以采用在线加载和离线加载的方式,如下:

  gdb 可执行文件 core文件

3.2 SET命令

详解gdb的使用技巧_第1张图片

    注:有时候使用p打印调试信息不完整或者不便于阅读,可以使用set print elelent 0和setprint pretty on设置。

3.3 handle命令

    handle命令

    handle SIGUSR1 nostop noprint

    handle SIGUSR2 nostop noprint

    handle SIGPIPE nostop noprint

    handle SIGALARM nostop

    handle SIGHUP nostop

    handle SIGTERM nostop noprint

    注:设置GDB调试时对信号的相关动作。

3.4 设置断点

    打断点还是比较有技巧的,虽然有很多打断点的方法,但是实际调试中一般就使用以下几种:

   函数打断点:b 函数名

   某一行打断点:b 源文件:行号

    条件断点:

    break 断点 if 条件

    continue 断点编号(执行一次表示设定,再次执行表示取消)

    continue 断点编号 条件

    注:条件断点非常有用,实际调试中往往需要调试特定场景下函数调用关系,此时就需要设置断点触发的条件。

    查看断点:info breakpoint/info break/info b

    删除断点:delete 断点号/delete(删除所有断点)

    禁用/开启断点:disable/enable breakpoint

    ignore:

    断点条件的一个特殊用法是,程序只有在到达断点一定次数之后才会停止,此时可以使用指令:

    ignore 断点编号 次数

    ignore 2 10触发断点10次后才会停止,每次触发断点count自动减1

    说明:打完断点是不是执行continue就可以等待着运行到断点了呢?不一定,有时候断点处代码的执行需要外部出发,比如web发送特定消息后才可以触发执行,如果一直等待没有消息出发永远执行不到断点处,此时就需要结合自己的业务逻辑,手动设置出发条件。

3.5 执行程序

    执行程序的方法有两种:一种是从main函数开始执行逐步分析,一种是执行到断点处。

    重新运行:r/run

    继续执行:c/continue

    单步执行:n/next/next N(执行N次next)

    单步进入:step(遇到函数进入函数内部,退出函数时使用finish)

    结束函数:finish

    强制返回:return(忽略当前未执行的部分,强制返回)

3.6 显示堆栈

    (gbd) backstrace/bt

    有时候跳转的次数太多,不知道具体调用的层级关系了,可以使用bt查看堆栈,该命令会产生一张列表,包含着运行过程和相关的参数。

3.7 变量操作

  设置变量:set 变量=表达式

    在调试的时候,有时候需要设置一些假数据查看对应输出,比如根据布尔值查看流程执行情况,此时就需要在执行到指定位置时手动设置一下数据的取值。

   监控变量:

    watch 变量 (数值改变时暂停运行)

    awatch <表达式> (被访问或改变时暂停运行)

    rwatch <表达式>  (被访问时暂停运行)

    有时候我们需要观察一个变量的变化过程,比如一个全局变量如何初始化,如何调用的,这就需要使用watch监控变量。

    变量类型:

    ptype var 变量类型

    whatis var 显示一个变量var的类型

    打印变量/表达式:

    打印变量:p 变量      

    打印字符/表达式:p “%s”,字符/表达式

    格式化输出:p/格式控制符 打印内容

    说明:

    gdb可支持的变量显示格式有:

    x:按16进制格式显示变量

    d:按10进制格式显示变量

    u:按16进制格式显示无符号整型

    o:按8进制格式显示变量

    t:按2进制格式显示变量

    c:按字符格式显示变量

    f:按浮点数格式显示变量

    也可以使用x(Examination)来打印需要显示的字符信息,格式如下:

    x/格式 地址

    格式(可选)一般是NFU:

    1、N表示重复次数(表示显示内存的长度,也就是说从当前向后显示几个地址的内容)

    2、F表示显示格式

    3、U表示单位(b:字节,h:半字[2字节],w:字[4字节,默认],g:双字[8字节])。表示多少个字节作为一个值取出来,如果不指定的话,GDB默认是1个byte,当我们指定了字节长度后,GDB会从指定内存的地址开始,读取指定字节,并把其作为一个值取出来。

    参数u可使用下面字符代替:

    b:表示单字节

    h:表示双字节

    w:表示四字节

    g:表示八字节

3.8 调试函数

    disassemble    

    可以使用反汇编的指令disassemble去探究究竟在函数中发生了哪些操作,具体如下:

    1、disassemble

    2、disassemble 程序计数器

    3、disassemble 开始地址 结束地址

    格式1表示反汇编当前整个函数,格式2表示反汇编计数器所在函数的整个函数,格式3表示反汇编从开始地址到结束地址的部分。

    call

    强制调用函数:call 表达式

3.9 退出调试

    q/quit

    在执行到断点后,采用q/quit指令退出。

04 多进程调试

4.1 配置

    detach-on-fork

    该属性决定了gdb是同时调试父子进程,还是在fork了子进程之后,将子进程分离出去。

    on:子进程(或者父进程,取决于gdb在初始时,要调试的进程,也就是follow-fork-mode的值)

    off:同时调试父子进程,一个进程处于被调试的状态,而另一个则被gdb挂起

设置:set detach-on-fork on/off

    follow-fork-mode

    该属性决定了gdb在进程调用fork之后的行为。

    set follow-fork-mode parent:默认情况下,在调用fork之后,gdb选择跟随(也就是调试)父进程,而子进程则在处于运行的状态(此时父进程处于阻塞的状态)。

    set follow-fork-mode child:fork之后gdb选择调试子进程,而父进程处于运行的状态。

4.2 查看进程

    查看当前调试的进程:info inferiors

05 多线程调试

5.1查看线程

    查看线程:info threads

    注:输出信息前面有“*”表示调试的当前线程(一般thread切换线程后查看)。

    有的程序会在运行过程中主线程创建多个子线程,所以前后执行info threads显示的线程数是会动态变化的。

5.2 查看线程堆栈

    查看所有线程堆栈:thread apply all bt

    查看指定线程堆栈:thread apply thread1 thread2... bt

5.3 切换线程

    切换线程:thread N

    注:通过打印counter,可以看到多个线程都是在运行的,如果想要让其他线程处于停止状态,只有当前调试的线程执行,可以采用set scheduler-locking on。

5.4 阻塞线程

    阻塞其他线程,仅调试当前线程工作:

 set scheduler-locking [on|off|step]

    运行指定线程并允许其他线程并行执行:

    thread apply N command

06 总结

    对于C语言开发,必须熟练使用gdb进行调试,这可以帮助我们快速定位问题并解决问题,在开发中可以帮助我们及时找到测试出现的问题,在现场问题中如果日志打印不是很充分,日志信息量不够的情况下,gdb调试显得非常重要。

    在实际应用中,我们通常是利用gdb分析core文件,这就需要结合寄存器,汇编,内存相关知识综合分析,后面会详细介绍相关分析技巧。

 

你可能感兴趣的:(操作系统,gcc/gdb编译调试)