函数帧和可变参数和内存对齐

在堆栈里面,一个函数所占的栈空间称为函数帧。在函数调用时,先将函数调用的下一条指令地址入栈(即函数调用语句的下面一个语句编译产生的第一条指令),然后将函数的各个参数依右向左的顺序入栈,当然,参数入栈的顺序是与函数调用约定相关的,常用的有:

__cdecl,即C调用约定,参数从右到左入栈,函数本身不清理栈,由调用者自行清理。

__stdcall,即pascal调用约定,它的参数也是从右到左入栈,函数本身清理栈。

__thiscall,它是C++里面专用的调用约定,即它自动为函数增加了一个this指针,也是从右向左入栈,this指针在最后一个入栈。

接着将函数中的局部变量入栈,静态变量不入栈,它存放在静态区(初始化数据段或BBS)。函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。这里要特别注意:函数的返回值的出栈不是像函数里面写的那样作为第一个出栈,实际上,函数的返回值是作为函数的引用参数来实现的。所有的函数其实不真正地具备向外界抛出一个值的能力,它只是很普通的将某值保存在某个寄存器并由caller取回的操作。

我们再来考虑C语言里面可变参数的实现。形如:void printf(const char*,...);后面三个省略号表示printf这个函数能接受不定数个参数。很明显,如果一个函数存在不定个参数,往这个函数传递不定个参数不是难点,因为只需要往函数栈里面扔进参数即可。语法层面只需要随便定义什么规则让编译器识别不定参数即可。我们只需要关注,函数内部如何使用这些参数。很容易想到的是,我们可以借用__cdecl这个调用约定来实现可变参数。这个调用约定没有强制要求知道参数的个数。该调用约定从最后一个参数开始反向入栈的,而且,它必须由调用者清理函数栈。

如函数:int sum(int a,int b,int c)在函数栈中的相对位置为:




__cdecl和__stdcall都是从右向左入栈,但前者由调用者清理函数线,后者由函数本身清理栈,这是因为,__cdecl规定函数内部并不知道参数有多少个,所以它并不能在函数内部清理栈,而只能由调用者清理栈(调用者肯定知道参数的个数),如上例中,调用者就可以使用 esp = esp+3*sizeof(int)来清理栈。而__stdcall要求参数个数必须是明确的,所以它可以在函数内部清理栈保持栈的平衡,pop a; pop b; pop c。

在__cdecl中,如果我们知道最下面一个参数的地址和每个参数的大小,只需要将前者作为基准地址,加上前者大小就可以得知下一个参数的地址,于是也就可以在函数内部任意使用了。所以,__cdecl给出要求,函数必须要知道一个确定地址的参数,即可变参数的函数至少有一个确定地址的参数,该确定参数前面入栈的参数(即在函数调用中,写在个确定参数后面的参数)都可以被寻址。__cdecl规定,写在最后面的一个参数,其下一个字节上的值为0,所以我们就可以界定参数的起始地址和结束地址,再根据每个参数所占的大小即可寻址每个参数。


下面关于内存对齐的数理推理是引自http://www.360doc.com/content/12/0804/11/3725126_228273988.shtml的。

对于两个正整数 x, n 总存在整数 q, r 使得x = nq + r, 其中  0<= r <n   //最小非负剩余


q, r 是唯一确定的。q = [x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来: q = x/n, r = x % n.
所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:
x = nq + r', 其中 -n < r' <=0                //最大非正剩余   
nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:
x+n = qn + (n+r'),其中 0<n+r'<=n            //最大非正剩余
x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1 <n    //最小非负剩余
所以 qn = [(x+n-1)/n]n. 用 c 语言计算就是:
((x+n-1)/n)*n
若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1 的最低 m 个二进制位清 0就可以了。得到:
(x+n-1) & (~(n-1))。

有了这个公式,就可以计算任意一个不定参数的地址了,再由函数里面不定参数的类型,就可以对应该类型去解析数据了。

代码及解释见下面的代码和注释。

int d = 13;
char *str = "i know it";

void myprintf2(char* s,...)
{
	char *fs ;
	myva_list ap;               //依次指向每一个不定参数
	myva_start(ap,s);           //先让 ap 指向第一个不定参数
	int d;
	char c;
	char *q;
	for(fs =s;*fs;++fs)
	{
		if(*fs != '%')
		{
			putchar(*fs);
			continue;
		}
		switch(*++fs)
		{
		case 'd':
			d = myva_arg(ap,int);       //myva_arg完成二件事,第一是由ap当前指定的不定参数和给出的类型得到该不定参数的值。
			                               //第二是让ap自动指向下一个不定参数
			printf("%d",d);
			break;
		case 'c':
			c = myva_arg(ap,char);
			putchar(c);
			break;
		case 's':
			q = myva_arg(ap,char*);
			printf("%s",q);
			break;
		default:
			printf("\r\nundefined!\r\n");
			break;
		}
	}
	myva_end();
}

上面提到的各个调用其实是宏,在这里使用宏比使用函数有很多好处,最大的好处是宏允许将类型作为“参数”且显得更为直观,另一个好
处是少写代码。
typedef char *myva_list;

#define __va_rounded_size(TYPE) ((sizeof(TYPE)+sizeof(int)-1)/sizeof(int)*sizeof(int))  //一个类型经过内存对齐后应该占多大的空间

#define myva_start(AP,LASTPARAM) (AP=(char*)&LASTPARAM + __va_rounded_size(LASTPARAM))  //由最后一个参数(定参)得到首个不定参数的地址

#define myva_end(AP) (AP=(char*)0)  //清零

#define myva_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),*(TYPE*)((char*)AP-__va_rounded_size(TYPE)))     //由当前不定参数地址和其类型得到值,
//并且自动让AP指向下一个不定参数

有一个限制,可变参数函数没有通用获得参数个数的方法,要么将参数个数作为一个定参传入到函数中,要么通过其它的约定去获得,比如, printf(const char* fmt, ...) 函数是通过解析 fmt 得到的参数个数。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值