• gdb调试方法总结



    tags: C++ gdb
    categories: [Debug]

    写在前面

    Ubuntu 20.04 x86_64
    gdb: 9.2

    总结一下 C/C++ 代码调试的艺术, 这本书讲了 gdb 和 vc 的调试方法, 虽然有一些小错误, 但是不影响看, 突击面试确实是很方便的.

    啃文档的话可以, 不过时间来不及了, 虽然技术是要慢慢沉淀的…

    当然我还看了 软件调试的艺术 这本书, 中外作者的文笔还是有所不同的…

    gdb 主要功能

    总览:

    支持的功能描述命令
    断点管理设置断点, 查看断点, 条件断点b(break), condition
    调试执行逐语句, 逐过程执行r(run), n(next),
    s(step), c(continue)
    查看数据查看变量数据, 内存数据p(print), bt, i(info)
    运行时修改变量值调试状态下修改变量的值
    显示源代码查看对应的源码l(list)
    搜索源代码查找源码search
    调用堆栈管理堆栈信息f(frame)
    线程管理多线程调试, 查看, 线程间跳转thread, i(info)
    进程管理调试多个进程
    核心转储文件分析分析 core dumped 文件
    调试启动方式不同方式调试进程(加载参数启动, 附加到进程, 通过 PID)-p

    其他常见命令

    • q: quit 退出
    • set args : 设置命令行参数(传入程序的命令行参数)
    • gdb attach : 附加到进程(通过 ps aux | grep 来查看PID)

    杂项

    通用的 Makefile

    EXECUTABLE:= main
    LIBDIR:=
    LIBS:=pthread
    INCLUDES:=.
    SRCDIR:=
    
    CC:=g++
    CFLAGS:= -g -Wall -O0 -static -static-libgcc -static-libstdc++ 
    CPPFLAGS:= $(CFLAGS)
    CPPFLAGS+= $(addprefix -I,$(INCLUDES))
    CPPFLAGS+= -I.
    CPPFLAGS+= -MMD
    
    RM-F:= rm -f
    
    SRCS:= $(wildcard *.cpp) $(wildcard $(addsuffix /*.cpp, $(SRCDIR)))
    OBJS:= $(patsubst %.cpp,%.o,$(SRCS))
    DEPS:= $(patsubst %.o,%.d,$(OBJS))
    MISSING_DEPS:= $(filter-out $(wildcard $(DEPS)),$(DEPS))
    #MISSING_DEPS_SOURCES:= $(wildcard $(patsubst %.d,%.cpp,$(MISSING_DEPS)))
    
    
    .PHONY : all deps objs clean
    all:$(EXECUTABLE)
    deps:$(DEPS)
    
    objs:$(OBJS)
    clean:
    	@$(RM-F) *.o
    	@$(RM-F) *.d
    
    ifneq ($(MISSING_DEPS),)
    $(MISSING_DEPS):
    	@$(RM-F) $(patsubst %.d,%.o,$@)
    endif
    -include $(DEPS)
    $(EXECUTABLE) : $(OBJS)
    	$(CC) -o $(EXECUTABLE) $(OBJS) $(addprefix -L,$(LIBDIR)) $(addprefix -l,$(LIBS))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    人生苦短, 我用 cmake

    窗口管理

    layout src
    layout asm
    layout split
    layout regs
    
    • 1
    • 2
    • 3
    • 4

    python 集成

    直接用python 或者py即可使用 python 命令.

    一个简单的例子如下:

    (gdb) py print("this is python")
    this is python
    
    • 1
    • 2

    当然主要是用来使用gdb库的.

    shell 集成

    shell 命令
    
    • 1

    快捷键

    • C-x a: 切换 TUI 模式(Text User Interface), 展示源码
    • C-n/C-p: 下一个/上一个命令
    • C-L: redraw 窗口, 刷新显示
    • C-x 1:
    • C-x 2:

    程序运行管理

    attach到进程

    ps aux | grep a.out
    
    gdb attach -p <PID>
    
    • 1
    • 2
    • 3

    运行

    启动程序

    r
    run
    
    • 1
    • 2

    继续运行

    中断点之后继续运行

    c
    cont
    continue
    
    • 1
    • 2
    • 3

    继续运行并跳过断点 N 次

    c N
    continue N
    
    • 1
    • 2

    继续运行直到当前函数结束(直接到函数调用位置)

    fin
    finish
    
    • 1
    • 2

    单步执行

    s
    step
    
    • 1
    • 2

    逐过程执行(跳过函数)

    n
    next
    
    • 1
    • 2

    逐指令执行

    // 从第一条指令开始(可以看到被 strip 的程序的 entry point 信息, 比较有用的一条命令
    starti
    // 单指令执行  Step one instruction exactly.
    si 
    stepi
    
    • 1
    • 2
    • 3
    • 4
    • 5

    跳转执行

    jump 位置
    
    • 1

    位置: 代码行或者函数地址

    一定要让 jump 之后程序执行仍有意义(正确执行), 就像 C 的 goto 一样, 最好不要轻易使用.

    assert 宏

    定义了NDEBUG之后, assert 不会中断程序, 用于调试

    查看/修改信息

    查看源码

    l
    list
    layout src
    
    • 1
    • 2
    • 3

    还可以指定函数名

    l main
    
    • 1

    设置一下每次显示代码的行数:

    (gdb) set listsize 1
    (gdb) l main
    5	int main(void) {
    
    • 1
    • 2
    • 3

    搜索源码

    search 正则表达式
    forward-search 正则表达式
    reverse-search 正则表达式
    
    • 1
    • 2
    • 3

    查看函数参数

    i args
    info args
    
    • 1
    • 2

    查看变量的值

    p variable_name
    print variable_name
    
    • 1
    • 2

    查看内存大小

    p sizeof (int)
    p sizeof (struct sockaddr)
    
    • 1
    • 2

    查看数组

    (gdb) l
    1	int main(int argc, char *argv[]) {
    2	    int a[10] = {0};
    3
    4	    return 0;
    5	}
    (gdb) p a
    $2 = {-134536472, 32767, 1431654832, 21845, 0, 0, 1431654496, 21845,
      -8496, 32767}
    (gdb) set print array on // 优化打印
    (gdb) p a
    $3 =   {-134536472,
      32767,
      1431654832,
      21845,
      0,
      0,
      1431654496,
      21845,
      -8496,
      32767}
    (gdb) n
    2	    int a[10] = {0};
    (gdb) n
    4	    return 0;
    (gdb) p a
    $5 =   {0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    查看变量类型

    ptype 可选参数 变量或类型
    
    • 1

    可选参数:

    • /r: raw 原始数据显示, 不替换 typedef
    • /m: member 不显示类的方法, 仅显示成员变量
    • /M: 显示类的方法
    • /t: typedef 不打印类中的 typedef 数据
    • /o: offset 打印结构体字段偏移量和大小
    whatis 变量或者表达式
    
    • 1

    信息较为简略

    查看结构体

    例如下面这个经典的二叉树节点结构体:

    struct TreeNode {
        int val;
        TreeNode *left;
        TreeNode *right;
        TreeNode() : val(0), left(nullptr), right(nullptr) {}
        TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
        TreeNode(int x, TreeNode *left, TreeNode *right)
            : val(x), left(left), right(right) {}
    };
    
    int main(int argc, char *argv[]) {
        TreeNode t1;
        auto t2 = new TreeNode(1);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    编译一波:

    g++ tree.cpp -g
    gdb a.out
    
    • 1
    • 2

    然后就可以直接打印节点信息:

    (gdb) start
    Temporary breakpoint 1 at 0x555555555149: file tree.cpp, line 12.
    Starting program: /home/zorch/code/book_debug/chapter_3.3/a.out
    
    Temporary breakpoint 1, main (argc=21845, argv=0x7ffff7fb22e8 <__exit_funcs_lock>) at tree.cpp:12
    12	int main(int argc, char *argv[]) {
    (gdb) p t1
    $1 = {val = 0, left = 0x555555555060 <_start>, right = 0x7fffffffded0}
    (gdb) set print pretty // 格式更漂亮
    (gdb) p t1
    $2 = {
      val = 0,
      left = 0x555555555060 <_start>,
      right = 0x7fffffffded0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    指针变量就用: p *pTreeNode来打印.

    Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
    t(binary), f(float), a(address), i(instruction), c(char), s(string)
    and z(hex, zero padded on the left).
    Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).

    自动显示变量值

    display 
    
    • 1

    例子:

    (gdb) l
    1	int t1(int i) {
    2	    if (i == 1) return 1;
    3	    return i + t1(i - 1);
    4	}
    5	int main(void) {
    6	    int ans = t1(10);
    7	    return 0;
    8	}
    
    (gdb) start // main 函数处命中临时断点
    Temporary breakpoint 1 at 0x1159: file x.c, line 5.
    Starting program: /home/zorch/code/book_debug/chapter_3.3/a.out
    
    Temporary breakpoint 1, main () at x.c:5
    5	int main(void) {
        
    (gdb) b 2 // 第二行设断点
    Breakpoint 2 at 0x555555555138: file x.c, line 2.
    
    (gdb) c // 往下走
    Continuing.
    
    Breakpoint 2, t1 (i=10) at x.c:2
    2	    if (i == 1) return 1;
    
    (gdb) display i // 设置自动变量
    1: i = 10
    (gdb) n
    3	    return i + t1(i - 1);
    1: i = 10
    (gdb) // 回车默认执行上一条命令
    
    Breakpoint 2, t1 (i=9) at x.c:2
    2	    if (i == 1) return 1;
    1: i = 9
    (gdb)
    3	    return i + t1(i - 1);
    1: i = 9
    (gdb)
    
    Breakpoint 2, t1 (i=8) at x.c:2
    2	    if (i == 1) return 1;
    1: i = 8
    
    (gdb) i display // 显示自动显示的变量
    Auto-display expressions now in effect:
    Num Enb Expression
    1:   y  i
    (gdb) undisplay 1 // 取消自动显示
    (gdb) n
    3	    return i + t1(i - 1);
    (gdb)
    
    Breakpoint 2, t1 (i=6) at x.c:2
    2	    if (i == 1) return 1;
    (gdb)
    3	    return i + t1(i - 1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    还可以用delete display 1 删除第一个自动显示变量, 或者(关闭/开启自动显示变量, 不删除)

    disable display 1
    enable display 1
    
    • 1
    • 2

    查看内存

    x /option address
    
    • 1

    具体选项包括:nfu 即,

    • n, number: 显示的单元数量, 默认 1 个单元(u 选项保证)

    • f, format: 格式

      Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
      t(binary), f(float), a(address), i(instruction), c(char), s(string)
      and z(hex, zero padded on the left).
      Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).

      • 十六进制 x(默认),
      • 八进制 o,
      • 二进制 t,
      • null 结尾字符串 s,
      • 机器指令 i
    • u, unit size:

      • 单元长度: 可选为b(字节),
      • h(半字,2 字节),
      • w(一字, 4 字节, 默认),
      • g(八字节)

    都用默认的话就只加/即可

    x / &node
    
    • 1

    查看寄存器

    i r
    info r
    info registers
    i all-registers
    
    • 1
    • 2
    • 3
    • 4

    查看调用栈

    当程序进行函数调用时, 这些调用信息(where, how)称为栈帧(frame).

    每一个栈帧的内容还包括调用函数的参数, 局部变量等.

    所有这些栈帧组成的信息称为调用栈.

    bt (可选参数, 指定显示数量)
    backtrace
    
    i f
    i frame // 栈信息
    i locals // 查看局部变量
    i args // 查看当前帧的所有函数参数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    切换栈帧:

    f 栈帧号
    f ad 栈帧地址
    frame 栈帧号
    frame ad 栈帧地址
    // 切换
    up
    down
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    MISC

    如果需要指定 entry point 然后执行调试, 需要使用 starti 命令, 然后使用i files, 即可查看 strip 之后的程序 的 entry-point, 否则直接用i files显示的并不是真实的 entry point

    断点

    普通断点: break

    源码某行设置断点

    break file_name:row_number
    // 例如
    break test.cpp:23 // 在第 23 行设置断点
    
    • 1
    • 2
    • 3

    与此同时, 可以通过行号偏移量设置断点, 如下:

    b +offset
    b -offset
    
    • 1
    • 2

    函数设置断点

    break func_name // (可通过 tab 补全)
    // 例如
    break main
    
    • 1
    • 2
    • 3
    1. 函数重载情况下, 会为每一个同名函数都设置断点, 如果需要指定函数, 可以加上函数签名(int) 或者类作用域限定::

    通过正则表达式设置断点

    rb <regex>
    rbreak <regex>
    // 例如: 
    rb func*
    
    • 1
    • 2
    • 3
    • 4

    指令地址

    如果没有调试信息(编译时未添加`-g), 需要通过地址信息来设置条件断点

    p func // 获取函数地址
    b * 0x304f0b
    
    • 1
    • 2

    条件断点( ★ \bigstar )

    基于行号

    b Breakpoint condition
    // 例如
    b test.cpp:80 if i==10
    
    • 1
    • 2
    • 3

    基于函数名

    b func if a==10
    
    • 1

    临时断点

    只命中一次, 就被自动销毁, 后续即使代码被调用多次也不会再次命中.

    tb breakpoint
    tbreak breakpoint
    
    • 1
    • 2

    事实上start命令就是相当于在main处设置临时断点然后开始执行

    断点管理

    查看断点信息

    i b
    i break
    i breakpoint
    info b
    info break
    info breakpoint
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    得到断点编号, 下面会用到.

    或者用

    info b 断点编号
    
    • 1

    开启/禁用断点

    enable 编号
    disable 编号
    
    • 1
    • 2

    可以使用范围:

    enable 4-6 // 启用编号为 4~6 的断点
    
    • 1

    开启一次

    类似于临时断点, 只命中一次, 与临时断点的不同在于, 开启一次的断点命中后不会被删除, 而是处于禁用状态

    enable once 断点编号
    
    • 1

    启用断点并删除

    相当于把一个被禁用的断点转换为临时断点

    enable delete 断点编号
    
    • 1

    启用断点并命中 N 次

    enable count N 断点编号
    
    • 1

    忽略前 N 次命中

    ignore 断点编号 N
    
    • 1

    删除断点

    删除所有

    delete
    
    • 1

    删除指定编号断点

    delete 断点编号 断点编号 ... 
    delete 5-7 // 指定范围
    
    • 1
    • 2

    删除指定行号的断点

    clear main.cpp:23
    
    • 1

    删除指定函数的断点

    clear func_name // 存在重载则全部删除
    
    • 1

    观察点/捕捉点

    很多时候, 程序只在一些特定条件下才出现 bug, 观察点就可以用来发现或者定位该类型的 bug.

    观察点 可以设置为监控一个变量或者一个表达式的值, 当这个值或者表达式的值发生变化时, 程序会暂停, 而不需要提前设置断点.

    设置观察点

    watch 条件
    // e.g.:
    watch count==5
    
    • 1
    • 2
    • 3

    读取/读写观察点

    rwatch 变量或表达式 // 读取观察点
    awatch 变量或表达式 // 读写观察点
    
    • 1
    • 2

    查看观察点

    i watchpoints
    
    • 1

    捕获点

    catch 事件
    
    • 1

    用于以下几种情况

    1. throw: C++抛出异常
    2. catch: C++捕获之后的语句块
    3. exec,fork,vfork: C 系统调用
    4. 动态链接库相关

    线程管理

    查看线程

    i threads // 查看当前进程所有线程的信息
    
    • 1

    切换线程

    thread 线程 ID // 通过 `i threads` 查看
    
    • 1

    指定线程设断点

    b 断点 thread 线程 ID
    
    • 1

    指定线程运行命令

    thread apply 线程 ID 线程 ID(可以有多个) 命令
    
    • 1

    例子:

    thread apply 2 3 i locals
    
    • 1

    核心转储文件调试

    Ubuntu20.04 为例, 需要先写入 core 文件格式:

    sudo systemctl disable apport.service # 关闭系统的日志分析
    echo "core-%e-%p-%t"> /proc/sys/kernel/core_pattern
    
    # 开 limit:
    ulimit -c unlimited
    ulimit -a
    -t: cpu time (seconds)              unlimited
    -f: file size (blocks)              unlimited
    -d: data seg size (kbytes)          unlimited
    -s: stack size (kbytes)             8192
    -c: core file size (blocks)         unlimited
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以备份一下默认的 core 格式:

    // Ubuntu
    |/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
    
    // ArchLinux
    |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • %e: 可执行程序名称
    • %p: PID
    • %t: 时间戳

    随便访问一下空地址, 就爆了:

    struct P{
        int a;
        char b;
    };
    
    int main(){
        P *p = 0;
        p->a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    结果:

    $ gcc aa.c && ./a.out
    Segmentation fault (core dumped)
    
    • 1
    • 2

    编译加上-g, 然后开gdb:

    gdb a.out
    
    • 1

    接着调试就好

    死锁调试

    bt

    th

    f

    l

    基本常用的就这么几个

    动态库调试

    分为静态加载和动态加载(dlopen), 如果有调试信息直接加载, 没有的话需要看堆栈

    内存调试

    主要通过 Asan 在编译阶段定位内存问题, 除此之外就是自己下断点设置自动变量(display)查看变量§和内存(x)的值, 进一步分析

  • 相关阅读:
    SaaSpace:9种最佳免费时间管理软件
    Redhat(3)-Bash-Shell-正则表达式
    【精讲】async,await简介及与Ajax合用案例(内含面试内容)
    作业 day4
    认识ICMP协议 —— ping命令的作用过程
    Dubbo invoke命令使用
    Tomcat中GET和POST请求时乱码解决
    Android 线程池源码详解(一)
    【译】C# 11 特性的早期预览
    Windows程序意外挂掉,但显存依然被占用
  • 原文地址:https://blog.csdn.net/qq_41437512/article/details/134264619