函数的工作原理

函数的工作借助于栈。

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

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开始。

时间: 2024-08-27 01:46:33

函数的工作原理的相关文章

C++学习笔记27,虚函数的工作原理

C++规定了虚函数的行为,但是将实现交给了编译器的作者. 通常,编译器处理虚函数的方法是给每一个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针. 这个数组称为虚函数表(virtual function table,vtbl).虚函数表中存储了为类对象进行声明的虚函数的地址. 例如:基类对象包含一个指针,该指针指向基类的虚函数表. 派生类对象包含一个指针,该指针指向一个独立的虚函数表.如果派生类提供了虚函数的新定义,虚函数表将保存新的函数地址. 如果派生类没有重新定义虚函数,虚函

[转组第7天] | 函数的工作原理

2018-05-04 <C++反汇编和逆向技术>第六章 函数的工作原理 读书笔记 debug版本的函数调用: call func func: push ebp ;保存ebp mov ebp,esp sub esp,40h ;抬高esp,开辟栈空间 push ... ;保存寄存器 ... pop ... ;还原寄存器 add esp,40h ;降低esp,释放局部变量空间 cmp ebp,esp ;检测栈平衡 call __chkesp ;进入栈平衡错误检测函数 mov esp,ebp ;还原e

函数的工作原理——划分RAM搞不懂啊???

1.看到<21天学会C++>P92的函数工作原理之划分RAM,感觉还是迷迷糊糊,不太明白,进一步查询??? 2.程序启动时,操作系统(如DOS,Windows等)将依据编译器的需求设置各种内存区域. 对于一个C++程序员来说,经常需要关心的是全局名称空间.自由存储器.寄存器.代码空间和堆栈. 3.寄存器:CPU中的一个特殊存储区域,任意给定时刻指向下一行代码代码的寄存器组的寄存器被称为指令指针.指令指针的任务是跟踪接下来将执行哪一行代码. 4.代码空间:代码本身存放在代码空间中,每行代码都被转

关于OnPaint函数的工作原理(很详细,很实用) [转载]

地址:http://blog.csdn.net/foreverhuylee/article/details/21889025 用了两年的VC,其实对OnPaint的工作原理一直都是一知半解.这两天心血来潮,到BBS上到处发帖询问,总算搞清楚了,现在总结一下. 对于窗口程序,一般有个特点:窗口大部分的区域保持不变,只有不分区域需要重新绘制.如果将整个窗口全部刷新的画,就做了许多不必要的工作,因而,MFC采用了一套基于无效区的处理机制.在分析无效区处理之前,我们要明白一个现实,现在的机器还不够牛,如

基于arm的C++反汇编 函数的工作原理

栈帧的形成和关闭 各种调用方式的考擦 使用 fp或sp寻址 函数的参数 与返回值 arm指令中立即数存放位置 gdbserver 调试环境 栈帧的形成和关闭 ??栈在内存中是一块特殊的存储空同, 它的存储原则是"先进后出", 即最先被存储的数据最后被释放, 汇编过程通常使用 push 指令与 POP指令对栈空间执行数据压入和数据弹出操作. ??栈结构在内存中占用一段连续的存储空间, 通过sp与 fp这两个栈指针寄存器(在x86上是esp,ebp)来保存当前栈的起始地址与结束地址(又称为

虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte

#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout << "A:A" <<endl; } virtual void getb(){ cout << "A:B" <<endl; } }; class B :public A{ public: B(){} virtual void g

C++中虚函数工作原理和(虚)继承类…

转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531 一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当

C++中虚函数工作原理

一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针. 

python中的函数、生成器的工作原理

1.python中函数的工作原理 def foo(): bar() def bar(): pass python的解释器,也就是python.exe(c编写)会用PyEval_EvalFramEx(c函数)运行foo()函数 首先会创建一个栈帧(stack Frame),在栈帧对象的上下文里面去运行这个字节码. import dis print(dis.dis(foo)) #打印字节码 可以尝试着去打印foo的字节码: 关于字节码的解释: LOAD_GLOBAL:首先导入bar这个函数 CALL