一、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)从临时空间拷贝数据
其中,传参和返回数据的方式有两种:通过寄存器(由于寄存器大小和数量的限制,所以只能小型参数)和通过栈; 返回值的方式也有两种寄存器和栈
四、下面再给出一个以值传递方式传递参数的例子