GDB实现原理和使用范例

一、前言

这篇文章为了让你深入了解gdb的工作原理,以及如何在linux环境下使用强大的gdb调试程序功能。

二、gdb工作原理

2.1. 程序调试信息是怎么生成的

我们先了解下程序是个什么东西?

root@100.115.21.77:/data/mariolu/tls13/openssl-OpenSSL_1_1_1/apps#file openssl

openssl: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=264f60f58cadd21c0df8c40dabe0b4babe9f9014, not stripped

输出信息显示openssl是个可执行程序,并且是ELF格式的文件。

使用readelf命令解析这个程序,-S指定打印section信息,-W不选择换行显示。这里会显示很多section,可能会包括名字带有stab或者debug的东西。

readelf -WS ./a.out | egrep '\.(stab |debug)'

这里的stabs或者debug又是什么东西呢。顾名思义,这些是编译进程序的debug信息。Linux当前主流的debug信息格式有STABS或者DWARF格式。如果二进制有STABS,会看到.stab。如果有DWARF,你将看到.debug_info,.debug_line等。如果你根本没有调试信息,那么你什么也看不到。

图1、elf程序的section信息包含了.debug块

这里的openssl编译使用的是DWARF的debug信息,所以可以使用dwarfdump -a openssl解析出dwarf基本单元DIE(DIE, Debugging Information Entry)。程序有很多像如下图所示的基本单元信息。

图2、dwarf的基本信息

每个DIE有:

  • 一个 TAG 属性表达描述什么类型的东西, 如: TAG_subprogram(函数)、TAG_formal_parameter(形式参数)、TAG_variable(变量)、TAG_base_type(基础类型)
  • N 个属性(attribute), 用于具体描述一个DIE

AT_low_pc, AT_high_pc 分别代表函数的 起始/结束 PC地址 AT_frame_base 表达函数的栈帧基址(frame base) 为寄存器 rbp 的值 AT_name 描述函数的名字为 s_client_main AT_decl_file 说这个函数在 apps/s_client.c 文件中声明 AT_decl_line 说这个函数在 foo.c 第879(十六进制36F)行声明 AT_prototyped 为一个 Bool 值, 为 True 时代表这是一个子程序/函数(subroutine) AT_type 属性描述这个函数返回值的类型是什么 AT_external Bool值, 这个函数是否为全局可访问

AT_sibling连接了兄弟DIE,通过它连接了所有的基本实体(DIE)

这些信息依赖于gdb编译时候加上-g生成(除了-g,还可以显示指定格式-gdwarf、-gstabs、-gxconff,但是-g可移植性更强,-ggdb可以生成针对gdb特定的格式。-ggdb3或者-ggdb2可以生成额外的调试信息,比如宏定义等)

-g 选项可以产生符合操作系统本地格式的调试信息(stabs、COFF、XCOFF ,或者 DWARF 2)。gdb可以基于这里调试信息进行工作。

GCC 允许你同时使用 -g 和 -O 选项。代码在-Ox(x是数字,代表优化等级)编译优化后可能会产生令人惊奇的结果:一些你声明的变量可能已经不存在了;控制流可能走到了你未曾想象到的位置;一些语句可能不会被执行,因为其计算结果是常量,或者其结果早已经被获得;一些语句可能在不同的地方被执行,因为其被移出了当前循环。 所以编译调试信息时务必加上-O0选项。

除了用readelf,dwarfdump,如果你更熟悉objdump,也可以用以下命令查看调试信息。

objdump -d openssl

如果知道是dwarf格式,可以显示指定

objdump -dwarf openssl | less

当然如果格式不对,objdump会输出无效的文件格式目标

objdump -stabs openssl

objdump: openssl: Invalid bfd target

2.2. gdb如何实现跟踪程序

一个elf程序中有symbol table,symbol是一段程序或者变量的符号链接,使用#nm -g openssl可以查看。当程序执行到某个symbol,根据dwarf描述的对应的debug信息就可以还原出当时的源代码(文件,行号)信息。

那么gdb是如果跟踪到进程当前执行的symbol所包含的信息。原来linux有个ptrace系统调用。ptrace该系统调用运行父进程追踪子进程的运行数据。gdb的跟踪调试功能设置程序的断点break point,父进程通过ptrace接管子进程除了SIGKILL之外所有的信号。当子进程(就是我们调试的程序)在发送break point或者单步调试,会产生一个信号SIGTRAP,被父进程(这里的gdb)捕获到,这时用户就可以通过gdb实时观察到当前的子进程状态。

三、gdb的使用方法

3.1. 启动gdb

gdb有三种使用方法:

  • 一个程序gdb <prog_name, 程序文件位置>。或者gdb后,然后在交互式框输入r <prog_name>。
  • 如果是一个正在运行的进程,比如服务器进程,那么可以用gdb attach <process id,需要从ps获取pid>, 或者gdb -p <process id>
  • 程序core掉了,gdb <prog_name> <core fie>

3.2. 选择core文件

常用的有:

  • -c <core file>:使用-c指定的core文件
  • -d <directory>指定源代码路径

这里我比较少用到有,读者有兴趣可以继续做这个方面的调研

  • -s <symbol file>读取符号文件
  • -e<exec file>执行gdb命令
  • -readnow,立即加载所有的符号表,启动变慢,以后的操作变快
  • -x:选择符号文件

3.3. gdb工作模式相关

  • -q 不输出基本信息和版权
  • -batch和-ex一般结合使用,实现批处理任务。比如说gdb binary –batch -ex “info functions clock”,显示binary中所有.*clock.*函数 , 然后退出gdb

举个比较实用的例子:

下面是非常有用的shell脚本用来查找指定函数,并在这些函数上设置断点,然后运行程序,在每次这些函数被调用的时候,打印出5层堆栈。程序结束,自动退出。如果设置足够多的函数断点,可以打印出所有的函数调用关系,然后后处理该脚本的输出,可以得到一个函数调用图。这是一个比较快捷的方法。

最后的args 文件中需要保存运行workbinary命令的参数。

funcs=`gdb –batch ../bin/Debug/workbinary -ex “info functions Foo” | sed -n “/(/ p” | cut -f 1 -d ‘(‘ | cut -f 2 -d ‘ ‘` rm -rf gdbcommands touch gdbcommands cnt=0 for f in $funcs do echo “b $f” >> gdbcommands cnt=`expr $cnt + 1` done

times=`seq 1 $cnt` for t in $times do echo “commands $t” >>gdbcommands echo “bt 5” >>gdbcommands echo “c” >> gdbcommands echo “end” >> gdbcommands echo “” >> gdbcommands done

gdb –batch ../bin/Debug/workbinary -x gdbcommands -x args

3.4. gdb运行相关

3.4.1查看变量

  • info local(当前函数的局部变量)
  • info b(当前设置的断点)
  • info registers(当前的寄存器值)
  • info watchpoint(观察点)
  • info args(函数传入形参)
  • info threads(当前线程)
  • info os(系统的一些信息,包含进程、信号量,共享内存,文件描述符等)
  • info sharedlibrary(动态库)
  • info stack(当前堆栈)

3.4.2. 打印

  • p <变量名>:打印某个变量名
  • x/<长度><n进制><字宽>,比如说x/10xb <地址>:打印<地址>开始的10个地址,以十六进制的单字节输出。这里的10可以是任意的长度,x还可以用x’, ‘d’, ‘u’, ‘o’, ‘t’, ‘a’, ‘c’, ‘f’, ‘s’),比如这里的d是数字的意思,s是字符串的意思。b可以用(’h’, ‘w’, ‘g’)替代,分别是双字节,4个字节,8个字节

3.4.3. 断点

  • b <文件名>:<行号>设置断点, b <symbol, function>在函数级别设置断点。
  • 删除断点:info b,然后在del <number>响应的断点
  • 设置条件断点:

break [break-args] if (condition)

condition <break_list> (condition)

  • 监控点:watch <var>,当var的值有变化时,程序暂停
  • c: 继续运行,直到遇到断点或者watchpoint

3.4.4. 堆栈相关:

  • bt:打印当前堆栈
  • finish:完成当前堆栈顶的函数,并退出到调用者
  • down:切换到调用者
  • up:切换到被调用者
  • f <number>: 堆栈的第几层
  • s 进入到下一层,如果有调用函数,则跳入到调用函数的里面
  • n:在该函数执行下一条语句指令,如果有函数,相当于把该函数执行完,这是同N最大的区别。

还有几个比较实用的命令:

  • set disassemble-next-line on,开启编译信息
  • set print pretty on:打印数据结构体信息更直观
  • info proc mappings:查看当前程序的进程空间地址分布
  • set env LD_LIBRARY_PATH /data/mariolu/tls13/openssl-OpenSSL_1_1_1/设置当前的运行环境
  • gdb -tui -p <进程id>:可视化界面调试
  • set指令:某些指针是void类型,但是知道是某种数据接口,就可以set $a=(SSL*)0x143a308,在这里定义一个临时变量$a,$a使用描述的地址指针并且解析成SSL指针的数据结构体。
  • 如果啥命令不敲直接回车,默认执行上一条语句

3.4.5. 其他:

当然gdb的命令还有很多,可以在gdb的help菜单中查看到“aliases,data,breakpoints,flies,internals,obscure,running,stack,support,tracepoints,user-defined主题”,感兴趣的读者可以进一步深入研究。

四、gdb使用注意事项

  • 检查有没有生成相对应的符号信息-g,如果要调试链接动态库的符号,也要确保LD_LIBRARY链接的库编译进了debug信息,
  • gcc编译时候除了开启-g选项,还需要-O0选项表明不进行代码优化。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏布尔

js也可以有自定义事件 注入就是这么爽

  在c#中有delegate,还有特殊的可以直接应用于事件编程的delegate,那就是event。而在js中没有c#的event,更没有delegate,有...

22270
来自专栏我爱编程

Day15进程和线程

多进程 multiprocessing multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结...

29350
来自专栏Linyb极客之路

工作流引擎之activiti任务监听器

任务监听器只能添加到流程定义中的用户任务中。 注意它必须定义在BPMN 2.0 extensionElements的子元素中, 并使用activiti命名空间,...

34320
来自专栏听Allen瞎扯淡

如何在单元测试中设置系统环境变量

有时我们需要通过读取系统环境变量来获取一些有用的信息,比如系统路径、临时目录等。在系统真正运行的时候我们可以通过启动命令行,如:java -Dxxx.xxx=x...

56820
来自专栏地方网络工作室的专栏

Shell 命令行统计 apache 网站日志访问IP以及IP归属地

Shell 命令行统计 apache 网站日志访问IP以及IP归属地 我的一个站点用 apache 服务跑着,积攒了很多的日志。我想用 shell 看看有哪些人...

26760
来自专栏技术翻译

JVM体系结构的解释

每个Java开发人员都知道字节码将由JRE(Java运行时环境)执行。但许多人并不知道JRE是Java虚拟机(JVM)的实现,它分析字节码,解释代码并执行它。作...

13420
来自专栏耕耘实录

几个Linux命令及脚本使用中的奇淫巧技

版权声明:本文为耕耘实录原创文章,各大自媒体平台同步更新。欢迎转载,转载请注明出处,谢谢

10820
来自专栏Python绿色通道

Python的进程

Python实现多进程的方式主要有两种:一种方法是使用os模块中的fork方法; 另一种是使用multiprocessing模块。这两种方法的区别在于前者仅适用...

12020
来自专栏java、Spring、技术分享

JVM监控及诊断工具

jstat用法 其中-gc可以换成-class 、-gcnew、-gcold等参数;而54992表示的JVM的进程id(可能通过上面的jps命令查看...

49020
来自专栏JAVA烂猪皮

JAVA多线程与并发学习总结

使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行;当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存...

8610

扫码关注云+社区

领取腾讯云代金券