转自:http://www.rainsts.net/article.asp?id=863
GDB 调试演示
[ 2009-08-30 00:02:01 | 作者: yuhen ]作为内置和最常用的调试器,GDB 显然有着无可辩驳的地位。熟练使用 GDB,就好像所有 Linux 下的开发人员建议你用 VIM 一样,是个很 "奇怪" 的情节。
测试用源代码。
#include <stdio.h>
int test(int a, int b)
{
int c = a + b;
return c;
}
int main(int argc, char* argv[])
{
int a = 0x1000;
int b = 0x2000;
int c = test(a, b);
printf("%d\n", c);
printf("Hello, World!\n");
return 0;
}
编译命令 (注意使用 "-g" 参数生成调试符号):
$ gcc -g -o hello hello.c
开始调试:
$ gdb hello
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)
1. 源码
在调试过程中查看源代码是必须的。list (缩写 l) 支持多种方式查看源码。
(gdb) l # 显示源代码
2
3 int test(int a, int b)
4 {
5 int c = a + b;
6 return c;
7 }
8
9 int main(int argc, char* argv[])
10 {
11 int a = 0x1000;
(gdb) l # 继续显示
12 int b = 0x2000;
13 int c = test(a, b);
14 printf("%d\n", c);
15
16 printf("Hello, World!\n");
17 return 0;
18 }
(gdb) l 3, 10 # 显示特定范围的源代码
3 int test(int a, int b)
4 {
5 int c = a + b;
6 return c;
7 }
8
9 int main(int argc, char* argv[])
10 {
(gdb) l main # 显示特定函数源代码
5 int c = a + b;
6 return c;
7 }
8
9 int main(int argc, char* argv[])
10 {
11 int a = 0x1000;
12 int b = 0x2000;
13 int c = test(a, b);
14 printf("%d\n", c);
可以用如下命令修改源代码显示行数。
(gdb) set listsize 50
2. 断点
可以使用函数名或者源代码行号设置断点。
(gdb) b main # 设置函数断点
Breakpoint 1 at 0x804841b: file hello.c, line 11.
(gdb) b 13 # 设置源代码行断点
Breakpoint 2 at 0x8048429: file hello.c, line 13.
(gdb) b # 将下一行设置为断点 (循环、递归等调试很有用)
Breakpoint 5 at 0x8048422: file hello.c, line 12.
(gdb) tbreak main # 设置临时断点 (中断后失效)
Breakpoint 1 at 0x804841b: file hello.c, line 11.
(gdb) info breakpoints # 查看所有断点
Num Type Disp Enb Address What
2 breakpoint keep y 0x0804841b in main at hello.c:11
3 breakpoint keep y 0x080483fa in test at hello.c:5
(gdb) d 3 # delete: 删除断点 (还可以用范围 "d 1-3",无参数时删除全部断点)
(gdb) disable 2 # 禁用断点 (还可以用范围 "disable 1-3")
(gdb) enable 2 # 启用断点 (还可以用范围 "enable 1-3")
(gdb) ignore 2 1 # 忽略 2 号中断 1 次
当然少不了条件式中断。
(gdb) b test if a == 10
Breakpoint 4 at 0x80483fa: file hello.c, line 5.
(gdb) info breakpoints
Num Type Disp Enb Address What
4 breakpoint keep y 0x080483fa in test at hello.c:5
stop only if a == 10
可以用 condition 修改条件,注意表达式不包含 "if"。
(gdb) condition 4 a == 30
(gdb) info breakpoints
Num Type Disp Enb Address What
2 breakpoint keep y 0x0804841b in main at hello.c:11
ignore next 1 hits
4 breakpoint keep y 0x080483fa in test at hello.c:5
stop only if a == 30
3. 执行
通常情况下,我们会先设置 main 入口断点。
(gdb) b main
Breakpoint 1 at 0x804841b: file hello.c, line 11.
(gdb) r # 开始执行 (Run)
Starting program: /home/yuhen/Learn.c/hello
Breakpoint 1, main () at hello.c:11
11 int a = 0x1000;
(gdb) n # 单步执行 (不跟踪到函数内部, Step Over)
12 int b = 0x2000;
(gdb) n
13 int c = test(a, b);
(gdb) s # 单步执行 (跟踪到函数内部, Step In)
test (a=4096, b=8192) at hello.c:5
5 int c = a + b;
(gdb) finish # 继续执行直到当前函数结束 (Step Out)
Run till exit from #0 test (a=4096, b=8192) at hello.c:5
0x0804843b in main () at hello.c:13
13 int c = test(a, b);
Value returned is $1 = 12288
(gdb) c # Continue: 继续执行,直到下一个断点。
Continuing.
12288
Hello, World!
Program exited normally.
4. 堆栈
查看调用堆栈(call stack)无疑是调试过程中非常重要的事情。
(gdb) where # 查看调用堆栈 (相同作用的命令还有 info s 和 bt)
#0 test (a=4096, b=8192) at hello.c:5
#1 0x0804843b in main () at hello.c:13
(gdb) frame # 查看当前堆栈帧,还可显示当前代码
#0 test (a=4096, b=8192) at hello.c:5
5 int c = a + b;
(gdb) info frame # 获取当前堆栈帧更详细的信息
Stack level 0, frame at 0xbfad3290:
eip = 0x80483fa in test (hello.c:5); saved eip 0x804843b
called by frame at 0xbfad32c0
source language c.
Arglist at 0xbfad3288, args: a=4096, b=8192
Locals at 0xbfad3288, Previous frame's sp is 0xbfad3290
Saved registers:
ebp at 0xbfad3288, eip at 0xbfad328c
可以用 frame 修改当前堆栈帧,然后查看其详细信息。
(gdb) frame 1
#1 0x0804843b in main () at hello.c:13
13 int c = test(a, b);
(gdb) info frame
Stack level 1, frame at 0xbfad32c0:
eip = 0x804843b in main (hello.c:13); saved eip 0xb7e59775
caller of frame at 0xbfad3290
source language c.
Arglist at 0xbfad32b8, args:
Locals at 0xbfad32b8, Previous frame's sp at 0xbfad32b4
Saved registers:
ebp at 0xbfad32b8, eip at 0xbfad32bc
5. 变量和参数
(gdb) info locals # 显示局部变量
c = 0
(gdb) info args # 显示函数参数(自变量)
a = 4096
b = 8192
我们同样可以切换 frame,然后查看不同堆栈帧的信息。
(gdb) p a # print 命令可显示局部变量和参数值
$2 = 4096
(gdb) p/x a # 十六进制输出 (d: 十进制; u: 十进制无符号; x: 十六进制; o: 八进制; t: 二进制; c: 字符)
$10 = 0x1000
(gdb) p a + b # 还可以进行表达式计算
$5 = 12288
set variable 可用来修改变量值。
(gdb) set variable a=100
(gdb) info args
a = 100
b = 8192
6. 内存及寄存器
x 命令可以显示指定地址的内存数据。
格式: x/nfu [address]
* n: 显示内存单位(组或者行)。
* f: 格式 (除了 print 格式外,还有 字符串s 和 汇编 i)。
* u: 内存单位 (b: 1字节; h: 2字节; w: 4字节; g: 8字节)。
(gdb) x/8w 0x0804843b # 按四字节(w)显示 8 组内存数据
0x804843b <main+49>: 0x8bf04589 0x4489f045 0x04c70424 0x04853024
0x804844b <main+65>: 0xfecbe808 0x04c7ffff 0x04853424 0xfecfe808
(gdb) x/8i 0x0804843b # 显示 8 行汇编指令
0x804843b <main+49>: mov DWORD PTR [ebp-0x10],eax
0x804843e <main+52>: mov eax,DWORD PTR [ebp-0x10]
0x8048441 <main+55>: mov DWORD PTR [esp+0x4],eax
0x8048445 <main+59>: mov DWORD PTR [esp],0x8048530
0x804844c <main+66>: call 0x804831c <printf@plt>
0x8048451 <main+71>: mov DWORD PTR [esp],0x8048534
0x8048458 <main+78>: call 0x804832c <puts@plt>
0x804845d <main+83>: mov eax,0x0
(gdb) x/s 0x08048530 # 显示字符串
0x8048530: "%d\n"
除了通过 "info frame" 查看寄存器值外,还可以用如下指令。
(gdb) info registers # 显示所有寄存器数据
eax 0x1000 4096
ecx 0xbfad32d0 -1079168304
edx 0x1 1
ebx 0xb7fa1ff4 -1208344588
esp 0xbfad3278 0xbfad3278
ebp 0xbfad3288 0xbfad3288
esi 0x8048480 134513792
edi 0x8048340 134513472
eip 0x80483fa 0x80483fa <test+6>
eflags 0x286 [ PF SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) p $eax # 显示单个寄存器数据
$11 = 4096
7. 反汇编
我对 AT&T 汇编不是很熟悉,还是设置成 intel 格式的好。
(gdb) set disassembly-flavor intel # 设置反汇编格式
(gdb) disass main # 反汇编函数
Dump of assembler code for function main:
0x0804840a <main+0>: lea ecx,[esp+0x4]
0x0804840e <main+4>: and esp,0xfffffff0
0x08048411 <main+7>: push DWORD PTR [ecx-0x4]
0x08048414 <main+10>: push ebp
0x08048415 <main+11>: mov ebp,esp
0x08048417 <main+13>: push ecx
0x08048418 <main+14>: sub esp,0x24
0x0804841b <main+17>: mov DWORD PTR [ebp-0x8],0x1000
0x08048422 <main+24>: mov DWORD PTR [ebp-0xc],0x2000
0x08048429 <main+31>: mov eax,DWORD PTR [ebp-0xc]
0x0804842c <main+34>: mov DWORD PTR [esp+0x4],eax
0x08048430 <main+38>: mov eax,DWORD PTR [ebp-0x8]
0x08048433 <main+41>: mov DWORD PTR [esp],eax
0x08048436 <main+44>: call 0x80483f4 <test>
0x0804843b <main+49>: mov DWORD PTR [ebp-0x10],eax
0x0804843e <main+52>: mov eax,DWORD PTR [ebp-0x10]
0x08048441 <main+55>: mov DWORD PTR [esp+0x4],eax
0x08048445 <main+59>: mov DWORD PTR [esp],0x8048530
0x0804844c <main+66>: call 0x804831c <printf@plt>
0x08048451 <main+71>: mov DWORD PTR [esp],0x8048534
0x08048458 <main+78>: call 0x804832c <puts@plt>
0x0804845d <main+83>: mov eax,0x0
0x08048462 <main+88>: add esp,0x24
0x08048465 <main+91>: pop ecx
0x08048466 <main+92>: pop ebp
0x08048467 <main+93>: lea esp,[ecx-0x4]
0x0804846a <main+96>: ret
End of assembler dump.
可以用 "b *address" 设置汇编断点,然后用 "si" 和 "ni" 进行汇编级单步执行,这对于分析指针和寻址非常有用。
8. 进程
查看进程相关信息,尤其是 maps 内存数据是非常有用的。
(gdb) help info proc stat
Show /proc process information about any running process.
Specify any process id, or use the program being debugged by default.
Specify any of the following keywords for detailed info:
mappings -- list of mapped memory regions.
stat -- list a bunch of random process info.
status -- list a different bunch of random process info.
all -- list all available /proc info.
(gdb) info proc mappings # 相当于 cat /proc/{pid}/maps
process 22561
cmdline = '/home/yuhen/Learn.c/hello'
cwd = '/home/yuhen/Learn.c'
exe = '/home/yuhen/Learn.c/hello'
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0 /home/yuhen/Learn.c/hello
0x8049000 0x804a000 0x1000 0 /home/yuhen/Learn.c/hello
0x804a000 0x804b000 0x1000 0x1000 /home/yuhen/Learn.c/hello
0x8a33000 0x8a54000 0x21000 0x8a33000 [heap]
0xb7565000 0xb7f67000 0xa02000 0xb7565000
0xb7f67000 0xb80c3000 0x15c000 0 /lib/tls/i686/cmov/libc-2.9.so
0xb80c3000 0xb80c4000 0x1000 0x15c000 /lib/tls/i686/cmov/libc-2.9.so
0xb80c4000 0xb80c6000 0x2000 0x15c000 /lib/tls/i686/cmov/libc-2.9.so
0xb80c6000 0xb80c7000 0x1000 0x15e000 /lib/tls/i686/cmov/libc-2.9.so
0xb80c7000 0xb80ca000 0x3000 0xb80c7000
0xb80d7000 0xb80d9000 0x2000 0xb80d7000
0xb80d9000 0xb80da000 0x1000 0xb80d9000 [vdso]
0xb80da000 0xb80f6000 0x1c000 0 /lib/ld-2.9.so
0xb80f6000 0xb80f7000 0x1000 0x1b000 /lib/ld-2.9.so
0xb80f7000 0xb80f8000 0x1000 0x1c000 /lib/ld-2.9.so
0xbfee2000 0xbfef7000 0x15000 0xbffeb000 [stack]
9. 线程
可以在 pthread_create 处设置断点,当线程创建时会生成提示信息。
(gdb) c
Continuing.
[New Thread 0xb7e78b70 (LWP 2933)]
(gdb) info threads # 查看所有线程列表
* 2 Thread 0xb7e78b70 (LWP 2933) test (arg=0x804b008) at main.c:24
1 Thread 0xb7e796c0 (LWP 2932) 0xb7fe2430 in __kernel_vsyscall ()
(gdb) where # 显示当前线程调用堆栈
#0 test (arg=0x804b008) at main.c:24
#1 0xb7fc580e in start_thread (arg=0xb7e78b70) at pthread_create.c:300
#2 0xb7f478de in clone () at ../sysdeps/unix/sysv/linux/i386/clone.S:130
(gdb) thread 1 # 切换线程
[Switching to thread 1 (Thread 0xb7e796c0 (LWP 2932))]#0 0xb7fe2430 in __kernel_vsyscall ()
(gdb) where # 查看切换后线程调用堆栈
#0 0xb7fe2430 in __kernel_vsyscall ()
#1 0xb7fc694d in pthread_join (threadid=3085405040, thread_return=0xbffff744) at pthread_join.c:89
#2 0x08048828 in main (argc=1, argv=0xbffff804) at main.c:36
10. 其他
调试子进程。
(gdb) set follow-fork-mode child
临时进入 Shell 执行命令,Exit 返回。
(gdb) shell
调试时直接调用函数。
(gdb) call test("abc")
使用 "--tui" 参数,可以在终端窗口上部显示一个源代码查看窗。
$ gdb --tui hello
查看命令帮助。
(gdb) help b
最后就是退出命令。
(gdb) q
和 Linux Base Shell 习惯一样,对于记不住的命令,可以在输入前几个字母后按 Tab 补全。
----------- 分隔线 ---------------
GDB 还有很多指令,功能也异常强大。不过对于熟悉了 VS 那种豪华 IDE 的人来说,这种命令行调试是种巨大的痛苦。尽管我个人建议多用 GDB,但也不反对用 GUI 调试器来加快调试进程。Nemiver 就不错,推荐一下。