转载

使用gdb

开始

  • 近来在写网络服务器程序,涉及到了多线程,且由于网络的环境复杂的原因,常有未知情况发生,导致在程序中自行添加调试语句显得有些吃力
  • gdb 请出山来是一个最为正确的选择

使用

  • gdb 看起来是全命令行的东西,而且一启动就是一大片英文,实在让没用过的望而却步,其实真正常用的就只有那几个功能。
  • 接下来我可能不会对这些命令进行明确的定义功能,因为有时候某些功能放在不同的地方意义就不同了,所以我只用最简单的语言来记录它们。
  1. 首先是安装,在( Debian )系的发行版 Linux 上,使用

    apt-get install gdb 

    回车等一会就行,可能有的发行版已经装好了,但有的却没有。

  2. 首先不要打开这个 gdb ,因为这里面的东西的确看起来挺复杂的

  3. 我们要先有一个程序的源文件,假设是 C语言main.c
  4. 在里面写好正确的语句后,对它进行编译,链接,也就是调用 GCC ,这里使用 GCC 的命令是

    gcc -o main -g main.c 

    这个 -g 标志非常必要,意味着生成一些 符号信息 ,用人话说就是 gdb 所需要的东西,没有这个东西是无法使用 gdb 进行调试的。

  5. 紧接着,在当前目录下就有一个名为 main 的可执行文件,看好当前用户是否有权限执行它之后,使用如下命令开启调试旅程:

    gdb ./main 

    紧接着,出现一大堆英文,不要慌,静静的等待它们装逼,最后会出现一个

    (gdb)  

    光标在闪烁的时候,就证明 gdb 加载这个程序的符号信息完毕,你可以准备执行了

  6. 这时候,你只要输入一个字母 r , 就能让这个 main 程序执行起来

    (gdb) r 
  7. 如果这个程序没有问题的话,几乎就和在外面正常运行时一样, gdb 没有起到任何作用,但是一旦有任何问题的话, gdb 就会暂停在那个出错的地方,而不是继续执行。

    1. 这里说的出错,不一定代表着就是程序出错或者奔溃退出,有可能是程序接收到了某个信号,信号在 Linux 中并不少见,这对于某些特殊用途的程序而言,看到接收到的信号十分重要,例如网络程序。
  8. 对于 r 这个命令除了这么孤零零的使用以外,还可以对它进行传递参数,例如你的 main 程序需要两个参数,你可以这样

    (gdb) r 192.168.141.149 8889 

    这就和在外面使用:

    ./main 192.168.141.149 8889  

    的效果一样

  9. 第二个命令自然是,最常用的设置断点,这也是十分简单, 命令就一个字母 b

    1. 最简单的用法就是直接接着一个数字,代表着要设置断点的地点( ),例如

      (gdb) b 10 

      表示在当前的执行文件中的第十行设置一个断点,设置完了会有成功信息:

      Breakpoint 1 at 0x400556: file main.c, line 10.  

      意思就是,设置了第一个断点,在内存位置 0x400556 地方,人类能懂得地方在 main.c 文件的第十行

    2. 另一种情况就是,当你的程序是多个文件编译而成的时候,你如果不指定文件名,那么 默认就是在当前执行的这个地方所属的文件里设定

      (gdb) b main.c:10 

      这就是完整的语法,十分简单,这就是设置断点最常用的方式

    3. 但是在执行的时候,我怎么能记得住我要在哪里设置断点?难道要背下来?这里有几个方法可以参考一下。
      1. 首先当然就是你事先已经有一个完整的调试计划,记下在哪里设置断点自然是万事大吉
      2. 但通常由于程序员的自信小宇宙,我们坚信自己的程序是完美的,所以会先执行程序,在这个过程中假设我们碰到一个 错误gdb 帮我们及时停了下来,我们可以借助三个常用命令(后面写到)来帮助我们知道,自己现在身处何处,再设置断点
  10. 上面说到的 三个常用命令 之一,二 wherebt

    1. 之所以放在一起,因为这两个命令的意义差不多,至少在用的时候是这样,都是显示出当前执行位置的 函数栈 ,这对调试十分重要,所谓函数栈就是我们当前进入到哪个函数了,如果程序的函数嵌套挺深的时候,就能够用它来理清思路
    2. 并且这里说的函数栈并不只限于 程序员编写的函数 ,还包括所调用的系统函数。

      (gdb) where 

      直接在光标处写就行,写完回车就能够看见函数栈,但是如果你的函数嵌套过深这也不是一个好办法,但聊胜于无

      (gdb) bt 

      与上方一致。输出信息大概是这样(用我调试网络程序时收到一个SIGPIPE的情况为例)

      #0  0x00007ffff727cbc0 in __write_nocancel () at ../sysdeps/unix/syscall-template.S:81 #1  0x0000000000401ea6 in write_n (handle=5,  out_buf=0x7fffffff1a10 "Http/1.0 200 OK/r/nServer: Wu shxin HTTP Server/r/nContent-length: 276/r/nContent-type: text/html/r/n/r/n", buf_len=94) at /root/ClionProjects/httpd2/iofunc.cpp:68 #2  0x0000000000402657 in serve_static (client_fd=5,  filename=0x7fffffff9a80 "./home.html", file_size=276) at /root/ClionProjects/httpd2/deal_client/server_static.cpp:37 #3  0x00000000004021b4 in deal_client (client_fd=5) at /root/ClionProjects/httpd2/deal_client/dealclient.cpp:77 #4  0x0000000000401cac in deal_server (listen_fd=3, listen_fd_type=2) at /root/ClionProjects/httpd2/server.cpp:154 #5  0x00000000004017c4 in main (argc=3, argv=0x7fffffffe1b8) at /root/ClionProjects/httpd2/main.cpp:31 

      稍微有点长,但是还是很清楚的,每个 # 后面是一个栈, 括号中是参数名和参数真实值。

    3. 稍微分析一下就能大概确定断点应该设置在什么地方,如果觉得栈太深,可以在某些栈的入口处设断点,好好分析,但总归函数嵌套太深就是模块化做的不好。( 比如我这个程序就是。所以推倒重构了。 ,都是泪,写程序前一定要先画好构造图,不然会越写越复杂,写的时候不要想着做更多的事,而要做好一件事。)
  11. 三个常用的最后一个就是 list

    1. 在你运行到某处,被 gdb 停下来的时候,除了使用 where 或者 bt ,总体浏览一下函数栈,还能具体的查看一下现在执行到哪行代码了

      (gdb) list 

      当然这个可能对系统函数没什么作用,因为它可能会显示:

      76      in ../sysdeps/unix/syscall-template.S 

      这个时候你就需要去它的上一层中查看错误了,但正常情况下, list 会将你所执行位置语句的上下几行一起显示出来。

  12. 回到最开始的 r 命令,现在可以知道它是让程序开始执行的命令,那么和他有相似效果,但是用处不同的另外两个命令分别是 cn

  13. c 代表着 continue ,也就是忽略这个断点或者错误继续执行的意思(比如收到某个信号)
  14. n 代表着 next , 也就是执行到下一条语句,然后暂停。

  15. 最后就是怎么退出 gdb

    (gdb) q 

    就行了。

    小结

  • r <para1> ... 是运行程序
  • c 是继续运行程序
  • n 是执行下一条语句
  • wherebt 是显示函数栈
  • list 是显示当前运行语句的上下文( 此上下文非操作系统概念中的上下文 )
  • q 是退出 gdb 调试
  • 这些就是最常用的几个单线程的调试方式,没错单线程。

    以上是逻辑清晰的 单线程/单进程 程序的调试,其实多进程/多线程的调试也是差不多的类型

多进程/多线程的调试

  • 两个除了操作上有些命令不同,实质上的调试思想是差不多的,都是“变量控制”的方法。
  • 有个命令首先要说: attach

    • 这是用来调试多进程的命令,想要调试多进程的程序,会稍微麻烦一些,首先要启动程序(不需要加载到 gdb 中),启动后,在 shell 中用 ps 命令查看该进程延伸出的子进程,并且记下 子进程的进程号
    • 启动 gdb ,并且使用 attach ,假设要调试的进程号是 4488

      $ gdb ... (gdb) attach 4488 # ... 加载符号信息 # 加载完毕后,会停在有光标的状态,这时候你可以设置断点,或者选择继续 (gdb) ... 
  • 当然,如果一开始只有一个进程,但是在你调试的某个过程中会有 fork 操作,这时候可以使用另一个命令来明确说明要继续跟踪父进程还是调试子进程

    (gdb) set follow-fork-mode child 

    这个命令有点长,不太好记,可以抄写在某个地方,用到的时候再看(我就是如此)

    与之对应的自然就是 set follow-fork-mode parent

  • 多线程,也很类似

    • 两个重要的命令先说 info threadsthread <ID>
    • info threads 是显示当前程序的所有线程信息,并且会给每个线程配上一个相应的 ID ,也就是第一列显示的 ID ,而不是后面显示的。
    • 使用这个 ID 可以在调试线程之间进行切换
    • 切换的命令则是 thread

      (gdb) info threads Id   Target Id         Frame                    * 1    process 19515 "main" main (argc=1,    argv=0x7fffffffe1f8) at main.c:12  ... 
    • 当前线程前方会有一个 * 显示。

      (gdb) thread 2 

      表示切换到 2 号线程调试

  • 最后,关于多线程调试有一个高级一点的命令,就是如果我们在调试的时候,不希望其它线程运行而干扰到本线程的调试,可以使用

    (gdb) set scheduler-locking on 

    这样一来就只有所调试的线程才会执行,对于这个命令的其他两个参数 offstep
    分别是 关闭这个功能 单步执行( n )情况下才不允许其他线程运行 的效果。

总结

  • 以上便是全部 gdb 常用的操作,记住了他们也就能满足绝大部分的调试工作了,比带图形界面的调试器更有效率。
原文  http://www.wushxin.top/2016/03/19/使用gdb.html
正文到此结束
Loading...