c里面的变长参数,c++里面也有。提供了:
一个类型
va_list
3个宏
va_start
va_arg
va_end
使用的例子是这样的
int foo(char* fmt, ...){
va_list args;
va_start(args, fmt);
int i = va_arg(args, int);
double f = va_arg(args, double);
va_end(args);
}
使用还是很方便的,但是实现是怎么样的呢? 要讲实现,先得讲讲c里面的函数调用约定。
在x86下,c语言多多种调用约定,而支持变长参数的只有__cdecl。 参数是放在栈上的,调用者负责调整栈.
所以,参数实际上是从第一个到最后一个从低到高排列在栈上的,实现的方法就是
va_list是一个char*.让va_list指向最后一个不是变长参数的参数的后面,也就是第一个变长参数.
va_arg就是按照类型取值,并且把指针往后移动sizeof(type)就行了。
但是到了x64上,就没这么简单了。x64上不论是vc还是gcc都只有一种类似fastcall的调用约定。参数首先是放在寄存器里,不够了再往栈上放。这样一来,参数就不是在栈的内存上连续排列了。变长参数的实现就需要多做点事了。
先讲比较简单的一种,VC的处理方式
vc的调用约定[1] 是,前4个参数,如果是整数,指针或者其他1,2,4,8个字节的参数,放在rcx,rdx,r8,r9里,如果是浮点数,就放在xmm0,xmm1,xmm2,xmm3里,如果大小不满足要求,把参数的指针放在寄存器里。后面的参数,如果大小不符,也是放指针,大小符号,就放值在栈上。强调一下,第一到第四个参数如果是需要放寄存器,一定是放在rcx,rdx,r8,r9或者xmm0,xmm1,xmm2,xmm3。比如,如果第一个参数是int,第二个是double,第三个char*,第四个double,那么就是依次放在rcx,xmm1,r8,xmm3里。
在调用函数之前,需要预留32个字节的寄存器区,这个区域就是用来存放前4个参数的。
在这种调用约定下,实现的va_list的方式倒也简单。如果函数使用了va_start,就会把前4个参数从寄存器里rcx,rdx,r8,r9拷贝到寄存器区,也就跟后面的栈上参数连成一块了。后面就简单了。
但是,如果是浮点数怎么办。这就需要调用者解决了,如果是要对变长参数传参,就需要在把浮点数放到xmm寄存器的同时,给对应的通用寄存器也放一份。这就看出来保证前4个参数和寄存器对应关系的重要性了。