函数的堆栈调用

本文探讨了函数堆栈调用的过程,包括寄存器的使用、指令解析以及堆栈的操作。通过反汇编代码解释了开栈(压入实参、下一条指令地址、调用方栈底地址)、清栈、返回值处理等步骤,强调了函数返回值不能为局部变量地址的原因,并介绍了不同的调用约定,如_cdecl、_stdcall、_fastcall和_thiscall。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数堆栈调用

我们先来写一个简单的代码,写一个相加的函数,然后在main函数中调用它,并把结果打印出来。我们知道它最终打印的结果是30,但是main函数(调用方函数)到底是怎么去调用sum函数(被调用方函数)的呢,它的内存布局又是怎么样的,我们可以转到反汇编去看一看它的真面目。

#include <stdio.h>
int sum(int left, int right)
{
	int tmp = 0;
	tmp = left + right;
	return tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	int rt = 0;
	rt = sum(a, b);
	printf("rt:%d\n", rt);
	return 0;
}

我们在查看反汇编之前,首先了解反汇编的指令都代表了什么意思:

寄存器

eax ebx ecx edx 寄存器,存放数据
esp,栈顶指针寄存器
ebp,栈底指针寄存器
ebp和esp组合起来形成一个栈帧

指令

linux的指令是自左向右看的
Windows(inter x86)的指令是自右向左看的
mov,数据传送指令,将一个数据从源地址传送到目标地址
lea,移地址指令,一个变量用[a]括起来表明这是一个单元的地址如果前面加ptr表明该内存单元
push,压栈操作,例如push 0ah:表示将10(0xa)压到栈顶上面
pop,出栈操作,将栈顶的元素拿出放到寄存器中
add,累加指令,例如add eax, 0a:eax + 0a ==> eax 等价于eax += 0a
sub,减法指令, 例如sub eax, 0a:eax -= 0a
call,保存当前指令的下一条指令并跳转到目标函数,分为两步:
1:压入下一行指令地址
2:jmp跳转到被调用方

接下来转到反汇编代码:
首先给main函数开辟空间并作初始化

 8: int main()
    10: 	int a = 10;
005418B8  mov         dword ptr [ebp-8],0Ah  //将A放入到ebp-8的位置,即A的内存块
    11: 	int b = 20;
005418BF  mov         dword ptr [ebp-14h],14h  //将B放到ebp-14的位置
    12: 	int rt = 0;
005418C6  mov         dword ptr [ebp-20h],0  //将rt放到ebp-20的位置
    13: 	rt = sum(a, b);**//实参传递,传递顺序是自右向左的,因为存在可变参参数**
005418CD  mov         eax,dword ptr [ebp-14h]//通过ebp-14的位置即B的位置拿出来放到eax寄存器
005418D0  push        eax  //将eax压栈
005418D1  mov         ecx,dword ptr [ebp-8]  //通过ebp-8的位置即B的位置拿出来放到ecx寄存器
005418D4  push        ecx  //将ecx压栈
005418D5  call        0054107D  //近址相对位移调用指令,用来调用SUM函数
005418DA  add         esp,8  

这是sum函数的反汇编

     2: int sum(int left, int right)
00541720  push        ebp  //将main函数栈底指针压栈
00541721  mov         ebp,esp  //让ebp指向esp指向的位置
00541723  sub         esp,0CCh  //esp-=cc,给sum开辟空间
00541729  push        ebx  //压入三个寄存器
0054172A  push        esi  
0054172B  push        edi  
0054172C  lea         edi,[ebp+FFFFFF34h]  
00541732  mov         ecx,33h  
00541737  mov         eax,0CCCCCCCCh  //将cccccccc赋给寄存器
0054173C  rep stos    dword ptr es:[edi]  //循环指令将开辟好的空间全部用eax来初始化

在这里插入图片描述

总结

开栈的过程:
1.压入实参,开辟形参并赋值
2.压入下一行指令,被调用放函数处理完能沿着调用方下一行指令继续执行
3.压入调用方栈底地址,方便被调用方函数处理完成退回到调用方
4.开辟被调用方的活动空间并初始化位0xcccccccc
清栈的过程:
1.清理被调用方函数开辟的空间
2.出栈ebp,ebp回退到调用方栈帧上
3.出栈下一行指令寄存器,函数调用完成,沿着下一行指令继续执行
4.清理形参
返回值处理:
当返回值<4个字节时,由eax寄存器将返回值带出
当返回值在4到8个字节时,由eax,edx寄存器将返回值带出
当返回值大于8个字节时,由我们的临时量带出。临时量由调用方开辟,临时量在表达式结束时结束,但是空间还在
其中返回值可以分为内置类型和自定义类型,内置类型是指系统类型如int,char,short,double。用户自定义类型像枚举类型,结构体,联合体,类类型
**还有一点,函数返回值不能返回局部变量的地址,**我们知道当被调用方函数回退到调用方函数时,它的空间并没有销毁,而是被回收了。这就意味着那块地址有可能被重新赋值,这时候我们再指向它,就有可能出现越界的情况。这也是临时量为什么要在调用方开辟内存的原因。
调用约定:
在c中由三种调用约定:_cdecl,_stdcall和_fastcall,其中_stdcall调用约定是windows平台的。
在c++中还有一种约定:_thiscall调用约定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值