GDB调试总结

本文详细介绍GDB调试工具的使用方法,包括启动程序、设置断点、单步执行、查看内存和寄存器等内容,适用于嵌入式软件开发工程师提升问题定位能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 GDB简介

GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具,GDB主要可帮助工程师完成下面4个方面的功能:

  • 启动程序,可以按照工程师自定义的要求随心所欲的运行程序。
  • 让被调试的程序在工程师指定的断点处停住,断点可以是条件表达式。
  • 当程序奔溃时,可以检查此时程序中所发生的事,并追索上文。
  • 动态地改变程序的执行环境。

作为一个嵌入式开发工程师没有VS这等可视化的强大IDE,而GDB其功能更加强大。掌握GDB调试对我们嵌入式软件开发工程师而言,能有效提升我们定位问题和解决问题的能力。

 

二:GDB调试实例

首先以如下代码文件hellogdb.c开始介绍

#include <stdio.h>

int g_var = 0;

static int _add(int a, int b) {
    printf("_add callad, a:%d, b:%d\n", a, b);
    return a+b;
}

int main(void) {
    int n = 1;
    
    printf("one n=%d, g_var=%d\n", n, g_var);
    ++n;
    n--;
    
    g_var += 20;
    g_var -= 10;
    n = _add(1, g_var);
    printf("two n=%d, g_var=%d\n", n, g_var);
    
    return 0;
}

图片中的第一个指令为使用gcc对hellogdb.c文件进行编译。-g 表示添需要使用gdb调试。

第二个指令gdb hellogdb.out表示开始调试。

第三行开始的信息为GDB信息的打印。

List指令

在gdb中运行list命令(缩写l)可以列出代码,list的具体形式包括:

list <linenum>   显示linenum行附近的代码

list <function>  显示function函数附近的代码

通常共显示10行内容。

run指令

在gdb中,运行程序使用run命令。在程序运行前,我们可以设置如下4方面的工作环境:

程序运行参数

  set args 可指定运行时参数,如:set args 10 20 30 40 50;

  show args 命令可以查看设置好的运行参数。

运行环境

  path <dir> 可设定程序的运行路径;

  how paths可查看程序的运行路径;

  set environment varname [=value]用于设置环境变量,如set env USER=baohua;

  show environment [varname]则用于查看环境变量。

工作目录

  cd <dir> 相当于shell的cd命令;

  pwd 显示当前所在的目录。

程序的输入输出

  info terminal 用于显示程序用到的终端的模式;

  gdb中也可以使用重定向控制程序输出,如run > outfile;

  tty命令可以指定输入输出的终端设备,如:tty /dev/ttyS1。

break命令

在gdb中用break命令来设置断点,设置断点的方法包括:

break <function>

  在进入指定函数时停住,C++中可以使用class::function或function(type, type)格式来指定函数名。

break <linenum>

  在指定行号停住。

break +offset / break -offset

  在当前行号的前面或后面的offset行停住,offiset为自然数。

break filename:linenum

  在源文件filename的linenum行处停住。

break filename:function

  在源文件filename的function函数的入口处停住。

break *address

  在程序运行的内存地址处停住。

break

  break命令没有参数时,表示在下一条指令处停住。

break ... if <condition>

  “...”可以是上述的break <linenum>、break +offset / break –offset中的参数,condition表示条件,在条件成立时停住。比如在循环体中,可以设置break if i=100,表示当i为100时停住程序。

info

  查看断点时,可使用info命令,如info breakpoints [n]、info break [n](n表示断点号)。

清除断点

       有如下两种方式可用于清除断点:

1)delete

       delete [breakpoints num] [range...]

       可删除单个断点,也可删除一个断点的集合,这个集合用连续的断点号来描述。如 delete 3、delete 1-5

2) clear

        Clear breakpoint at specified location    删除所选定的环境中所有的断点

        包含函数、文件中对应函数、指定行号的断点。

        删除断点是基于行的,不是把所有的断点都删除。

 

单步命令

在调试过程中,next命令用于单步执行,类似VC++中的step over。next的单步不会进入函数的内部,与next对应的step(缩写s)命令则在单步执行一个函数时,会进入其内部,类似VC++中的step into。

单步执行的更复杂用法包括:

step <count>

  单步跟踪,如果有函数调用,则进入该函数(进入函数的前提是,此函数被编译有debug信息)。step后面不加count表示一条条地执行,加表示执行后面的count条指令,然后再停住。

next <count>

  单步跟踪,如果有函数调用,它不会进入该函数。同样地,next后面不加count表示一条条地执行,加表示执行后面的count条指令,然后再停住。

set step-mode

  set step-mode on用于打开step-mode模式,这样,在进行单步跟踪时,程序不会因为没有debug信息而不停住,这个参数的设置可便于查看机器码。set step-mod off用于关闭step-mode模式。

finish

  运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。

until (缩写u)

  一直在循环体内执行单步,退不出来是一件令人烦恼的事情,until命令可以运行程序直到退出循环体。

stepi(缩写si)和nexti(缩写ni)

  stepi和nexti用于单步跟踪一条机器指令,一条程序代码有可能由数条机器指令完成,stepi和nexti可以单步执行机器指令。 另外,运行“display/i $pc”命令后,单步跟踪会在打出程序代码的同时打出机器指令,即汇编代码。

continue命令

当程序被停住后,可以使用continue命令(缩写c,fg命令同continue命令)恢复程序的运行直到程序结束,或到达下一个断点,命令格式为: 

continue [ignore-count]
c [ignore-count]
fg [ignore-count]

ignore-count表示忽略其后多少次断点。 

print命令

在调试程序时,当程序被停住时,可以使用print命令(缩写为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式是: 

 print <expr>
 print /<f> <expr>

<expr>是表达式,是被调试的程序中的表达式,

<f>是输出的格式,比如,如果要把表达式按16进制的格式输出,那么就是/x。在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中,“@”是一个和数组有关的操作符,“::”指定一个在文件或是函数中的变量,“{<type>} <addr>”表示一个指向内存地址<addr>的类型为type的一个对象。

当需要查看一段连续内存空间的值的时间,可以使用GDB的“@”操作符,“@”的左边是第一个内存地址,“@”的右边则是想查看内存的长度。例如如下动态申请的内存:

int *array = (int *) malloc (len * sizeof (int));

在GDB调试过程中这样显示出这个动态数组的值:

p *array@len

print的输出格式包括:

  • x 按十六进制格式显示变量。
  • d 按十进制格式显示变量。
  • u 按十六进制格式显示无符号整型。
  • o 按八进制格式显示变量。
  • t 按二进制格式显示变量。
  • a 按十六进制格式显示变量。
  • c 按字符格式显示变量。
  • f 按浮点数格式显示变量。

我们可用display命令设置一些自动显示的变量,当程序停住时,或是单步跟踪时,这些变量会自动显示。 如果要修改变量,如x的值,可使用如下命令:

print x=4

当用GDB的print查看程序运行时的数据时,每一个print都会被GDB记录下来。GDB会以$1,$2,$3 …这样的方式为每一个print命令编号。我们可以使用这个编号访问以前的表达式,如$1。

watch命令

watch一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。

我们有下面的几种方法来设置观察点:

  watch <expr>:为表达式(变量)expr设置一个观察点。一旦表达式值有变化时,马上停住程序。

  rwatch <expr>:当表达式(变量)expr被读时,停住程序。

  awatch <expr>:当表达式(变量)的值被读或被写时,停住程序。

  info watchpoints:列出当前所设置了的所有观察点。

examine命令

我们可以使用examine命令(缩写为x)来查看内存地址中的值。examine命令的语法如下所示:

x/<n/f/u> <addr>

<addr>表示一个内存地址。“x/”后的n、f、u都是可选的参数,n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容;f 表示显示的格式,如果地址所指的是字符串,那么格式可以是s,如果地址是指令地址,那么格式可以是i;u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4字节。u参数可以被一些字符代替:b表示单字节,h表示双字节,w表示四字节,g表示八 字节。当我们指定了字节长度后,GDB会从指定的内存地址开始,读写指定字节,并把其当作一个值取出来。n、f、u这3个参数可以一起使用,例如命令“x/3uh 0x54320”表示从内存地址0x54320开始以双字节为1个单位(h)、16进制方式(u)显示3个单位(3)的内存。 == 

譬如下面的例子:

main()
{
        char *c = "hello world";
        printf("%s\n", c);
}

我们在

char *c = "hello world";

下一行设置断点后:

复制代码

(gdb) l
1    main()
2    {
3        char *c = "hello world";
4        printf("%s\n", c);
5    }
(gdb) b 4
Breakpoint 1 at 0x100000f17: file main.c, line 4.
(gdb) r
Starting program: /Users/songbarry/main
Reading symbols for shared libraries +. done

Breakpoint 1, main () at main.c:4
4        printf("%s\n", c);

复制代码

可以通过多种方式看C指向的字符串:

方法1:

(gdb) p c
$1 = 0x100000f2e "hello world"

方法2:

(gdb) x/s 0x100000f2e
0x100000f2e:     "hello world"

方法3:

(gdb) p (char *)0x100000f2e
$3 = 0x100000f2e "hello world"

将第一个字符改为大写:

(gdb) p *(char *)0x100000f2e='H'
$4 = 72 'H'

再看看C:

(gdb) p c
$5 = 0x100000f2e "Hello world"

set命令

修改寄存器:

(gdb) set $v0 = 0x004000000
(gdb) set $epc = 0xbfc00000 

修改内存:

(gdb) set {unsigned int}0x8048a51=0x0

譬如对于examine节的例子:

(gdb) set {unsigned int}0x100000f2e=0x0       
(gdb) x/10cb 0x100000f2e
0x100000f2e:    0 '\0'    0 '\0'    0 '\0'    0 '\0'    111 'o'    32 ' '    119 'w'    111 'o'
0x100000f36:    114 'r'    108 'l'
(gdb) p c
$10 = 0x100000f2e ""

jump命令

一般来说,被调试程序会按照程序代码的运行顺序依次执行,但是GDB也提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,从而让程序随意跳跃。这个功能可以由GDB的jump命令:jump <linespec> 来指定下一条语句的运行点。<linespec>可以是文件的行号,可以是file:line格式,也可以是+num这种偏移量格式,表示下一条运行语句从哪里开始。jump <address> 这里的<address>是代码行的内存地址。 注意,jump命令不会改变当前的程序栈中的内容,所以,如果使用jump从一个函数跳转到另一个函数,当跳转到的函数运行完返回,进行出栈操作时必然会发生错误,这可能导致意想不到的结果,所以最好只用jump在同一个函数中进行跳转。

signal命令

使用singal命令,可以产生一个信号量给被调试的程序,如中断信号“Ctrl+C”。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号的方法非常有利于程序的调试。 signal命令的语法是:signal <signal>,UNIX的系统信号量通常从1到15,所以<signal>取值也在这个范围。

return命令

如果在函数中设置了调试断点,在断点后还有语句没有执行完,这时候我们可以使用return命令强制函数忽略还没有执行的语句并返回。 

return
return <expression>

上述return命令用于取消当前函数的执行,并立即返回,如果指定了<expression>,那么该表达式的值会被作为函数的返回值。

call命令

call命令用于强制调用某函数: call <expr> 表达式中可以一是函数,以此达到强制调用函数的目的,它会显示函数的返回值(如果函数返回值不是void)。 其实,前面介绍的print命令也可以完成强制调用函数的功能。

info命令

info命令可以在调试时用来查看寄存器、断点、观察点和信号等信息。

要查看寄存器的值,可以使用如下命令:

  info registers (查看除了浮点寄存器以外的寄存器)

  info all-registers (查看所有寄存器,包括浮点寄存器)

  info registers <regname ...> (查看所指定的寄存器)

  info break 查看断点信息

  info watchpoints 列出当前所设置的所有观察点,

  info signals info handle 查看有哪些信号正在被GDB检测,

  info line命令来查看源代码在内存中的地址。

  info threads可以看多线程。

  info line后面可以跟行号、函数名、文件名:行号、文件名:函数名等多种形式,例如下面的命令会打印出所指定的源码在运行时的内存地址:

info line tst.c:func

set scheduler-locking off|on|step

off 不锁定任何线程,也就是所有线程都执行,这是默认值。 
on 只有当前被调试程序会执行。 
step 在单步的时候,除了next过一个函数的情况以外,只有当前线程会执行。

与多线程调试相关的命令还包括:

thread ID 
  切换当前调试的线程为指定ID的线程。  
break thread_test.c:123 thread all
  在所有线程中相应的行上设置断点
thread apply ID1 ID2 command 
  让一个或者多个线程执行GDB命令command。 
thread apply all command 
  让所有被调试线程执行GDB命令command。

disassemble

disassemble命令用于反汇编,它可被用来查看当前执行时的源代码的机器码,其实际上只是把目前内存中的指令dump出来。下面的示例用于查看函数func的汇编代码:

复制代码

(gdb) disassemble func
Dump of assembler code for function func:
0x8048450 <func>:       push   %ebp
0x8048451 <func+1>:     mov    %esp,%ebp
0x8048453 <func+3>:     sub    $0x18,%esp
0x8048456 <func+6>:     movl   $0x0,0xfffffffc(%ebp)
...
End of assembler dump.

复制代码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值