【技术分享】利用GDB调试ARM代码

http://p4.qhimg.com/t01e77b70e5d8fa1673.png

译者:shan66

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


在本文中,我们将简要介绍如何利用GDB完成ARM二进制文件的编译和基本调试。当然,阅读过程中,如果读者想要对ARM汇编代码动手进行实验的话,则需要准备一个备用的ARM设备,或者在虚拟机中设置相应的实验室环境,具体操作请参考How-To这篇文章。

此外,您还将通过下面取自 Part 7 – Stack and Functions这篇文章中的代码来熟悉GDB的基本调试功能。 

.section .text
.global _start

_start:
    push {r11, lr}    /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
    add r11, sp, #0   /* Setting up the bottom of the stack frame */
    sub sp, sp, #16   /* End of the prologue. Allocating some buffer on the stack */
    mov r0, #1        /* setting up local variables (a=1). This also serves as setting up the first parameter for the max function */
    mov r1, #2        /* setting up local variables (b=2). This also serves as setting up the second parameter for the max function */
    bl max            /* Calling/branching to function max */
    sub sp, r11, #0   /* Start of the epilogue. Readjusting the Stack Pointer */
    pop {r11, pc}     /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */

max:
    push {r11}        /* Start of the prologue. Saving Frame Pointer onto the stack */
    add r11, sp, #0   /* Setting up the bottom of the stack frame */
    sub sp, sp, #12   /* End of the prologue. Allocating some buffer on the stack */
    cmp r0, r1        /* Implementation of if(a<b) */
    movlt r0, r1      /* if r0 was lower than r1, store r1 into r0 */
    add sp, r11, #0   /* Start of the epilogue. Readjusting the Stack Pointer */
    pop {r11}         /* restoring frame pointer */
    bx lr             /* End of the epilogue. Jumping back to main via LR register */

就个人而言,我更喜欢使用作为GDB增强版的GEF,它用起来要更加得心应手,具体下载地址https://github.com/hugsy/gef

将上述代码保存到名为max.s的文件中,然后使用以下命令进行编译: 

$ as max.s -o max.o
$ ld max.o -o max

这个调试器是一个强大的工具,可以:

在代码崩溃后加载内存dump(事后剖析调试)

附加到正在运行的进程(用于服务器进程)

启动程序并进行调试

根据二进制文件、核心文件或进程ID启动GDB:

附加到一个进程:$ gdb -pid $(pidof <process>)

调试二进制代码:$ gdb ./file

检查内核(崩溃)文件:$ gdb -c ./core.3243 

$ gdb max

如果您安装了GEF,将会显示gef>提示符。

可以通过下列方式获取帮助: 

(gdb) h

(gdb) apropos <search-term>

gef> apropos registers
collect -- Specify one or more data items to be collected at a tracepoint
core-file -- Use FILE as core dump for examining memory and registers
info all-registers -- List of all registers and their contents
info r -- List of integer registers and their contents
info registers -- List of integer registers and their contents
maintenance print cooked-registers -- Print the internal register configuration including cooked values
maintenance print raw-registers -- Print the internal register configuration including raw values
maintenance print registers -- Print the internal register configuration
maintenance print remote-registers -- Print the internal register configuration including each register's
p -- Print value of expression EXP
print -- Print value of expression EXP
registers -- Display full details on one
set may-write-registers -- Set permission to write into registers
set observer -- Set whether gdb controls the inferior in observer mode
show may-write-registers -- Show permission to write into registers
show observer -- Show whether gdb controls the inferior in observer mode
tui reg float -- Display only floating point registers
tui reg general -- Display only general registers
tui reg system -- Display only system registers

断点命令: 

break (or just b) <function-name>

break <line-number>

break filename:function

break filename:line-number

break *<address>

break  +<offset>  

break  –<offset>

tbreak (设置一个临时断点) 

del <number>  (删除编号为x的断点) 

delete (删除所有断点) 

delete <range>(删除指定编号范围内的断点) 

disable/enable <breakpoint-number-or-range> (不删除断点,只是启用/禁用它们) 

continue (or just c) – (继续执行,直到下一个断点) 

continue <number> (继续,但忽略当前断点指定次数。对循环内的断点非常有用) 

finish继续,直至函数末尾) 

gef> break _start
gef> info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x00008054 <_start>
 breakpoint already hit 1 time
gef> del 1
gef> break *0x0000805c
Breakpoint 2 at 0x805c
gef> break _start

这将删除第一个断点,并在指定的内存地址处设置一个断点。当您运行程序时,它将在这个指定的位置停下来。 如果不删除第一个断点,然后又设置一个断点并运行,则它还是在第一个断点处停下来。

启动和停止: 

启动一个程序,从头开始执行 

run

r

run <command-line-argument>

停止程序的运行 

kill

退出GDB调试器 

quit

q

gef> run

http://p4.qhimg.com/t01ab20a84e1ef66b0e.png

现在,我们的程序在指定的位置停下来了,这样就可以开始检查内存了。 命令“x”可以用来以各种格式显示内存内容。 

语法 : x/<count><format><unit>

格式单位 

x –  十六进制 b – 字节

d – 十进制h – 半字(2字节)

i – 指令w – 字(4字节)

t – 二进制(two)g – 巨字(8字节)

o – 八进制

u – 无符号整数

s – 字符串

c – 字符

gef> x/10i $pc
=> 0x8054 <_start>: push {r11, lr}
 0x8058 <_start+4>: add r11, sp, #0
 0x805c <_start+8>: sub sp, sp, #16
 0x8060 <_start+12>: mov r0, #1
 0x8064 <_start+16>: mov r1, #2
 0x8068 <_start+20>: bl 0x8074 <max>
 0x806c <_start+24>: sub sp, r11, #0
 0x8070 <_start+28>: pop {r11, pc}
 0x8074 <max>: push {r11}
 0x8078 <max+4>: add r11, sp, #0
gef> x/16xw $pc
0x8068 <_start+20>: 0xeb000001  0xe24bd000  0xe8bd8800  0xe92d0800
0x8078 <max+4>:     0xe28db000  0xe24dd00c  0xe1500001  0xb1a00001
0x8088 <max+20>:    0xe28bd000  0xe8bd0800  0xe12fff1e  0x00001741
0x8098:             0x61656100  0x01006962  0x0000000d  0x01080206

用于单步调试的命令:单步执行下一条命令。可以进入函数内部

stepi

s

step <number-of-steps-to-perform>

执行下一行代码。不会进入函数内部 

nexti

n

next <number>

继续处理,直到达到指定的行号、函数名称、地址、文件名函数或文件名:行号 

until

until <line-number>、

显示当前行号以及所在的函数 

where

gef> nexti 5
...
0x8068 <_start+20> bl 0x8074 <max> <- $pc
0x806c <_start+24> sub sp, r11, #0
0x8070 <_start+28> pop {r11, pc}
0x8074 <max> push {r11}
0x8078 <max+4> add r11, sp, #0
0x807c <max+8> sub sp, sp, #12
0x8080 <max+12> cmp r0, r1
0x8084 <max+16> movlt r0, r1
0x8088 <max+20> add sp, r11, #0

使用info registers或i r命令检查寄存器的值 

gef> info registers
r0     0x1     1
r1     0x2     2
r2     0x0     0
r3     0x0     0
r4     0x0     0
r5     0x0     0
r6     0x0     0
r7     0x0     0
r8     0x0     0
r9     0x0     0
r10    0x0     0
r11    0xbefff7e8 3204446184
r12    0x0     0
sp     0xbefff7d8 0xbefff7d8
lr     0x0     0
pc     0x8068  0x8068 <_start+20>
cpsr   0x10    16

命令“info registers”能够提供当前的寄存器状态。 我们可以看到,这里包括通用寄存器r0-r12,专用寄存器SP、LR和PC,以及状态寄存器CPSR。 函数的前四个参数通常存储在r0-r3中。 在这种情况下,我们可以通过手动方式将其值移动到r0和r1。

显示进程内存映射: 

gef> info proc map
process 10225
Mapped address spaces:

 Start Addr   End Addr    Size     Offset objfile
 0x8000     0x9000  0x1000          0   /home/pi/lab/max
 0xb6fff000 0xb7000000  0x1000          0          [sigpage]
 0xbefdf000 0xbf000000 0x21000          0            [stack]
 0xffff0000 0xffff1000  0x1000          0          [vectors]

通过命令“disassemble”,我们可以查看函数max的反汇编输出。 

gef> disassemble max
 Dump of assembler code for function max:
 0x00008074 <+0>: push {r11}
 0x00008078 <+4>: add r11, sp, #0
 0x0000807c <+8>: sub sp, sp, #12
 0x00008080 <+12>: cmp r0, r1
 0x00008084 <+16>: movlt r0, r1
 0x00008088 <+20>: add sp, r11, #0
 0x0000808c <+24>: pop {r11}
 0x00008090 <+28>: bx lr
 End of assembler dump.

GEF特有的命令(可以使用命令“gef”查看更多命令):

将所有已加载的ELF镜像的所有节dump到进程内存中

X档案

proc map的增强版本,包括映射页面中的RWX属性

vmmap

给定地址的内存属性

xinfo

检查运行的二进制文件内置的编译器级保护措施

checksec 

gef> xfiles
     Start        End  Name File
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
0x00008054 0x00008094 .text /home/pi/lab/max
gef> vmmap
     Start        End     Offset Perm Path
0x00008000 0x00009000 0x00000000 r-x /home/pi/lab/max
0xb6fff000 0xb7000000 0x00000000 r-x [sigpage]
0xbefdf000 0xbf000000 0x00000000 rwx [stack]
0xffff0000 0xffff1000 0x00000000 r-x [vectors]
gef> xinfo 0xbefff7e8
----------------------------------------[ xinfo: 0xbefff7e8 ]----------------------------------------
Found 0xbefff7e8
Page: 0xbefdf000 -> 0xbf000000 (size=0x21000)
Permissions: rwx
Pathname: [stack]
Offset (from page): +0x207e8
Inode: 0
gef> checksec
[+] checksec for '/home/pi/lab/max'
Canary:                  No
NX Support:              Yes
PIE Support:             No
RPATH:                   No
RUNPATH:                 No
Partial RelRO:           No
Full RelRO:              No

故障排除

为了更高效地使用GDB进行调试,很有必要了解某些分支/跳转的目标地址。 某些(较新的)GDB版本能够解析分支指令的地址,并能显示目标函数的名称。 例如,下面是缺乏这些功能的GDB版本的输出内容: 

...
0x000104f8 <+72>: bl 0x10334
0x000104fc <+76>: mov r0, #8
0x00010500 <+80>: bl 0x1034c
0x00010504 <+84>: mov r3, r0
...

而下面则是提供了上述功能的GDB版本的的输出结果: 

0x000104f8 <+72>:    bl      0x10334 <free@plt>
0x000104fc <+76>:    mov     r0, #8
0x00010500 <+80>:    bl      0x1034c <malloc@plt>
0x00010504 <+84>:    mov     r3, r0

如果您的GDB版本中没有提供这些功能,可以升级Linux(前提是它们提供了更新的GDB),或者自己编译较新的GDB。 如果您选择自己编译GDB,可以使用以下命令: 

cd /tmp
wget https://ftp.gnu.org/gnu/gdb/gdb-7.12.tar.gz
tar vxzf gdb-7.12.tar.gz
sudo apt-get update
sudo apt-get install libreadline-dev python-dev texinfo -y
cd gdb-7.12
./configure --prefix=/usr --with-system-readline --with-python && make -j4
sudo make -j4 -C gdb/ install
gdb --version

我使用上面提供的命令来下载、编译和运行Raspbian(jessie)上的GDB,并且没有遇到任何问题。同时,这些命令也将取代以前版本的GDB。如果您不想这样做的话,请跳过以单词install结尾的命令。此外,我在QEMU中模拟Raspbian时也是这样做的,不过这个过程非常耗时,大概需要几个小时,因为模拟环境的资源(CPU)有限。 我使用的GDB版本为7.12,但是你还可以使用更高的版本,为此可以点击此处查看其他版本。 

(完)