现在的位置: 首页 > 综合 > 正文

处理器栈帧布局

2018年02月10日 ⁄ 综合 ⁄ 共 4061字 ⁄ 字号 评论关闭

处理器栈帧布局

函数的调用会导致隐式的内存分配,栈帧就是在这一过程构造的。显式的内存分配和释放可以使用函数malloc(),calloc(),realloc(),new,free()
delete等,这时候的分配的内存是位于堆上的。典型的栈帧布局如下,可能在不同的操作系统上有不同的组织方式:

  • 函数参数(Function parameters).

  • 函数返回值(Function’s return address).

  • 栈帧指针(Frame pointer).

  • 异常处理帧(Exception Handler frame).

  • 局部变量(Locally declared variables).

  • Buffer.

  • 保存调用者的寄存器(Callee save registers).

Typical illustration of a stack layout during the function call

Figure 1 典型的函数调用过程中栈帧的布局

从这张图上可以很清晰的看出,如果发生buffer overflow,将会有可能覆盖位于Buffer地址之上的其他变量,比如 局部变量,异常处理帧,栈帧指针,返回值,以及函数参数(为什么覆盖的是Buffer之上的内存?比如我们在函数内部定义int i = 0;char
a[4];这个时候i的地址是高地址,a的地址是低地址。在通过for循环给a赋值的时候是从a的低地址开始,向高地址内存单元赋值。如果for循环的次数超过4,就可能破环i的值。)

Windows/Intel为例子,一般而已,栈上面的数据存储按照以下方式进行:

  1. 在函数调用之前按照从右到左的顺序把函数参数压入栈中。

  2. 在x86体系中通过CALL指令将函数的返回地址压入栈中,返回地址保存的是当前EIP寄存器的值。

  3. 接着,EBP中保存的指向前一个栈帧的指针被压入栈中。

  4. 如果一个函数包含try/catch或者其他的异常处理结构,编译器生成的异常处理信息将会保存到栈上。

  5. 接着就是局部定义的变量。

  6. 接着在buffer中分配需要临时存放的数据

  7. 最后,保存函数调用者的寄存器,如果这些寄存器在接下来的函数调用过程中会被用到的话。比如 ESI, EDI, 以及EBX 。对于Linux/Intel体系, 这一步紧跟着第4步。

处理器的栈操作

对于函数调用栈来说有两个寄存器是非常重要的,因为他们保存着访问内存数据的信息。在32位的系统中,这两个寄存器是ESP,EBP。ESP保存着栈顶的地址。ESP寄存器中的值可以被直接或者间接的修改。

直接修改(Windows/Intel)

add esp, 0Ch

这条指令将导致栈的大小减少12个字节。

sub esp, 0Ch

这条指令会导致栈的大小增大12字节。这点可能很让人混淆,实际上,ESP寄存器的值越大,栈的值越小,反之亦然。因为栈是向下增长的。

间接修改:通过PUSH或者POP指令向栈中添加或者删除数据元素。

push   ebp    ; 保存ebp,把它压入栈中

pop    ebp    ; 恢复ebp,从栈中删除

除了栈指针(指向栈顶,具有较低的地址),为了方便还有另外的一个指针:FP(栈帧指针stack frame pointer),它指向栈帧中一个固定的内存地址。通过查看栈帧结构,我们知道局部变量可以通过ESP加上偏移引用来访问。然而随着数据的进栈和出栈,这个offset偏移值会产生变化。这就导致对于局部变量的访问,变的不一致。所以很多的编译器使用另外一个寄存器FB(
Frame Pointer
),用来访问局部变量和参数。因为局部变量和参数到FB的偏移值不会随着PUSH和POP的操作而改变。在Intel中,EBP(Extended Base Pointer)寄存器充当的就是FB的角色。因为栈上向下增长的,所以实际参数有相对于FB有正的偏移量,而局部变量有负的偏移量。让我们看下面的例子:

    #include <stdio.h>
    int MyFunc(int parameter1, char parameter2)
    {
        int local1 = 9;
        char local2 = ‘Z’;
        return 0;
    }

    int main(int argc, char *argv[])
    {
        MyFunc(7, ‘8’);
        return 0;
    }


上面的例子内存布局如下:

Function call: The memory layout

Figure 2: 函数调用的内存布局

EBP是一个指向栈底的静态寄存器。栈底是一个固定的地址。更准确的说法是:EBP含有栈底的地址,作为执行函数的便宜量。依赖于函数所执行的任务,内核会动态的调整栈的大小。每当一个新的函数被调用,旧的EBP就会被压入栈中,然后把新的ESP赋值给EBP。当在新分配的栈中寻找局部变量时EBP中的值就变为参考基地值。之前已经提到过,栈向下增长,大多数的计算机体系栈增长都采用这种方式。

当一个函数被调用的时候,第一件事情就是保存之前的EBP(在函数退出之后,复制保存的值到EIP寄存器)。紧接着把ESP的值复制到EBP中来创建一个新的栈帧指针。然后调整ESP来为局部变量分配空间。以上是为了函数调用在栈中所做的准备工作(procedure prolog),当函数调用结束,需要在栈中执行清理过程,这一个过程称为procedure
epilog
Intel 提供的 ENTERLEAVE 指令还有 Motorola
提供的
LINKUNLINK执行,都可以高效的完成大部分函数调用的准备工作和函数退出的清理工作。之前已经讲过,栈操作中两个重要的指令是PUSH和POP,PUSH用于在栈顶压入一个元素,POP,相反的作用,用于从栈顶移除一个元素。

其他的用于栈操作的指令如下表所列:

Instruction

Description

PUSH

减少栈指针的值,把源操作数压入栈中

POP

取出栈顶元素的值到目的操作数制定的地址,然后增加栈指针的值。

PUSHAD

把普通寄存器中的内容压入栈中

POPAD

取出栈中的值到一个普通寄存器

PUSHFD

EFLAGS寄存器中的内容压入栈中

POPFD

从栈中取出四个字节到EFLAGS寄存器中

Windows操作系统上栈的做法

Microsoft Visual C++编译器将所有传入函数的参数扩展为32位,即四个字节。函数返回值也被扩展为四个四节,被保存在EAX寄存器中。如果返回值是8个字节,就会被保存在EDX:EAX寄存器对中。

如果返回值是更大的结构,将会把返回值的地址保存在EAX中。编译器将自动生成prolog andepilog指令,用来保存和恢复ESI, EDI, EBX, 以及 EBP寄存器。

函数调用以及栈帧分配

让我们通过一个例子,从一般函数调用的视角看看栈帧是如何构造和销毁的。我们使用__cdecl的参数入栈方式,这些步骤靠Microsoft Visual C++ 6.0编译器自动生成。尽管这些步骤不是所有的函数调用都会产生,因为有的函数没有参数,没有局部变量。程序的代码之前已经列出。

通过F11开始单步调试,调出Disassembly窗口。汇编代码如下:

9:    int main(int argc, char *argv[])
10:   {
00401060   push        ebp                          ;将上一个栈帧的FB压入当前栈中
00401061   mov         ebp,esp                      ;调整FB为当前esp
00401063   sub         esp,40h                      ;分配buffer
00401066   push        ebx                          ;压入ebx
00401067   push        esi                          ;压入esi
00401068   push        edi                          ;压入edi
00401069   lea         edi,[ebp-40h]                ;将edi的值赋为FB为起始偏移40H字节
0040106C   mov         ecx,10h                      ;
00401071   mov         eax,0CCCCCCCCh               ;
00401076   rep stos    dword ptr [edi]              ;将刚刚分配的40H字节初始化为0xCCCCCCCCh
11:       MyFunc(7,'8');
00401078   push        38h                          ;把参数'8'压入当前栈帧
0040107A   push        7                            ;把参数 7 压入当前栈帧
0040107C   call        @ILT+5(MyFunc) (0040100a)    ;把返回地址00401081压入当前栈帧,然后调用MyFunc(0040100a)
00401081   add         esp,8                        ;清除传入参数
12:       return 0;
00401084   xor         eax,eax
13:   }
00401086   pop         edi
00401087   pop         esi
00401088   pop         ebx
00401089   add         esp,40h
0040108C   cmp         ebp,esp
0040108E   call        __chkesp (004010b0)
00401093   mov         esp,ebp
00401095   pop         ebp
00401096   ret

1:    #include <stdio.h>
2:    int MyFunc(int parameter1, char parameter2)
3:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,48h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-48h]
0040102C   mov         ecx,12h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
4:        int local1 = 9;
00401038   mov         dword ptr [ebp-4],9          ;复制局部参数到栈中
5:        char local2 = 'Z';
0040103F   mov         byte ptr [ebp-8],5Ah
6:        return 0;
00401043   xor         eax,eax
7:    }
00401045   pop         edi                          ;恢复edi
00401046   pop         esi                          ;恢复esi
00401047   pop         ebx                          ;恢复ebx
00401048   mov         esp,ebp                      ;恢复上一个栈帧的esp指针
0040104A   pop         ebp                          ;恢复上一个栈帧的ebp
0040104B   ret                                      ;回到返回地址00401081

下图是以上代码调试过程中的栈帧布局。蓝色和黄色部分分别代表一个完整的栈帧。

参考资料:

http://www.tenouk.com/Bufferoverflowc/Bufferoverflow2a.html


抱歉!评论已关闭.