C语言之gdb

本章我们介绍一种非常强大的调试工具gdb,它可以完全操控程序的运行,可以对程序进行断点,也可以查看程序中所有的内部状态,比如各个变量的值、传给函数的参数、当前执行的语句位置等。

先来看下面的程序:

#include <stdio.h>

int add_range(int low, int high)
{
    int i = low, sum = 0;
    for (; i <= high; i++)
    {
        sum += i;
    }
    return sum;
}

int main(int argc, char const *argv[])
{
    int a = 0, b = 0;
    a = add_range(1, 5);
    b = add_range(1, 8);
    printf("a = %d\nb = %d\n", a, b);
    return 0;
}

  

我们都知道,编译一个文件,要在命令行上输入gcc main.c,main.c是我们的c文件,如果要用gdb进行调试的话,在编译的时候还要加上-g选项

[root@localhost ~]# gcc -g main.c -o main
[root@localhost ~]# gdb main
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-110.el7
Copyright (C) 2013 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-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/lf/linux_c/main...done.

  

-g选项的作用是在目标文件中加入源代码的信息,比如目标文件中第几条机器指令对应源代码的第几行,并不是把整个源文件都嵌入到目标文件中,所以调试时目标文件必须保证gdb也能找到源文件。gdb提供了一个类似shell的命令环境,上面的gdb就是提示符,在这个提示符下输入help可以查看命令的类别:

(gdb) help
List of classes of commands:

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
…………

Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.
(gdb) help files

    

上面的例子我们省略部分输出,可以根据显示的类别可以进一步查看某一类别中有哪些命令,例如查看files类别下有哪些命令:

(gdb) help files
Specifying and examining files.

List of commands:

add-symbol-file -- Load symbols from FILE
add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file
cd -- Set working directory to DIR for debugger and program being debugged
core-file -- Use FILE as core dump for examining memory and registers
directory -- Add directory DIR to beginning of search path for source files
edit -- Edit specified file or function
…………

Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

  

list命令可以列出源代码:

(gdb) list
1	#include <stdio.h>
2	
3	int add_range(int low, int high)
4	{
5	    int i = low, sum = 0;
6	    for (; i <= high; i++)
7	    {
8	        sum += i;
9	    }
10	    return sum;

  

一次只列10行,如果要从第11行开始读,可以接着输入list,或者直接回车,list可以简写成l,一样有往下显示源码的效果

(gdb) (直接回车)
11	}
12	
13	int main(int argc, char const *argv[])
14	{
15	    int a = 0, b = 0;
16	    a = add_range(1, 5);
17	    b = add_range(1, 8);
18	    printf("a = %d\nb = %d\n", a, b);
19	    return 0;
20	}

  

 用l命令列出源代码的某个函数名

(gdb) l add_range
1	#include <stdio.h>
2	
3	int add_range(int low, int high)
4	{
5	    int i = low, sum = 0;
6	    for (; i <= high; i++)
7	    {
8	        sum += i;
9	    }
10	    return sum;
(gdb) 

  

退出gdb环境

(gdb) quit
[root@localhost linux_c]# 

  

现在,将源代码改名或迁移至别处,再用gdb调试目标文件,就列不出源代码了

[root@localhost linux_c]# mv main.c mian.c
[root@localhost linux_c]# gdb main 
…………
(gdb) l
1	main.c: No such file or directory.

  

由此可见,gcc -g选项并不是把源代码嵌入到目标文件中,在调试目标文件时也需要源文件,现在,将源文件名恢复,继续我们的调试

[root@localhost linux_c]# gdb main
…………
(gdb) start
Temporary breakpoint 1 at 0x40056c: file main.c, line 15.
Starting program: /home/lf/linux_c/main 

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe5c8) at main.c:15
15	    int a = 0, b = 0;

  

这表示停在main函数中变量定义之后的第一条语句处等待我们发送命令,gdb列出这条语句表示它还没执行,但马上要执行,我们可以用next命令(简写为n)控制这些语句一条一条执行:

(gdb) n
16	    a = add_range(1, 5);
(gdb) 
17	    b = add_range(1, 8);
(gdb) 
18	    printf("a = %d\nb = %d\n", a, b);
(gdb) 
a = 15
b = 36
19	    return 0;

  

用n命令依次执行两行赋值语句和一行打印语句,在执行打印语句时结果立即显示出来了,然后停在return处等待我们发送命令

现在,再来我们用step命令进入add_range函数调试

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x40056c: file main.c, line 15.
Starting program: /home/lf/linux_c/main 

Temporary breakpoint 2, main (argc=1, argv=0x7fffffffe5c8) at main.c:15
15	    int a = 0, b = 0;
(gdb) s
16	    a = add_range(1, 5);
(gdb) s
add_range (low=1, high=5) at main.c:5
5	    int i = low, sum = 0;

  

这次停在了add_range函数中变量定义之后的第一条语句处,在函数中有几种查看状态的办法,backtrack命令(简称为bt)可以查看函数调用的栈帧:

(gdb) bt
#0  add_range (low=1, high=5) at main.c:5
#1  0x0000000000400589 in main (argc=1, argv=0x7fffffffe5c8) at main.c:16

  

可见当前的add_range函数是被main函数调用的,main传进来的参数low=1,high=5。main函数的栈帧编号为1,add_range的栈帧编号为0。现在可以用info命令(简写为i)查看add_range局部变量的值:

(gdb) s
6	    for (; i <= high; i++)
(gdb) i locals
i = 1
sum = 0

  

如果想看main函数当前局部变量的值也可以做到,先用frame命令(简写为f)选择1号栈帧然后查看局部变量:

(gdb) f 1
#1  0x0000000000400589 in main (argc=1, argv=0x7fffffffe5c8) at main.c:16
16	    a = add_range(1, 5);
(gdb) i locals
a = 0
b = 0

  

用s或n往下走几步,再用print命令(简写为p)打印出sum和i的值

(gdb) s
6	    for (; i <= high; i++)
(gdb) s
8	        sum += i;
(gdb) s
6	    for (; i <= high; i++)
(gdb) p sum
$1 = 3
(gdb) p i
$2 = 2

  

第一次循环,i是1,第二次循环,i是2,加起来sum为3,而我们打印了i的值,这时候i也为2,这里的$1表示gdb保存着这些中间结果,$后面的编号是自动增长的,命令中用$1、$2、$3等编号代替相应的值。

用finish命令让程序一直运行到从当前函数返回为止:

(gdb) finish
Run till exit from #0  add_range (low=1, high=5) at main.c:6
0x0000000000400589 in main (argc=1, argv=0x7fffffffe5c8) at main.c:16
16	    a = add_range(1, 5);
Value returned is $3 = 15

  

这里再介绍一个set var命令,这个命令可以在程序运行的时候,修改变量的值

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 3 at 0x40056c: file main.c, line 15.
Starting program: /home/lf/linux_c/main 

Temporary breakpoint 3, main (argc=1, argv=0x7fffffffe5c8) at main.c:15
15	    int a = 0, b = 0;
(gdb) s
16	    a = add_range(1, 5);
(gdb) s
add_range (low=1, high=5) at main.c:5
5	    int i = low, sum = 0;
(gdb) finish
Run till exit from #0  add_range (low=1, high=5) at main.c:5
0x0000000000400589 in main (argc=1, argv=0x7fffffffe5c8) at main.c:16
16	    a = add_range(1, 5);
Value returned is $4 = 15
(gdb) s
17	    b = add_range(1, 8);
(gdb) s
add_range (low=1, high=8) at main.c:5
5	    int i = low, sum = 0;
(gdb) set var high=6
(gdb) finish
Run till exit from #0  add_range (low=1, high=6) at main.c:5
0x000000000040059b in main (argc=1, argv=0x7fffffffe5c8) at main.c:17
17	    b = add_range(1, 8);
Value returned is $5 = 21

  

我们重新执行这个程序,在第11行,我们一旦进行add_range这个函数,就在第14行执行finish,执行这个函数直到返回到main函数,然后再第18行我们看到,5的累加是15,再接着我们又进入add_range函数,这次low和high分别是1和8,从1加到8应该是36,但是我们进入add_range函数后,在第24行用set var函数设置high为6,于是main函数中b的值原先应该从1加到8变成从1加到6,然后我们调用finish直接返回到main函数,最终我们可以看到,b的返回值是21,我们知道5的累加是15,那么6的来累加就是6加上5的累加,所以就是21,并不是一开始我们设定好的从1加到8返回值是36

总结一下之前的命令:

命令 描述
backtrace(bt) 查看各级函数调用及参数
finish 执行到当前函数返回,然后停下来等待命令
frame(f) 帧编号 选择栈帧
info(i) locals 查看当前栈帧局部变量的值得
list(l) 列出源代码,接着上次的位置往下列,每次列10行
list 函数名 列出某个函数的源代码
list 行号 列出从第几行开始的源代码
next(n) 执行下一行语句
print(p) 打印表达式的值,通过表达式可以修改变量的值或者调用函数
set var 修改变量的值
start 开始执行程序,停在main函数第一行语句前面等待命令
step(s) 执行下一行语句,如果有函数调用则进入到函数中

现在,让我们再来看一段程序:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int sum = 0, i = 0;
    char input[5];
    while (1)
    {
        scanf("%s", input);
        for (i = 0; input[i] != '\0'; i++)
        {
            sum = sum * 10 + input[i] - '0';
        }
        printf("input = %d\n", sum);
    }
    return 0;
}

  

这个程序是从键盘读入一串数字到字符数组input中,然后转换成整型存储到sum变量中。scanf("%s", input);会将用户输入的字符存入到input字符数组中,并在末尾自动加上'\0'。接下来的循环从左到右扫描各个字符并把字符累加到结果中,例如输入2345,则循环累加的过程是(((0*10+2)*10+3)*10+4)*10+5=2345。注意,字符型的'2'要减去'0'的ASCII码才能转换成整数型的2,'0'的ASCII码是48,而'\0'的ASCII码是0,二者是不同的。下面编译运行看看程序有什么问题:

[root@localhost linux_c]# gcc -g main.c -o main
[root@localhost linux_c]# ./main 
123
input = 123
456
input = 123456
(按CTRL+C退出)

  

这里我们看到,在第一次输入123,程序正确打印出123,但是在第二次我们输入456的时候,程序却打出123456,而不是我们理想中的456,让我们再次用gdb调试程序看看是哪里出错了

[root@localhost linux_c]# gdb main
…………
Starting program: /home/lf/linux_c/main 

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe5c8) at main.c:5
5	    int sum = 0, i = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7_3.5.x86_64

  

由于sum才是负责存储字符数组转整型的变量,所以,我们用display命令跟踪一下sum的值,使得每次程序停下来时都打印一下sum的值

(gdb) display sum
1: sum = 0
(gdb) n
9	        scanf("%s", input);
1: sum = 0
(gdb) 
123
10	        for (i = 0; input[i] != '\0'; i++)
1: sum = 0

  

用undisplay可以取消对先前设置的那些变量的跟踪,然后我们在用break命令(简写为b)在第九行设置一个断点:

(gdb) l
5	    int sum = 0, i = 0;
6	    char input[5];
7	    while (1)
8	    {
9	        scanf("%s", input);
10	        for (i = 0; input[i] != '\0'; i++)
11	        {
12	            sum = sum * 10 + input[i] - '0';
13	        }
14	        printf("input = %d\n", sum);
(gdb) b 9
Breakpoint 2 at 0x4005ba: file main.c, line 9.

  

break命令的参数也可以是函数名,表示在某一函数开头设断点。现在用continue命令(简写为c)连续运行而非单步运行,程序到达断点会自动停下来,这样就可以停下再一次循环的开头了:

(gdb) c
Continuing.
input = 123

Breakpoint 2, main (argc=1, argv=0x7fffffffe5c8) at main.c:9
9	        scanf("%s", input);
1: sum = 123

  

然后输入新的字符串准备转换:

(gdb) n
456
10	        for (i = 0; input[i] != '\0'; i++)
1: sum = 123

  

问题暴露出来了,新的转换应该再次从sum为0开始,而sum仍然保留着上一次的值123,原因在于新的循环没有把sum归零。

我们可以设置多个断点,在用info(简写为i) 加上breakpoints查看之前设置过的断点:

(gdb) b 12
Breakpoint 3 at 0x4005d9: file main.c, line 12.
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x00000000004005ba in main at main.c:9
	breakpoint already hit 1 time
3       breakpoint     keep y   0x00000000004005d9 in main at main.c:12

  

每一个断点都有一个编号,我们可以根据编号来删除断点:

(gdb) delete breakpoints 2
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00000000004005d9 in main at main.c:12

  

如果有一个断点不想删掉而是想禁用,,等到下次有需求的时候再启用,而不必从源码找到要设置断点的行数,也可以用disable和enable命令:

(gdb) disable breakpoints 3
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
3       breakpoint     keep n   0x00000000004005d9 in main at main.c:12
(gdb) enable breakpoints 3
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00000000004005d9 in main at main.c:12
(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) i breakpoints
No breakpoints or watchpoints.

  

break命令非常的灵活,还可以设置满足某个条件的时候才激活,现在我们重新打印一下源代码

(gdb) start
Temporary breakpoint 1 at 0x4005ac: file main.c, line 5.
Starting program: /home/lf/linux_c/main 

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe5c8) at main.c:5
5	    int sum = 0, i = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7_3.5.x86_64
(gdb) l
1	#include <stdio.h>
2	
3	int main(int argc, char const *argv[])
4	{
5	    int sum = 0, i = 0;
6	    char input[5];
7	    while (1)
8	    {
9	        scanf("%s", input);
10	        for (i = 0; input[i] != '\0'; i++)

  

然后决定在第九行scanf处打断点,当sum不为0的时候,断点激活,我们可以看到,第一次并没有停留在scanf处,因为那个时候sum为0,之后每次执行完字符数组转整型后都会停留在scanf处,因为sum不为0

(gdb) b 9 if sum != 0
Breakpoint 2 at 0x4005ba: file main.c, line 9.
(gdb) c
Continuing.
123
input = 123

Breakpoint 2, main (argc=1, argv=0x7fffffffe5c8) at main.c:9
9	        scanf("%s", input);
(gdb) 
Continuing.
456
input = 123456

Breakpoint 2, main (argc=1, argv=0x7fffffffe5c8) at main.c:9
9	        scanf("%s", input);
(gdb) 
Continuing.
789
input = 123456789

Breakpoint 2, main (argc=1, argv=0x7fffffffe5c8) at main.c:9
9	        scanf("%s", input);

  

然后我们删除断点,用run命令(简写为r)重新运行程序:

(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/lf/linux_c/main 
123
input = 123
456
input = 123456
789
input = 123456789

  

可以看到,一旦失去断点,程序执行完字符数组转整型的程序后就会停留在scanf,而不会返回到gdb界面

这里再总结一下现在学到的命令:

命令 描述
break(b) 行号 在某一行设置断点
break 函数名 在某个函数开头设置断点 
break if  设置条件断点
continue (c)   从当前位置开始连续而并非单步执行程序
delete breakpoints  删除断点 
display 变量名  跟踪查看一个变量,每次停下来都显示它的值 
disable breakpoints  禁用断点 
enable breakpoints  启用断点 
info(i) breakpoints  查看当前设置了哪些断点 
run(r)  从头开始连续而非单步执行程序
undisplay  取消对先前设置的变量的跟踪 

  

现在,让我们修改原来将字符数组转成整型的程序,每次输入新的字符时将sum置为0:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int sum = 0, i = 0;
    char input[5];
    while (1)
    {
        scanf("%s", input);
        sum = 0;
        for (i = 0; input[i] != '\0'; i++)
        {
            sum = sum * 10 + input[i] - '0';
        }
        printf("input = %d\n", sum);
    }
    return 0;
}

  

我们知道,input是一个数组,现在让我们尝试着打印input的内容,我们可以用print(p)命令打印,examine命令(简写为x)来查看内存地址中的值,格式: x /nfu

n 表示要显示的内存单元的个数

f 表示显示方式, 可取如下值:

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

u 表示一个地址单元的长度:

  • b表示单字节
  • h表示双字节
  • w表示四字节
  • g表示八字节

  

  

  

猜你喜欢

转载自www.cnblogs.com/beiluowuzheng/p/9252240.html