您的位置:首页 > 编程语言 > C语言/C++

基于arm的C++反汇编 函数的工作原理

2016-05-23 19:04 399 查看
栈帧的形成和关闭

各种调用方式的考擦

使用 fp或sp寻址

函数的参数 与返回值

arm指令中立即数存放位置

gdbserver 调试环境

栈帧的形成和关闭

  栈在内存中是一块特殊的存储空同, 它的存储原则是“先进后出”, 即最先被存储的数据最后被释放, 汇编过程通常使用 push 指令与 POP指令对栈空间执行数据压入和数据弹出操作。

  栈结构在内存中占用一段连续的存储空间, 通过sp与 fp这两个栈指针寄存器(在x86上是esp,ebp)来保存当前栈的起始地址与结束地址(又称为 栈顶与 栈底)。 在 栈结构中, 每4字的 栈空间保存一个数据, 像这样的 栈顶到 栈底之间的存储空间被称为 栈帧.

在arm中没有x86那样丰富的栈操作指令,也没有专门的栈指针寄存器, 按照PCS规定用
$r13
寄存器来作为sp寄存器,还有一个可选的
$r11
作为fp寄存器。

  栈帧是如何形成的呢? 当栈顶指针 sp小于栈底指针 fp时, 就形成了栈帧。 通常, 栈帧中可以寻址的数据有局部变量、函数返回地址等(关于栈帧中函数返回地址的寻址可见我以前博文mips体系堆栈回溯分析与实现)。

  不同的两次函数调用, 所形成的栈帧也不相同 。 当由一个函数进入到另一个函数中时, 就会针対调用的函数开辟出其所需的栈空间, 形成此函数的栈。 当这个函数结束调用时, 需要清除掉它所使用的栈空同, 关闭栈帧,我们把这一过程称为栈平衡。

  为什么要进行栈平衡呢?这就像借钱一样,”有惜有还.再借不难’。如果某一函数在开辞了新的栈空间后没有进行恢复, 或者过度恢复, 那么将会造成栈空间的上溢或下溢, 极有可能给程序带来致命性的错误

现在的高级语言中,没有让程序猿操作栈的机会,都是由编译器自动进行栈帧的开辟和释放操作,一般除了缓冲区溢出,或者强制指针操作不会导致栈的异常。

还以上文的代码为例子:

#include <iostream>

using namespace std;

int main()
{
// 将变量 nConst 修饰为const
const int nConst = 5;

//  定义int 类型的指针,保存nConst 地址
int *pConst = (int*)&nConst;

//  修改指针pConst 并指向地址中的数据
*pConst = 6;

//  将修饰为const 的变量nConst 赋值给nVar
int nVar = nConst;

cout << nVar << endl;
}


反汇编代码如下:

000091fc <main>:
91fc:       e92d4800        push    {fp, lr}
9200:       e28db004        add     fp, sp, #4
9204:       e24dd010        sub     sp, sp, #16
9208:       e3a03005        mov     r3, #5
920c:       e50b3010        str     r3, [fp, #-16]  ;nConst = 5;
9210:       e24b3010        sub     r3, fp, #16
9214:       e50b3008        str     r3, [fp, #-8]   ;pConst = &nConst
9218:       e51b3008        ldr     r3, [fp, #-8]   ;r3 = pConst
921c:       e3a02006        mov     r2, #6          ;r2 = 6
9220:       e5832000        str     r2, [r3]        ;*pConst = r2 = 6;
9224:       e3a03005        mov     r3, #5
9228:       e50b300c        str     r3, [fp, #-12]  ;nVar = r3 = 5
922c:       e59f0024        ldr     r0, [pc, #36]   ; 9258 <main+0x5c>
9230:       e51b100c        ldr     r1, [fp, #-12]  ;r1 = nVar
9234:       eb000971        bl      b800 <_ZNSolsEi>
9238:       e1a03000        mov     r3, r0
923c:       e1a00003        mov     r0, r3
9240:       e59f1014        ldr     r1, [pc, #20]   ; 925c <main+0x60>
9244:       eb000469        bl      a3f0 <_ZNSolsEPFRSoS_E>
9248:       e3a03000        mov     r3, #0
924c:       e1a00003        mov     r0, r3
9250:       e24bd004        sub     sp, fp, #4
9254:       e8bd8800        pop     {fp, pc}


  在上述代码中,进入函数后,先保存原来的fp,然后调整fp的位置到sp+4,接下来通过“sub sp, 16”这句指令打开了 0x10 字节大小的栈空间, 这是留给局部变量使用的。并且注意到,设置完毕fp和sp指针之后,在函数返回之前不再修改这两个寄存器的值,而是通过它们来寻址局部变量。

  由于在进入函数前打开了一定大小的栈空间, 在函数调用结束后需要将这些栈空间释放,因此需要还原环境
sub     sp, fp, #4
与 pop ,以降低栈顶这样的指令。

另外再提一点 gcc 还有一个关于stack frame的优化选项:

  -fomit-frame-pointer


关于这个选项说明如下

Don't keep the frame pointer in a register for functions that don't need one. This avoids the instructions to save, set up and restore frame pointers; it also makes an extra register available in many functions. It also makes debugging impossible on some machines.

  On some machines, such as the VAX, this flag has no effect, because the standard calling sequence automatically handles the frame pointer and nothing is saved by pretending it doesn't exist. The machine-description macro "FRAME_POINTER_REQUIRED" controls whether a target machine supports this flag.


  大意是说在不需要的函数里面不保存 frame指针,这样在很多函数里面多了一个寄存器可用,但是同样也使调试机制在某些机器上无法使用。

加入
 -fomit-frame-pointer
选项之后生成的栈帧开辟和恢复指令就没有了fp寄存器操作了:

00008704 <main>:
8704:       e52de004        push    {lr}            ; (str lr, [sp, #-4]!)
8708:       e24dd014        sub     sp, sp, #20
..........
8754:       e28dd014        add     sp, sp, #20
8758:       e49df004        pop     {pc}            ; (ldr pc, [sp], #4)


arm支持4种栈操作方式,分别是
满减栈,满增栈,空减栈,空增栈
。实际使用中还是用和x86指令集相同的栈类型,具体详见 ARM的栈指令

各种调用方式的考擦

  在x86下有各种调用方式出名的主要有
_cdecl
_stdcall
_fastcall
,主要规定了在函数调用和返回的时候参数如何传递,栈如何使用,函数返回的时候栈由谁来清理。

关于这个可见我以前转帖的博客: cdecl、stdcall、fastcall函数调用约定区别

  在arm下 函数参数都是通过寄存器,当参数多了之后就用栈传递,关于这一点网上有人已经写的很好了,这里直接引用。理解APCS– ARM过程调用标准

使用 fp或sp寻址

  在前面的内容中, 我们接触到很多高级语言中的变量访问 。 将高级语言转换成汇编代码后, 就变成了对 fp或 sp的加减法操作(寄存器相对寻址方式)来获取变量在内存中的数据,比如以下代码

9208:       e3a03005        mov     r3, #5
920c:       e50b3010        str     r3, [fp, #-16]  ;nConst = 5;

9224:       e3a03005        mov     r3, #5
9228:       e50b300c        str     r3, [fp, #-12]  ;nVar = r3 = 5


  由此可见, 局部变量是通过栈空间来保存的. 根据这两个变量以fp 寻址方式可以看出,在内存中,局部变量是以连续排列的方式存储在栈内的。

  由于局部变量使用栈空间进行存储, 因此进入函数后的第一件事就是开辟函数中局部变量所需的栈空间。这时函数中的局部変量就有了各自的内存空间 。在函数结尾处执行释放栈空间的操作,因此局部变量是有生命周期的, 它的生命周期在进入函数体的时候开始, 在函数执行结束的时候结束 。

  加入
-fomit-frame-pointer
参数,使用了 sp 寻址后, 不必在每次进入函数后都调整栈底 fp, 这样既減少了fp的使用, 又省去了维护 fp的相关指令 因此可以有效提升程序的执行效率。

函数的参数 ,与返回值

  在x86上因为寄存器比较少,而且栈指令功能强大,函数通过栈传递参数,但是在arm上因为寄存器比较多,函数参数直接通过寄存器传递,当寄存器不够的时候采用栈传递。

看一个例子:

#include <iostream>

using namespace std;

int Add(int var1,int var2)
{
return var1 + var2;
}

int main()
{
int nVar1   = 0x123;
int nVar2   = 0x456;
int sum;

sum = Add(nVar1,nVar2);

cout << sum << endl;
}


其对应的反汇编代码如下所示:

000091fc <_Z3Addii>:
91fc:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
9200:       e28db000        add     fp, sp, #0
9204:       e24dd00c        sub     sp, sp, #12
9208:       e50b0008        str     r0, [fp, #-8]
920c:       e50b100c        str     r1, [fp, #-12]
9210:       e51b2008        ldr     r2, [fp, #-8]
9214:       e51b300c        ldr     r3, [fp, #-12]
9218:       e0823003        add     r3, r2, r3
921c:       e1a00003        mov     r0, r3
9220:       e24bd000        sub     sp, fp, #0
9224:       e49db004        pop     {fp}            ; (ldr fp, [sp], #4)
9228:       e12fff1e        bx      lr

0000922c <main>:
922c:       e92d4800        push    {fp, lr}
9230:       e28db004        add     fp, sp, #4
9234:       e24dd010        sub     sp, sp, #16
9238:       e59f3044        ldr     r3, [pc, #68]   ; 9284 <main+0x58>
923c:       e50b3008        str     r3, [fp, #-8]
9240:       e59f3040        ldr     r3, [pc, #64]   ; 9288 <main+0x5c>
9244:       e50b300c        str     r3, [fp, #-12]
9248:       e51b0008        ldr     r0, [fp, #-8]
924c:       e51b100c        ldr     r1, [fp, #-12]
9250:       ebffffe9        bl      91fc <_Z3Addii>
9254:       e50b0010        str     r0, [fp, #-16]
9258:       e59f002c        ldr     r0, [pc, #44]   ; 928c <main+0x60>
925c:       e51b1010        ldr     r1, [fp, #-16]
9260:       eb000973        bl      b834 <_ZNSolsEi>
9264:       e1a03000        mov     r3, r0
9268:       e1a00003        mov     r0, r3
926c:       e59f101c        ldr     r1, [pc, #28]   ; 9290 <main+0x64>
9270:       eb00046b        bl      a424 <_ZNSolsEPFRSoS_E>
9274:       e3a03000        mov     r3, #0
9278:       e1a00003        mov     r0, r3
927c:       e24bd004        sub     sp, fp, #4
9280:       e8bd8800        pop     {fp, pc}
9284:       00000123        andeq   r0, r0, r3, lsr #2
9288:       00000456        andeq   r0, r0, r6, asr r4
928c:       000f7334        andeq   r7, pc, r4, lsr r3      ; <UNPREDICTABLE>
9290:       0000af04        andeq   sl, r0, r4, lsl #30


上述代码中 [fp, #-8] 对应变量 nVar1 ,因为用gdbserver调试执行到 0x923c的时候r3寄存器为 0x123



  [fp, #-12] 对应就是 nVar2 了,然后当执行到指令0x9250 的时候 r0 对应nVar1 ,r1对应nVar2 。所以可以看出arm是通过使用寄存器传递参数的。

当从Add函数出来之后指令
str     r0, [fp, #-16]
因此可以看出函数返回值是通过r0传递,在x86上这个是eax传递。

静态分析下Add函数,分析下指令,刚进去这个函数的时候开辟栈帧

add     fp, sp, #0
sub     sp, sp, #12


  然后分别把参数r0,r1存储到开辟的栈中,如果此时有对局部变量的写操作,最终结果还是反馈到Add的栈帧里面,对main函数的栈帧没影响,因此可以得出结论 “形参是实参的副本,对形参修改不形象实参”

  继续用 gdbserver 动态调试跟踪到指令 0x9250 ,这是一条bl指令,调用子函数
bl      91fc <_Z3Addii>
发现单步执行之后栈指针sp的值没改变,lr寄存器里面却保存了函数的返回值:



这个lr寄存器在子函数里面会被妥善安置,并且在子函数返回的时候很有用:

1)如果这个子函数是叶子函数,那么lr就不压栈返回时候直接
bx      lr


所谓叶子函数就是 这个函数不再调用其它子函数。

2)如果这个函数不是叶子函数,那么就要压栈,并且出栈的时候直接用pc寄存器,这样就实现子函数返回,详见上面的main函数最后的指令。

这点跟x86也不一样,x86上用
call
ret
指令来实现函数调用返回。

arm指令中立即数存放位置

  x86属于复杂指令集每条指令长度不固定,arm属于精简指令集每条指令限制了四字节,

  x86如果要操作一个四字节立即数这个立即数可以编码到指令里面,这样一条指令就大于或者等于5个字节。但是arm就没有这个能力,函数里面用到的立即数都被放到函数末尾紧挨返回指令的地方,比如上面的main函数后面的0x123,0x456。这样做好出就是立即数存放位置距离当前PC指针不远,可以用pc指针加上一个偏移量来寻址。

gdbserver 调试环境

  有时候单靠静态分析无法知道一些arm指令的细节,这时候就需要单步动态调试了,gdbserver就是一个很重要的工具,gdbserver 调试依赖于网络,因此需要按照本系列教程第一篇搭建好环境,弄好网络。

然后官网下载 源码 Index of /gnu/gdb

这里我们只需要gdbserver ,进入目录
/gdb/gdbserver
配置

./configure   --build=i686-pc-linux-gnu --host=arm-linux --target=arm-linux


  关于build host target 含义可见博文: 交叉编译: –host –build –target到底什么意思?

修改Makefile
LDFLAGS= -static
,实际上arm工具链里面已经有了 gdbserver了,但是动态链接的,在busybox上无法顺利运行,为了省事这里简单的编译一个静态链接的gdbserver,编译完成后放到busybox即可。

后续步骤可见博文 嵌入式arm linux环境中gdb+gdbserver调试
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  arm gdbserver 函数 调试