首页 > 代码库 > 函数的工作原理

函数的工作原理

函数的工作借助于栈。

栈在内存中是一块特殊的存储空间,它的存储原则是“先进后出”,最先被存储的数据最后被释放。

技术分享

esp被称为栈顶指针,ebp称为栈底指针,通过这两个指针寄存器保存当前栈的起始地址与结束地址。

esp与ebp之间所构成的空间便成为栈帧。通常,在VC++中,栈帧中可以寻址的数据有局部变量、函数返回地址、函数参数等。不同的两次函数调用,所形成的栈帧也不同。当由一个函数进入到另一个函数中时,就会针对所调用的函数形成所需的栈空间,形成此函数的栈帧。当这个函数结束调用时,需要清除掉它所使用的栈空间,关闭栈帧,这一过程称为栈平衡。

int main()
{
	return 0;
}
汇编代码讲解
int main()
{
00C81380  push        ebp                             ;进入函数后的第一件事,保存栈底指针ebp
00C81381  mov         ebp,esp                         ;调整栈底指针到栈顶位置
00C81383  sub         esp,0C0h                        ;抬高新的栈顶,开辟栈空间C0h,作为局部变量的存储空间,形成此main函数的栈帧
00C81389  push        ebx  
00C8138A  push        esi  
00C8138B  push        edi  
00C8138C  lea         edi,[ebp-0C0h]  
00C81392  mov         ecx,30h  
00C81397  mov         eax,0CCCCCCCCh  
00C8139C  rep stos    dword ptr es:[edi]  

	return 0;
00C8139E  xor         eax,eax  
}                                                     ;在退出时,恢复原来的栈帧,ebp的值是原来的esp,然后再pop出ebp
00C813A0  pop         edi  
00C813A1  pop         esi  
00C813A2  pop         ebx  
00C813A3  mov         esp,ebp                         ;还原esp
00C813A5  pop         ebp                             ;还原ebp
00C813A6  ret  

上面代码在退出时并没有检测栈平衡,如下

00C813A0  pop         edi  
00C813A1  pop         esi  
00C813A2  pop         ebx  
多了这一段检测
add	esp,40h		;降低栈顶esp,此时局部变量空间被释放
cmp	ebp,esp		;检测栈平衡,如ebp与esp不等,则不平衡
call	_chkesp		;进入栈平衡错误检查函数

00C813A3  mov         esp,ebp  		;还原esp
00C813A5  pop         ebp  		;还原ebp
00C813A6  ret  


在VC++环境下有三种函数调用约定:_cdecl、_stdcall、fastcall。

_cdecl:C/C++默认的调用方式,调用方平衡栈,不定参数的函数可以使用。

_stdcall:被调用方平衡栈,不定参数的函数无法使用。

_fastcall:寄存器方式传参,被调用方平衡栈,不定参数的函数无法使用。

#include<stdio.h>

void _stdcall showstd(int number)
{
	printf("%d\r\n", number);
}

void _cdecl showcde(int number)
{
	printf("%d\r\n", number);
}

void main()
{
	showstd(5);
	showcde(6);
}
void _stdcall showstd(int number)
{
00391C60  push        ebp  
00391C61  mov         ebp,esp  
00391C63  sub         esp,0C0h  
00391C69  push        ebx  
00391C6A  push        esi  
00391C6B  push        edi  
00391C6C  lea         edi,[ebp-0C0h]  
00391C72  mov         ecx,30h  
00391C77  mov         eax,0CCCCCCCCh  
00391C7C  rep stos    dword ptr es:[edi]  
	printf("%d\r\n", number);
00391C7E  mov         esi,esp  
00391C80  mov         eax,dword ptr [number]  
00391C83  push        eax  
00391C84  push        3958A8h  
00391C89  call        dword ptr ds:[399114h]  
00391C8F  add         esp,8  
00391C92  cmp         esi,esp  
00391C94  call        __RTC_CheckEsp (03911DBh)  
}
00391C99  pop         edi  
00391C9A  pop         esi  
00391C9B  pop         ebx  
00391C9C  add         esp,0C0h  
00391CA2  cmp         ebp,esp  
00391CA4  call        __RTC_CheckEsp (03911DBh)  
}
00391CA9  mov         esp,ebp  
00391CAB  pop         ebp  
00391CAC  ret         4                                  ;这里结束后平衡栈顶4,相当于esp+4


void _cdecl showcde(int number)
{
00391780  push        ebp  
00391781  mov         ebp,esp  
00391783  sub         esp,0C0h  
00391789  push        ebx  
0039178A  push        esi  
0039178B  push        edi  
0039178C  lea         edi,[ebp-0C0h]  
00391792  mov         ecx,30h  
00391797  mov         eax,0CCCCCCCCh  
0039179C  rep stos    dword ptr es:[edi]  
	printf("%d\r\n", number);
0039179E  mov         esi,esp  
003917A0  mov         eax,dword ptr [number]  
003917A3  push        eax  
003917A4  push        3958A8h  
003917A9  call        dword ptr ds:[399114h]  
003917AF  add         esp,8  
003917B2  cmp         esi,esp  
003917B4  call        __RTC_CheckEsp (03911DBh)  
}
003917B9  pop         edi  
003917BA  pop         esi  
003917BB  pop         ebx  
003917BC  add         esp,0C0h  
003917C2  cmp         ebp,esp  
003917C4  call        __RTC_CheckEsp (03911DBh)  
}
003917C9  mov         esp,ebp  
003917CB  pop         ebp  
003917CC  ret                             ;这里直接返回并没有自己平衡,当执行权到了调用方时平衡       A

void main()
{
00392520  push        ebp  
00392521  mov         ebp,esp  
00392523  sub         esp,0C0h  
00392529  push        ebx  
0039252A  push        esi  
0039252B  push        edi  
0039252C  lea         edi,[ebp-0C0h]  
00392532  mov         ecx,30h  
00392537  mov         eax,0CCCCCCCCh  
0039253C  rep stos    dword ptr es:[edi]  
	showstd(5);
0039253E  push        5  
00392540  call        showstd (03911D1h)  
	showcde(6);
00392545  push        6  
00392547  call        showcde (03911CCh)  
0039254C  add         esp,4                                     ;A处并没有平衡栈顶,现在平衡
}
0039254F  xor         eax,eax                                   ;下面的栈帧关闭是main函数的
00392551  pop         edi  
00392552  pop         esi  
00392553  pop         ebx  
00392554  add         esp,0C0h  
0039255A  cmp         ebp,esp  
0039255C  call        __RTC_CheckEsp (03911DBh)  
00392561  mov         esp,ebp  
00392563  pop         ebp  
}
00392564  ret  


为什么要平衡栈顶呢?以前我一直弄不明白,我一直认为当我调用函数执行时,该函数形成了自己的栈帧,保存了它可以用到的局部变量等等,当它结束时,直接恢复到原来的栈顶就可以了,也就是mov esp,ebp这句就可以了,但为什么会在返回时还要对esp进行更改。现在弄明白了,原因是该函数有参数,在调用函数前,参数会先被压入栈中,所以在函数结束后,该函数的栈帧也关闭了,但是调用方的栈帧中还保存着刚才函数所需要的参数,现在它成了没有用的数据,当然要把它踢出去。

技术分享

当showcde函数调用结束后,黄色区域栈的数据也就没用了,所以降低栈顶。

下面看一下用寄存器方式传参方式,fastcall

#include<stdio.h>

void _fastcall showfast(int one, int two, int three, int four)
{
	printf("%d %d %d %d\r\n",one,two,three,four);
}

void main()
{
	showfast(1, 2, 3, 4);
}

void _fastcall showfast(int one, int two, int three, int four)
{
00132F90  push        ebp  
00132F91  mov         ebp,esp  
00132F93  sub         esp,0D8h  
00132F99  push        ebx  
00132F9A  push        esi  
00132F9B  push        edi  
00132F9C  push        ecx  
00132F9D  lea         edi,[ebp-0D8h]  
00132FA3  mov         ecx,36h  
00132FA8  mov         eax,0CCCCCCCCh  
00132FAD  rep stos    dword ptr es:[edi]  
00132FAF  pop         ecx  
00132FB0  mov         dword ptr [two],edx        ;用临时变量存储参数二的值2,在ida中显示mov dword ptr [ebp-8],edx
00132FB3  mov         dword ptr [one],ecx        ;用临时变量存储参数一的值1,mov dword ptr [ebp-4],ecx

	printf("%d %d %d %d\r\n",one,two,three,four);
00132FB6  mov         esi,esp  
00132FB8  mov         eax,dword ptr [four]       ;mov eax,dword ptr [ebp+0ch],取得参数4
00132FBB  push        eax  
00132FBC  mov         ecx,dword ptr [three]      ;mov ecx,dword ptr [ebp+8],取得参数3
00132FBF  push        ecx  
00132FC0  mov         edx,dword ptr [two]  
00132FC3  push        edx  
00132FC4  mov         eax,dword ptr [one]  
00132FC7  push        eax  
00132FC8  push        1358A8h  
00132FCD  call        dword ptr ds:[139114h]  
00132FD3  add         esp,14h  
00132FD6  cmp         esi,esp  
00132FD8  call        __RTC_CheckEsp (01311DBh)  
}
00132FDD  pop         edi  
00132FDE  pop         esi  
00132FDF  pop         ebx  
00132FE0  add         esp,0D8h  
00132FE6  cmp         ebp,esp  
00132FE8  call        __RTC_CheckEsp (01311DBh)  
00132FED  mov         esp,ebp  
00132FEF  pop         ebp  
00132FF0  ret         8                     ;由于在传参的时候使用个两个寄存器帮助传参,用栈传参只用了两个,故ret 8


void main()
{
00131C60  push        ebp  
00131C61  mov         ebp,esp  
00131C63  sub         esp,0C0h  
00131C69  push        ebx  
00131C6A  push        esi  
00131C6B  push        edi  
00131C6C  lea         edi,[ebp-0C0h]  
00131C72  mov         ecx,30h  
00131C77  mov         eax,0CCCCCCCCh  
00131C7C  rep stos    dword ptr es:[edi]  
	showfast(1, 2, 3, 4);                                          ;看到这里
00131C7E  push        4                                                ;用栈传递参数3和4
00131C80  push        3  
00131C82  mov         edx,2                                            ;用edx传递第二个参数2
00131C87  mov         ecx,1                                            ;用ecx传递第一个参数1
00131C8C  call        showfast (01311E5h)  
}
00131C91  xor         eax,eax  
00131C93  pop         edi  
00131C94  pop         esi  
00131C95  pop         ebx  
00131C96  add         esp,0C0h  
00131C9C  cmp         ebp,esp  
00131C9E  call        __RTC_CheckEsp (01311DBh)  
00131CA3  mov         esp,ebp  
00131CA5  pop         ebp  
}
00131CA6  ret 
上面的注释已经很明白了,另外需要注意,在使用ebp相对寻址定位参数3和4时,为什么不是从ebp+4开始的,原因是在调用函数时,会将该call的下一条指令的地址压入栈中,所以定位从ebp+8开始。

函数的工作原理