今天突然看到有人私信我说一直没写函数调用过程 (栈帧的形成和销毁过程) 这篇博文, 赶紧补上
刚看的栈帧内容时, 我很迷惑, 我觉得栈帧创建和销毁很麻烦, 几句话根本说不完, 而且我好像描述不清楚他的过程, 所以在博文里面遇到函数调用我就规避了现在再写栈帧调用过程, 我觉得其实这个过程没有那么困难 (不过还是有些抽象, 毕竟计算机底层怎么运行我们也不是很明白)
栈帧的创建的销毁过程例子代码:
int Add(int x,int y)
{
int sum = 0;
sum = x + y;
return sum;
}
int main ()
{
int a = 10;
int b = 12;
int ret = 0;
ret = Add(a,b);
return 0;
}
今天主要用汇编代码去讲述这个过程, 首先介绍几个寄存器和简单的汇编指令的意思
先看几个函数调用过程涉及到的寄存器:
(1)esp: 栈指针寄存器 (extended stack pointer), 其内存放着一个指针, 该指针永远指向系统栈最上面一个栈帧的栈顶
(2)ebp: 基址指针寄存器 (extended base pointer), 其内存放着一个指针, 该指针永远指向系统栈最上面一个栈帧的底部
(3)eax 是累加器 (accumulator), 它是很多加法乘法指令的缺省寄存器
(4)ebx 是基地址 (base) 寄存器, 在内存寻址时存放基地址
(5)ecx 是计数器 (counter), 是重复 (REP) 前缀指令和 LOOP 指令的内定计数器
(6)edx 则总是被用来放整数除法产生的余数
(7)esi/edi 分别叫做源 / 目标索引寄存器 (source/destination index), 因为在很多字符串操作指令中, DS:ESI 指向源串, 而 ES:EDI 指向目标串.
在 32 位平台上, ESP 每次减少 4 字节
再看几条简单的汇编指令:
mov : 数据传送指令, 也是最基本的编程指令, 用于将一个数据从源地址传送到目标地址 (寄存器间的数据传送本质上也是一样的)
sub: 减法指令
lea: 取偏移地址
push: 实现压入操作的指令是 PUSH 指令
pop: 实现弹出操作的指令
call: 用于保存当前指令的下一条指令并跳转到目标函数
这些指令当然能看懂最好, 可以让你很深刻的理解函数调用过程, 不能看懂就只能通过我的描述去理解了
进行分析之前, 先来了解下内存地址空间的分布:
栈空间是向低地址增长的, 主要是用来保存函数栈帧 栈空间的大小很有限, 仅有区区几 MB 大小
汇编代码实现:
main 函数汇编代码:
int main ()
{
011B26E0 push ebp
011B26E1 mov ebp,esp
011B26E3 sub esp,0E4h
011B26E9 push ebx
011B26EA push esi
011B26EB push edi
011B26EC lea edi,[ebp-0E4h]
011B26F2 mov ecx,39h
011B26F7 mov eax,0CCCCCCCCh
011B26FC rep stos dword ptr es:[edi]
int a = 10;
011B26FE mov dword ptr [a],0Ah
int b = 12;
011B2705 mov dword ptr [b],0Ch
int ret = 0;
011B270C mov dword ptr [ret],0
ret = Add(a,b);
011B2713 mov eax,dword ptr [b]
011B2716 push eax
011B2717 mov ecx,dword ptr [a]
011B271A push ecx
011B271B call @ILT+640(_Add) (11B1285h)
011B2720 add esp,8
011B2723 mov dword ptr [ret],eax
return 0;
011B2726 xor eax,eax
}
011B2728 pop edi
011B2729 pop esi
011B272A pop ebx
011B272B add esp,0E4h
011B2731 cmp ebp,esp
011B2733 call @ILT+450(__RTC_CheckEsp) (11B11C7h)
011B2738 mov esp,ebp
011B273A pop ebp
011B273B ret
Add 函数汇编代码:
int Add(int x,int y)
{
011B26A0 push ebp
011B26A1 mov ebp,esp
011B26A3 sub esp,0CCh
011B26A9 push ebx
011B26AA push esi
011B26AB push edi
011B26AC lea edi,[ebp-0CCh]
011B26B2 mov ecx,33h
011B26B7 mov eax,0CCCCCCCCh
011B26BC rep stos dword ptr es:[edi]
int sum = 0;
011B26BE mov dword ptr [sum],0
sum = x + y;
011B26C5 mov eax,dword ptr [x]
011B26C8 add eax,dword ptr [y]
011B26CB mov dword ptr [sum],eax
return sum;
011B26CE mov eax,dword ptr [sum]
}
011B26D1 pop edi
011B26D2 pop esi
011B26D3 pop ebx
011B26D4 mov esp,ebp
011B26D6 pop ebp
011B26D7 ret
下面图中详细描述了调用过程地址变化 (此处所有地址是取自 32 位 windows 系统 vs 编辑器下的调试过程):
过程描述:
1 参数拷贝 (参数实例化)
2 保存当前指令的下一条指令, 并跳转到被调函数
这些操作均在 main 函数中进行
接下来是调用 Add 函数并执行的一些操作, 包括:
1 移动 ebpesp 形成新的栈帧结构
2 压栈 (push) 形成临时变量并执行相关操作
3return 一个值
这些操作在 Add 函数中进行
被调函数完成相关操作后需返回到原函数中执行下一条指令, 操作如下:
1 出栈 (pop)
2 回复 main 函数的栈帧结构 (pop )
3 返回 main 函数
这些操作也在 Add 函数中进行 至此, 在 main 函数中调用 Add 函数的整个过程已经完成
总结起来整个过程就三步:
1) 根据调用的函数名找到函数入口;
2) 在栈中审请调用函数中的参数及函数体内定义的变量的内存空间
3) 函数执行完后, 释放函数在栈中的审请的参数和变量的空间, 最后返回值 (如果有的话)
如果你学了微机原理, 你会想到 cpu 中断处理过程, 是的, 函数调用过程和中断处理过程一模一样
函数调用约定:
这里再补充一下各种调用规定的基本内容
_stdcall 调用约定
所有参数按照从右到左压入堆栈, 由被调用的子程序清理堆栈
_cdecl 调用约定 (The C default calling convention,C 调用规定)
参数也是从右到左压入堆栈, 但由调用者清理堆栈
_fastcall 调用约定
顾名思义,_fastcall 的目的主要是为了更快的调用函数它主要依靠寄存器传递参数, 剩下的参数依然按照从右到左的顺序压入堆栈, 并由被调用的子程序清理堆栈
本篇博文是按调用约定__stdcall 调用函数
来源: http://blog.csdn.net/qq_38646470/article/details/79213082