首发于 编程技术

gdb

作者:phylips@bmy 2020-10-28

如何定位coredump的直接原因?gdb查看汇编代码?如何对应到源代码?
如何编写gdb脚本?如何阅读gdb源码?如何修改gdb源码?

1 gdb常用命令及技巧

blog.sina.com.cn/s/blog

wizardforcel.gitbooks.io

sourceware.org/gdb/curr

gdb Debugging Full Example (Tutorial): ncurses

Give me 15 minutes & I'll change your view of GDB


0.变量符号与表达式


gdb可以看做是对当前程序语言的扩展,在语法上它支持当前程序中定义的变量/函数/表达式。同时用户可以在调试时增加自定义的临时变量,也可以直接调用某个函数,或者直接修改内存内容。


a.变量

sourceware.org/gdb/curr

Variables accessible are those of the lexical environment of the selected

stack frame, plus all those whose scope is global or an entire file.


变量是组成表达式的基本元素之一。对于gdb来说,首先是针对当前frame来说,可见的变量有如下几种:

1.全局/static变量

2.根据本程序设计语言的名字空间规则,当前frame所在的执行点visible的那些变量


可以通过如下方式引用函数/文件内部定义的static变量:

'file'::variable
function::variable
namespace::class::name #c++类静态变量


变量分两种:被debug的程序本身定义的变量,这种变量直接用对应的变量名引用即可;在gdb中为了方便支持使用临时定义的变量,这种变量需要通过 $ 引用,以与程序本身定义的变量进行区分,另外寄存器内容也需要通过$register进行引用,因此这种变量不能与寄存器重名。


b.符号

sourceware.org/gdb/curr

符号包含:函数 变量名 类型


c.表达式

sourceware.org/gdb/curr

Any kind of constant, variable or operator defined by the programming language you are using is valid in an expression in GDB. This includes conditional expressions, function calls, casts, and string constants. It also includes preprocessor macros, if you compiled your program to include this information


当前程序设计语言中的常量/变量/operator在gdb中都是合法的表达式。这包括条件表达式/函数调用/casts/字符串常量。

c++支持:

ftp.gnu.org/old-gnu/Man

d.函数调用

sourceware.org/gdb/onli

可以直接在gdb中调用源程序中定义的函数。需要有一个正在gdb中的程序,如果只是gdb core文件只能print core中变量,不允许调用函数。


1.print

print/FMT EXP

对于c++程序来说,print的变量要满足名字空间查找规则,需要结合当前上下文,加上合适的namespace,如果类型仍然无法识别,可以再加上单引号试试。除了变量之外,如果我们知道一个addr及它对应的type,也可以对它进行print。如下所示:

p  *(('namespace::classname' *) 0xthis_address) #或者 p {type}addr
b '(anonymous namespace)::foo'
p 'id<int>(int)'(i) #调用模板函数

对于c++的函数或者变量,如果在gdb中不方便通过原始符号名找到,可以尝试使用nm找到它mangle之后的符号名进行print或其他操作。

a.字符串/数组

set print elements 0 #有时候字符串/数组太长,print会被截断,通过如下命令可以打印所有内容
p array[index]@num #print数组从index开始的num个元素
set print array-indexes on #打印数组下标


b.stl map


将附件: stl_views.gdb.txt 下载到本地。

打开gdb,在里面执行:source stl_views.gdb.txt

之后即可使用pmap pset等命令查看map set的内容。

stl支持: sourceware.org/gdb/wiki

对于嵌套的stl类型,可以先用pvector pmap打印,打印出的内容会用$i表示里面的元素,对于嵌套类型来说,直接再对应$i执行pvector pmap等命令。具体可参考: yolinux.com/TUTORIALS/G

c.ptype

后面跟变量或者类型名。

d.x

与print的区别是,print针对的是变量,会结合符号表进行解析。x针对的内存数据,都是bytes。

x/nfu addr

n:输出单元的个数

f:输出单元的格式, o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char) and s(string)

u:输出单元的大小,b(byte), h(halfword), w(word), g(giant, 8 bytes)


e.模板类

有时候想print某个类的静态成员变量,但是该类属于模板类,不好确定某个模板类的symbol,可以通过whatis具体的模板类对象,找到正确的类类型,再进行print。

(gdb) whatis tcmalloc::Static::pageheap_->pagemap_
type = TCMalloc_PageMap3<35>

2.break

sourceware.org/gdb/onli

breakpoint 可以根据行号、函数、条件生成断点。

break [PROBE_MODIFIER] [LOCATION] [thread THREADNUM] [if CONDITION]

break 可带如下参数:

linenum 本地行号,即list命令可见的行号

filename:linenum 指定文件的行号

function 函数,可以是自定义函数也可是库函数,如open

filename:function 指定文件中的函数

*address 地址 可是函数,变量的地址,此地址可以通过info add命令得到。

namespace::class::memberfunc c++成员函数


条件断点:CONDITION支持任意合法的条件表达式


tbreak:设置方法与break相同,只不过tbreak只在断点停一次,过后会自动将断点删除


3.catch

catchpoint 监测事件的产生。支持多种事件类型,例如c++的throw,或者动态库的加载,fork/exec/signal。

catch throw #c++程序有时候会因为异常没有捕获导致程序退出,直接捕获抛出异常时的堆栈
catch syscall [name | number] #系统调用
catch load [REGEX] #在load动态库时停住,可以调查加载的动态库路径

tcatch:与catch的区别是只捕获一次

4.watch

watchpoint 监测变量或者表达式的值发生变化时产生断点。

watch [-l|-location] EXPRESSION
watch #变量值被改变时,程序就会暂停住
watch expr thread threadnum #只有编号为threadnum的线程改变了变量的值,程序才会停下来
rwatch #当发生读取变量行为时,程序就会暂停住
awatch #当发生读写变量行为时,程序就会暂停住


5.thread

sourceware.org/gdb/onli

a.基本命令

info threads [1 2 3]
thread 1 #切换到线程1
thread apply [thread-id-list | all [-ascending]] [flag]… command
thread apply all bt #打印所有线程堆栈 

b.调试多线程程序时,一旦程序断住,所有的线程都处于暂停状态。当你调试其中一个线程时(比如执行“step”,“next”命令),所有的线程都会同时执行。如果想在调试一个线程时,让其它线程暂停执行,可以使用“set scheduler-locking on”命令。


c.thead local变量

thread local变量: Debugging __thead variables from coredumps


6.子进程

sourceware.org/gdb/onli

set follow-fork-mode child #调试子进程
set follow-fork-mode parent #调试父进程
set detach-on-fork off #同时调试父子进程,在调试一个进程时,另外一个进程处于挂起状态
info inferior #显示当前被调试的进程状态,*代表正在被调试的进程
inferior 2 #切换到另一个进程进行调试
set schedule-multiple on #让父子进程都同时运行

7.debug info

sourceware.org/gdb/onli

1.从binary中分离debug info

objcopy --only-keep-debug foo foo.debug
strip -g foo

2.gdb时debug info文件搜索路径

以/usr/bin/ls为例,当然ls也可以换成so文件。

- /usr/lib/debug/.build-id/ab/cdef1234.debug
- /usr/bin/ls.debug
- /usr/bin/.debug/ls.debug
- /usr/lib/debug/usr/bin/ls.debug.


8.其他

set logging on
set logging file log.txt
gdb --batch --ex "set height 0" -ex "bt" [可执行文件] [core文件] #非交互式模式
gdb --batch -x 脚本文件 [可执行文件] [core文件] #非交互式模式
set args a b c #设置程序支持参数
list *addr #查看addr处指令对应的代码片段
info symbol 0x400a46 #查看某地址对应的符号表信息
info line *0x400a46 #查看某地址对应的代码行,类似addr2line
add-inferior [ -copies n ] [ -exec executable ] #同一个回话加载多个binary进行调试
clone-inferior [ -copies n ] [ infno ] #clone现有的inferior
gcore #产生core文件
directory xxx #设置源文件查找路径
!cmd #执行shell命令cmd
gdb --tui #进入文本图形界面,可以方便的看到多个窗口,类似可视化界面
command 2 #设置断点2 hit时执行的命令,可以利用这个进行问题复现,并在复现时停住,回退状态
reverse-step i 
reverse-continue 

2 gdb脚本基础


有时候为了方便打印需要debug的内容,需要编写自己的gdb脚本,比如前面的stl打印。

gdb脚本编写有两个最重要的内容:函数定义,与条件/循环语句编写。

函数里面可以调用任意gdb命令,同时gdb里面执行:source xxx.gdb.script之后即可调用里面定义的相关函数。


这里需要理解的一点是:gdb脚本本身是gdb命令,同时也是当前debug的程序设计语言的扩展,支持当前程序设计语言的相关语法。所以有些语句需要避免gdb在解析这些语句时产生混淆,需要加入新的关键词和语法。 因此脚本的语法本身是gdb命令和程序设计语言的结合。


关闭分页显示


有时候print的行数太多,gdb会进行分页,并要求用户输入回车才能继续打印:

---Type <return> to continue, or q <return> to quit---

如果要关闭该功能可以采用如下命令:

set pagination off   

1.变量与表达式

支持所有合法的gdb变量和表达式,具体语法详见1.1.1或 web.mit.edu/gnu/doc/htm

主要需要说明的是$与赋值语句。对于变量来说,支持引用源程序中的变量(直接引用变量名),gdb临时定义的变量以及寄存器(需要用$引用)。对于赋值语句,不能直接像c++里面那样赋值:x=4。

sourceware.org/gdb/curr

(gdb) $var = 4
Undefined command: "$var".  Try "help".

从命令解析的角度说,直接那样赋值,gdb会把第一个单词当成自己的命令进行解析。因此需要增加一个关键词进行标识,避免混淆。gdb中给一个变量可以采用如下方法赋值:

print x=4 #先设置x值为4,然后再print
set x=4 #直接设置x值为4

但是这样依然可能混淆,因为gdb本身也是用set来设置自己的各种配置参数,比如如果用户程序里面刚好定义了一个与gdb配置参数同名的变量,直接set也会报错:

(gdb) set width=47
A syntax error in expression, near `=47'.

需要在加上新的关键词var标识这是程序变量,避免这种情况:

set var width=47


2.define

sourceware.org/gdb/curr

通过如下特殊变量对函数参数进行引用

$argc代表参数个数,$arg0....$argN,用来引用输入参数

define add
  if $argc == 2
    print $arg0 + $arg1
  if $argc == 3
    print $arg0 + $arg1 + $arg2
end


3.条件与循环

sourceware.org/gdb/curr

核心语句格式:

if ... [else] ... end
while ...[loop_break]...[loop_continue]... end


示例如下:

define ptclist
    set $head='tcmalloc::ThreadCache::thread_heaps_'
    while $head != 0
        print $head
        if $head == 0x576ff700
            p *($head)
            p $head->list_[31]
            p $head->list_[31].list_
            loop_break
        set $head = $head->next_
end


4.printf

与c语言类似:printf (template, expressions…);

printf "foo, bar-foo = 0x%x, 0x%x\n", foo, bar-foo


5.其他

a.如果想要在gdb执行某个命令的时候插入一些自定义动作可以通过hook机制:

sourceware.org/gdb/curr


3 gdb汇编基础

深入浅出GNU X86-64 汇编

cs.cmu.edu/~fp/courses/

3.1 基本语法、寄存器、寻址

a.语法风格

AT&T 语法和 Intel 语法
注意GNU工具使用传统的AT&T语法。类Unix操作系统上,AT&T语法被用在各种处理器上。
Intel语法则一般用在DOS和Windows系统上。下面是AT&T语法的指令:
movl %esp, %ebp
movl是指令名称。%则表明esp和ebp是寄存器.在AT&T语法中, 第一个是源操作数,第二个是目的操作数。
在其他地方,例如interl手册,你会看到是没有%的intel语法, 它的操作数顺序刚好相反。下面是Intel语法:
MOVQ EBP, ESP
当在网页上阅读手册的时候,你可以根据是否有%来确定是AT&T 还是 Intel 语法。

对于AT&T语法,左边是源操作数,右边是目的操作数。通常是读源操作数,写目的操作数。

gdb默认是att风格,可以通过如下命令修改默认配置为intel

set disassembly-flavor intel # att代表at&t风格


b.寄存器

x86的 通用寄存器 eax ebx ecx edx edi esi 。这些寄存器在 大多数 指令中是可以任意使用的。但 有些指令限制只能用其中某些寄存器做某种用途 ,例如除法指令idivl规定被除数在eax寄存器中,edx寄存器必须是0,而除数可以是任何寄存器中。计算结果的商数保存在eax寄存器中(覆盖被除数),余数保存在edx寄存器。

x86的 特殊寄存器 ebp esp eip eflags 。eip是程序计数器。eflags保存计算过程中产生的标志位,包括进位、溢出、零、负数四个标志位,在x86的文档中这几个标志位分别称为CF、OF、ZF、SF。ebp和esp用于维护函数调用的栈帧。

esp 为栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部),而 ebp 为帧指针,指向当前活动记录的底部。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;esp所指的栈帧顶部和系统栈的顶部是同一个位置。


c.寻址

如下是使用各种寻址模式加载一个64位值到%rax:

MOVQ x,%rax
MOVQ $56,%rax
MOVQ %rbx,%rax
MOVQ (%rsp),%rax
MOVQ -8(%rsp),%rax
MOVQ -16(%rbx,%rcx,8),%rax


$56:表示立即数

$rbx:表示寄存器rbx中的值

(%rsp):表示%rsp指向的内存中的值

-8(%rbp):表示把%rbp指向的地址前移8个字节后对应的内存值

-16(%rbx,%rcx,8):表示-16+%rbx+%rcx*8对应的地址的内存值,这种寻址模式在访问元素大小特殊的数组时很有用


d.操作数宽度

B(byte):1字节
W(ord) :2字节 
L(long) :4字节
Q(uadWord):8字节

3.2 常用指令


1.test & je

test 指令用于两个操作数的按位AND运算,并根据结果设置标志寄存器,结果本身不会写回到目的操作数。

JE 当ZF标志位为零,就跳转

test非常普遍的用法是用来测试寄存器是否为空: 寄存器为空, 则ZF零标志为1,则跳转

 0x00000000020aecd0 <+0>: test   %rdi,%rdi
 0x00000000020aecd3 <+3>: je     0x20aecdb <tc_version(int*, int*, char const**)+11>
//这两句含义为:rdi寄存器为空,就跳转


与CMP指令结合使用,实现循环。

JE ==
JNE !=
JLE <=
JGE >=


%rax 从0累加到5:

        MOVQ $0, %rax
loop:
        INCQ %rax
        CMPQ $5, %rax
        JLE  loop


2.move

加载内存数据到内存,或者copy寄存器内容到内存。


3.算术运算

ADDQ %rbx, %rax #把%rbx加到%rax,结果存在%rax
IMULQ %rbx #把%rax的值乘以操作数,把结果的低64位存在%rax,高64位放在%rdx
MOVQ a,  %rax    # set the low 64 bits of the dividend
CDQO             # sign-extend %rax into %rdx
IDIVQ $5         # divide %rdx:%rax by 5, leaving result in %eax,商在%rax 余数在%rdx
INCQ %rax #%rax寄存器中的值+1并保持在%rax
DECQ %rax #%rax寄存器中的值-1并保持在%rax


4.push/pop

栈是一个辅助的数据结构,主要用来记录函数的调用历史和相关的局部变量(没有放到寄存器的)。一般栈是从高地址到低地址向下生长的。%rsp是栈指针,指向栈最底部(其实是平常所说的栈顶)元素。

所以push %rax(8字节),会把%rsp减去8,并把%rax写到 %rsp指向的位置。

SUBQ $8, %rsp
MOVQ %rax, (%rsp)

pop则刚好相反:

MOVQ (%rsp), %rax
ADDQ $8, %rsp

要丢弃最后压入栈中的值,只需要修改%rsp的值即可

ADDQ $8, %rsp

当然,由于压栈和出栈非常常见,所以这两个操作有两个专有指令,他们的行为和上面描述的完全相同:

PUSHQ %rax
POPQ  %rax


3.3 函数调用


X86-64的调用方式有些不同,称作 System V ABI 。整个约定相当复杂,下面是简化版,但对我们来说足够了:

  • 整数参数(包含指针)依次放在%rdi, %rsi, %rdx, %rcx, %r8, 和 %r9 寄存器中。
  • 浮点参数依次放在寄存器%xmm0-%xmm7中。
  • 寄存器不够用时,参数放到栈中。
  • 可变参数哈函数(比如printf), 寄存器%eax需记录下浮点参数的个数。
  • 被调用的函数可以使用任何寄存器,但它必须保证%rbx, %rbp, %rsp, and %r12-%r15恢复到原来的值(如果它改变了它们的值)。
  • 返回值存储在 %eax中.

调用函数前,先要把参数放到寄存器中。然后,调用方要把寄存器%r10 和%r11的值保存到栈中。之后,执行CALL指令,把IP指针的值保存到栈中,并跳到函数的起始地址执行。从函数返回后,恢复%r10 和%r11,并从%eax获取返回值。

编译器产生的函数一般都需要使用栈来保存和恢复寄存器,这样在函数内部执行时就可以使用这些寄存器。因此栈上的变量主要有三种:被复用但是调用后需要恢复的寄存器/本地变量/输入参数。


3.4 通过汇编代码理解coredump


a.找到当前汇编代码对应到源代码里面的哪一行

首先通过bt命令可以看到当前core的对应的代码行

然后通过如下命令查看汇编代码,进一步确认coredump发生的上下文

disas
info line *0x400a46 #查看某地址对应的代码行,类似addr2line

或者直接通过如下命令,显示汇编代码和源代码的对应关系

disas /m


b.当前的寄存器、堆/栈变量对应源代码里面的哪个变量(常量/全局/静态/局部/指针变量)


函数开始时,通常会把保存输入参数的寄存器压栈,同时函数内部的局部变量通常也是保存在栈上。因此通过这个来定位当前寄存器里面的值到底对应源代码里面的哪种变量。如果要访问的是寄存器或者地址是在栈空间上,则结合压栈过程,定位确定是哪个参数或者局部变量。


如果是全局/静态/局部变量/参数,则可以通过如下gdb命令辅助进行解析

info reg #查看寄存器当前值 
info locals
p &localVar #或者 info address localVar
info args
p &args #或者 info address args
info variables #查看所有全局/静态变量的名称和类型
p &globalVar #或者 info address globalVar

info address symbol 与 print &symbol区别:对于栈上的local变量 print的是绝对地址,info addr是相对地址。


如果是堆上的变量,则需要结合tcmalloc定位分配的地址对于变量,或者代码本身加入magic number进行标识。

对于c++代码,如果对象有虚函数,那么可以通过vtbl指针来确定对象类型。

如果内存数据本身没有标识标志,还有一种办法就是记录内存分配的轨迹,然后通过这个分配记录进行定位。


3.5 堆栈被破坏的情况

栈被破坏的原因:

1.ebp寄存器内容被破坏,函数调用返回后会用stack上保存的值恢复ebp,如果stack上的值被破坏,ebp的值也会被破坏

2.stack上保存的ebp值被破坏,进行堆栈回溯时无法正确解析到正确的函数地址


恢复原理:

ebp或者堆栈只是部分被损坏。esp里面的栈顶指针是对的,结合eip可以找到最上层函数,然后查看汇编代码可以确定该函数的stack frame布局,之后再找到调用者的地址,可以准备恢复。

其他场景:

1.查看被优化掉的局部变量值

局部变量保存到了寄存器中,没有保存到栈,coredump时该寄存器的值是最上层函数执行时的结果,因此无法通过寄存器当前值确定该局部变量的具体值。但是在这个变量作为另一个函数的参数情况下,可以通过查看被调用函数的stack找到它。作为参数会被存放到寄存器中,被调用者如何又调用了其他函数,这个值就会被保存到被调用者的stack中,从而可以通过stack查到这个变量的值。

4 gdb扩展

1.源码修改


定位到具体command的代码实现:

1.gdb/command.h 中是用于添加命令的接口,包含add_cmd add_com add_info

2.比如如果要想找到backtrace对应的代码,可以采用如下命令:

[gdb-7.11.1]
$grep -E "add_cmd|add_com" -r ./ |grep backtrace
./gdb/stack.c:  add_com ("backtrace", class_stack, backtrace_command, _("\
./gdb/stack.c:  add_com_alias ("bt", "backtrace", class_stack, 0);
./gdb/stack.c:  add_com_alias ("where", "backtrace", class_alias, 0);

3.找到backtrace_command函数

[gdb-7.11.1]
$grep backtrace_command -r ./|grep -v ChangeLog|grep -v Make|grep -v Binary
./gdb/stack.c:backtrace_command_1 (char *count_exp, int show_locals, int no_filters,
./gdb/stack.c:backtrace_command (char *arg, int from_tty)
./gdb/stack.c:  backtrace_command_1 (arg, fulltrace_arg >= 0 /* show_locals */,
./gdb/stack.c:  add_com ("backtrace", class_stack, backtrace_command, _("\
./gdb/stack.c:  add_info ("stack", backtrace_command,


2.python扩展

sourceware.org/gdb/curr

github.com/openresty/op


0.检查当前gdb是否带上了python支持

$gdb --config

1.如果没有--with-python,需要重新编译一个gdb带上python支持

python支持是从GDB 7.0版本之后才引入的。

cd gdb-7.11.1
./configure --with-python=/usr/local/bin/python2.7 --prefix=/home/amdin/gdb-7.11.1
make install

2.设置脚本自动识别

show script-extension
set script-extension strict

3.source

(gdb) source xx_gdb.py


4.print pretty

show print pretty