第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 调试过程中非常常用的命令,请读者务必掌握。

原文地址:https://www.cnblogs.com/wzqstudy/p/10249925.html