函数调用时栈的变化收藏
1.1 标准调用(__stdcall函数)
1.1.1 代码
#include
int __stdcall Callee(int param1,int param2)
{
int iV ar = param1;
return 10;
}
int main()
{
Callee(1,2);
return 0;
}
1.1.2 函数int main()
int main()
{
00411460 push ebp
00411461 mov ebp,esp
00411463 sub esp,0C0h
00411469 push ebx
0041146A push esi
0041146B push edi
0041146C lea edi,[ebp-0C0h]
00411472 mov ecx,30h
00411477 mov eax,0CCCCCCCCh
0041147C rep stos dword ptr es:[edi]
Callee(1,2);
0041147E push 2
00411480 push 1
00411482 call CrackFun (4111EAh)
【Jeffrey:在函数调用前先将参数从左到右入栈,然哈欧调用参数。】return 0;
00411487 xor eax,eax
}
00411489 pop edi
0041148A pop esi
0041148B pop ebx
0041148C add esp,0C0h
00411492 cmp ebp,esp
00411494 call @ILT+300(__RTC_CheckEsp) (411131h)
00411499 mov esp,ebp
0041149B pop ebp
0041149C ret
--- No source file -------------------------------------------------------------
......
004111E5 jmp Callee (411410h)
004111EA jmp Callee (411410h)
【Jeffrey:此时已经将函数的返回值地址压入栈中。】
004111EF int 3
004111F0 int 3
......
1.1.3 函数int __stdcall Callee(int param1,int param2)
int __stdcall Callee(int param1,int param2)
{
00411410 push ebp
00411411 mov ebp,esp
00411413 sub esp,0CCh
00411419 push ebx
0041141A push esi
0041141B push edi
【Jeffrey:在子函数入口处,依次将以上4个寄存器的值入栈。】
0041141C lea edi,[ebp-0CCh]
00411422 mov ecx,33h
00411427 mov eax,0CCCCCCCCh
0041142C rep stos dword ptr es:[edi]
int iV ar = param1;
0041142E mov eax,dword ptr [param1]
00411431 mov dword ptr [iV ar],eax
return 10;
00411434 mov eax,0Ah
【Jeffrey:函数返回值放在寄存器EAX中。】
}
00411439 pop edi
0041143A pop esi
0041143B pop ebx
0041143C mov esp,ebp
0041143E pop ebp
【Jeffrey:在函数返回前,将入口处入栈的寄存器逆次出栈。】
0041143F ret 8
【Jeffrey:标准调用中,函数返回前负责恢复堆栈信息,“8”为传入参数的大小。】--- No source file -------------------------------------------------------------
1.2 C调用(__cdecl函数)
1.2.1 代码
#include
int __cdecl Callee(int param1,int param2)
{
int iV ar = param1;
return 10;
}
int main()
{
Callee(1,2);
return 0;
}
1.2.2 函数int main()
int main()
{
004113D0 push ebp
004113D1 mov ebp,esp
004113D3 sub esp,0C0h
004113D9 push ebx
004113DA push esi
004113DB push edi
004113DC lea edi,[ebp-0C0h]
004113E2 mov ecx,30h
004113E7 mov eax,0CCCCCCCCh
004113EC rep stos dword ptr es:[edi]
Callee(1,2);
004113EE push 2
004113F0 push 1
004113F2 call Callee (411136h)
004113F7 add esp,8
【Jeffrey:函数返回后主调函数负责恢复堆栈信息。】return 0;
004113FA xor eax,eax
}
004113FC pop edi
004113FD pop esi
004113FE pop ebx
004113FF add esp,0C0h
00411405 cmp ebp,esp
00411407 call @ILT+300(__RTC_CheckEsp) (411131h)
0041140C mov esp,ebp
0041140E pop ebp
0041140F ret
--- No source file -------------------------------------------------------------
1.2.3 函数int __cedcl Callee(int param1,int param2)
int __cdecl Callee(int param1,int param2)
{
00411390 push ebp
00411391 mov ebp,esp
00411393 sub esp,0CCh
00411399 push ebx
0041139A push esi
0041139B push edi
0041139C lea edi,[ebp-0CCh]
004113A2 mov ecx,33h
004113A7 mov eax,0CCCCCCCCh
004113AC rep stos dword ptr es:[edi]
int iV ar = param1;
004113AE mov eax,dword ptr [param1]
004113B1 mov dword ptr [iV ar],eax
return 10;
004113B4 mov eax,0Ah
}
004113B9 pop edi
004113BA pop esi
004113BB pop ebx
004113BC mov esp,ebp
004113BE pop ebp
004113BF ret
【Jeffrey:C调用中,子函数不负责堆栈信息。】
--- No source file -------------------------------------------------------------
1.3 函数调用过程中栈数据的变化
本文来自CSDN博客,转载请标明出处:90a94a3143323968011c9265/Fy2007/archive/2007/04/01/1548248.aspx
浅谈c程序函数调用过程收藏
关键词:
栈区:就是一个内存地址空间,每调用一次函数就会在栈区为此函数分配一段空间(主要用于存储局部变量,
此段空间下面就直接定义为函数栈)
ebp :用于存放函数栈的栈顶地址
esp:用于存放此函数栈的栈底地址
注意:栈顶地址大于栈底地址,栈是从栈顶向栈底增长。即ebp-->esp;
下面我们分析如下代码例子,看看它的调用机制
#include
int fun(int c)
{
int d=c+1;
return d;
}
int main()
{
int a=1;
int b=fun(a);
return 0;
}
int a=1;
汇编代码:mov dword ptr [ebp-4],1
很显然,ebp是用作的了基址寄存器,注意ebp的值是不会变的,它会固定指向函数栈的栈顶,在这里ebp此时的值是0x0013FF80
int b=fun(a);
汇编代码:
mov eax,dword ptr [ebp-4]
push eax
call @ILT+0(fun) (00401005)
add esp,4
mov dword ptr [ebp-8],eax
注意前面两句,[ebp-4]显然是a的地址,这两句是将参数a压入esp指向的栈中(即栈底)在这里esp此时的值是0x0013FF2C(
即编译器给main函数局部变量分配的栈空间为0x0013FF80-0x0013FF2C=54H的栈空间)
当执行完了第二条指令之时esp的值减4H变为0x0013FF28,所以可以看出函数参数是被放在了main函数栈的下面,紧挨栈底。
然后系统自动继续将函数返回地址(即上面第四条语句的地址)压入栈中,此时esp的值0x0013FF24.
call指令调用fun函数,在fun函数开始执行自己代码之前会先执行如下代码(这在函数调用中是必须的)
push ebp
mov ebp,esp
sub esp,44h
push ebx
push esi
push edi
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
具体工作如下:
现将main函数栈顶指针ebp压入栈中(这很重要,在函数返回时有用),此时esp为0x0013FF20,然后将此值再存入ebp中,
即下一个fun函数栈的栈顶。fun函数栈底则变为0x0013FF20-44h即,编译器为fun函数局部变量分配了44h的栈空间,
此时esp指向了0x0013FEDC。然后再将ebx,esi,edi中的值压入栈中(在这个简单的例子中,这些寄存器都没用到)然后esp减去CH,
变为了0x0013FED0。后面四条指令与我们分析的主题无直接关系,就不说了,而且在此例中无用。
下面将注意力放在fun函数是如何取参数的:
int d=c+1;
mov eax,dword ptr [ebp+8]
add eax,1
mov dword ptr [ebp-4],eax
从第一条指令可以看出fun函数的参数是在[ebp+8]位置的,即在fun函数栈顶的上面,这种机制可以很好的区分函数参数与局部
变量,之所以加8是因为在ebp的上面还有main函数返回地址。然后才是函数参数。
现在可以将注意力集中在fun函数是如何返回的:
返回代码如下:
mov eax,dword ptr [ebp-4]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
第一条指令就是return d;将d的值保存在eax寄存器中。
后面三条POP分别对应前面三条push,注意三条pop之后,在fun函数栈底一端任务是完成了。
后面的mov指令将esp重新指向了0x0013FF20,此地址中的值正好是main函数栈的栈顶的地址。
然后执行POP ebp将ebp重新指向main函数栈顶。
注意ret返回指令不是这么简单的,是由硬件自动执行了隐藏的指令,我的分析是这样的:
在执行POP ebp之后,esp则正好指向了原来保存的main函数返回地址。即相当于这条指令:pop eip;
这就是整个调用过程了。
但是不要忘了esp还没有恢复原值呢!这时候esp的上面还有上次传参时保留的参数值a。所以这也就解释了前面第一段汇编代码的
add esp,4指令。这时整个调用过程才彻底结束!
本文来自CSDN博客,转载请标明出处:90a94a3143323968011c9265/tdd108158/archive/2010/04/16/5488557.aspx
通过汇编看调用协定收藏
调用函数的时候,有各种不同的调用约定。它们规定了参数的传送方式、参数是否可变,由谁来处理堆栈等。常用的调用约定有两种:C语言调用约定和Pascal语言调用约定。
可以在工程设置中设定自定义函数的调用规则,也可以在函数声明和定义的时候在函数名前加关键词或API宏定义(如_cdecl、__stdcall、__fastcall、WINAPI、APIENTRY等)明确表示函数的调用协定。
下面将分别说明各种调用协定的用法和意义,并附以相应的汇编代码分析。
1、C语言调用约定
采用C语言编程的时候,默认使用C/C++语言调用约定。也可以手工指定,这需要在函数声明时加上__cdecl关键字。采用本约定时,参数从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由调用函数者负责堆栈清理。由于参数可变,此约定比较灵活,但是性能比较低。生成的代码中函数名有一个_(下划线)做前缀。
举例:
int __cdecl Add(int a, int b)
{
return (a + b);
}
函数调用:
Add(1, 2);
push 2
push 1
call @Add ;其实还有编译器用于定位函数的表达式这里把它省略了
add esp,8 ;清栈
函数体:
push ebp
mov ebp,esp
sub esp,40h ;函数使用的栈默认为40H(16*4),增加一个变量加4bytesWIN32下栈的粒度为4bytes。
push ebx
push esi
push edi
lea edi,[ebp-40h] ;初始化用于该函数的栈空间为0XCCCCCCCC
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
return (a + b);
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp ;如果在此函数中对ESP进行操作,则会有add esp, 40h cmp esp,ebp call chkesp, 检查弹出的ESP指针是否和EBP相同,若不同则调用chkesp抛出异常
pop ebp
ret
下图指出了该函数的栈的使用情况:
2、Pascal语言调用约定
大部分的Windows API都采用Pascal语言调用约定。采用C语言编程的时候,如果要采用这种调用约定,需要在函数声明的时候加上__stdcall关键字。windows.h头文件中也定义了一个WINAPI的宏,起同样的作用。采用本约定时,参数从右到左入栈,个数固定。因此,函数体本身就能知道传进来的参数个数,可以用一条ret n指令直接清理堆栈。牺牲灵活性换来的,是性能的提高。生成的代码中函数名一个_(下划线)做前缀、一个@和参数总