- - - - -草稿- - - - -栈帧部分待细化
前言:
不积跬步,无以至千里
栈帧是编程书目中鲜有提及的概念,但其与函数调用细节息息相关,在此做简单总结。
无参考书目
主要参考资料:
- 互联网
以及
- Write by 张鹏霄, zpx736312737@126.com
概要:
- 定义
- 调用细节
- 可变参数函数
定义
易知,函数的调用需要申请栈,建立栈帧
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构
栈帧对应着一个未运行完的函数,其中保存了该函数的返回地址和局部变量。
调用细节
首先,
栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。
通过寄存器ebp(维护栈底),esb(维护栈顶),我们维护一个正在运行的函数的栈帧:
定义一个函数fuc
int fuc()
{
int a = 1;
return a;
}
int main()
{
int num=fuc();
Next command:
getchar();
getchar();
return 0;
}
Vs2017下调试代码转到反汇编
int fuc()
{
001E16A0 push ebp
001E16A1 mov ebp,esp
001E16A3 sub esp,0CCh
001E16A9 push ebx
001E16AA push esi
001E16AB push edi
001E16AC lea edi,[ebp-0CCh]
001E16B2 mov ecx,33h
001E16B7 mov eax,0CCCCCCCCh
001E16BC rep stos dword ptr es:[edi]
int a = 1;
001E16BE mov dword ptr [a],1
return a;
001E16C5 mov eax,dword ptr [a]
}
图解汇编代码:(图片来自网络,部分内容与上述代码有出入,不影响意思)
我们将状态分为入栈前和入栈后,在入栈前,main函数作为新栈帧之前的栈帧如下
之后开始入栈,push mov后,将参数与返回地址(开辟一空间记录返回地址,可以理解为代码块Next command:的地址)
移动ebp指向当前栈帧
之后为a开辟空间并记录值,移动esp
之后出栈,将值返回给返回地址的值
可变参数函数的设计
假设我们有一需求,将未知个数字求和。
以函数重构的形式
int fuc(int a)
int fuc(int a, int b)
int fuc(int a, int b, int c)
...
显然不合理
易想到,printf参数为可变参数
vs下调取帮助,查看printf函数参数表
int printf(
const char *format [,
argument]...
);
事实上,编译器提供一种语法,实现函数可变参数,以求和函数为例
int(int n,...)//n为参与求和的变量个数,...表示可变参数
求和函数代码如下
#include<stdarg.h>//va_list va_start等的定义
int fuc(const int n,...)
{
va_list arg;//va_list为宏定义,可以建立一个数组
va_start(arg, n);//va_list为宏定义,可以根据第一个参数n的地址找到可变参数第一个元素(联想栈帧的原理)
int sum = 0;
for (int i = 0; i < n; i++)
{
sum += va_arg(arg, int);//va_list为宏定义,访问该元素并将地址跳到下一元素
}
va_end(arg);//va_list为宏定义,出于安全性考虑,将指针指为空
return sum;
}
其中上述涉及到的宏定义在新版vs中多层嵌套,研究vs6.0源代码可得到可读性较强的宏定义如下(可不必研究细节)
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
综上,结合栈帧原理,我们可以使用可变参数接受未知个数参数。