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

  • 栈帧的形成和关闭
  • 各种调用方式的考擦
  • 使用 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调试

时间: 2024-07-29 18:53:51

基于arm的C++反汇编 函数的工作原理的相关文章

[转组第7天] | 函数的工作原理

2018-05-04 <C++反汇编和逆向技术>第六章 函数的工作原理 读书笔记 debug版本的函数调用: call func func: push ebp ;保存ebp mov ebp,esp sub esp,40h ;抬高esp,开辟栈空间 push ... ;保存寄存器 ... pop ... ;还原寄存器 add esp,40h ;降低esp,释放局部变量空间 cmp ebp,esp ;检测栈平衡 call __chkesp ;进入栈平衡错误检测函数 mov esp,ebp ;还原e

C++学习笔记27,虚函数的工作原理

C++规定了虚函数的行为,但是将实现交给了编译器的作者. 通常,编译器处理虚函数的方法是给每一个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针. 这个数组称为虚函数表(virtual function table,vtbl).虚函数表中存储了为类对象进行声明的虚函数的地址. 例如:基类对象包含一个指针,该指针指向基类的虚函数表. 派生类对象包含一个指针,该指针指向一个独立的虚函数表.如果派生类提供了虚函数的新定义,虚函数表将保存新的函数地址. 如果派生类没有重新定义虚函数,虚函

关于OnPaint函数的工作原理(很详细,很实用) [转载]

地址:http://blog.csdn.net/foreverhuylee/article/details/21889025 用了两年的VC,其实对OnPaint的工作原理一直都是一知半解.这两天心血来潮,到BBS上到处发帖询问,总算搞清楚了,现在总结一下. 对于窗口程序,一般有个特点:窗口大部分的区域保持不变,只有不分区域需要重新绘制.如果将整个窗口全部刷新的画,就做了许多不必要的工作,因而,MFC采用了一套基于无效区的处理机制.在分析无效区处理之前,我们要明白一个现实,现在的机器还不够牛,如

函数的工作原理——划分RAM搞不懂啊???

1.看到<21天学会C++>P92的函数工作原理之划分RAM,感觉还是迷迷糊糊,不太明白,进一步查询??? 2.程序启动时,操作系统(如DOS,Windows等)将依据编译器的需求设置各种内存区域. 对于一个C++程序员来说,经常需要关心的是全局名称空间.自由存储器.寄存器.代码空间和堆栈. 3.寄存器:CPU中的一个特殊存储区域,任意给定时刻指向下一行代码代码的寄存器组的寄存器被称为指令指针.指令指针的任务是跟踪接下来将执行哪一行代码. 4.代码空间:代码本身存放在代码空间中,每行代码都被转

函数的工作原理

函数的工作借助于栈. 栈在内存中是一块特殊的存储空间,它的存储原则是"先进后出",最先被存储的数据最后被释放. esp被称为栈顶指针,ebp称为栈底指针,通过这两个指针寄存器保存当前栈的起始地址与结束地址. esp与ebp之间所构成的空间便成为栈帧.通常,在VC++中,栈帧中可以寻址的数据有局部变量.函数返回地址.函数参数等.不同的两次函数调用,所形成的栈帧也不同.当由一个函数进入到另一个函数中时,就会针对所调用的函数形成所需的栈空间,形成此函数的栈帧.当这个函数结束调用时,需要清除掉

基于arm的C++反汇编 数组和指针的寻址

数组在函数内 数组作为参数 数组作为返回值 下标寻址和指针寻址 下标值为整型常量的寻址 下标值为整型变量的寻址 下标值为整型表达式的寻址 数组越界 多维数组 存放指针类型数据的数组 指向数组的指针变量 函数指针 ??虽然数组和指针都是针对地址操作,但它们有许多不同之处.数组是相同数据类型的数 据集合,以线性方式连续存储在内存中:而指针只是一个保存地址值的4字节变量.在使用中,数组名是一个地址常量值,保存数组首元素地址不可修改,只能以此为基地址访问内 存数据:而指针却是一个变量,只要修改指针中所保

虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte

#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout << "A:A" <<endl; } virtual void getb(){ cout << "A:B" <<endl; } }; class B :public A{ public: B(){} virtual void g

C++中虚函数工作原理和(虚)继承类…

转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531 一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当

C++中虚函数工作原理

一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针.