本科核心内容:

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

第05课:GDB常用命令详解(中)的更多相关文章

  1. 第05课:GDB 常用命令详解(上)

    本课的核心内容如下: run 命令 continue 命令 break 命令 backtrace 与 frame 命令 info break.enable.disable 和 delete 命令 li ...

  2. 第04课:GDB常用命令详解(上)

    本课的核心内容如下: run命令 continue命令 break命令 backtrace与frame命令 info break.enable.disable和delete命令 list命令 prin ...

  3. 第07课:GDB 常用命令详解(下)

    本课的核心内容: disassemble 命令 set args 和 show args 命令 tbreak 命令 watch 命令 display 命令 disassemble 命令 当进行一些高级 ...

  4. 第06课:GDB 常用命令详解(中)

    本课的核心内容: info 和 thread 命令 next.step.util.finish.return 和 jump 命令 info 和 thread 命令 在前面使用 info break 命 ...

  5. 第06课:GDB 常用命令详解(下)

    本课的核心内容: disassemble 命令 set args 和 show args 命令 tbreak 命令 watch 命令 display 命令 6.1 disassemble 命令 当进行 ...

  6. hbase shell基础和常用命令详解(转)

    HBase shell的基本用法 hbase提供了一个shell的终端给用户交互.使用命令hbase shell进入命令界面.通过执行 help可以看到命令的帮助信息. 以网上的一个学生成绩表的例子来 ...

  7. samtools常用命令详解(转)

    转自:samtools常用命令详解 samtools的说明文档:http://samtools.sourceforge.net/samtools.shtml samtools是一个用于操作sam和ba ...

  8. GDB scheduler-locking 命令详解

    GDB scheduler-locking 命令详解 GDB> show scheduler-locking     //显示线程的scheduler-locking状态GDB> set ...

  9. cisco常用命令详解

    cisco常用命令详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.常用命令用法展示 1.命令行模式的来回切换 yinzhengjie>enable #从用户模式切换到 ...

随机推荐

  1. maven将自己的springboot项目打包成jar包后,作为工具包引入其他项目,找不到jar中的类

    将springboot项目打包成jar包,作为工具包导入项目后,找不到jar中的类. 原因是:springboot项目使用了自动的打包插件. 原先的插件配置: <build> <pl ...

  2. Elastic Stack学习

    原文链接 Elastic Stack简称ELK,在本教程你将学习如何快速搭建并运行Elastic Stack. 首先你要安装核心开源产品: Elasticsearch: Kibana: Beats: ...

  3. Redis 入门 3.1 热身

    3.1 热身 1. 获得符合规则的键名列表 KEYS pattern pattern 支持 glob 风格通配符格式 语言 字符组 ? 匹配一个字符 * 匹配任意个(包括0个)字符 [] 匹配括号间的 ...

  4. yum本地源和网络源的配置

    一.yum本地源 1. 删除YUM库[root@tianyun ~]# rm    -rf    /etc/yum.repos.d/* 2.挂载安装光盘(临时):[root@tianyun ~]# m ...

  5. python 并发编程 多线程 死锁现象与递归锁

    一 死锁现象 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等 ...

  6. Laravel中一些要记住 的写法

    模型篇: 1.根据数据库部分URL返回完整的URL public function getImageUrlAttribute() { // 如果 image 字段本身就已经是完整的 url 就直接返回 ...

  7. CDH管理节点扩容磁盘步骤

    把4个节点加12G内存,把hive的heap调到6G,按group重启服务让配置生效 注: 停服务前在yarn的application webui查flink的application id yarn ...

  8. js 获取ip和城市

    <script src="http://pv.sohu.com/cityjson?ie=utf-8"></script>

  9. 使用RSA算法对接口参数签名及验签

    在不同的服务器或系统之间通过API接口进行交互时,两个系统之间必须进行身份的验证,以满足安全上的防抵赖和防篡改. 通常情况下为了达到以上所描述的目的,我们首先会想到使用非对称加密算法对传输的数据进行签 ...

  10. 一个Accecc_Token生成和缓存和读取类,微信/小程序开发必须学

    Access_Token是调用微信和小程序各种接口的临时凭证,有效期2小时(7200秒),很多接口都需要调用access_token接口生成一个access_token的,例如微信支付,微信分享,公众 ...