第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.\n"); ) 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.\n", 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.\n", a); 7 8 int b = 8; 9 printf("b=%d.\n", b); 10 return a + b; 11 } 12 13 int main() 14 { 15 int c = func(); 16 printf("c=%d.\n"); 17 18 return 0; 19 }
在 main() 函数处加一个断点,然后运行程序,在第 15 行使用 step 命令进入 func() 函数,接着单步到代码第 8 行,直接输入 return 命令,这样 func() 函数剩余的代码就不会继续执行了,因此 printf("b=%d.\n", 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.\n", 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.\n"); (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.\n", 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.\n"); (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.\n", 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.\n"); (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 调试过程中非常常用的命令,请读者务必掌握。