通过汇编程序看函数调用过程中的原理

一、C++ 源代码,本文所有汇编、函数堆栈数据情况都是根据以下源代码得到的

int Add(int x,int y)
{
	int sum;
	sum = x+y;
	return sum;
}

int main(int argc, char * argv[])
{
	int z;

	z = Add(1,2);
}



二、需要知道的基础知识:


1、X86 寄存器基础


    (1)ESP:栈顶指针,X86中的栈是向下增长,所以入站push时 esp--,出栈pop时,esp++

    (2)EBP:函数的参数和局部变量都是存储在程序栈中,所以当一个函数想要获取它自己的参数或者局部变量时,想到的第一个方案就是使用(ESP寄存器的值+栈偏移量)推算出参数和局部变量的地址。但是栈顶指针的值会随着程序入栈和出栈操作不断变化。所以为了计算方便,可以将该值保存到另一个寄存器---EBP(extended base pointer,扩展基址寄存器)。这样获取参数可以用:EBP+偏移量,获取局部变量就可以用  EBP-偏移量了。(下图中的数据是根据第一部分的代码得到的)

            

    (3)EBX ,基址寄存器,在内存中寻址时使用。

    (4)ESI/EDI,源/目的地址寄存器,暂时不清楚有什么用

    (5)ECX,(extended counter )计数器寄存器,和rep和loop指令搭配使用。主要用来进行循环计数

2、汇编指令基础

        (1)lea指令,格式:lea + 目的寄存器+源操作数,作用:将源操作数的地址偏移量保存到目的寄存器中。学习lea指令可以和mov指令一起来记,他们格式相同,但mov指令是将操作数指向的内存中的数据保存到目的寄存器。

       (2)call 指令,格式 :call + 目标地址,作用:将程序调转到目标地址处执行。

                 call 指令使用的是相对寻址,所谓的相对寻址就是:基址+偏移量 = 最终地址。在call指令中,基址就是call指令的下一条指令的起始地址。偏移量就是call指令中后4字节的内容。

                 call指令返回地址会在指令执行过程中被压到程序栈中。

                 等价指令:push EIP+5 ,jmp 目标地址

       (3)ret指令,作用:将栈顶保存的地址弹入EIP指令寄存器,这个过程ESP要增大(因为执行了一次出栈操作)

       (4)rep 指令,格式 rep+其他指令,作用:重复rep后面的其他指令,重复次数记录在ECX寄存器中,每次循环ECX寄存器执行减减操作。

       (5)stos指令,格式 stos+目的地址,将寄存器EAX中的内容保存到目的地址处。目的地址格式 ES:[EDI] ,ES保存了段选择符,EDI保存了段偏移量。如果设置了direction flag, 那么EDI会在该指令执行后减小, 如果没有设置direction flag, 那么EDI的值会增加, 为下一次的存储做准备

三、正文,使用汇编分析函数调用并返回过程中的原理


        1、main函数反汇编:

int main(int argc, char * argv[])
{
001D1A70 55               push        ebp  
001D1A71 8B EC            mov         ebp,esp 
001D1A73 81 EC CC 00 00 00 sub         esp,0CCh 
001D1A79 53               push        ebx  
001D1A7A 56               push        esi  
001D1A7B 57               push        edi  
001D1A7C 8D BD 34 FF FF FF lea         edi,[ebp+FFFFFF34h] 
001D1A82 B9 33 00 00 00   mov         ecx,33h 
001D1A87 B8 CC CC CC CC   mov         eax,0CCCCCCCCh 
001D1A8C F3 AB            rep stos    dword ptr es:[edi] 
	int z;

	z = Add(1,2);
001D1A8E 6A 02            push        2          //将实参压栈,并且参数的压栈顺序是从右到左,
001D1A90 6A 01            push        1    
001D1A92 E8 44 F7 FF FF   call        001D11DB   //执行call指令,将程序控制流转移到Add函数处
001D1A97 83 C4 08         add         esp,8 
001D1A9A 89 45 F8         mov         dword ptr [ebp-8],eax 
}


        2、Add函数反汇编

int Add(int x,int y)
{
00111410 55               push        ebp             //将main函数使用的ebp指针压到栈中
00111411 8B EC            mov         ebp,esp         //更新EBP寄存器的值,更新完成后,Add函数即可以使用EBP定位它的函数参数和局部变量

00111413 81 EC CC 00 00 00 sub         esp,0CCh       //栈指针自减,目的是在栈中创建一块内存,用来保存函数状态

00111419 53               push        ebx             //保存main函数的ebx,esi,edi寄存器
0011141A 56               push        esi  
0011141B 57               push        edi  

/*将栈中介于main函数EBP和main函数ebx之间的内存单元以4字节为单位,每单位写入0CCCCCCCh,*/
/*VS中,为了调试方便将栈中未经初始化的内存都设置成0CCCCCCCh,release版本就不再是0CC..了*/
0011141C 8D BD 34 FF FF FF lea         edi,[ebp+FFFFFF34h]  // FF FF FF FF 34h 是负数(-0xCC)的补码,所以相当于 lea edi[ebp-0xcc]
00111422 B9 33 00 00 00   mov         ecx,33h               //循环计数器,标志rep函数会执行0x33h = 51次
00111427 B8 CC CC CC CC   mov         eax,0CCCCCCCCh        //stos 指令会将eax寄存器中的内容复制到目的地址处
0011142C F3 AB            rep stos    dword ptr es:[edi]    //重复执行stos指令51次,每次写4个字节,--> 51*4 == 204字节 == 0xCC,刚好对应上面的sub esp 0CCh

	int sum;
	sum = x+y;
0011142E 8B 45 08         mov         eax,dword ptr [ebp+8] 
00111431 03 45 0C         add         eax,dword ptr [ebp+0Ch] 
00111434 89 45 F8         mov         dword ptr [ebp-8],eax 
	return sum;
00111437 8B 45 F8         mov         eax,dword ptr [ebp-8] 
}

/*Add函数结束时的动作*/

0011143A 5F               pop         edi                   //回复main函数的各种寄存器                  
0011143B 5E               pop         esi  
0011143C 5B               pop         ebx  

0011143D 8B E5            mov         esp,ebp               //调整栈顶指针,使esp指向保存有main函数EBP的地址处,为恢复main函数ebp做准备 
0011143F 5D               pop         ebp                   //正式恢复main函数的ebp
00111440 C3               ret                               //函数返回,会触发一次出栈操作


     3、分析,(汇编中为明显体现的部分)




(1)我们都知道函数返回时,会销毁局部变量。那么到底是怎么销毁的局部变量???从汇编中稍微分析下就发现原来只是简单的用main函数的EBP值覆盖掉Add函数的EBP值(对应上面的pop ebp 指令),函数定位局部变量就是用EBP寄存器作为基址,没了EBP,那么也就找不到局部变量了,换句话说也就是局部变量被销毁了。

(2)函数有返回值,返回值是如何返回到main数中的?

          

2)函数有返回值,返回值是如何返回到main数中的?

          关于这个问题,用上面的汇编指令解释就有点不合适了。贴出新的代码

C++ 源代码


           

struct myrd
{
    int i1;
    double i2;
    double i3;
};

myrd myfunc()
{
    myrd r1;

    r1.i1 = 1;
    r1.i2 = 2.0;
    r1.i3 = 3.0;
    return r1;
}

int main()
{
    myrd r;
    r = myfunc();           //注意这里的myfunc()是没有实参的

    r.i1 = 1;
        
    getchar();
}

        反汇编: 

        main函数

int main()
{
00EE1460 55               push        ebp  
00EE1461 8B EC            mov         ebp,esp 
00EE1463 81 EC 5C 01 00 00 sub         esp,15Ch 
00EE1469 53               push        ebx  
00EE146A 56               push        esi  
00EE146B 57               push        edi  
00EE146C 8D BD A4 FE FF FF lea         edi,[ebp+FFFFFEA4h] 
00EE1472 B9 57 00 00 00   mov         ecx,57h 
00EE1477 B8 CC CC CC CC   mov         eax,0CCCCCCCCh 
00EE147C F3 AB            rep stos    dword ptr es:[edi] 
   myrd r;
 r = myfunc();
/*main函数在调用myfunc之前实现开辟好内存空间,并将该内存空间的首地址(ebp+FFFFFEDCh)传递给myfunc*/
/*下面是具体过程*/

00EE147E 8D 85 DC FE FF FF lea         eax,[ebp+FFFFFEDCh]   // eax = ebp - 0x124h,让eax指向main函数分配好的返回值空间
00EE1484 50               push        eax                    //eax压栈,相当于将eax传递给myfunc函数
00EE1485 E8 52 FC FF FF   call        00EE10DC 
00EE148A 83 C4 04         add         esp,4 
00EE148D B9 0B 00 00 00   mov         ecx,0Bh 
00EE1492 8B F0            mov         esi,eax 
00EE1494 8D BD A8 FE FF FF lea         edi,[ebp+FFFFFEA8h] 
00EE149A F3 A5            rep movs    dword ptr es:[edi],dword ptr [esi] 
00EE149C B9 0B 00 00 00   mov         ecx,0Bh 
00EE14A1 8D B5 A8 FE FF FF lea         esi,[ebp+FFFFFEA8h] 
00EE14A7 8D 7D D0         lea         edi,[ebp-30h] 
00EE14AA F3 A5            rep movs    dword ptr es:[edi],dword ptr [esi] 


   r.i1 = 1;
00EE14AC C7 45 D0 01 00 00 00 mov         dword ptr [ebp-30h],1 
       
getchar();
00EE14B3 8B F4            mov         esi,esp 
00EE14B5 FF 15 BC 82 EE 00 call        dword ptr ds:[00EE82BCh] 
00EE14BB 3B F4            cmp         esi,esp 
00EE14BD E8 7E FC FF FF   call        00EE1140 
}

...省略...
}

           myfunc函数源代码:          

          在看myfunc反汇编前,先看下此时栈空间的使用情况


         

myrd myfunc()
{
/*...函数状态切换相关指令....*/
 
    myrd r1;

    r1.i1 = 1;
012113CE C7 45 D0 01 00 00 00 mov         dword ptr [ebp-30h],1 //ebp-30h就是局部变量r1的首地址
    return r1;
012113D5 B9 0B 00 00 00   mov         ecx,0Bh                //为rep指令设置循环计数器,0Bh = 11次,
012113DA 8D 75 D0         lea         esi,[ebp-30h]          //设置数据源寄存器,esi = ebp - 30h,数据源即局部变量r1,表明后面的指令将要从esi(局部变量r1处)读取数据
012113DD 8B 7D 08         mov         edi,dword ptr [ebp+8]  //设置目的寄存器,edi = ebp + 8,[ebp+8]就是main函数传递过来的用于存储函数返回值的内存空间的首地址
012113E0 F3 A5            rep movs    dword ptr es:[edi],dword ptr [esi]  //开始循环的从esi处读取数据到edi处,每次循环读写dwrod 4个字节,循环11次 -> 4 * 11 = 44 ,刚好是结构体myrd的大小
012113E2 8B 45 08         mov         eax,dword ptr [ebp+8]  //将返回值地址保存到eax中
}
012113E5 52               push        edx  
012113E6 8B CD            mov         ecx,ebp 
012113E8 50               push        eax  
012113E9 8D 15 00 14 21 01 lea         edx,ds:[01211400h] 
012113EF E8 93 FC FF FF   call        01211087 
012113F4 58               pop         eax  
012113F5 5A               pop         edx  
012113F6 5F               pop         edi  
012113F7 5E               pop         esi  
012113F8 5B               pop         ebx  
012113F9 8B E5            mov         esp,ebp 
012113FB 5D               pop         ebp  
012113FC C3               ret

好了,画出来整个过程的关系图:


          step1、lea    eax,[ebp+FFFFFEDCh]   ,在EBP指针下方开辟一段内存空间,用于保存返回值

          step2、push   eax     ,将eax寄存器压栈,即将值传递给myfunc函数

          step3、myfunc在函数结束时,将返回值内容填到eax指定的内存中

          mov         ecx,0Bh 
          lea         esi,[ebp-30h] 
          mov         edi,dword ptr [ebp+8] 
          rep movs    dword ptr es:[edi],dword ptr [esi] 
          mov         eax,dword ptr [ebp+8]

          直到ret执行前,eax的值一直没有变化(注意上文中给的代码在结尾处其实又使用了一次eax,但也是先push eax,使用eax,再pop eax,最后eax还是没有变)
         

         step4、main函数从返回值空间拷贝数据到另一个临时空间 ebp+FFFFFEA8h

         mov         ecx,0Bh 
         mov         esi,eax 
         lea         edi,[ebp+FFFFFEA8h] 
         rep movs    dword ptr es:[edi],dword ptr [esi] 

         step5、main函数最后终于从临时空间ebp+FFFFFEA8h拷贝了数据并赋值给局部变量r = ebp-30h


         也许你跟我一样奇怪,ebp+FFFFEA8h又是个球啊!!!,难道整个过程不应该是下面这样的吗??


 

         这不仅让我想到了那句“理想很丰满,现实很骨感”,问了下老师回复是这样的:

        "没有优化的结果。
 
        没有优化时,编译器按照自己的思路,一块一块的进行编译,不会过多考虑块块之间的关系。"

  

        好了这些问题个人认为再探讨下去也没什么意思了,做个小结:


       函数的调用环节可以分为:(1)传参(2)保存上下文(3)向返回值空间写值(4)恢复上下文(5)从临时空间拷贝数据

       其中,传参和返回数据的方式有两种:通过寄存器(由于寄存器大小和数量的限制,所以只能小型参数)和通过栈; 返回值的方式也有两种寄存器和栈


四、下面再给出一个以值传递方式传递参数的例子

    


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页