zoukankan      html  css  js  c++  java
  • 第05课:GDB常用命令详解(中)

    本科核心内容:

      info和thread命令

      next、step、util、finish和return命令

    5.1info和thread命令

      在前面使用info break命令查看当前断点时介绍过,info命令是一个复合指令,还可以用来查看当前进程的所有线程运行情况。下面以redis-server进程为例来演示一下,使用delete命令删除所有断点,然后使用run命令重启一下redis-server,等程序正常启动后,我们按快捷键Ctrl+C中断程序,然后使用info thread命令来查看当前进程有哪些线程,分别中断在何处:

    然后我们输入:info thread命令

      通过info thread的输出可以知道redis-server正常启动后,一共产生了4个线程,包括一个主线程和三个工作线程,线程编号(ID那一列)分别是1,2,3,4。三个工作线程(2,3,4)分别阻塞在Linux API futex_wait_cancelable处,而主线程(1)阻塞在epoll_wait处。

      注意:虽然第一栏的名称叫ID,但第一栏的数值不是线程的ID,第三栏括号里的内容(如LWP 17378)中,17378这样的数值才是当前线程真正的ID。那LWP是什么意思呢?在早期的Linux系统的内核里面,其实不存在真正的线程实现,当时所有的线程都是用进程来实现的,这些模拟线程的进程被称为Light Weight Process(轻量级进程),后来Linux系统有了真正的线程实现,这个名字仍然被保留了下来。

      读者可能会有疑问,怎么知道线程1就是主线程?线程2、线程3、线程4就是工作线程呢?是不是因为线程1前面有个星号(*)?错了,线程编号前面这个星号表示的是当前GDB作用于哪个线程,而不是主线程的意思。现在有4个线程,也就有4个调用栈堆,如果此时输入backtrace命令查看调用栈堆,由于当前GDB作用在线程1,因此backtrace命令显示的一定是线程1的调用堆栈。

      由此可见,堆栈#4的main()函数也证实了上面的说法,即线程编号为1的线程是主线程。

      如何切换到其他线程呢?可以通过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程2上去,只要输入thread 2即可,然后输入bt就能查看这个线程的调用堆栈了。

      因此利用info thread命令就可以调试多线程程序,当然用GDB调试多线程还有一个很麻烦的问题,我们将在后面的GDB高级调试技巧中介绍。请注意,当把GDB当前作用的线程切换到线程2上之后,线程2前面就被加上了星号;

      info命令还可以用来查看当前函数的参数值,组合命令是info args,我们找个函数值多一点的堆栈函数来试一下:

      上述代码片段切回至主线程1,然后切换到堆栈#2,堆栈#2调用处的函数是aeProcessEvents(),一共有两个参数,使用info args命令可以输出当前两个函数参数的值,参数eventLoop是一个指针类型的参数,对于指针类型的参数,GDG默认会输出该变量的指针地址值,如果想输出该指针指向对象的值,在变量名前面加*接引用即可,这里使用

    p *eventLoop命令。如果还要查看其成员值,继续使用变量名->字段名即可,在前面学习print命令时已经介绍过了,这里不在赘述。

      上面介绍的是info命令常用的三种方法,更多关于info的组合命令在GDB中输入help info就可以查看。

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

      这几个命令使我们用GDB调试程序时最常用的几个控制流命令,因此放在一起介绍。next命令(简写为n)是让GDB调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。举个例子:

    int a = 0;
    if(a == 0)
    {
        printf("a is equal to 9.
    ");
    )
    
    int b = 10;
    printf("b = %d.",b);
    

      如果当前GCB中断在上述代码第2行,此时输入next命令GDB将调到第7行,因为这里的if条件并不满足。

      这里有一个小技巧,在GDB命令行界面如果直接按下回车键,默认是将最近一条命令重新执行一遍,因此,当使用next命令单步调试时,不必反复输入n命令,直接回车就可以。

      next 命令用调试的术语叫“单步步过”(step over),即遇到函数调用直接跳过,不进入函数体内部。而下面的 step 命令(简写为 s)就是“单步步入”(step into),顾名思义,就是遇到函数调用,进入函数内部。举个例子,在 redis-server 的 main() 函数中有个叫 spt_init(argc, argv) 的函数调用,当我们停在这一行时,输入 s 将进入这个函数内部。

    //为了说明问题本身,除去不相关的干扰,代码有删减
    int main(int argc, char **argv) {
        struct timeval tv;
        int j;
        /* We need to initialize our libraries, and the server configuration. */
        spt_init(argc, argv);
        setlocale(LC_COLLATE,"");
        zmalloc_set_oom_handler(redisOutOfMemoryHandler);
        srand(time(NULL)^getpid());
        gettimeofday(&tv,NULL);
        char hashseed[16];
        getRandomHexChars(hashseed,sizeof(hashseed));
        dictSetHashFunctionSeed((uint8_t*)hashseed);
        server.sentinel_mode = checkForSentinelMode(argc,argv);
        initServerConfig();
        moduleInitModulesSystem();
        //省略部分无关代码...
     }
    

      演示一下,先使用 b main 命令在 main() 处加一个断点,然后使用 r 命令重新跑一下程序,会触发刚才加在 main() 函数处的断点,然后使用 n 命令让程序走到 spt_init(argc, argv) 函数调用处,再输入 s 命令就可以进入该函数了:

    (gdb) b main
    Breakpoint 3 at 0x423450: file server.c, line 3704.
    (gdb) r
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: /root/redis-4.0.9/src/redis-server 
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib64/libthread_db.so.1".
    
    Breakpoint 3, main (argc=1, argv=0x7fffffffe588) at server.c:3704
    3704    int main(int argc, char **argv) {
    (gdb) n
    3736        spt_init(argc, argv);
    (gdb) s
    spt_init (argc=argc@entry=1, argv=argv@entry=0x7fffffffe588) at setproctitle.c:152
    152     void spt_init(int argc, char *argv[]) {
    (gdb) l
    147
    148             return 0;
    149     } /* spt_copyargs() */
    150
    151
    152     void spt_init(int argc, char *argv[]) {
    153             char **envp = environ;
    154             char *base, *end, *nul, *tmp;
    155             int i, error;
    156
    (gdb)
    

      说到 step 命令,还有一个需要注意的地方,就是当函数的参数也是函数调用时,我们使用 step 命令会依次进入各个函数,那么顺序是什么呢?举个例子,看下面这段代码:

    1  int fun1(int a, int b)
    2  {
    3      int c = a + b;
    4      c += 2;
    5      return c;
    6  }
    7  
    8  int func2(int p, int q)
    9  {
    10      int t = q * p;
    11      return t * t;
    12  }
    13  
    14  int func3(int m, int n) 
    15  {
    16     return m + n;    
    17  }
    18  
    19  int main()
    20  {
    21      int c;
    22      c = func3(func1(1, 2),  func2(8, 9));
    23      printf("c=%d.
    ", c);
    24      return 0; 
    25  }
    

      

      上述代码,程序入口是 main() 函数,在第 22 行 func3 使用 func1 和 func2 的返回值作为自己的参数,在第 22 行输入 step 命令,会先进入哪个函数呢?这里就需要补充一个知识点了—— 函数调用方式,我们常用的函数调用方式有 _cdecl 和 _stdcall,C++ 非静态成员函数的调用方式是 _thiscall 。在这些调用方式中,函数参数的传递本质上是函数参数的入栈过程,而这三种调用方式参数的入栈顺序都是从右往左的,因此,这段代码中并没有显式标明函数的调用方式,采用默认 _cdecl 方式。

      当我们在第 22 行代码处输入 step 先进入的是 func2() ,当从 func2() 返回时再次输入 step 命令会接着进入 func1() ,当从 func1 返回时,此时两个参数已经计算出来了,这时候会最终进入 func3() 。理解这一点,在遇到这样的代码时,才能根据需要进入我们想要的函数中去调试。

      实际调试时,我们在某个函数中调试一段时间后,不需要再一步步执行到函数返回处,希望直接执行完当前函数并回到上一层调用处,就可以使用 finish 命令。与finish 命令类似的还有 return 命令,return 命令的作用是结束执行当前函数,还可以指定该函数的返回值。

      这里需要注意一下二者的区别:finish 命令会执行函数到正常退出该函数;而 return命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。我们用一个例子来验证一下:

    1  #include <stdio.h>
    2  
    3  int func()
    4  {
    5      int a = 9;
    6      printf("a=%d.
    ", a);
    7  
    8      int b = 8;
    9      printf("b=%d.
    ", b);
    10      return a + b;
    11  }
    12  
    13  int main()
    14  {
    15      int c = func();
    16      printf("c=%d.
    ");
    17  
    18      return 0;
    19  }
    

      在 main() 函数处加一个断点,然后运行程序,在第 15 行使用 step 命令进入 func() 函数,接着单步到代码第 8 行,直接输入 return 命令,这样 func() 函数剩余的代码就不会继续执行了,因此 printf("b=%d. ", b); 这一行就没有输出。同时由于我们没有在 return 命令中指定这个函数的返回值,因而最终在 main() 函数中得到的变量 c 的值是一个脏数据。这也就验证了我们上面说的:return 命令在当前位置立即结束当前函数的执行,并返回到上一层调用。

    (gdb) b main
    Breakpoint 1 at 0x40057d: file test.c, line 15.
    (gdb) r
    Starting program: /root/testreturn/test 
    
    Breakpoint 1, main () at test.c:15
    15          int c = func();
    Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
    (gdb) s
    func () at test.c:5
    5           int a = 9;
    (gdb) n
    6           printf("a=%d.
    ", a);
    (gdb) n
    a=9.
    8           int b = 8;
    (gdb) return
    Make func return now? (y or n) y
    #0  0x0000000000400587 in main () at test.c:15
    15          int c = func();
    (gdb) n
    16          printf("c=%d.
    ");
    (gdb) n
    c=-134250496.
    18          return 0;
    (gdb)
    

      再次用 return 命令指定一个值试一下,这样得到变量 c 的值应该就是我们指定的值。

    (gdb) r
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: /root/testreturn/test 
    
    Breakpoint 1, main () at test.c:15
    15          int c = func();
    (gdb) s
    func () at test.c:5
    5           int a = 9;
    (gdb) n
    6           printf("a=%d.
    ", a);
    (gdb) n
    a=9.
    8           int b = 8;
    (gdb) return 9999
    Make func return now? (y or n) y
    #0  0x0000000000400587 in main () at test.c:15
    15          int c = func();
    (gdb) n
    16          printf("c=%d.
    ");
    (gdb) n
    c=-134250496.
    18          return 0;
    (gdb) p c
    $1 = 9999
    (gdb)
    

      仔细观察上述代码应该会发现,虽然用 return 命令修改了函数的返回值,当使用print 命令打印 c 值的时候,c 值也确实被修改成了 9999 ,但是 GDB 本身认为的程序执行逻辑中,打印出来的 c 仍然是脏数据。这点在实际调试时需要注意一下。

      我们再对比一下使用 finish 命令来结束函数执行的结果。

    (gdb) r
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: /root/testreturn/test 
    
    Breakpoint 1, main () at test.c:15
    15          int c = func();
    (gdb) s
    func () at test.c:5
    5           int a = 9;
    (gdb) n
    6           printf("a=%d.
    ", a);
    (gdb) n
    a=9.
    8           int b = 8;
    (gdb) finish
    Run till exit from #0  func () at test.c:8
    b=8.
    0x0000000000400587 in main () at test.c:15
    15          int c = func();
    Value returned is $3 = 17
    (gdb) n
    16          printf("c=%d.
    ");
    (gdb) n
    c=-134250496.
    18          return 0;
    (gdb)
    

      结果和我们预期的一样,finish 正常结束函数,剩余的代码也会被正常执行。

      实际调试时,还有一个 until 命令(简写为 u)可以指定程序运行到某一行停下来,还是以 redis-server 的代码为例:

    1812    void initServer(void) {
    1813        int j;
    1814
    1815        signal(SIGHUP, SIG_IGN);
    1816        signal(SIGPIPE, SIG_IGN);
    1817        setupSignalHandlers();
    1818
    1819        if (server.syslog_enabled) {
    1820            openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
    1821                server.syslog_facility);
    1822        }
    1823
    1824        server.pid = getpid();
    1825        server.current_client = NULL;
    1826        server.clients = listCreate();
    1827        server.clients_to_close = listCreate();
    1828        server.slaves = listCreate();
    1829        server.monitors = listCreate();
    1830        server.clients_pending_write = listCreate();
    1831        server.slaveseldb = -1; /* Force to emit the first SELECT command. */
    1832        server.unblocked_clients = listCreate();
    1833        server.ready_keys = listCreate();
    1834        server.clients_waiting_acks = listCreate();
    1835        server.get_ack_from_slaves = 0;
    1836        server.clients_paused = 0;
    1837        server.system_memory_size = zmalloc_get_memory_size();
    1838
    1839        createSharedObjects();
    1840        adjustOpenFilesLimit();
    1841        server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    1842        if (server.el == NULL) {
    1843            serverLog(LL_WARNING,
    1844                "Failed creating the event loop. Error message: '%s'",
    1845                strerror(errno));
    1846            exit(1);
    1847        }
    

      这是 redis-server 代码中 initServer() 函数的一个代码片段,位于文件 server.c 中,当停在第 1813 行,想直接跳到第 1839 行,可以直接输入 u 1839,这样就能快速执行完中间的代码。当然,也可以先在第 1839 行加一个断点,然后使用 continue 命令运行到这一行,但是使用 until 命令会更简便。

    (gdb) r
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: /root/redis-4.0.9/src/redis-server 
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib64/libthread_db.so.1".
    
    Breakpoint 3, main (argc=1, argv=0x7fffffffe588) at server.c:3704
    3704    int main(int argc, char **argv) {
    (gdb) c
    Continuing.
    21574:C 14 Sep 06:42:36.978 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    21574:C 14 Sep 06:42:36.978 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=21574, just started
    21574:C 14 Sep 06:42:36.979 # Warning: no config file specified, using the default config. In order to specify a config file use /root/redis-4.0.9/src/redis-server /path/to/redis.conf
    
    Breakpoint 4, initServer () at server.c:1812
    1812    void initServer(void) {
    (gdb) n
    1815        signal(SIGHUP, SIG_IGN);
    (gdb) u 1839
    initServer () at server.c:1839
    1839        createSharedObjects();
    (gdb)
    

      

    5.3 小结

    本节课介绍了 info thread、next、step、util、finish 和 return 命令,这些也是 GDB 调试过程中非常常用的命令,请读者务必掌握。

  • 相关阅读:
    如何使用 Python 创建一名可操控的角色玩家
    Unity查找物体的四大主流方法及区别
    JavaFX桌面应用开发-鼠标事件和键盘事件
    profiler-gpu分析记录
    JavaFX桌面应用开发-Button(按钮)与事件
    CodeCombat代码全记录(Python学习利器)--Kithgard地牢代码1
    spine骨骼动画组件使用详解
    微信小程序animation
    LeetCode--不同路径
    Learning opencv续不足(七)线图像的设计D
  • 原文地址:https://www.cnblogs.com/wzqstudy/p/10249925.html
Copyright © 2011-2022 走看看