1. 单步执行和跟踪函数调用
看下面的程序:
例 10.1. 函数调试实例
- #include <stdio.h>
- int add_range(int low, int high)
- {
- int i, sum;
- for (i = low; i <= high; i++)
- sum = sum + i;
- return sum;
- }
- int main(void)
- {
- int result[100];
- result[0] = add_range(1, 10);
- result[1] = add_range(1, 100);
- printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- return 0;
- }
add_range
函数从low
加到high
,在main
函数中首先从1加到10,把结果保存下来,然后从1加到100,再把结果保存下来,最后打印的两个结果是:
- result[0]=55
- result[1]=5105
第一个结果正确[20],第二个结果显然不正确,在小学我们就听说过高斯小时候的故事,从1加到100应该是5050。一段代码,第一次运行结果是对的,第二次运行却不对,这是很常见的一类错误现象,这种情况不应该怀疑代码而应该怀疑数据,因为第一次和第二次运行的都是同一段代码,如果代码是错的,那为什么第一次的结果能对呢?然而第一次和第二次运行时相关的数据却有可能不同,错误的数据会导致错误的结果。在动手调试之前,读者先试试只看代码能不能看出错误原因,只要前面几章学得扎实就应该能看出来。
在编译时要加上-g
选项,生成的可执行文件才能用gdb
进行源码级调试:
- $ gcc -g main.c -o main
- $ gdb main
- GNU gdb 6.8-debian
- Copyright (C) 2008 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 "i486-linux-gnu"...
- (gdb)
-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
- internals -- Maintenance commands
- obscure -- Obscure features
- running -- Running the program
- stack -- Examining the stack
- status -- Status inquiries
- support -- Support facilities
- tracepoints -- Tracing of program execution without stopping the program
- user-defined -- User-defined commands
- 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.
也可以进一步查看某一类别中有哪些命令,例如查看files
类别下有哪些命令可用:
- (gdb) help files
- Specifying and examining files.
- List of commands:
- add-shared-symbol-files -- Load the symbols from shared objects in the dynamic linker's link map
- 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
- exec-file -- Use FILE as program for getting contents of pure memory
- file -- Use FILE as program to be debugged
- forward-search -- Search for regular expression (see regex(3)) from last line listed
- generate-core-file -- Save a core file with the current state of the debugged process
- list -- List specified function or line
- ...
现在试试用list
命令从第一行开始列出源代码:
- (gdb) list 1
- 1 #include <stdio.h>
- 2
- 3 int add_range(int low, int high)
- 4 {
- 5 int i, sum;
- 6 for (i = low; i <= high; i++)
- 7 sum = sum + i;
- 8 return sum;
- 9 }
- 10
一次只列10行,如果要从第11行开始继续列源代码可以输入
- (gdb) list
也可以什么都不输直接敲回车,gdb
提供了一个很方便的功能,在提示符下直接敲回车表示重复上一条命令。
- (gdb) (直接回车)
- 11 int main(void)
- 12 {
- 13 int result[100];
- 14 result[0] = add_range(1, 10);
- 15 result[1] = add_range(1, 100);
- 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- 17 return 0;
- 18
gdb
的很多常用命令有简写形式,例如list
命令可以写成l
,要列一个函数的源代码也可以用函数名做参数:
- (gdb) l add_range
- 1 #include <stdio.h>
- 2
- 3 int add_range(int low, int high)
- 4 {
- 5 int i, sum;
- 6 for (i = low; i <= high; i++)
- 7 sum = sum + i;
- 8 return sum;
- 9 }
- 10
现在退出gdb
的环境:
- (gdb) quit
我们做一个实验,把源代码改名或移到别处再用gdb
调试,这样就列不出源代码了:
- $ mv main.c mian.c
- $ gdb main
- ...
- (gdb) l
- 5 main.c: No such file or directory.
- in main.c
可见gcc
的-g
选项并不是把源代码嵌入到可执行文件中的,在调试时也需要源文件。现在把源代码恢复原样,我们继续调试。首先用start
命令开始执行程序:
- $ gdb main
- ...
- (gdb) start
- Breakpoint 1 at 0x80483ad: file main.c, line 14.
- Starting program: /home/akaedu/main
- main () at main.c:14
- 14 result[0] = add_range(1, 10);
- (gdb)
gdb
停在main
函数中变量定义之后的第一条语句处等待我们发命令,gdb
列出的这条语句是即将执行的下一条语句。我们可以用next
命令(简写为n
)控制这些语句一条一条地执行:
- (gdb) n
- 15 result[1] = add_range(1, 100);
- (gdb) (直接回车)
- 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- (gdb) (直接回车)
- result[0]=55
- result[1]=5105
- 17 return 0;
用n
命令依次执行两行赋值语句和一行打印语句,在执行打印语句时结果立刻打出来了,然后停在return
语句之前等待我们发命令。虽然我们完全控制了程序的执行,但仍然看不出哪里错了,因为错误不在main
函数中而在add_range
函数中,现在用start
命令重新来过,这次用step
命令(简写为s
)钻进add_range
函数中去跟踪执行:
- (gdb) start
- The program being debugged has been started already.
- Start it from the beginning? (y or n) y
- Breakpoint 2 at 0x80483ad: file main.c, line 14.
- Starting program: /home/akaedu/main
- main () at main.c:14
- 14 result[0] = add_range(1, 10);
- (gdb) s
- add_range (low=1, high=10) at main.c:6
- 6 for (i = low; i <= high; i++)
这次停在了add_range
函数中变量定义之后的第一条语句处。在函数中有几种查看状态的办法,backtrace
命令(简写为bt
)可以查看函数调用的栈帧:
- (gdb) bt
- #0 add_range (low=1, high=10) at main.c:6
- #1 0x080483c1 in main () at main.c:14
可见当前的add_range
函数是被main
函数调用的,main
传进来的参数是low=1, high=10
。main
函数的栈帧编号为1,add_range
的栈帧编号为0。现在可以用info
命令(简写为i
)查看add_range
函数局部变量的值:
- (gdb) i locals
- i = 0
- sum = 0
如果想查看main
函数当前局部变量的值也可以做到,先用frame
命令(简写为f
)选择1号栈帧然后再查看局部变量:
- (gdb) f 1
- #1 0x080483c1 in main () at main.c:14
- 14 result[0] = add_range(1, 10);
- (gdb) i locals
- result = {0, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480,
- ...
- -1208623680}
注意到result
数组中有很多元素具有杂乱无章的值,我们知道未经初始化的局部变量具有不确定的值。到目前为止一切正常。用s
或n
往下走几步,然后用print
命令(简写为p
)打印出变量sum
的值:
- (gdb) s
- 7 sum = sum + i;
- (gdb) (直接回车)
- 6 for (i = low; i <= high; i++)
- (gdb) (直接回车)
- 7 sum = sum + i;
- (gdb) (直接回车)
- 6 for (i = low; i <= high; i++)
- (gdb) p sum
- $1 = 3
第一次循环i
是1,第二次循环i
是2,加起来是3,没错。这里的$1
表示gdb
保存着这些中间结果,$后面的编号会自动增长,在命令中可以用$1
、$2
、$3
等编号代替相应的值。由于我们本来就知道第一次调用的结果是正确的,再往下跟也没意义了,可以用finish
命令让程序一直运行到从当前函数返回为止:
- (gdb) finish
- Run till exit from #0 add_range (low=1, high=10) at main.c:6
- 0x080483c1 in main () at main.c:14
- 14 result[0] = add_range(1, 10);
- Value returned is $2 = 55
返回值是55,当前正准备执行赋值操作,用s
命令赋值,然后查看result
数组:
- (gdb) s
- 15 result[1] = add_range(1, 100);
- (gdb) p result
- $3 = {55, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480,
- ...
- -1208623680}
第一个值55确实赋给了result
数组的第0个元素。下面用s
命令进入第二次add_range
调用,进入之后首先查看参数和局部变量:
- (gdb) s
- add_range (low=1, high=100) at main.c:6
- 6 for (i = low; i <= high; i++)
- (gdb) bt
- #0 add_range (low=1, high=100) at main.c:6
- #1 0x080483db in main () at main.c:15
- (gdb) i locals
- i = 11
- sum = 55
由于局部变量i
和sum
没初始化,所以具有不确定的值,又由于两次调用是挨着的,i
和sum
正好取了上次调用时的值,原来这跟例 3.7 “验证局部变量存储空间的分配和释放”是一样的道理,只不过我这次举的例子设法让局部变量sum
在第一次调用时初值为0了。i
的初值不是0倒没关系,在for
循环中会赋值为0的,但sum
如果初值不是0,累加得到的结果就错了。好了,我们已经找到错误原因,可以退出gdb
修改源代码了。如果我们不想浪费这次调试机会,可以在gdb
中马上把sum
的初值改为0继续运行,看看这一处改了之后还有没有别的Bug:
- (gdb) set var sum=0
- (gdb) finish
- Run till exit from #0 add_range (low=1, high=100) at main.c:6
- 0x080483db in main () at main.c:15
- 15 result[1] = add_range(1, 100);
- Value returned is $4 = 5050
- (gdb) n
- 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- (gdb) (直接回车)
- result[0]=55
- result[1]=5050
- 17 return 0;
这样结果就对了。修改变量的值除了用set
命令之外也可以用print
命令,因为print
命令后面跟的是表达式,而我们知道赋值和函数调用也都是表达式,所以也可以用print
命令修改变量的值或者调用函数:
- (gdb) p result[2]=33
- $5 = 33
- (gdb) p printf("result[2]=%d\n", result[2])
- result[2]=33
- $6 = 13
我们讲过,printf
的返回值表示实际打印的字符数,所以$6
的结果是13。总结一下本节用到的gdb
命令:
表 10.1. gdb基本命令1
命令 | 描述 |
---|---|
backtrace(或bt) | 查看各级函数调用及参数 |
finish | 连续运行到当前函数返回为止,然后停下来等待命令 |
frame(或f) 帧编号 | 选择栈帧 |
info(或i) locals | 查看当前栈帧局部变量的值 |
list(或l) | 列出源代码,接着上次的位置往下列,每次列10行 |
list 行号 | 列出从第几行开始的源代码 |
list 函数名 | 列出某个函数的源代码 |
next(或n) | 执行下一行语句 |
print(或p) | 打印表达式的值,通过表达式可以修改变量的值或者调用函数 |
quit(或q) | 退出gdb 调试环境 |
set var | 修改变量的值 |
start | 开始执行程序,停在main 函数第一行语句前面等待命令 |
step(或s) | 执行下一行语句,如果有函数调用则进入到函数中 |
习题
1、用gdb
一步一步跟踪第 3 节 “递归”讲的factorial
函数,对照着图 5.2 “factorial(3)的调用过程”查看各层栈帧的变化情况,练习本节所学的各种gdb
命令。
[20] 这么说不够准确,在有些平台和操作系统上第一个结果也未必正确,如果在你机器上运行第一个结果也不正确,首先检查一下程序有没有抄错,如果没抄错那就没关系了,顺着我的讲解往下看就好了,结果是多少都无关紧要。