1.栈帧
栈帧(stack frame)栈帧是在程序的运行时栈中分配的内存块,专门用于特定的函数调用。
栈帧(激活记录)调用函数的详细步骤
2.1.调用方将被调用函数所需的参数放入到该函数所采用的调用约定指定的位置。如果参数被放到运行时栈上,该操作可能导致程序的栈指针发生改变。
2.2.调用方将控制权转交给被调用的函数,这个过程常由X86 CALL或MIPS JAL等指令执行。然后,返回地址被保存到程序栈或CPU寄存器中。
2.3.如果有必要,被调用的函数会配置一个栈指针,并保存调用方希望保持不变的任何寄存器值。
2.4.被调用的函数为它可能需要的任何局部变量分配空间。一般通过调整程序栈指针运行时栈上保留空间来完成这一任务。
2.5.被调用的杉树执行其操作,可能会生成一个结果。在执行操作的过程中,被调用的函数可能会访问调用函数传递给他的参数。如果函数返回一个结果,此结果通常被放置到一个特定的寄存器中,或者放置到函数返回后调用可立即访问的寄存器中。
2.6.一旦函数完成其操作,任何为局部变量保留的栈空间即被释放。通常,逆向执行第4步中的操作,即可完成这个任务。
2.7.如果某个寄存器的值还未调用方保存(第3步)着,那么将其恢复到原始值。这包括恢复调用方得帧指针寄存器。
2.8.被调用的函数将控制权返还给调用方。实现这一操作的主要指令包括X86 RET和 MIPS JR。根据所使用的调用约定,这一操作可能还会从程序栈中清除一个或多个参数。
2.9.调用方一旦重新获得控制权,它可能需要删除程序栈中的参数,这时可能需要对栈进行调整,以将程序栈指针恢复到第1步以前的值。
2.调用约定
1.常见的X86编译器(MV C/C++或GUN的GCC/G++);创建栈帧时最重要的步骤,是通过调用函数将参数存入栈中。【调用函数必须存储调用的参数!】
C调用约定(C/C++程序中常用的_cdecl修饰符会迫使编译器利用C调用约定。)
(调用方按从右到左的顺序将函数参数放入栈,在被调用的函数完成操作时,调用方负责从栈中清除参数。)
(从右到左在战中放入参数的结果是,如果函数被调用,最左面的(第一个)参数将始终位于栈顶。这样,无论该函数需要多少个参数,我们都可轻易的找到第一个参数【因此,cdecl调用约定非常适用于那些参数数量可变的函数(Printf)】)
(要求调用函数从栈中删除参数,意味着:指令在由被调用的函数返回后,会立即对程序栈指针进行调整。如果函数能够接受数量可变的参数,则调用方非常适于进行这种调整。【因为它清除的指导,它向函数传递了多少个参数,因此能够轻松的调整。被调用方不知道自己会收多少个参数,因此很难对栈做出必要的调整】)
(无论采用哪一种方法,在调用函数时,栈指针都会指向最左面的参数)
标准调用约定(微软为自己的调用约定起的名称叫_stdcall)
(调用约定按从右到左的顺序将函数参数放在程序栈上。【区别在于:函数结束执行时,应由被调用的函数负责删除栈中的函数参数,只有参数数量固定不变才有可能】)
(优点:在每次函数调用之后,不需要通过代码从栈中清除参数,因而能生成体积稍小、速度稍快的程序。【如果你正尝试为谋个共享库(DLL)组件生成函数原型或与二进制兼容的替代者,一定要记住这一点!】)
X86 fastcall约定(fastcall是stdcall约定的一个变体,它向CPU寄存器(而非程序栈)最多传递两个参数)
(MV C/C++和GUN GCC/G++编译器能够识别函数声明中的fastcall修饰符!)
(如果指定使用fastcall约定,则传递给函数的前两个参数将分别位于ECX和EDX寄存器中。剩余的其他参数则以类似于stdcall约定的方式从右到左放入栈上。)
(与stdcall约定相似的是,在返回其调用方时,fastcall函数负责从栈中删除参数。)
(由于有两个参数被传递到寄存器中,被调用的函数仅仅需要从栈中清除8个字节,即使该函数拥有4个参数【fastcall只清理参数y和z】)
C++调用约定(需要this指针,该指针指向用于调用函数的对象)
(用于调用函数的对象的地址必须由调用方提供,因此,它在调用非静态成员函数时作为参数提供。)
(C++语言标准并未规定应如何向非静态成员函数传递this指针,因此,不同的编译器使用不同的技巧传递this指针)
(VC++提供thiscall调用约定,它将this传递到ECX寄存器中,并且和在stdcall中一样,它要求非静态成员函数清除栈中的参数)
(GUN G++编译器将this看成是任何非静态成员函数的第一个隐含参数,而在所有其他方面与使用cdecl约定相同【因此,对使用g++ 编译的代码来说,在调用非静态成员函数之前this被放置到栈顶,且调用方负责在函数返回时删除栈中的参数(至少有一个参数)】)
其他调用约定(调用约定特定于语言、编译器、CPU的,如果遇到由更少见的编译器生成的代码,可能需要你自己进行一番研究【注意:优化代码、定制汇编语言代码和系统调用】)
(如果输出函数(如库函数)是为了供其他程序员使用,必须遵守调用约定。;如果函数仅供内部程序使用,则该函数需要采用只有函数的程序才了解的调用约定,【VC++中使用/GL选项,以及在GUN gcc/g++中使用regparm关键字】)
(如果程序员不嫌麻烦,使用了汇编语言,那么,他们就能够完全控制如何向他们的创建的函数传递参数。吹飞他们希望创建供其他程序员使用的函数。否则,汇编语言程序员能够以任何他们认为适当的方式传递参数。【因此,在分析自定义汇编代码时注意:模糊例程(obfuscation routine)和Shellcode常见的自定义汇编代码】)
(系统调用是一种特殊的函数调用,用于请求一项操作系统服务。通常,系统调用会造成状态转换,由用户模式进入内核,以便操作系统内核执行用户的请求。)
(启动系统调用的方式因操作系统和CPU而异。例如:Linux X86系统调用使用int 0x80指令启动,其他x86操作系统可能使用Sysenter指令)
(在许多x86系统上(Linux例外)系统调用的参数位于运行时栈上,并在启动系统调用之前,在EAX寄存器中放入一个系统调用编号。【Linux系统调用接受位于特定寄存器中的参数,有时候,如果可用寄存器无法存储所有的参数,它也接受位于程序栈上的参数】)
}
3.栈帧详解
C++代码
Void bar(int j, int k);
void demo_stackframe(int a, int b, int c)
{
int x;
char buffer[64];
int y;
int z;
bar(z,y);
}
(计算的出,局部变量最少需要76个字节的空间:3个4字节的证书和1个64字节的缓冲区)
1
(stdcall或cdecl调用约定,它们的栈帧完全相同)
(在分析使用栈指针引用栈帧变量的函数时,你必须小心,注意栈指针任何变化,并对所有未来的变量偏移量进行相应调整。使用栈指针引用所有栈帧变量的好处在于:所有其他寄存器仍可用于其他目的。)
(在弹出返回地址之前,需要从栈顶删除局部变量,以便于在ret指令执行时,栈指针正确的指向所保存的返回地址)
(由于专门使用一个寄存器作为帧指针,并通过一段代码在函数入口点配置了帧指针,因此,计算局部变量的偏移量的工作变的更加轻松。)
(在X86程序中,EBP(extended [ik’stendid]扩展 base pointer)扩展基址指针,寄存器通常专门用作栈帧指针。)
(栈帧中并没有用于储存被保存寄存器的标准位置)
(一旦EBP被保存,就可以修改,使他指向当前的栈位置,用mov完成,它将栈指针的当前值复制到EBP中)
(使用一个专用的帧指针,所有变量相对于帧指针寄存器的偏移量得以计算出来:正偏移量用于访问函数参数,而负偏移量则用于访问局部变量。【使用专用的帧指针,我们可以自由更改栈指针,而不影响帧内其他变量的偏移量】)
(
mov esp, ebp |
pop ebp |(由于这项操作十分常见,因此,x86体系结构提供了leave指令完成) “leave
ret | ret "
)