CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • 第1章高频C++11重难点知识解析

  • 第2章Linux GDB高级调试指南

    • 2.1 SSH 工具与 FTP 工具
    • 2.2 Makefile 与 cmake
    • 2.3 使用Visual Studio管理和阅读开源项目代码
    • 2.4 gdb 调试准备
    • 2.5 gdb常用命令详解——利用gdb调试redis
    • 2.6 使用gdb调试多线程程序
    • 2.7 使用gdb调试多进程程序——以调试nginx为例
    • 2.8 gdb实用调试技巧
      • 2.9 gdb tui——gdb图形化界面
      • 2.10 gdb 的升级版——cgdb
      • 2.11 使用VisualGDB调试
      • 2.12 本章总结与扩展阅读
    • 第3章C++多线程编程从入门到进阶

    • 第4章C++网络编程重难点解析

    • 第5章网络通信故障排查常用命令

    • 第6章高性能网络通信协议设计精要

    • 第7章高性能服务结构设计

    • 第8章Redis 网络通信模块源码分析

    • 第9章后端服务重要模块设计探索

    • C++后端开发进阶
    • 第2章Linux GDB高级调试指南
    zhangxf
    2023-04-05
    目录

    2.8 gdb实用调试技巧

    # 2.8.1 将print输出的字符串或字符数组显示完整

    当我们使用print命令打印一个字符串或者字符数组时,如果该字符串太长,print命令默认显示不全的,我们可以通过在gdb中输入set print element 0设置一下,这样再次使用print命令就能完整地显示该变量所有字符串了。

    void ChatSession::OnGetFriendListResponse(const std::shared_ptr<TcpConnection>& conn)
    {
        std::string friendlist;
        MakeUpFriendListInfo(friendlist, conn);
        std::ostringstream os;
        os << "{\"code\": 0, \"msg\": \"ok\", \"userinfo\":" << friendlist << "}";
        Send(msg_type_getofriendlist, m_seq, os.str());    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    以上代码,当我们第一次用print命令输出friendlist变量值时,只能显示部分字符串。当使用set print element 0设置以后就能完整地显示出来了。

    (gdb) n
    563         os << "{\"code\": 0, \"msg\": \"ok\", \"userinfo\":" << friendlist << "}";
    (gdb) p friendlist
    $1 = "[{\"members\":[{\"address\":\"\",\"birthday\":19900101,\"clienttype\":0,\"customface\":\"\",\"facetype\":2,\"gender\":0,\"mail\":\"\",\"markname\":\"\",\"nickname\":\"bj_man\",\"phonenumber\":\"\",\"signature\":\"\",\"status\":0,\"userid\":4,"...
    (gdb) set print element 0
    (gdb) p friendlist       
    $2 = "[{\"members\":[{\"address\":\"\",\"birthday\":19900101,\"clienttype\":0,\"customface\":\"\",\"facetype\":2,\"gender\":0,\"mail\":\"\",\"markname\":\"\",\"nickname\":\"bj_man\",\"phonenumber\":\"\",\"signature\":\"\",\"status\":0,\"userid\":4,\"username\":\"13811411052\"},{\"address\":\"\",\"birthday\":19900101,\"clienttype\":0,\"customface\":\"\",\"facetype\":0,\"gender\":0,\"mail\":\"\",\"markname\":\"\",\"nickname\":\"Half\",\"phonenumber\":\"\",\"signature\":\"\",\"status\":0,\"userid\":5,\"username\":\"15618326596\"},{\"address\":\"\",\"birthday\":19900101,\"clienttype\":0,\"customface\":\"\",\"facetype\":34,\"gender\":0,\"mail\":\"\",\"markname\":\"\",\"nickname\":\"云淡风轻\",\"phonenumber\":\"\",\"signature\":\"\",\"status\":0,\"userid\":7,\"username\":\"china001\"},...太长了,这里省略...
    
    1
    2
    3
    4
    5
    6
    7

    # 2.8.2 让被gdb调试的程序接收信号

    看下面的代码:

    void prog_exit(int signo)
    {
        std::cout << "program recv signal [" << signo << "] to exit." << std::endl;
    }
    
    int main(int argc, char* argv[])
    {
        //设置信号处理
        signal(SIGCHLD, SIG_DFL);
        signal(SIGPIPE, SIG_IGN);
        signal(SIGINT, prog_exit);
        signal(SIGTERM, prog_exit);
    
        int ch;
        bool bdaemon = false;
        while ((ch = getopt(argc, argv, "d")) != -1)
        {
            switch (ch)
            {
            case 'd':
                bdaemon = true;
                break;
            }
        }
    
        if (bdaemon)
            daemon_run();
            
        //省略无关代码...
     }
    
    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

    这个程序中,我们让程序在接收到Ctrl + c信号(对应信号值是SIGINT)时简单打印一行信息,当我们用gdb调试这个程序时,由于Ctrl + c默认会被gdb接收到(让调试器中断下来),导致我们无法模拟程序接收这一信号。解决这个问题有两种方式:

    1. 在gdb中使用signal函数手动给我们的程序发送信号,这里就是signal SIGINT 。

    2. 改变gdb信号处理的设置,通过handle SIGINT nostop print告诉gdb在接收到SIGINT时不要停止、并把该信号传递给调试目标程序 。

      (gdb) handle SIGINT nostop print pass
      SIGINT is used by the debugger. 
      Are you sure you want to change it? (y or n) y  
      
      
      Signal Stop Print Pass to program Description  
      SIGINT No Yes Yes Interrupt
      (gdb)  
      
      1
      2
      3
      4
      5
      6
      7
      8

    # 2.8.3 明明函数存在,添加断点时却无效的解决方案

    有时候,一个函数明明存在,并且我们的程序也存在调试符号,我们使用break functionName添加断点时,gdb却提示:

    Make breakpoint pending on future shared library load? y/n
    
    1

    即使我们输入y,添加的断点可能也不会被正确地触发。此时我们就需要改变添加断点的策略,使用该函数所在的代码文件和行号这种方式添加断点就能添加同样效果的断点。

    # 2.8.4 调试中的断点

    实际调试中,我们一般会用到三种断点:普通断点、条件断点和数据断点。

    数据断点是被监视的内存值或者变量值发送变化时触发的断点,前面章节中介绍watch命令时添加的部分断点就是数据断点。

    普通断点就是我们添加的断点除去条件断点和硬件断点以外的断点。

    下面重点来介绍一下条件断点。所谓条件断点,就是满足某个条件才会触发的断点。这里先举一个直观的例子:

    void do_something_func(int i)
    {
       i ++;
       i = 100 * i;
    }
    
    int main()
    {
       for(int i = 0; i < 10000; ++i)
       {
          do_something_func(i);
       }
    
       return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    上述代码中,假如我们希望当变量i=5000时,进入do_something_func函数追踪一下这个函数的执行细节。此时我们可以修改代码增加一个i=5000的if条件,然后重新编译链接调试,这样显然比较麻烦,尤其是对于一些大型项目,每次重新编译链接都需要花一定的时间,而且调试完了还得把程序修改回来。有了条件断点,我们就不需要这么麻烦了。直接添加一个条件断点即可,添加条件断点的命令是:break [lineNo] if [condition],其中lineNo是程序触发断点后需要停的位置,condition是断点触发的条件。我们这里可以写成break 11 if i==5000,这里的11就是调用do_something_fun 函数所在的行号。当然这里的行号必须是合理的行号,如果行号非法或者行号位置不合理也不会触发这个断点。

    (gdb) break 11 if i==5000       
    Breakpoint 2 at 0x400514: file test1.c, line 10.
    (gdb) r
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: /root/testgdb/test1 
    
    Breakpoint 1, main () at test1.c:9
    9          for(int i = 0; i < 10000; ++i)
    (gdb) c
    Continuing.
    
    Breakpoint 2, main () at test1.c:11
    11            do_something_func(i);
    (gdb) p i
    $1 = 5000
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    把i打印出来,gdb确实是在i=5000时停下来了。

    添加条件断点,还有一个方法就是先添加一个普通断点,然后使用condition 断点编号 断点触发条件这样的格式来添加。我们使用这种方式添加一下上述断点:

    (gdb) b 11
    Breakpoint 1 at 0x400514: file test1.c, line 11.
    (gdb) info b
    Num     Type           Disp Enb Address            What
    1       breakpoint     keep y   0x0000000000400514 in main at test1.c:11
    (gdb) condition 1 i==5000
    (gdb) r
    Starting program: /root/testgdb/test1 
    y
    
    Breakpoint 1, main () at test1.c:11
    11            do_something_func(i);
    Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
    (gdb) p i
    $1 = 5000
    (gdb) 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    同样的规则,如果断点编号不存在,也无法添加成功,gdb会提示断点不存在:

    (gdb) condition 2 i==5000
    No breakpoint number 2.
    
    1
    2

    # 2.8.5 自定义gdb调试命令

    有些场景下,我们需要根据自己的程序情况,自定义一些可以在调试时输出我们程序特定信息的命令。这个在gdb中很容易做到,只要在Linux用户根目录下,root用户就是**/root目录,非root用户,是/home/用户名这个目录。在上述目录中自定义一个.gdbinit文件即可,注意在Linux系统中这是一个隐藏文件,可以使用ls -a**命令查看;如果不存在,创建一个就可以。然后在这个文件中写上你自定义命令的shell脚本就可以。

    这里以apache web服务器的源码为例(apache server的源码下载地址可参见链接5),在源码根目录下有个文件叫 .gdbinit,这个就是apache server自定义的gdb命令:

    # gdb macros which may be useful for folks using gdb to debug
    # apache.  Delete it if it bothers you.
    
    define dump_table
        set $t = (apr_table_entry_t *)((apr_array_header_t *)$arg0)->elts
        set $n = ((apr_array_header_t *)$arg0)->nelts
        set $i = 0
        while $i < $n
    	if $t[$i].val == (void *)0L
    	   printf "[%u] '%s'=>NULL\n", $i, $t[$i].key
    	else
    	   printf "[%u] '%s'='%s' [%p]\n", $i, $t[$i].key, $t[$i].val, $t[$i].val
    	end
    	set $i = $i + 1
        end
    end
    
    # 省略部分代码
    
    # Set sane defaults for common signals:
    handle SIGPIPE noprint pass nostop
    handle SIGUSR1 print pass nostop
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    当然在这个文件的最底部,apache的配置了让gdb调试器不要处理SIGPIPE和SIGUSR1这两个信号,而是将这两个信号直接传递给被调试的程序本身(这里就是apache server)。关于这个上文也介绍过了。

    上次更新: 2025/05/19, 16:52:22
    2.7 使用gdb调试多进程程序——以调试nginx为例
    2.9 gdb tui——gdb图形化界面

    ← 2.7 使用gdb调试多进程程序——以调试nginx为例 2.9 gdb tui——gdb图形化界面→

    最近更新
    01
    第二章 关键字static及其不同用法
    03-27
    02
    第一章 auto与类型推导
    03-27
    03
    第四章 Lambda函数
    03-27
    更多文章>
    Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式