zoukankan      html  css  js  c++  java
  • C函数调用与栈--代码真相

    前面详细的说了,C函数调用的过程中,栈的变化情况的原理部分,这里在看一下汇编代码的真正的实现。

    有关前面的那一片博客,主要记住的就是函数调用时栈的变化,4+3+2的步骤:

    (1)设置栈帧边界

    (2)开辟本函数的局部区域

    (3)保存寄存器的内容

    (4)初始化局部区域(int3)

    (5)如果有函数调用

    (a)push实参入栈

    (b)call执行,设置返回地址,然后执行被调函数代码

           (c)调整sp栈顶指针,删除实参

    (6)恢复之前保存的寄存器的值

    (7)取消栈帧的边界(main函数还要做一次校验)


    这段代码反汇编后,代码部分看一下:

    #include <stdio.h>
    
    long test(int a,int b)
    {
         a = a + 3;
         b = b + 5;
    
         return a + b;
    }
    
    int main(int argc, char* argv[])
    {
    
        printf("%d",test(10,90));
    
        return 0;
    }

    先来看一下main函数的汇编代码:

    16:   int main(int argc, char* argv[])
    17:   {
            //设置栈帧边界
    00401070   push        ebp
    00401071   mov         ebp,esp
            //设置局部变量区域
    00401073   sub         esp,40h
            //保存寄存器内容
    00401076   push        ebx
    00401077   push        esi
    00401078   push        edi
            //初始化局部变量区域
    00401079   lea         edi,[ebp-40h]
    0040107C   mov         ecx,10h
    00401081   mov         eax,0CCCCCCCCh
    00401086   rep stos    dword ptr [edi]
    
    18:       printf("%d",test(10,90));
            //push实参入栈
    00401088   push        5Ah
    0040108A   push        0Ah
            //call指令,把EIP保存到栈中
    0040108C   call        @ILT+0(test) (00401005)
            //删除实参的空间
    00401091   add         esp,8
            //test()函数的返回值存在了eax中,它是printf函数的实参,直接实参入栈
    00401094   push        eax
    00401095   push        offset string "%d" (0042201c)
            //call指令,把EIP保存到栈中
    0040109A   call        printf (004010d0)
            //删除实参的空间
    0040109F   add         esp,8
    19:       return 0;
    
    004010A2   xor         eax,eax
    20:   }

    下面来解释一下:

    image

    开始进入Main函数  esp=0x12FF84   ebp=0x12FFC0
    完成椭圆形框起来的部分
    00401070   push        ebp     ebp的值入栈,保存现场(调用现场,从test函数看,如红线所示,即保存的0x12FF80用于从test函数堆栈返回到main函数)
    00401071   mov         ebp,esp     此时ebp=0x12FF80 此时ebp就是“当前函数堆栈”的基址 以便访问堆栈中的信息;还有就是从当前函数栈顶返回到栈底
    00401073   sub      esp,40h  
    函数使用的堆栈,默认64个字节,堆栈上就是16个横条(密集线部分)此时esp=0x12FF40
    在上图中,上面密集线是test函数堆栈空间,下面是Main的堆栈空间    (补充,其实这个就叫做 Stack Frame)
    00401076   push        ebx
    00401077   push        esi
    00401078   push        edi    入栈
    00401079   lea         edi,[ebp-40h]
    0040107C   mov         ecx,10h
    00401081   mov         eax,0CCCCCCCCh
    00401086   rep stos    dword ptr [edi]     
    初始化用于该函数的栈空间为0XCCCCCCCC  即从0x12FF40~0x12FF80所有的值均为0xCCCCCCCC

    REP           CX不等于0 ,则重复执行字符串指令

    格式: STOS OPRD

    功能: 把AL(字节)或AX(字)中的数据存储到DI为目的串地址指针所寻址的存储器单元中去.指针DI将根据DF的值进行自动

    调整. 其中OPRD为目的串符号地址.

    以上的语句就是在栈中开辟一块空间放局部变量
    然后把这块空间都初始化为0CCCCCCCCh,就是int3断点,一个中断指令。
    因为局部变量不可能被执行,执行了就会出错,这时候发生中断提示开发者。


    18:       printf("%d",test(10,90));
    00401088   push        5Ah    参数入栈 从右至左 先90  后10
    0040108A   push        0Ah 
    0040108C   call        @ILT+0(test) (00401005)   
    函数调用,转向eip 00401005 
    注意,此时仍入栈,入栈的是call test 指令下一条指令的地址00401091   下一条指令是add esp,8
    @ILT+0(?test@@YAJHH@Z):
    00401005   jmp         test (00401020)

    00401005就是这个test函数在ILT静态表的入口,这里有个jmp指令,直接跳转到test函数的代码存储的区域。

    注意:

    汇编语言每条指令的最前面就是就是这条指令在内存代码区的位置,每次运行的时候,都根据事先把指令的地址放到程序计数器(EIP寄存器)中,指令是挨着盘存放的,所有两条指令的地址相减,就能看出这条汇编指令占用的字节数了。拿这两条指令为例:

            //call指令,把EIP保存到栈中
    0040108C   call        @ILT+0(test) (00401005)
            //删除实参的空间
    00401091   add         esp,8

    两个地址相减,得到的字节数就是call指令占用的存储空间。

     然后就转向了被调函数test:

    8:    long test(int a,int b)
    9:    {
    00401020   push        ebp
    00401021   mov         ebp,esp           
    00401023   sub         esp,40h
    00401026   push        ebx
    00401027   push        esi
    00401028   push        edi
    00401029   lea         edi,[ebp-40h]
    0040102C   mov         ecx,10h
    00401031   mov         eax,0CCCCCCCCh
    00401036   rep stos    dword ptr [edi]       //这些和上面一样
    10:        a = a + 3;                                    
    00401038   mov         eax,dword ptr [ebp+8]     //ebp=0x12FF24 加8 [0x12FF30]即取到了参数10
    0040103B   add         eax,3
    0040103E   mov         dword ptr [ebp+8],eax
    11:        b = b + 5;
    00401041   mov         ecx,dword ptr [ebp+0Ch]
    00401044   add         ecx,5
    00401047   mov         dword ptr [ebp+0Ch],ecx
    12:        return a + b;
    0040104A   mov         eax,dword ptr [ebp+8]
    0040104D   add         eax,dword ptr [ebp+0Ch]  //最后的结果保存在eax, 结果得以返回
    13:   }
    00401050   pop         edi                 
    00401051   pop         esi
    00401052   pop         ebx
    00401053   mov         esp,ebp     //esp指向0x12FF24, test函数的堆栈空间被放弃,从当前函数栈顶返回到栈底
    00401055   pop         ebp           //此时ebp=0x12FF80, 恢复现场  esp=0x12FF28
    00401056   ret                          ret负责栈顶0x12FF28之值00401091弹出到指令寄存器中,esp=0x12FF30

    因为win32汇编一般用eax返回结果 所以如果最终结果不是在eax里面的话 还要把它放到eax

    注意,从被调函数返回时,是弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。

    从test函数返回,执行
    00401091   add         esp,8       
    清栈,清除两个压栈的参数10 90 调用者main负责
    (所谓__cdecl调用由调用者负责恢复栈,调用者负责清理的只是入栈的参数,test函数自己的堆栈空间自己返回时自己已经清除,靠!一直理解错)

    00401094   push       eax          入栈,计算结果108入栈,即printf函数的参数之一入栈
    00401095   push        offset string "%d" (0042201c)     入栈,参数 "%d"  当然其实是%d的地址
    0040109A   call        printf (004010d0)      函数调用 printf("%d",108) 因为printf函数时
    0040109F   add         esp,8       清栈,清除参数 ("%d", 108)
    19:       return 0;           
    004010A2   xor         eax,eax     eax清零
    20:   }

    main函数执行完毕 此时esp=0x12FF34   ebp=0x12FF80
    004010A4   pop         edi
    004010A5   pop         esi
    004010A6   pop         ebx
    004010A7   add         esp,40h    //为啥不用mov esp, ebp? 是为了下面的比较
    004010AA   cmp         ebp,esp   //比较,若不同则调用chkesp抛出异常
    004010AC   call        __chkesp (00401150)   
    004010B1   mov         esp,ebp   
    004010B3   pop         ebp          //ESP=0X12FF84  EBP=0x12FFC0 尘归尘 土归土 一切都恢复最初的平静了  :)
    004010B4   ret

    注意:

    1. 如果函数调用方式是__stdcall 不同之处在于 main函数call 后面没有了 add esp, 8   test函数最后一句是ret 8 (由test函数清栈, ret 8意思是执行ret后,esp+8)

    2. 运行过程中0x12FF28 保存了指令地址 00401091是怎么保存的?
    栈每个空间保存4个字节(粒度4字节) 例如下一个栈空间0x12FF2C保存参数10  
    因此
    0x12FF28 0x12FF29 0x12FF2A 0x12FF2B   
      91              10            40           00       
    little-endian  认为其读的第一个字节为最小的那位上的数

    3. char a[] = "abcde"  
    对局部字符数组变量(栈变量)赋值,是利用寄存器从全局数据内存区把字符串“abcde”拷贝到栈内存中的

    下面这两行代码就能看出来,同时还能看出来内存中各变量的对齐方式:

    5:        char a[] = "abcde";
    00401028   mov         eax,[string "abcde" (0042b01c)]
    0040102D   mov         dword ptr [ebp-8],eax
    00401030   mov         cx,word ptr [string "abcde"+4 (0042b020)]
    00401037   mov         word ptr [ebp-4],cx
    6:        int c = 10;
    0040103B   mov         dword ptr [ebp-0Ch],0Ah


    4. int szNum[5] = { 1, 2, 3, 4, 5 }; 栈中是如何分布的?

         00401798   mov         dword ptr [ebp-14h],1
         0040179F   mov         dword ptr [ebp-10h],2
         004017A6   mov         dword ptr [ebp-0Ch],3
         004017AD   mov         dword ptr [ebp-8],4
         004017B4   mov         dword ptr [ebp-4],5

    可以看出来 是从右边开始入栈,所以是 5 4 3 2 1 入栈

    int *ptrA = (int*)(&szNum+1);
    int *ptrB = (int*)((int)szNum + 1);
    std::cout<< ptrA[-1] << *ptrB << std::endl;

    结果如何?

    28:       int *ptrA = (int*)(&szNum+1);
    004017BB   lea         eax,[ebp]
    004017BE   mov         dword ptr [ebp-18h],eax
    &szNum是指向数组指针;加1是加一个数组宽度;&szNum+1指向移动5个int单位之后的那个地方, 就是把EBP的地址赋给指针
    ptrA[-1]是回退一个int*宽度,即ebp-4
    29:       int *ptrB = (int*)((int)szNum + 1);
    004017C1   lea         ecx,[ebp-13h]
    004017C4   mov         dword ptr [ebp-1Ch],ecx
    如果上面是指针算术,那这里就是地址算术,只是首地址+1个字节的offset,即ebp-13h给指针

    实际保存是这样的
    01               00           00      00           02           00      00      00
    ebp-14h     ebp-13h                      ebp-10h
    注意是int*类型的,最后获得的是 00 00 00 02 
    由于Little-endian, 实际上逻辑数是02000000   转换为十进制数就为33554432
    最后输出:

    5

    33554432

  • 相关阅读:
    codeforces 55d记忆化搜索
    codeforces 698b 图论
    codeforces 716d 图论加二分
    求多边形面积模板***
    hdu 5869 区间gcd的求法及应用
    codeforces 589a(构造的字符串后,最后要加终止符,,,)
    凸包模板***
    2014ACM-ICPC 西安赛区总结
    Codeforces 475D CGCDSSQ(分治)
    Acdream1217 Cracking' RSA(高斯消元)
  • 原文地址:https://www.cnblogs.com/stemon/p/4425520.html
Copyright © 2011-2022 走看看