gdb 是一种强大的命令行调试器,可以用来检查和修改正在运行的程序。除了常用的 file、b、s、n、q、disp、p 等命令,gdb 还有许多高级技巧,可以让调试更加方便快捷。本文将介绍 10 个常用的 gdb 高级技巧,希望对你有所帮助
1. 查看宏
gdb 可以显示宏的定义和展开结果,这对于调试使用了复杂宏的代码很有用。你可以使用 info macro 命令来查看宏的定义,例如:
(gdb) info macro MAX
Defined at /usr/include/sys/param.h:224
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
你也可以使用 macro expand 命令来查看宏的展开结果,例如:
(gdb) macro expand MAX(x, y)
expands to: (((x) > (y)) ? (x) : (y))
2. 执行一连串命令
有时候,你可能想要在 gdb 中执行一系列的命令,而不是每次都输入。gdb 提供了几种方法来实现这一点。
- 你可以使用分号来分隔多个命令,例如
(gdb) b main; r; p argc
Breakpoint 1 at 0x4005ed: file test.c, line 5.
Starting program: /home/user/test
Breakpoint 1, main (argc=1, argv=0x7fffffffe4f8) at test.c:5
5 printf("Hello, world!\n");
$1 = 1
- 你可以使用 commands 命令来为一个断点指定一系列要执行的命令,例如:
(gdb) b main
Breakpoint 1 at 0x4005ed: file test.c, line 5.
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>p argc
>c
>end
(gdb) r
Starting program: /home/user/test
Breakpoint 1, main (argc=1, argv=0x7fffffffe4f8) at test.c:5
5 printf("Hello, world!\n");
$1 = 1
Continuing.
Hello, world!
[Inferior 1 (process 12345) exited normally]
- 你可以使用 define 命令来自定义一个新的 gdb 命令,例如:
(gdb) define mycmd
Type commands for definition of "mycmd".
End with a line saying just "end".
>b main
>r
>p argc
>end
(gdb) mycmd
Breakpoint 1 at 0x4005ed: file test.c, line 5.
Starting program: /home/user/test
Breakpoint 1, main (argc=1, argv=0x7fffffffe4f8) at test.c:5
5 printf("Hello, world!\n");
$1 = 1
3. 同时给多个函数打断点
如果你想要在多个函数中设置断点,你可以使用 rbreak 命令来根据正则表达式匹配函数名,并在所有匹配的函数中设置断点。例如:
(gdb) rbreak print
4. .gdbinit 文件
如果你经常使用 gdb,你可能会想要自定义一些 gdb 的设置,例如显示的格式、断点的行为、自定义的命令等。你可以使用 .gdbinit 文件来保存你的 gdb 配置,并在每次启动 gdb 时自动加载它们。.gdbinit 文件是一个普通的文本文件,它包含了一些 gdb 命令,每行一个。你可以将 .gdbinit 文件放在你的主目录或者当前工作目录中,gdb 会按照顺序查找它们。例如,你可以在 .gdbinit 文件中写入以下内容:
set print pretty on
set print array on
set print array-indexes on
set print elements 0
set pagination off
define hexdump
dump binary memory /tmp/dump.bin $arg0 $arg0+$arg1
shell hexdump -C /tmp/dump.bin
end
这样,每次启动 gdb 时,就会自动设置一些打印选项,并定义一个 hexdump 命令。你可以根据你的需要修改或添加 .gdbinit 文件中的内容。
5. 自定义命令
除了使用 define 命令来创建简单的 gdb 命令外,你还可以使用 python 命令来编写更复杂的 gdb 命令,利用 gdb 的 Python API 来访问和控制 gdb 的内部状态。例如,你可以使用 python 命令来编写一个名为 fib 的 gdb 命令,它可以计算并打印斐波那契数列的第 n 项,如下:
(gdb) python
>class FibCommand(gdb.Command):
>>def __init__(self):
>>>gdb.Command.__init__(self, "fib", gdb.COMMAND_USER)
>>
>>def invoke(self, arg, from_tty):
>>>n = int(arg)
>>>a = 0
>>>b = 1
>>>for i in range(n):
>>>>a, b = b, a + b
>>>
>>>print(a)
>>
>end
>FibCommand()
>end
(gdb) fib 10
55
6. 定义命令钩子
有时候,你可能想要在执行某个 gdb 命令之前或之后自动执行一些操作,例如打印一些信息、设置一些变量、检查一些条件等。你可以使用 hook 命令来定义一个命令钩子,它是一个与 gdb 命令同名的自定义命令,它会在执行原始命令之前或之后运行。例如,你可以定义一个 hook next 命令,它会在每次执行 next 命令之后打印当前行的源代码,如下:
(gdb) define hook next
Type commands for definition of "hook next".
End with a line saying just "end".
>list $pc
>end
(gdb) b main
Breakpoint 1 at 0x4005ed: file test.c, line 5.
(gdb) r
Starting program: /home/user/test
Breakpoint 1, main (argc=1, argv=0x7fffffffe4f8) at test.c:5
5 printf("Hello, world!\n");
(gdb) n
6 return 0;
Hello, world!
(gdb) n
7 }
[Inferior 1 (process 12345) exited normally]
7. GDB 中循环
有时候,你可能想要在 gdb 中重复执行某个命令多次,例如打印一个数组的元素、设置多个断点、修改多个变量等。你可以使用 while 命令来在 gdb 中创建一个循环,它接受一个条件表达式和一个命令块,当条件表达式为真时,就会重复执行命令块。例如,你可以使用 while 命令来打印一个数组的所有元素,如下:
(gdb) p arr
$1 = {1, 2, 3, 4, 5}
(gdb) set $i = 0
(gdb) while $i < 5
>p arr[$i]
>set $i = $i + 1
>end
$2 = 1
$3 = 2
$4 = 3
$5 = 4
$6 = 5
8. 函数测试
如果你想要在 gdb 中调用某个函数并查看其返回值或副作用,你可以使用 call 命令来实现。call 命令接受一个函数名和一组参数,并在当前上下文中执行该函数。例如,你可以使用 call 命令来调用 printf 函数并打印一些信息,如下:
(gdb) call printf("Hello from gdb!\n")
Hello from gdb!
$1 = 15
注意,call 命令会改变程序的状态,因此可能会影响程序的正常运行。你应该谨慎使用 call 命令,并在必要时使用 undo 命令来撤销 call 命令的效果。
9. 反汇编
如果你想要查看程序的汇编代码,你可以使用 disassemble 命令来实现。disassemble 命令接受一个函数名或一个地址范围,并显示该范围内的机器指令。例如,你可以使用 disassemble 命令来查看 main 函数的汇编代码,如下:
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004005e6 <+0>: push %rbp
0x00000000004005e7 <+1>: mov %rsp,%rbp
0x00000000004005ea <+4>: sub $0x10,%rsp
0x00000000004005ee <+8>: mov %edi,-0x4(%rbp)
0x00000000004005f1 <+11>: mov %rsi,-0x10(%rbp)
0x00000000004005f5 <+15>: mov -0x10(%rbp),%rax
0x00000000004005f9 <+19>: add $0x8,%rax
0x00000000004005fd <+23>: mov (%rax),%rax
0x0000000000400600 <+26>: mov %rax,%rdi
0x0000000000400603 <+29>: callq 0x4004d6 <atoi@plt>
0x0000000000400608 <+34>: mov %eax,%edi
0x000000000040060a <+36>: callq 0x400546 <f>
0x000000000040060f <+41>: mov %eax,%esi
0x0000000000400611 <+43>: lea 0xe9(%rip),%rdi # 0x400701
0x0000000000400618 <+50>: mov $0x0,%eax
0x000000000040061d <+55>: callq 0x4004e6 <printf@plt>
0x0000000000400622 <+60>: mov $0x0,%eax
0x0000000000400627 <+65>: leaveq
0x0000000000400628 <+66>: retq
End of assembler dump.
你可以使用 /m 参数来显示源代码和汇编代码的混合视图,如下:
(gdb) disassemble /m main
Dump of assembler code for function main:
5 int main(){
0x00005555555546e6 <+0>: push %rbp
0x00005555555546e7 <+1>: mov %rsp,%rbp
6 int n;
7 scanf("%d",&n);
0x00005555555546ea <+4>: sub $0x10,%rsp
=> 0x00005555555546ee <+8>: lea -0xc(%rbp),%rax
0x00005555555546f2 <+12>: mov %rax,%rsi
0x00005555555546f5 <+15>: lea -0xb(%rip),%rdi # 0x5555555546f1
0x00005555555546fc <+22>: mov $0x0,%eax
0x0000555555554701 <+27>: callq 0x555555554560 <__isoc99_scanf@plt>
8 printf("%d\n",f(n));
0x0000555555554706 <+32>: mov -0xc(%rbp),%eax
0x0000555555554709 <+35>: mov %eax,%edi
0x00