【GDB】常用操作

文章目录

  • GDB 常用操作
    • log
    • debug
    • 会话 / 历史 / 命令文件
    • watch
    • command
    • define
    • 记账
    • `.gdbinit` 文件
    • 参考

GDB 常用操作

log

(gdb) set logging file test.log # 设置 log 文件名,
(gdb) set logging enabled on # 打开 log
(gdb) info b # 实际操作,显示断点
(gdb) set logging enabled off # 关闭 log
(gdb) quit # 退出 gdb 调试

log 文件示例

Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001200 in main at test.c:20
        i locals
        i args
2       breakpoint     keep y   0x000000000000117b in binary_search at test.c:5
	stop only if target == 5
        i locals
        i args

debug

代码调试一般有两种方法:printf 和 debug。

print 要比用 debugger 设下断点更为简单粗暴,有时甚至会更有用。不过 debugger 对比于 print 有三个优点:

  • 无需重新编译

  • 可以在调试时改变变量

  • debugger 可以实现 print 做不到的复杂操作

会话 / 历史 / 命令文件

通常我们只有在程序出问题才会启动 gdb,开始调试工作,调试完毕后退出。不过,让 gdb 一直开着未尝不是更好的做法。每个 gdb 老司机都懂得,gdb 在 r 的时候会加载当前程序的最新版本。也即是说,就算不退出 gdb,每次运行的也会是当前最新的版本。不退出当前调试会话有两个好处:

  • 调试上下文可以得到保留。不用每次运行都重新设一轮断点。

  • 一旦 core dump 了,可以显示 core dump 的位置,无需带着 core 重新启动一次。

在开发 C/C++ 项目,一般的工作流程:一个窗口开着编辑器,编译也在这个窗口执行;另一个窗口开着 gdb,这个窗口同时也用来运行程序。一旦要调试了(或者,又 segment fault 了),随手就可以开始干活。

当然了,劳作一天之后,总需要关电脑回家。这时候只能退出 gdb。不想明天一早再把断点设上一遍?gdb 提供了保留断点的功能。输入 save br .gdb_bp,gdb 会把本次会话的断点存在. gdb_bp 中。明天早上一回来,启动 gdb 的时候,加上 - x .gdb_bp,让 gdb 把. gdb_bp 当做命令文件逐条重新执行,一切又回到昨晚。

.gdb_bp 文件的内容如下

break /home/tyustli/code/c_project/test.c:5
break /home/tyustli/code/c_project/test.c:6
break /home/tyustli/code/c_project/test.c:7

实际就是将 gdb 命令保存下来,加载的时候在将命令自动添加到调试中。

watch

二分法代码如下

#include 

int binary_search(int *ary, unsigned int ceiling, int target)
{
    unsigned int floor = 0;
    while (ceiling> floor)
    {
        unsigned int pivot = (ceiling + floor) / 2;
        if (ary[pivot] < target)
            floor = pivot + 1;
        else if (ary[pivot] > target)
            ceiling = pivot - 1; /* 正确的代码应该为: ceiling = pivot */
        else
            return pivot;
    }
    return -1;
}

int main(int argc, char *argv)
{
    int a[] = {1, 2, 4, 5, 6};
    printf("%d\r\n", binary_search(a, 5, 7)); /* -1 */
    printf("%d\r\n", binary_search(a, 5, 6)); /* 4 */
    printf("%d\r\n", binary_search(a, 5, 5)); /* 期望 3,实际运行结果是 - 1 */

    return 0;
}

编译并运行 gdb 调试

gcc -g test.c
gdb a.out

打算调试下 binary_search(a, 5, 5) 这个组合。若如果用 print 大法,就在 binary_search 中插入几个 print,运行后扫一眼,看看 target=5 的时候运行流是怎样的。

debugger 大法看似会复杂一点,如果在 binary_search 中插断点,那么前两次调用只能连按 c 跳过。其实没那么复杂,gdb 允许用户设置条件断点。你可以这么设置:

b binary_search if target == 5

现在就只有第三次调用会触发断点。

问题看上去跟 floor 和 ceiling 值的变化有关。要想观察它们的值,可以 p floor 和 p ceiling。不过有个简单的方法,你可以对它们设置 watch 断点:wa floor if target == 5。当 floor 的值变化时,就会触发断点。

对于我们的示例程序来说,靠脑补也能算出这两个值的变化,专门设置断点似乎小题大做。不过在调试真正的程序时,watch 断点非常实用,尤其当你对相关代码不熟悉时。使用 watch 断点可以更好地帮助你理解程序流程,有时甚至会有意外惊喜。另外结合 debugger 运行时修改值的能力,你可以在值变化的下一刻设置目标值,观察走不同路径会不会出现类似的问题。如果有需要的话,还可以给某个内存地址设断点:wa *0x7fffffffda40。

除了 watch 之外,gdb 还有一类 catch 断点,可以用来捕获异常 / 系统调用 / 信号。因为用途不大(我从没实际用过),就不介绍了,感兴趣的话在 gdb 里面 help catch 看看。

command

参考 GDB command 命令

define

参考 GDB 自定义命令

几个 GDB 内建变量

$argc  自定义命令的参数个数
$arg0  自定义命令的第一个参数
$arg1  自定义命令的第二个参数
$arg2  自定义命令的第三个参数
$argN  自定义命令的第 N+1 个参数

举个有实际意义的例子。由于源代码的改变,我们需要更新断点的位置。通常的做法是删掉原来的断点,并新设一个。让我们现学现用,用宏把这两步合成一步:

# gdb_macro
define mv
    if $argc == 2
        delete $arg0
        # 注意新创建的断点编号和被删除断点的编号不同
        break $arg1
    else
        print "输入参数数目不对,help mv 以获得用法"
    end
end

# (gdb) help mv 会输出以下帮助文档
document mv
Move breakpoint.
Usage: mv old_breakpoint_num new_breakpoint
Example:
    (gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`
end
# vi:set ft=gdb ts=4 sw=4 et

将上述命令放在 .gdbinit 文件中,启动调试。
使用 mv 命令之前,查看断点

info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001200 in main at test.c:20
        i locals
        i args
2       breakpoint     keep y   0x000000000000117b in binary_search at test.c:5
	stop only if target == 5
        i locals
        i args
Breakpoint 3 at 0x117b: file test.c, line 5.

使用 mv

mv 1 5
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x000000000000117b in binary_search at test.c:5
	stop only if target == 5
        i locals
        i args
3       breakpoint     keep y   0x000000000000117b in binary_search at test.c:5

记账

设想如下的情景:某个项目出现内存泄露的迹象。事先分配好的内存池用着用着就满了,一再地吞噬系统的内存。内存管理是自己实现的,所以无法用 valgrind 来分析。鉴于内存管理部分代码最近几个版本都没有改动过,猜测是业务逻辑代码里面有谁借了内存又不还。现在你需要把它揪出来。一个办法是给内存的分配和释放加上日志,再编译,然后重新运行程序,谋求复现内存泄露的场景。不过更快的办法是,敲上这一段代码:

假设分配内存的接口是 my_malloc(size_t size),释放内存的接口是 my_free(char *p)

b my_malloc
comm
printf "my_malloc %lu\n", size
end

b my_free
comm
printf "my_free 0x%x\n", p
end

c 源码如下


void * my_malloc(size_t size)
{
    return malloc(size);
}

void my_free(void *p)
{
    free(p);
}

int main(int argc, char *argv)
{
    void *p1 = my_malloc(6);
    void *p2 = my_malloc(7);
    void *p3 = my_malloc(8);
    void *p4 = my_malloc(9);

    my_free(p1);
    my_free(p2);
    my_free(p3);

    return 0;
}

启动调试之后将 log 文件保存到 test.log
最终和 malloc 和 free 相关的日志如下

Breakpoint 3, my_malloc (size=6) at test.c:23
my_malloc 6
0x0000555555555247 in main (argc=1, argv=0x7fffffffe0a8 "D\343\377\377\377\177") at test.c:33
Value returned is $2 = (void *) 0x5555555592a0
Continuing.

Breakpoint 3, my_malloc (size=7) at test.c:23
my_malloc 7
0x0000555555555255 in main (argc=1, argv=0x7fffffffe0a8 "D\343\377\377\377\177") at test.c:34
Value returned is $3 = (void *) 0x5555555592c0
Continuing.

Breakpoint 3, my_malloc (size=8) at test.c:23
my_malloc 8
0x0000555555555263 in main (argc=1, argv=0x7fffffffe0a8 "D\343\377\377\377\177") at test.c:35
Value returned is $4 = (void *) 0x5555555592e0
Continuing.

Breakpoint 3, my_malloc (size=9) at test.c:23
my_malloc 9
0x0000555555555271 in main (argc=1, argv=0x7fffffffe0a8 "D\343\377\377\377\177") at test.c:36
Value returned is $5 = (void *) 0x555555559300
Continuing.

Breakpoint 4, my_free (p=0x5555555592a0) at test.c:28
my_free 0x555592a0
Continuing.

Breakpoint 4, my_free (p=0x5555555592c0) at test.c:28
my_free 0x555592c0
Continuing.

Breakpoint 4, my_free (p=0x5555555592e0) at test.c:28
my_free 0x555592e0
Continuing.

.gdbinit 文件

layout src

b main
b binary_search if target == 5

# 断点 1 触发执行的命令
command 1
i locals
i args
end

# 断点 2 触发执行的命令
comm 2
i locals
i args
end

# 自定义一个 print-tyustli 命令
define print-tyustli
    echo hello, world\n
end

# 自定义命令 print-tyustli 的帮助文档
document print-tyustli
    usage: print-list LIST NODE_TYPE NEXT_FIELD [COUNT]
    打印 tyustli

    data:   2023-09-27
    author: tyustli
end

# gdb_macro
define mv
    if $argc == 2
        delete $arg0
        # 注意新创建的断点编号和被删除断点的编号不同
        break $arg1
    else
        print "输入参数数目不对,help mv以获得用法"
    end
end

# (gdb) help mv 会输出以下帮助文档
document mv
Move breakpoint.
Usage: mv old_breakpoint_num new_breakpoint
Example:
    (gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`

end
# vi:set ft=gdb ts=4 sw=4 et

b my_malloc
comm
printf "my_malloc %lu\n", size
finish # 查看函数 malloc 返回的指针
end

b my_free
comm
printf "my_free 0x%x\n", p
end

参考

https://segmentfault.com/a/1190000005367875

你可能感兴趣的:(#,qemu-基础篇,gdb,命令脚本)