随想录(函数压栈)

来源:互联网 发布:linux用什么浏览器 编辑:程序博客网 时间:2024/05/23 12:10

【 声明:版权所有,欢迎转载,请勿用于商业用途。  联系信箱:feixiaoxing @163.com】

 

    在编译器编译文件的时候,软件会根据程序本身的要求对函数作不同的压栈处理。有的压栈是按照从左到右进行压栈,有的压栈是按照从右到左进行压栈,有的不压栈、直接用寄存器代替,有的是需要被调用函数自身自己平衡堆栈。下面,我们就可以一个一个自己看看。首先,随便写一个函数,

int add(int a, int b){return a + b;}

 

    (1)从右到左压栈

    _cdelc是编译器默认的一种压栈方式,在函数上定义不定义其实意义不大。不过,我们愿意贴上代码说明一下,

int __cdecl add(int a, int b){return a + b;}

    下面,在main函数里面调用此add函数,我们看看对应的汇编是怎么处理的,

14:       int p = add(2, 3);00401068   push        30040106A   push        20040106C   call        @ILT+0(_add) (00401005)00401071   add         esp,800401074   mov         dword ptr [ebp-4],eax15:       return 1;00401077   mov         eax,1

    从上面的代码来看,3先压栈,然后是数据2,很明显的从右向左压栈。

 

    (2)从左向右压栈

    其实,在windows之前的编译器是支持从左向右进行压栈的,但是现在不支持了。比如说,如果你输入下面这段代码,

int __pascal add(int a, int b){return a + b;}

    此时,编译器会给你贴上一个错误提示,error C4226: nonstandard extension used : '__pascal' is an obsolete keyword。提示说的很明白,__pascal是一个过时的关键字,现在不支持了。其实堆栈压栈从左向右、还是从右向左其实无所谓。但是如果遇到的函数是变参的话,那么此时就存在问题了。因为对于__pascal而言,最后一个参数不知道究竟是在ebp的哪个偏移位置了?

 

    (3)用寄存器代替压栈

    用寄存器代替数据压栈是arm、powerpc等cpu使用的比较多的一种方法。因为用寄存器代替压栈,主要是考虑到速度方面的原因。毕竟取数据、保存数据相比较寄存器操作还是非常耗时间的,其此就是这两种cpu的寄存器资源特别丰富。同样,首先我们要用__fastcall装饰一下函数,

int __fastcall add(int a, int b){return a + b;}

    那接下来,我们看看调用的时候发生了什么变化,

14:       int p = add(2, 3);00401068   mov         edx,30040106D   mov         ecx,200401072   call        @ILT+10(_add) (0040100f)00401077   mov         dword ptr [ebp-4],eax15:       return 1;0040107A   mov         eax,1

    和上面的压栈不同,这里用edx保存了数据3,用eax保存了数据eax。毕竟寄存器运算要比内存运算快得多。


    (4)被调用和自行进行压栈恢复
    __stdcall是我们这里讲到的最后一种压栈模式。在函数压栈的方面,他和__cdelc是一样的,但是关键就在add函数结束的位置发生了变化。首先,我们需要用__stdcall装饰了一下函数,

int __stdcall add(int a, int b){return a + b;}

    那么此时函数汇编的时候,代码发生了变化呢?

7:    int __stdcall add(int a, int b)8:    {00401020   push        ebp00401021   mov         ebp,esp00401023   sub         esp,40h00401026   push        ebx00401027   push        esi00401028   push        edi00401029   lea         edi,[ebp-40h]0040102C   mov         ecx,10h00401031   mov         eax,0CCCCCCCCh00401036   rep stos    dword ptr [edi]9:        return a + b;00401038   mov         eax,dword ptr [ebp+8]0040103B   add         eax,dword ptr [ebp+0Ch]10:   }0040103E   pop         edi0040103F   pop         esi00401040   pop         ebx00401041   mov         esp,ebp00401043   pop         ebp00401044   ret         8

    这里的汇编代码没有什么特别之处。但是最后一个ret 8是什么意思呢?其实因为之前有两个参数a和b,那么8就是这两个参数占有的空间。此时ret b事实上就是让ebp恢复到原来的空间。仅此而已。但是我们发现,可能这一步运行之后,ebp加了不是8,而是12,这又是为什么呢?因为还有4个字节的返回地址没有加上呢。