Linux程序调试-GDB调试器 - STEMHA's Blog

Linux程序调试-GDB调试器

我们需要知道什么?

GDB是什么?有什么作用呢?
GDB的用法有哪些?

GDB的功能

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

  • 启动程序,并给定初始化的环境,可以按照工程师自定义的要求运行程序。
  • 设置断点,让被调试的程序在工程师指定的断点处停住,断点可以是条件表达式。
  • 查询程序执行的各种数据,当程序被停住时,可以检查此时程序中所发生的事,并追踪上文。
  • 动态地改变程序的执行环境

不管是调试Linux内核空间的驱动还是调试用户空间的应用程序,都必须掌握GDB的用法。在调试内核和调试应用程序时使用的GDB命令是完全相同的。

用于调试的代码

以下面代码表示的应用程序为例演示GDB调试器的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int add(int a, int b)
{
return a + b;
}
int main()
{
int sum[10] ={0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int i;
int array1[10] ={48, 56, 77, 33, 33, 11, 226, 544, 78, 90};
int array2[10] ={85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4};

for (i = 0; i < 10; i++)
{
sum[i] = add(array1[i], array2[i]);
}
return 0;
}

使用下面的命令编译上述程序,得到包含调试信息的二进制文件gdb_example。注意:编译程序时需要加上-g,之后才能用gdb进行调。

1
gcc -g gdb_example.c -o gdb_example

ps:

  • -c和-o是gcc编译器的可选参数。
  • -c表示只编译源文du件但不链接,会把.c或.cc的c源程序编译成目标文件,一般是.o文件。
  • -o用于指定输出文件名。不用-o的话,一般会在当前文件夹下生成默认的a.out文件作为可执行程序。
  • -g 把调试信息加到可执行文件中,如果没有-g,将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。

执行下面的命令进入调试状态

1
gdb gdb_example

然后会出现如下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@ubuntu:~/cpptest# gdb gdb_example 
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from gdb_example...done.

调试示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) break add
Breakpoint 1 at 0x674: file gdb_example.c, line 3.
(gdb) run
Starting program: /root/cpptest/gdb_example

Breakpoint 1, add (a=48, b=85) at gdb_example.c:3
3 return a + b;
(gdb) next
4 }
(gdb) next
main () at gdb_example.c:12
12 for (i = 0; i < 10; i++)
(gdb) next
14 sum[i] = add(array1[i], array2[i]);
(gdb) print sum
$1 = {133, 0, 0, 0, 0, 0, 0, 0, 0, 0}

GDB的启动

启动GDB的方法有以下几种:
1、gdb program
program也就是你的执行文件,一般在当前目录下。
2、gdb program core
用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件。
3、gdb program 1234
如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb -help查看。列举一些比较常用的参数:

1
--symbols=SYMFILE

从指定文件中读取符号表

1
--se=FILE

从指定文件中读取符号表信息,并把他用在可执行文件中。

1
--core=COREFILE

调试时core dump的core文件。

1
--directory=DIR

加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。

GDB相关命令

list命令

作用:在GDB中运行list命令可以列出代码
list的具体形式如下:

  1. list<linenum>:用于显示程序第linenum行周围的源程序(我的测试是一共显示10行)。效果如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    (gdb) list 8  //列出文件第九行附近的代码
    3 return a + b;
    4 }
    5 int main()
    6 {
    7 int sum[10] ={0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    8 int i;
    9 int array1[10] ={48, 56, 77, 33, 33, 11, 226, 544, 78, 90};
    10 int array2[10] ={85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4};
    11
    12 for (i = 0; i < 10; i++)
  2. list<function>:显示函数名为function的函数对应的源程序。如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    (gdb) list main
    1 int add(int a, int b)
    2 {
    3 return a + b;
    4 }
    5 int main()
    6 {
    7 int sum[10] ={0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    8 int i;
    9 int array1[10] ={48, 56, 77, 33, 33, 11, 226, 544, 78, 90};
    10 int array2[10] ={85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4};
  3. 说明:
    • list, 显示当前行后面的源程序。
    • list-,显示当前行前面的源程序。

examine命令

examine命令来查看内存地址中的值,在GDB中使用的时候缩写为x。
examine命令格式:

1
x/<number/format/u>  <addr>

参数说明

  • 表示一个内存地址。
  • “x/”后的n、f、u都是可选的参数。
  • number是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容;
  • format表示显示的格式,和c语言中的格式缩写一样:
    • 如果地址所指的是字符串,那么格式可以是s
    • 如果地址是指令地址,那么格式可以是i;
    • d:整数integer
    • s:字符串string
    • c:字符char
    • u:无符号整数 unsigned integer
    • o:八进制格式显示变量
    • x:十六进制格式
    • f: 浮点数格式float
  • u表示从当前地址往后请求的字节数,如果不指定的话,GDB默认的是4字节。
    • u参数可以被一些字符代替:
      • b表示单字节
      • h表示双字节
      • w表示四字节
      • g表示八字节。
    • 当我们指定了字节长度后,GDB会从指定的内存地址开始,读写指定字节,并把其当作一个值取出来
  • number,format,u这3个参数可以一起使用,如下面示例所示。

示例:

1
2
(gdb) x/3uh 0x54320
效果:表示从内存地址0x54320开始以双字节为1个单位(h)、16进制方式(u)显示3个单位(3)的内存。

set命令

set命令用于修改内存。它的命令格式是:

1
set *有类型的指针=value

比如,下列程序,在用gdb运行起来后,通过Ctrl+C停住。

1
2
3
4
5
main()
{
void *p = malloc(16);
while(1);
}

我们可以在运行中用如下命令来修改p指向的内存。

1
2
3
4
5
(gdb) set *(unsigned char *)p='h'    //令p是指向无符号字符的指针变量
(gdb) set *(unsigned char *)(p+1)='e'
(gdb) set *(unsigned char *)(p+2)='l'
(gdb) set *(unsigned char *)(p+3)='l'
(gdb) set *(unsigned char *)(p+4)='o'

看看结果:

1
2
(gdb) x/s p
0x804b008: "hello"

也可以直接使用地址常数:

1
2
3
4
5
6
7
8
9
(gdb) p p  //print命令(缩写为p)
$2 = (void *) 0x804b008
(gdb) set *(unsigned char *)0x804b008='w'
(gdb) set *(unsigned char *)0x804b009='o'
(gdb) set *(unsigned char *)0x804b00a='r'
(gdb) set *(unsigned char *)0x804b00b='l'
(gdb) set *(unsigned char *)0x804b00c='d'
(gdb) x/s 0x804b008
0x804b008: "world"

run命令

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

  1. 程序运行参数
    set args可指定运行时参数,如
    1
    set args 10 20 30 40 50
    show args命令可以查看设置好的运行参数。
  2. 运行环境
    path<dir>可设定程序的运行路径;
    how paths可查看程序的运行路径;
    set environment varname[=value]可设置环境变量,如set env USER=baohua;
    show environment[varname]则可查看环境变量
  3. 工作目录
    cd<dir>相当于shell的cd命令,pwd可显示当前所在的目录。
  4. 程序的输入输出
    info terminal用于显示程序用到的终端的模式;
    在GDB中也可以使用重定向控制程序输出,如run>outfile
    用tty命令可以指定输入输出的终端设备,如tty /dev/ttyS1

break命令

在GDB中用break命令来设置断点,设置断点的方法如下:

  1. break<function>:在进入指定函数时停住。 在C++中可以使用class::function或function(type,type)格式来指定函数名。
    1
    2
    (gdb) break add
    Breakpoint 1 at 0x674: file gdb_example.c, line 3.
  2. break <linenum>:在指定行号停住。
    1
    2
    (gdb) break 12
    Breakpoint 2 at 0x752: file gdb_example.c, line 12.
  3. break +offset/break -offset:在当前行号的前面或后面的offset行停住,offiset为自然数。
  4. break filename:linenum :在源文件filename的linenum行处停住。
  5. break filename:function :在源文件filename的function函数的入口处停住。
  6. break *address :在程序运行的内存地址处停住。
    1
    2
    3
    (gdb) break *0x0000000000000674
    Note: breakpoint 1 also set at pc 0x674.
    Breakpoint 7 at 0x674: file gdb_example.c, line 3.
  7. break:break命令没有参数时,表示在下一条指令处停住。
  8. break … if<condition>
    • …可以是上述的break、break+offset/break–offset中的参数
    • condition表示条件,在条件成立时停住。比如在循环体中,可以设置break if i=100,表示当i为100时停住程序。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000000674 in add at gdb_example.c:3
2 breakpoint keep y 0x0000000000000752 in main at gdb_example.c:12
3 breakpoint keep y 0x0000000000000689 in main at gdb_example.c:5
4 breakpoint keep y 0x0000000000000752 in main at gdb_example.c:12
5 breakpoint keep y 0x000000000000070c in main at gdb_example.c:10
6 breakpoint keep y 0x0000000000000698 in main at gdb_example.c:7
(gdb) info break 5
Num Type Disp Enb Address What
5 breakpoint keep y 0x000000000000070c in main at gdb_example.c:10
(gdb) info breakpoint 5
Num Type Disp Enb Address What
5 breakpoint keep y 0x000000000000070c in main at gdb_example.c:10

单步命令

next命令:用于单步执行,类似于VC++中的step over。 next的单步不会进入函数的内部。
step(缩写为s)命令:与next正好相对,在单步执行一个函数时,进入其内部,类似于VC++中的step into。

1
2
3
4
5
6
7
8
9
10
(gdb) break 14
Breakpoint 1 at 0x75e: file gdb_example.c, line 14.
(gdb) run
Starting program: /root/cpptest/gdb_example

Breakpoint 1, main () at gdb_example.c:14
14 sum[i] = add(array1[i], array2[i]);
(gdb) step
add (a=48, b=85) at gdb_example.c:3
3 return a + b;

单步执行高级用法如下:

  1. step <count> 也就是上面的示例
    单步跟踪,如果有函数调用,则进入该函数(进入函数的前提是,此函数被编译有debug信息)。
    step后面不加count表示一条条地执行,加count表示执行后面的count条指令,然后再停住。
  2. next <count> 也就是上面的示例
    单步跟踪,如果有函数调用,它不会进入该函数。
    同理,next后面不加count表示一条条地执行,加count表示执行后面的count条指令,然后再停住。
  3. set step-mode
  • set step-mode on用于打开step-mode模式,这样,在进行单步跟踪(运行step指令)时,若跨越某没有调试信息的函数,程序的执行则会在该函数的第一条指令处停住,而不会跳过整个函数。这样我们可以查看该函数的机器指令。
  • set step-mod off用于关闭step-mode模式
  1. finish
    运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址、返回值及参数值等信息
  2. until(缩写为u)
    一直在循环体内执行单步而退不出来是一件令人烦恼的事情,用until命令可以运行程序直到退出循环体。
  3. stepi(缩写为si)和nexti(缩写为ni) (选看)
    stepi和nexti用于单步跟踪一条机器指令。比如,一条C程序代码有可能由数条机器指令完成,stepi和nexti可以单步执行机器指令,相反,step和next是C语言级别的命令。
    另外,运行display/i$pc命令后,单步跟踪会在打出程序代码的同时打出机器指令,即汇编代码。

continue命令

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

1
2
3
continue [ignore-count] //ignore-count表示忽略其后多少次断点。
c [ignore-count]
fg [ignore-count]

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) break add
Breakpoint 1 at 0x674: file gdb_example.c, line 3.
(gdb) break 14
Breakpoint 2 at 0x75e: file gdb_example.c, line 14.
(gdb) break 10
Breakpoint 3 at 0x70c: file gdb_example.c, line 10.
(gdb) run
Starting program: /root/cpptest/gdb_example

Breakpoint 3, main () at gdb_example.c:10
10 int array2[10] ={85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4};
(gdb) continue
Continuing.

Breakpoint 2, main () at gdb_example.c:14
14 sum[i] = add(array1[i], array2[i]);

print命令

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

1
2
print <expr>
print /<format> <expr>

< expr >是表达式,也是被调试的程序中的表达式
< format >是输出的格式,比如,如果要把表达式按十六进制的格式输出,那么就是/x。
在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中:

  • @是一个和数组有关的操作符
  • ::指定一个在文件或是函数中的变量
  • {<type>}<addr>表示一个指向内存地址的类型为type的对象

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

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

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

1
p *array@len

print的输出格式如下。

1
2
3
4
5
6
7
8
x:按十六进制格式显示变量。
d:按十进制格式显示变量。
u:按十六进制格式显示无符号整型。
o:按八进制格式显示变量。
t:按二进制格式显示变量。
a:按十六进制格式显示变量。
c:按字符格式显示变量。
f:按浮点数格式显示变量。

可用display命令设置一些自动显示的变量,当程序停住时,或是单步跟踪时,这些变量会自动显示。

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

修改变量值

如果要修改变量,如x的值,可使用如下命令:

1
print x=4

watch命令

watch一般用来观察某个表达式(变量也是一种表达式)的值是否有了变化,如果有变化,马上停止程序运行。
设置观察点有如下几种方法来。

  • watch <expr>:为表达式(变量)expr设置一个观察点。一旦表达式值有变化时,马上停止程序运行。
  • rwatch <expr>:当表达式(变量)expr被读时,停止程序运行。
  • awatch <expr>:当表达式(变量)的值被读或被写时,停止程序运行。
  • info watchpoints:列出当前所设置的所有观察点。

jump命令

GDB可以修改程序的执行顺序,从而让程序随意跳跃。这个功能可以由GDB的jump命令实现。
jump <linespec>来指定下一条语句的运行点。

  • < linespec >可以是文件的行号,可以是file:line格式,也可以是+num这种偏移量格式,表示下一条运行语句从哪里开始。

jump <address>

  • 这里的< address > 是代码行的内存地址。

注意:
jump命令不会改变当前程序栈中的内容,如果使用jump从一个函数跳转到另一个函数,当跳转到的函数运行完返回,进行出栈操作时必然会发生错误,这可能会导致意想不到的结果,因此最好只用jump在同一个函数中进行跳转。

signal命令

singal命令,可以产生一个信号量给被调试的程序,如中断信号Ctrl+C。于是,可以在程序运行的任意位置处设置断点,并在该断点处用GDB产生一个信号量,这种精确地在某处产生信号的方法非常有利于程序的调试。
signal命令的语法是

1
signal <signal>

UNIX的系统信号量通常为1~15,因此的取值也在这个范围内。

return命令

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

1
2
return
return <expression>

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

call命令

call命令用于强制调用某函数:

1
call <expr>

表达式可以是函数,以此达到强制调用函数的目的,它会显示函数的返回值(如果函数返回值不是void)。比如在下列程序执行while(1)的时候:

1
2
3
4
5
main()
{
void *p = malloc(16);
while(1);
}

我们可以强制要求其执行strcpy()和printf():

1
2
3
4
5
(gdb) call strcpy(p, "hello world")
$3 = 134524936
(gdb) call printf("%s\n", p)
hello world
$4 = 12

info命令

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

  1. 查看寄存器的值,使用如下命令:
    1
    2
    3
    info registers (查看除了浮点寄存器以外的寄存器)
    info all-registers (查看所有寄存器,包括浮点寄存器)
    info registers <regname ...> (查看所指定的寄存器)
  2. 查看断点信息,使用如下命令:
    1
    info break
  3. 要列出当前所设置的所有观察点,可使用如下命令:
    1
    info watchpoints
  4. 查看有哪些信号正在被GDB检测,使用如下命令:
    1
    2
    info signals
    info handle
  5. info line命令来查看源代码在内存中的地址。
  • info line后面可以跟行号、函数名、文件名:行号、文件名:函数名等多种形式,例如用下面的命令会打印出所指定的源码在运行时的内存地址:
    1
    info line tst.c:func

disassemble

disassemble命令用于反汇编(将机器语言转换为汇编语言)。可用它来查看当前执行时的源代码的机器码,实际上只是把目前内存中的指令冲刷出来。
下面的示例用于查看函数func的汇编代码:

1
2
3
4
5
6
7
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.

注意事项

  • GDB并不具备记忆功能,每次断开重新开启,之前的断点设置等信息都会丢失的。

总结

参考资料

《Linux设备驱动开发详解:基于最新的Linux4.0内核》
GDB用法详解
gdb —- x命令详解
gdb学习笔记(一)
用GDB调试程序(七) //包含修改变量的值

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×