汇编函数调用过程研究
在c/c++等许多程序设计语言中,可以将一段经常需要使用的代码封装起来,在需要使用时可以直接调用,这就是程序中的函数,函数内封装了对数据和逻辑的处理。通过函数调用来执行函数体,下面根据代码进行分析。
#include <iostream>
using namespace std;
int test(int a,int b)
{
return a+b;
}
int main(int argc, char* argv[])
{
int a=2;
int b=3;
int c=test(2,3);
cout<<c<<endl;
system("pause");
return 0;
}
using namespace std;
int test(int a,int b)
{
return a+b;
}
int main(int argc, char* argv[])
{
int a=2;
int b=3;
int c=test(2,3);
cout<<c<<endl;
system("pause");
return 0;
}
BP为基指针(Base Pointer)寄存器,用它可直接存取堆栈中的数据; SP为堆栈指针(Stack Pointer)寄存器,用它只可访问栈顶。
在code::block(GUN GCC编译器)反汇编如下:
int test(int a,int b)对应的汇编代码:
004013EE push ebp ;ebp入栈
004013EF mov ebp,esp;把test函数的调用者的栈帧的栈顶作为一个新的栈帧的开始,即为test函数开辟栈帧。
;基本上每个函数的调用都会有这两句。
004013F1 mov eax,DWORD PTR [ebp+0xc] ;把ebp+0xc地址处的四个字节内存里的数据放到eax寄存器中。
004013F4 add eax,DWORD PTR [ebp+0x8] ;将eax数据和ebp+0x8地址开始四个字节内存里的数据相加,结果放在eax中
004013F7 pop ebp ;把栈顶数据push到ebp寄存器中
004013F8 ret ;这条指令等同于pop eip,jmp eip;
004013EF mov ebp,esp;把test函数的调用者的栈帧的栈顶作为一个新的栈帧的开始,即为test函数开辟栈帧。
;基本上每个函数的调用都会有这两句。
004013F1 mov eax,DWORD PTR [ebp+0xc] ;把ebp+0xc地址处的四个字节内存里的数据放到eax寄存器中。
004013F4 add eax,DWORD PTR [ebp+0x8] ;将eax数据和ebp+0x8地址开始四个字节内存里的数据相加,结果放在eax中
004013F7 pop ebp ;把栈顶数据push到ebp寄存器中
004013F8 ret ;这条指令等同于pop eip,jmp eip;
main函数对应的汇编代码:
通过上述分析,我们大致可以了解了函数调用的具体过程:004013FA push ebp
004013FB mov ebp,esp;main函数开辟栈帧
004013FD sub esp,0x18;抬高栈顶为局部变量分配空间。
00401400 and esp,0xfffffff0 ;对齐用,一个字节对齐。
00401403 mov eax,0x0
00401408 add eax,0xf
0040140B add eax,0xf
0040140E shr eax,0x4
00401411 shl eax,0x4
00401414 mov DWORD PTR [ebp-0x10],eax
00401417 mov eax,DWORD PTR [ebp-0x10]
0040141A call 0x40d154 <_alloca>
0040141F call 0x40ccd4 <__main>
00401424 mov DWORD PTR [ebp-0x4],0x14 ;为临时变量a分配空间,并赋值20
0040142B mov DWORD PTR [ebp-0x8],0x1e ;为临时变量b分配空间,并赋值30
00401432 mov eax,DWORD PTR [ebp-0x8] ;eax的值30
00401435 mov DWORD PTR [esp+0x4],eax ;将esp+0x4处往下涵盖四个字节的内存值设为30
00401439 mov eax,DWORD PTR [ebp-0x4]
0040143C mov DWORD PTR [esp],eax;将esp处往下涵盖四个字节的内存值设为20,
;和上面对应的语句其实等同于push 0x1e,push 0x14,vc++下基本是这两条语句
0040143F call 0x4013ee <test(int, int)>;call 指令中隐含push eip操作
00401444 mov DWORD PTR [ebp-0xc],eax ;test函数的返回值存在eax中,函数返回值如果是整形,
;则返回值在eax中。
00401447 mov eax,DWORD PTR [ebp-0xc]
0040144A mov DWORD PTR [esp+0x4],eax
0040144E mov DWORD PTR [esp],0x4453c0 ;0040144A和 0040144E 这两句指令相当于push 变量c的值
;再 push std::out对象的地址 而vc++编译器 一般会把this指针放在ecx寄存器中的。
00401455 call 0x42de94 <std::ostream::operator<<(int)>
0040145A mov DWORD PTR [esp+0x4],0x43d3dc
00401462 mov DWORD PTR [esp],eax
00401465 call 0x42d084 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))>
0040146A mov DWORD PTR [esp],0x442000
00401471 call 0x415154 <system>
00401476 mov eax,0x0
0040147B leave
0040147C ret
004013FB mov ebp,esp;main函数开辟栈帧
004013FD sub esp,0x18;抬高栈顶为局部变量分配空间。
00401400 and esp,0xfffffff0 ;对齐用,一个字节对齐。
00401403 mov eax,0x0
00401408 add eax,0xf
0040140B add eax,0xf
0040140E shr eax,0x4
00401411 shl eax,0x4
00401414 mov DWORD PTR [ebp-0x10],eax
00401417 mov eax,DWORD PTR [ebp-0x10]
0040141A call 0x40d154 <_alloca>
0040141F call 0x40ccd4 <__main>
00401424 mov DWORD PTR [ebp-0x4],0x14 ;为临时变量a分配空间,并赋值20
0040142B mov DWORD PTR [ebp-0x8],0x1e ;为临时变量b分配空间,并赋值30
00401432 mov eax,DWORD PTR [ebp-0x8] ;eax的值30
00401435 mov DWORD PTR [esp+0x4],eax ;将esp+0x4处往下涵盖四个字节的内存值设为30
00401439 mov eax,DWORD PTR [ebp-0x4]
0040143C mov DWORD PTR [esp],eax;将esp处往下涵盖四个字节的内存值设为20,
;和上面对应的语句其实等同于push 0x1e,push 0x14,vc++下基本是这两条语句
0040143F call 0x4013ee <test(int, int)>;call 指令中隐含push eip操作
00401444 mov DWORD PTR [ebp-0xc],eax ;test函数的返回值存在eax中,函数返回值如果是整形,
;则返回值在eax中。
00401447 mov eax,DWORD PTR [ebp-0xc]
0040144A mov DWORD PTR [esp+0x4],eax
0040144E mov DWORD PTR [esp],0x4453c0 ;0040144A和 0040144E 这两句指令相当于push 变量c的值
;再 push std::out对象的地址 而vc++编译器 一般会把this指针放在ecx寄存器中的。
00401455 call 0x42de94 <std::ostream::operator<<(int)>
0040145A mov DWORD PTR [esp+0x4],0x43d3dc
00401462 mov DWORD PTR [esp],eax
00401465 call 0x42d084 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))>
0040146A mov DWORD PTR [esp],0x442000
00401471 call 0x415154 <system>
00401476 mov eax,0x0
0040147B leave
0040147C ret
首先函数参数的按照调用约定依次入栈,也就是说函数参数的对应的内存分配在栈上,而且处于函数的被调用者的栈帧上,函数体内通过ebp或esp寻址,找到参数的地址,并对其操作。
上述函数的调用的栈布局如下: eip 保存有main函数调用完毕后下一条指令的地址。
高地址 ebp main函数的栈帧起始位置,此内存单位的内存地址值即当前ebp的值(在main函数中发生其他函数调用之前)
20 ebp-0x4到ebp这段内存对应变量a
30 ebp-0x8到ebp-0x4这段内存对应b
main函数的栈帧区域 …
…
…
30 esp+0x4到esp+0x8对应形参b
20 esp到esp+0x4对应形参a
eip eip的值为test函数调用完毕后后一条指令的地址
ebp 保存main函数的栈帧基址,即此处内存的地址
低地址
高地址 ebp main函数的栈帧起始位置,此内存单位的内存地址值即当前ebp的值(在main函数中发生其他函数调用之前)
20 ebp-0x4到ebp这段内存对应变量a
30 ebp-0x8到ebp-0x4这段内存对应b
main函数的栈帧区域 …
…
…
30 esp+0x4到esp+0x8对应形参b
20 esp到esp+0x4对应形参a
eip eip的值为test函数调用完毕后后一条指令的地址
ebp 保存main函数的栈帧基址,即此处内存的地址
低地址
上面提到call指令相当于push eip然后跳转到相应的函数体执行指令。以上述的代码为例,call test后eip寄存器的值入栈,紧接着ebp寄存器的值入栈,然后ebp寄存器的值修改成esp的值。显然[ebp+0xc]能寻址到形参b, [ebp+0x8] 寻址到形参a。我们平时所说的函数调用时,对于传入变量函数体内总是会产生一份副本,在本程序中也就是说[ebp+0xc]和[ebp+0x8] 对应的存储区域,由于传入的变量的存储位置和函数体内所操纵的形参存储位置隶属于两个区域,因此函数以值传递的方式,函数体内对形参进行操作,对实参是没有影响的。所以下述函数是无法完成两个数交换的。
void swap(int a,int b)
{
int temp=b;
b=a;
a=temp;
}
ps:由于编译器和平台的差异,可能所产生的汇编代码不一致,但基本原理应该是一致。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/Demon__Hunter/archive/2009/04/27/4123537.aspx