• Linux GDB 调试


    1  调试信息和调试原理

      一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要调试程序含有调试符号信息。使用 gcc 编译程序时,如果加上 -g 选项即可在编译后的程序中保留调试符号信息。以下命令将生成一个带调试信息的程序 hello_world。

    gdb -g -o hello_world hello_world.c

    当然我们可以通过gdb来判断程序是否带有调试信息:

    gdb hello_world 

    如果gdb 加载成功以后,会显示如下信息:

    Reading symbols from /root/testclient/hello_server...done

    我们也可以使用 Linux 的 strip 命令移除掉某个程序中的调试信息。

    strip hello_world

    调试时建议关闭编译器的程序优化选项,因为程序优化后调试显示的代码和实际代码可能就会有差异了,这会给排查问题带来困难。

    gdb -g -O0 hello_world world_world.c
    

      


    2  启动GDB调试

    2.1  直接调试目标程序

    gdb hello_world   // gdb + 程序名

    2.2  附加进程

    当一个程序已经启动,我们想调试这个程序,但又不想重启这个程序时,可以通过使用 gdb attach 进程ID 来将gdb调试器附加到想要调试的程序上。

    gdb attach 进程ID

    当⽤ gdb attach 上⽬标进程后,调试器会暂停下来,此时可以使⽤ continue 命令让程序继续运⾏,或者加上相应的断点再继续运⾏程序。当调试完程序想结束此次调试时,⽽且不对当前进程有任何影响,可以在 GDB 的命令⾏界⾯输⼊ detach 命令 让程序与 GDB 调试器分离。

    (gdb)  detach

    2.3  调试core文件

    Linux 系统默认是不开启程序崩溃产⽣ core ⽂件这⼀机制的,我们可以使⽤ ulimit -c 命令来查看系统是否开启了这⼀机制。使用ulimit -c unlimited 直接将core文件的大小修改成不限制大小。然后就可以通过以下命令调试core文件:

    gdb filename corename

    通过调试core文件可以看到程序崩溃的地方,使用bt命令查看崩溃时的调用堆栈,进一步分析找到崩溃的原因。当有多个程序崩溃时,有时很难通过core文件的名称来判断对应的core文件。我们可以自己修改core文件的名称来解决该问题。通过修改/proc/sys/kernel/core_uses_pid 可以控制产生的 core 文件的文件名,修改方式如下:

    echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

     文件名各个参数的说明如下:

    参数名称参数含义(中文)
    %p 添加 pid 到 core 文件名中
    %u 添加当前 uid 到 core 文件名中
    %g 添加当前 gid 到 core 文件名中
    %s 添加导致产生 core 的信号到 core 文件名中
    %t 添加 core 文件生成时间(UNIX)到 core 文件名中
    %h 添加主机名到 core 文件名中
    %e 添加程序名到 core 文件名中

    假设现在的程序叫 test,我们设置该程序崩溃时的 core 文件名如下:

    echo "/root/testcore/core-%e-%p-%t" > /proc/sys/kernel/core_pattern 

    那么最终会在 /root/testcore/ 目录下生成的 test 的 core 文件名格式如下:

    -rw-------. 1 root root 409600 Jan 14 13:54 core-test-13154-1547445291  

     

    3  GDB常用调试命令

    命令名称命令缩写命令说明
    run r 运行一个程序
    continue c 让暂停的程序继续运行
    next n 运行到下一行
    step s 如果有调用函数,进入调用的函数内部,相当于 step into
    until u 运行到指定行停下来
    finish fi 结束当前调用函数,到上一层函数调用处
    return return 结束当前调用函数并返回指定值,到上一层函数调用处
    jump j 将当前程序执行流跳转到指定行或地址
    print p 打印变量或寄存器值
    backtrace bt 查看当前线程的调用堆栈
    frame f 切换到当前调用线程的指定堆栈,具体堆栈通过堆栈序号指定
    thread thread 切换到指定线程
    break b 添加断点
    tbreak tb 添加临时断点
    delete del 删除断点
    enable enable 启用某个断点
    disable disable 禁用某个断点
    watch watch 监视某一个变量或内存地址的值是否发生变化
    list l 显示源码
    info info 查看断点 / 线程等信息
    ptype ptype 查看变量类型
    disassemble dis 查看汇编代码
    set args   设置程序启动命令行参数
    show args   查看设置的命令行参数

    3.1  run 命令

    前面说的 gdb filename 命令只是附加的一个调试文件,并没有启动这个程序,需要输⼊ run 命令(简写为 r)启动这个程序。

    3.2  continue 命令

    当 GDB 触发断点或者使⽤ Ctrl + C 命令中断下来后,想让程序继续运⾏,只要输⼊ continue 命令即可(简写为 c)。

    3.3  break 命令

    break 命令(简写为 b)即我们添加断点的命令,可以使⽤以下⽅式添加断点:

    • break functionname,在函数名为 functionname 的⼊⼝处添加⼀个断点;
    • break LineNo,在当前⽂件⾏号为 LineNo 处添加⼀个断点;
    • break filename:LineNo,在 filename ⽂件⾏号为 LineNo 处添加⼀个断点。

    3.4  backtrace 与 frame 命令

    backtrace 命令(简写为 bt)⽤来查看当前调⽤堆栈。查看调用的堆栈信息后可以使⽤ frame + 堆栈编号 命令(简写为 f),切换⾄指定堆栈顶部。

    3.5  info break、 enable、 disable 和 delete 命令

    在程序中加了很多断点,⽽我们想查看加了哪些断点时,可以使⽤ info break 命令(简写为 info b)。

    (gdb) info b
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x0000000000423450 in main at server.c:3709 breakpoint already hit 1 time
    2 breakpoint keep y 0x000000000049c1f0 in _redisContextConnectTcp at net.c:267

      由上面的内容片段可以知道,目前一共增加了2个断点,断点1触发1次,断点2未触发过。我们想禁⽤某个断点时,使⽤“ disable 断点编号 ”就可以禁⽤这个断点了,同理,被禁⽤的断点也可以使⽤“ enable 断点编号 ”重新启⽤。使⽤“delete 编号”可以删除某个断点,如果输⼊ delete 不加命令号,则表示删除所有断点。

    3.6  list命令

      第⼀次输⼊ list 命令会显示断点处前后的代码,继续输⼊ list 指令会以递增⾏号的形式继续显示剩下的代码⾏,⼀直到⽂件结束为⽌。当然 list 指令还可以往前和往后显示代码,命令分别是“list + (加号) ”和“list - (减号) ”。

    3.7  print 和 ptype 命令

      通过 print + 变量名 可以打印出指定变量的值,print 命令也可以显示进⾏⼀定运算的表达式计算结果值,甚⾄可以显示⼀些函数的执⾏结果值。举个例子,我们可以使用 p a+b+c 来打印这三个变量的结果值;也可以使用 p func() 命令输出一个可执行函数 func() 的执行结果。

      print 命令不仅可以输出表达式结果,同时也可以修改变量的值,我们尝试将端⼝号从 6379 改成 6400 试试:

    (gdb) p server.port=6400
    $24 = 6400
    (gdb) p server.port
    $25 = 6400
    (gdb)

      ptype 命令,其含义是“print type”,就是输出⼀个变量的类型。

    3.8  info thread 和 info args命令

    ⽤ info thread命令来查看当前进程有哪些线程,分别中断在何处。

    (gdb) info thread
    Id Target Id Frame
    4 Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
    3 Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
    2 Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
    * 1 Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6

       通过 info thread 的输出可以知道 redis-server 正常启动后,⼀共产⽣了 4 个线程,包括⼀个主线程和三个⼯作线程,线程编号(Id 那⼀列)分别是 4、 3、 2、 1。三个⼯作线程(2、 3、 4)分别阻塞在 Linux API pthread_cond_wait 处,⽽主线程(1)阻塞在 epoll_wait 处。当有多个线程时,我们可以使用 backtrace 命令查看调用堆栈,通过过堆栈判断 GDB 作用在哪个线程上面。如何切换到其他线程呢?可以通过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输⼊ thread 2 即可。

       info 命令还可以⽤来查看当前函数的参数值,组合命令是 info args

    3.9  next、 step、 util、 finish 和 return 命令

      next 命令(简写为n)是让 GDB 调到下⼀条命令去执⾏,这⾥的下⼀条命令不⼀定是代码的下⼀⾏,⽽是根据程序逻辑跳转到相应的位置。这⾥有⼀个⼩技巧,在 GDB 命令⾏界⾯如果直接按下回⻋键,默认是将最近⼀条命令重新执⾏⼀遍,因此,当使⽤ next 命令单步调试时,不必反复输⼊ n 命令,直接回⻋就可以了。

      step 命令(简写为 s)就是“单步步⼊”(step into),顾名思义,就是遇到函数调⽤,进⼊函数内部。

      finish 命令会执⾏函数到正常退出该函数;⽽ return 命令是⽴即结束执⾏当前函数并返回,也就是说,如果当前函数还有剩余的代码未执⾏完毕,也不会执⾏了。

      until 命令(简写为 u)可以指定程序运⾏到某⼀⾏停下来。比如直接输入 u 1888,就可以快速执行完中间的内容,直接跳到1888行。当然也可以使用断点的方式,但是使用until命令会更便捷。

    3.10  set args 和 show args 命令

      很多程序需要我们传递命令⾏参数。在 GDB 调试中,很多⼈会觉得可以使⽤ gdb filename args 这种形式来给 GDB 调试的程序传递命令⾏参数,这样是不⾏的。正确的做法是在⽤ GDB 附加程序后,在使⽤ run 命令之前,使⽤“ set args 参数内容 ”来设置命令⾏参数

      如果单个命令⾏参数之间含有空格,可以使⽤引号将参数包裹起来。

    (gdb) set args "999 xx" "hu jj"
    (gdb) show args
    Argument list to give program being debugged when it is started is ""999 xx" "hu j
    j"".
    (gdb) 

      如果想清除掉已经设置好的命令⾏参数,使⽤ set args 不加任何参数即可。

    (gdb) set args
    (gdb) show args
    Argument list to give program being debugged when it is started is "".
    (gdb)

    3.11  tbreak 命令

      tbreak 命令也是添加⼀个断点,第⼀个字⺟“t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是⼀旦该断点触发⼀次后就会⾃动删除。添加断点的⽅法与上⾯介绍的 break命令⼀模⼀样,这⾥不再赘述。

    3.12  watch 命令

      watch 命令是⼀个强⼤的命令,它可以⽤来监视⼀个变量或者⼀段内存,当这个变量或者该内存处的值发⽣变化时, GDB 就会中断下来。被监视的某个变量或者某个内存地址会产⽣⼀个 watch point(观察点)。

     

    3.13  display 命令

      display 命令监视的变量或者内存地址,每次程序中断下来都会⾃动输出这些变量或内存的值。例如,假设程序有⼀些全局变量,每次断点停下来我都希望 GDB 可以⾃动输出这些变量的最新值,那么使⽤“ display变量名 ”设置即可。


    4  GDB 调试技巧

    4.1  将 print 打印结果显示完整

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

    4.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();
      //省略⽆关代码...
    }

      在这个程序中,我们接收到 Ctrl + C 信号(对应信号 SIGINT)时会简单打印⼀⾏信息,⽽当⽤ GDB 调试这个程序时,由于 Ctrl + C 默认会被 GDB 接收到(让调试器中断下来),导致⽆法模拟程序接收这⼀信号。解决这个问题有两种⽅式:在 GDB 中使⽤ signal 函数⼿动给程序发送信号,这⾥就是 signal SIGINT;改变 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)

    4.3  多线程下禁⽌线程切换

    假设现在有 5 个线程,除了主线程,⼯作线程都是下⾯这样的⼀个函数:

    void thread_proc(void* arg)
    {
      //代码⾏1
      //代码⾏2
      //代码⾏3
      //代码⾏4
      //代码⾏5
      //代码⾏6
      //代码⾏7
      //代码⾏8
      //代码⾏9
      //代码⾏10
      //代码⾏11
      //代码⾏12
      //代码⾏13
      //代码⾏14
      //代码⾏15
    }

      为了能说清楚这个问题,我们把四个⼯作线程分别叫做 A、 B、 C、 D。假设 GDB 当前正在处于线程 A 的代码⾏ 3 处,此时输⼊ next 命令,我们期望的是调试器跳到代码⾏ 4 处;或者使⽤“u 代码⾏10”,那么我们期望输⼊ u 命令后调试器可以跳转到代码⾏ 10 处。但是在实际情况下, GDB 可能会跳转到代码⾏ 1 或者代码⾏ 2 处,甚⾄代码⾏ 13、代码⾏ 14 这样的地⽅也是有可能的,这不是调试器 bug,这是多线程程序的特点,当我们从代码⾏ 4 处让程序 continue 时,线程A 虽然会继续往下执⾏,但是如果此时系统的线程调度将 CPU 时间⽚切换到线程 B、 C 或者 D 呢?那么程序最终停下来的时候,处于代码⾏ 1 或者代码⾏ 2 或者其他地⽅就不奇怪了,⽽此时打印相关的变量值,可能就不是我们需要的线程 A 的相关值。

       为了解决调试多线程程序时出现的这种问题, GDB 提供了⼀个在调试时将程序执⾏流锁定在当前调试线程的命令: set scheduler-locking on。当然也可以关闭这⼀选项,使⽤ set scheduler-locking off。

     

    4.4  条件断点

    所谓条件断点,就是满⾜某个条件才会触发的断点,这⾥先举⼀个直观的例⼦

    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;
    }

      在上述代码中,假如我们希望当变量 i=5000 时,进⼊ do_something_func() 函数追踪⼀下这个函数的执⾏细节。添加条件断点的命令是 break [lineNo] if [condition],其中 lineNo 是程序触发断点后需要停下的位置, condition 是断点触发的条件。这⾥可以写成 break 11 if i==5000,其中, 11 就是调⽤ do_something_fun() 函数所在的⾏号。当然这⾥的⾏号必须是合理⾏号,如果⾏号⾮法或者⾏号位置不合理也不会触发这个断点。

    4.5  使⽤ GDB 调试多进程程序

      在实际的应⽤中,如有这样⼀类程序,如 Nginx,对于客户端的连接是采⽤多进程模型,当 Nginx 接受客户端连接后,创建⼀个新的进程来处理这⼀路连接上的信息来往,新产⽣的进程与原进程互为⽗⼦关系,那么如何⽤ GDB 调试这样的⽗⼦进程呢?⼀般有两种⽅法:⽤ GDB 先调试⽗进程,等⼦进程 fork 出来后,使⽤ gdb attach 到⼦进程上去,当然这需要重新开启⼀个 session 窗⼝⽤于调试, gdb attach 的⽤法在前⾯已经介绍过了;GDB 调试器提供了⼀个选项叫 follow-fork,可以使⽤ show follow-fork mode 查看当前值,也可以通过set follow-fork mode 来设置是当⼀个进程 fork 出新的⼦进程时, GDB 是继续调试⽗进程还是⼦进程取值是 child),默认是⽗进程( 取值是 parent)。

  • 相关阅读:
    web安全性测试用例
    Postman界面介绍及实例(转)
    基于RFS(robot framework selenium)框架模拟POST/GET请求执行自动化接口测试
    python 将list中的元素按字母排序
    Python操作字典取Key对应的值
    excel的常用工具类
    事务隔离机制
    如何上传附件
    sql函数认识
    对导出poi报表的更深层次了解
  • 原文地址:https://www.cnblogs.com/lizhimin123/p/10416975.html
Copyright © 2020-2023  润新知