取势 明道 优术

作者为 扶 凯 发表

这是 2011 年参加亚嵌的内核开发时,C 语言中 GDB 调试部分的笔记。因混于其它笔记一起, 特单独立放一文章, 并且照原来的笔记复习了一下。

使用 GDB 调试程序

打开 C 程序的调试功能

编译程序, 我们可以使用 gcc -S main.c 这样来打开调试并且这样也能见到二进制的汇编. 编译程序时使用 -g 更加方便不但有二进制汇编,还有代码本身 (注, 这时我们想看二进制结构,可以使用 objdump 加 -dS 参数).
测试样例代码

#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\n result[1]=%d\n", result[0], result[1]);
    return 0;
}

常用 GDB 的调试命令

计算机在分配变量的时候, 局部变量是存储在栈中, 全局变量是存储在全局量段。进入函数时, 变量会放到栈顶,退出时会从栈顶拿掉。它是从存储器顶部开始向下增长的。
启动 GDB 的时候, 一定要保证加了 -g 来增加代码到二进制文件中来. 代码是在指定路径,所以代码文件不存在并不行。
启动调试:

# gcc 11.c -g -o 11
# gdb main
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-64.el6_5.2)
Copyright (C) 2010 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 /root/fukai/c/11...done.

帮助和显示

gdb 提供了一个类似 shell 一样的命令行环境, 可以在这个提示符下输入 help 命令来查看可用的命令类别。我们可以进一步查看看 help 显示的内容类别下有哪些类别, 比如 files 只需要输入 help 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
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
load -- Dynamically load FILE into the running program
nosharedlibrary -- Unload all shared object library symbols
path -- Add directory DIR(s) to beginning of search path for object files
pwd -- Print working directory

进入后,就可以使用 list 1 来列出源代码。一次只列出 10 行。如果要在显示第 11 行开始, 需要在次输入 list, 也可以什么都不输入直接回车。
列出指定函数代码: l 函数名

GDB 基本命令

进入后, 输入 start 来启动程序. 启动后我们可以使用 next (简写为 n) 来控制代码一条一条执行, 注意 n 并不会进入函数内部. 这时如果执行到一个指定的函数上, 想进入,可以使用 s ,然后就会进入这个函数的内部。

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x4004f8: file 11.c, line 13.
Starting program: /root/fukai/c/11 

Temporary breakpoint 2, main () at 11.c:13
13          result[0] = add_range(1, 10);
(gdb) s
add_range (low=1, high=10) at 11.c:5
5           for (i = low; i <= high; i++) {

进入函数后状态的查看

上面讲到使用 step 进入当前的函数后, 下面是怎么样来查看看函数内部的内容。
命令 backtrace (简写 bt)命令可以查看函数调用的栈帧

(gdb) bt
#0  add_range (low=1, high=10) at 11.c:5
#1  0x0000000000400507 in main () at 11.c:13

这时可以见到 main 函数的栈帧是 1 , add_range 的栈帧是 0,我们可以使用 info 命令来查看函数内部的局部变量的值。

(gdb) i locals
i = 0
sum = 0

这时想查看 main 函数内的局部变量也可以需要使用 fram 1(简写 f) 来转到 1 号栈帧。接下来基本上都是使用 p 来显示变量的内容

(gdb) p sum
$5 = 6
(gdb) finish
Run till exit from #0  add_range (low=1, high=10) at 11.c:6
0x0000000000400507 in main () at 11.c:13
13          result[0] = add_range(1, 10);
Value returned is $6 = 55

如果要退出当前函数,就象上面一样,执行 finish 就可以退出当前函数来执行其它的了。
在使用过程中想修改变量的话,可以象下面这样操作

(gdb) set var sum=0
(gdb) p sum        
$8 = 0
(gdb) finish
Run till exit from #0  add_range (low=1, high=100) at 11.c:6
0x000000000040051c in main () at 11.c:14
14          result[1] = add_range(1, 100);
Value returned is $9 = 5050

 gdb 基本命令

brcktrace 或 bt  查看各级函数调用和参数
finish 连续运行到当前函数结束,然后等待下一个命令。
frame 或 f   帧编号 选择指定栈帧
info 或 locals 查看当前栈帧局部变量
list 或 l 列出上次位置以下的 10 行源码
list 行号 列出第几行开始的 10 行源码
list 函数名 列出指定函数源码
next 或 n 执行下一语句
print 或 p 打印表达式, 通过表达式可以修改变量值或者调用函数
quit 或 q 退出 gdb 环境
set var 修改变量值
start 开始执行到 main 函数第一行语句
step 或 s 执行下一语句, 如果有函数就进入

高级调试

如果我们想每次查看指定的变量的变化, 我们不需要每次都使用 printf 来打印, 我们只需要使用 display 就行。

(gdb) display sum
1: sum = 1634469985
(gdb) n
9                   sum = sum*10 + input[1] - '0';
1: sum = 1634469985
(gdb) n
8               for (i = 0; input[i] != '\0'; i++)
1: sum = -835169374

这样每就到了 sum 都会打印出来给我们显示.不想显示时可以使用 undisplay 来取消跟踪显示。
每次这样显示很累, 我们可以直接使用 break 命令来在指定行上打外断点,如

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

Breakpoint 2, main () at 12.c:9
9                   sum = sum*10 + input[1] - '0';
1: sum = -1254259506

break 可以指定行,也可以指定函数名。这个时候,在内部需要使用 continue 命令来连续运行,而非单步运行, 程序达到断点就会自动停止下来。这样可以自动停止在下一次循环内。
一次调试可以设置多个断点, 可以使用 info 来查看设置好的断点

(gdb) i break
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x000000000040056c in main at 12.c:9
        breakpoint already hit 6 times

可以使用 delete break 2 来删除指定的断点。

(gdb) disable b 3
(gdb) i b
Num     Type           Disp Enb Address            What
3       breakpoint     keep n   0x000000000040059c in main at 12.c:10
4       breakpoint     keep y   0x0000000000400563 in main at 12.c:8

在这通过 disable b 3 来禁用指定 Num 的断点,如上所显示在 Enb 下禁用的会变成 n. 这时也可以使用 enable 3 来启用断点。
条件断点。 我们可以根据一些条件来选择指定条件启用断点, 如我们要 sum 不为 0 时显示。我们现在使用 run 命令重新启动程序,然后我们可以这样。

(gdb)break 9 if sum != 0          

Breakpoint 4, main () at 12.c:8

本节 gdb 命令

break 或 b 行号 在指定行设置断点
break 函数名 在指定函数开头设置断点
break … if … 设置断点条件
cotinue 或 c 从当前位置开始连续运行程序
delete breakpoints 断点号 删除断点
display 变量名 跟踪查看变量,每次到这都会显示它的值
disable breakpoints 断点号 禁用断点
enable 断点号 启用断点
info 或 i breakpoints 查看当前设置了哪些断点
run 或 r 从头开始连接运行程序
undisplay 显示跟踪号 取消跟踪显示

除了断点外, 我们有时可能想知道, 当程序访问有些存储单元时中断了, 我们不知道是哪个地方修改了这个存储单元, 这时我们可以使用观察点。

(gdb) watch input[5]
Hardware watchpoint 2: input[5]
(gdb) c
Continuing.
123
input=123
12312
Hardware watchpoint 2: input[5]

Old value = 127 '\177'
New value = 0 '\000'
0x00007ffff7aa1e09 in _IO_vfscanf_internal () from /lib64/libc.so.6

这样有修改就会显示。注意上面, 上面这个地方出现段错误, gdb 每次会在段错误时会停止下来。这时我们可以看看到底哪行引起的段错误, gdb 上面显示在 _IO_vfscanf 的函数上出错, 我们使用 bt 命令看看是行引起的

Old value = 127 '\177'
New value = 0 '\000'
0x00007ffff7aa1e09 in _IO_vfscanf_internal () from /lib64/libc.so.6
(gdb) bt
#0  0x00007ffff7aa1e09 in _IO_vfscanf_internal () from /lib64/libc.so.6
#1  0x00007ffff7aad44d in __isoc99_scanf () from /lib64/libc.so.6
#2  0x000000000040056a in main () at 13.c:10

上面显示在 13.c 的第 10 行出错。 有时我们调试, 查出段错误的时候,并没有详细内容, 段错误发生在 } 括号结束的位置, 这可是常见问题,如果一个函数局部变量发生访问越界, 有可能并不立刻产生段错误, 而是在函数返回的时候生产段错误。
如果对于数组,我们想显示整组的信息可以象下如下操作。

(gdb) x/7b input
0x7fffffffe5c0: 49      50      51      0       -1      127     0

这我们可以使用 x 命令来打印指定的存储单元的内容, 7b 是打印格式, b 表示每个字节一组, 7 表示打印 7 组。 从 input 数组的第一个字节开始连续打印 7 组。

watch 设置观察点
info 或 i watchpoints 查看所有的观察点
x 从指定位置的存储单元开始打印内容,给内容会部当成字节来看,并不区分是哪个变量。

反汇编:  disassemble 默认这个是反汇编当前的函数, 如果要查看其它的,需要指定函数名或地址.
显示寄存器信息: info registers 注意在 gdb 中表示寄存器需要前面加 $ .如  p $esp 可以打印这个寄存器的值

查看当前程序栈的内容: x/10x $sp–>打印stack的前10个元素
查看当前程序栈的参数: info args—lists arguments to the function
查看当前寄存器的值:info registers(不包括浮点寄存器) info all-registers(包括浮点寄存器)
查看当前栈帧中的异常处理器:info catch(exception handlers)

汇编常识:

默认 At&T 的汇编是从左向右,有前缀的.
esp 指向栈顶
ebp 指向栈低
eip 程序的计数器, 下次程序运行地址
eax 通用的寄存器, 函数中的返回值就存在这个中

来了就留个评论吧! 2个评论



    sleetdrop 2015年03月18日 的 22:55

    笔记很好,有些typo建议修正下。

    some typo
    *display* breakpoints 断点号
    我们现在使用 *rum* 命令重新启动程*度*
    我们不知道是*那*个地方修改了这个存储单元

    不顺的句子
    *其实*出现段错误,*我* gdb 每次在段错误时会停止下来。
    这可是*常用*问题