前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GDB实现原理和使用范例

GDB实现原理和使用范例

原创
作者头像
mariolu
修改2018-11-19 10:57:18
4.9K0
修改2018-11-19 10:57:18
举报

一、前言

这篇文章为了让你深入了解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块
图1、elf程序的section信息包含了.debug块

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

图2、dwarf的基本信息
图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选项表明不进行代码优化。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、gdb工作原理
    • 2.1. 程序调试信息是怎么生成的
      • 2.2. gdb如何实现跟踪程序
      • 三、gdb的使用方法
        • 3.1. 启动gdb
          • 3.2. 选择core文件
            • 3.3. gdb工作模式相关
              • 3.4. gdb运行相关
                • 3.4.1查看变量
                • 3.4.2. 打印
                • 3.4.3. 断点
                • 3.4.5. 其他:
            • 四、gdb使用注意事项
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档