本科核心内容:

  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. leetcode 297二叉树的序列化与反序列化

    to_string(x) 将数字x转化为string atoi(x) 将char转化为int stoi(x) 将string 转化为int 采用中序遍历的顺序存储,NULL用#表示,以,分隔,O(n) ...

  2. Sublime text3 Version 3.2.2, Build 3211破解

    一.修改hosts hosts地址: C:\Windows\System32\drivers\etc #sublimetext 127.0.0.1 www.sublimetext.com 127.0. ...

  3. python之拷贝文件

    做了个小实验, 用于拷贝文件夹下面的jpg. 用于拓展, 可以引入类和方法, 拷贝你指定的任意类型的文件. import os src = 'C:\\Users\\Administrator\\Des ...

  4. web开发(三) 会话机制,Cookie和Session详解

    在网上看见一篇不错的文章,写的详细. 以下内容引用那篇博文.转载于<http://www.cnblogs.com/whgk/p/6422391.html>,在此仅供学习参考之用. 一.会话 ...

  5. centos7.5搭建svn

    1.安装svnyum install subversion 2.查看安装位置rpm -ql subversion 3.创建svn版本库目录mkdir -p /var/svn/svnrepos 4.创建 ...

  6. CSRF token的原理

    参考: http://www.cnblogs.com/zhaof/p/6281482.html 简介 django为用户实现防止跨站请求伪造的功能,通过中间件 django.middleware.cs ...

  7. centos v7.0配置sftp

    需求: 1.建立三个sftp帐号,admin,test1,test22.三个帐号分别在/home/sftp下拥有相应的目录3.test1和test2只能进入自己的目录,admin可以进入三个目录(ch ...

  8. 【数字图像处理】Bilateral Filters

    [数字图像处理]Bilateral Filters https://www.yuque.com/lart/idh721/bf 简单介绍 双边滤波是一种非线性的可以模糊图像并且能保留一定的边缘信息的技术 ...

  9. vue中,基于echarts 地图实现一个人才回流的大数据展示效果

    0.引入echarts组件,和中国地图js import eCharts from 'echarts' import 'echarts/map/js/china.js'// 引入中国地图 1. 设置地 ...

  10. MySQL_基础

    ## 数据库的基本概念 1. 数据库的英文单词: DataBase 简称 : DB 2. 什么数据库? * 用于存储和管理数据的仓库. 3. 数据库的特点: 1. 持久化存储数据的.其实数据库就是一个 ...