C函数库中的printf函数与Linux内核中printk函数都是可变参数的函数,其参数的个数是不确定的。
函数参数个数可变的原理:当执行一个函数的时候,会先将函数的参数列表入栈,然后入栈函数的返回地址,接着入栈函数的执行代码。最终在栈中,从栈底到栈顶的顺序,依次是:函数参数列表,函数返回地址,函数执行代码段。
而在参数列表中,各个参数的分布情况是倒序的,会先将最后一个参数入栈,接着入栈倒数第二个参数,最后才是第一个参数。这样最后一个参数在栈中参数列表更靠近栈底,第一个参数在栈中参数列表更靠近栈顶。参数列表在栈中的分布情况如下:
![](http://hi.csdn.net/attachment/201202/1/0_13280836102dpL.gif)
读取可变参数的过程其实就是使用指针遍历栈中的可变参数列表,一个一个地读取可变参数。
linux内核中用va_list实现可变参数的。使用va_list需要用到的一些宏。
include/acpi/platform/acenv.h
先看一下如何使用va_list: 首先在函数里定义一个va_list型的变量,这个变量是指向参数的指针;如:va_list args;
然后用va_start宏初始化刚才定义的va_list变量,宏的第一个参数为va_list变量,第二个参数是第一个可变参数的前一个参数,一般是一个已知类型的参数。如:va_start(args,v);这儿的v是可变参数的前一个固定参数,是参数本身,不是参数的地址。
然后用va_arg返回可变的参数,宏的第一个参数为va_list变量,第二个参数是要返回的参数的类型。如:type j = va_arg(args,type);
最后使用va_end宏结束可变参数的获取。如果函数有多个可变参数,可依次调用va_arg获取各个参数,最后才调用va_end,如:va_end(args);
在调用va_start(args,v)后,args指向第一个可变参数。这个宏的作用就是在固定参数v的内存地址上增加v所占的内存大小,这样就得到了第一个可变参数的地址。
接着调用va_arg(args,type),它先将args指向下一个可变参数(下一个参数类型为type),然后减去当前可变参数的内存大小即得到当前可变参数的内存地址,再做个类型转换,返回它的值。最后返回为第一个可变参数。
最后一个宏va_end使得args不再指向有效的内存地址。
从上面的过程中可以看出,要实现可变参数,有两个重要的条件:
一,在可变参数前面必须有固定的参数;
二,要知道每个可变参数的参数类型。要确定每个可变参数的类型,要么都是默认的类型,要么就在固定参数中包含足够的信息让程序可以确定每个可变参数的类型。比如,printf,程序通过分析format字符串就可以确定每个可变参数的类型。
可以看下snprintf的函数实现:
lib/vsprintf.c
函数参数个数可变的原理:当执行一个函数的时候,会先将函数的参数列表入栈,然后入栈函数的返回地址,接着入栈函数的执行代码。最终在栈中,从栈底到栈顶的顺序,依次是:函数参数列表,函数返回地址,函数执行代码段。
而在参数列表中,各个参数的分布情况是倒序的,会先将最后一个参数入栈,接着入栈倒数第二个参数,最后才是第一个参数。这样最后一个参数在栈中参数列表更靠近栈底,第一个参数在栈中参数列表更靠近栈顶。参数列表在栈中的分布情况如下:
![](http://hi.csdn.net/attachment/201202/1/0_13280836102dpL.gif)
读取可变参数的过程其实就是使用指针遍历栈中的可变参数列表,一个一个地读取可变参数。
linux内核中用va_list实现可变参数的。使用va_list需要用到的一些宏。
include/acpi/platform/acenv.h
typedef char *va_list;
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd(T,_ADNBND))))
#define va_end(ap) (void) 0
#define va_start(ap, A)(void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
先看一下如何使用va_list: 首先在函数里定义一个va_list型的变量,这个变量是指向参数的指针;如:va_list args;
然后用va_start宏初始化刚才定义的va_list变量,宏的第一个参数为va_list变量,第二个参数是第一个可变参数的前一个参数,一般是一个已知类型的参数。如:va_start(args,v);这儿的v是可变参数的前一个固定参数,是参数本身,不是参数的地址。
然后用va_arg返回可变的参数,宏的第一个参数为va_list变量,第二个参数是要返回的参数的类型。如:type j = va_arg(args,type);
最后使用va_end宏结束可变参数的获取。如果函数有多个可变参数,可依次调用va_arg获取各个参数,最后才调用va_end,如:va_end(args);
在调用va_start(args,v)后,args指向第一个可变参数。这个宏的作用就是在固定参数v的内存地址上增加v所占的内存大小,这样就得到了第一个可变参数的地址。
接着调用va_arg(args,type),它先将args指向下一个可变参数(下一个参数类型为type),然后减去当前可变参数的内存大小即得到当前可变参数的内存地址,再做个类型转换,返回它的值。最后返回为第一个可变参数。
最后一个宏va_end使得args不再指向有效的内存地址。
从上面的过程中可以看出,要实现可变参数,有两个重要的条件:
一,在可变参数前面必须有固定的参数;
二,要知道每个可变参数的参数类型。要确定每个可变参数的类型,要么都是默认的类型,要么就在固定参数中包含足够的信息让程序可以确定每个可变参数的类型。比如,printf,程序通过分析format字符串就可以确定每个可变参数的类型。
可以看下snprintf的函数实现:
lib/vsprintf.c
int snprintf(char *buf, size_t size, const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
i = vsnprintf(buf, size, fmt, args);
va_end(args);
return i;
}