1.C语言可变参数函数
熟悉C的人都知道,C语言支持可变参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用(...)表示,比如我们常用的printf()\execl函数等;printf函数的原型如下:
int printf(const char *format, ...);
注意,采用这种形式定义的可变参数函数,至少需要一个普通的形参,比如上面代码中的*format,后面的省略号是函数原型的一部分。
C语言之所以可以支持可变参数函数,一个重要的原因是C调用规范中规定C语言函数调用时,参数是从右向左压入栈的;这样一个函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个;以printf为例:
printf("%d%s\n",i,s);
printf函数在定义的时候,不知道函数调用的时候会传递几个参数。在实现上,printf函数只需关心第一个参数,即字符串“%d%s\n”,当读到%d的时候,printf知道自己需要第二个参数,这时只需要去栈上寻找即可;当读到%s时,再去栈上网上寻找一个参数即可。简单说,printf不关心栈上到底压了多少参数,只关心自己需要多少。
那么对于一个定义为可变参数的函数,函数定义的时候并没有定义形参原型,怎么使用参数呢?
C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_start、va_arg和va_end;在ANSI C标准下,这些宏定义在stdarg.h中。三个宏的原型如下:
void va_start(va_list ap, last);//取第一个可变参数(如上述printf中的i)的指针给ap,last是函数声明中的最后一个固定参数(比如printf函数原型中的*fromat);
type va_arg(va_list ap, type);//返回当前ap指向的可变参数的值,然后ap指向下一个可变参数;type表示当前可变参数的类型(支持的类型位int和double);
void va_end(va_list ap);//将ap置为NULL
当一个函数被定义位可变参数函数时,其函数体内首先要定义一个va_list的结构体类型,这里沿用原型中的名字,ap。
va_start使ap指向第一个可选参数。va_arg返回参数列表中的当前参数并使ap指向参数列表中的下一个参数。va_end把ap指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。
下面是一个具体的示例(摘自wikipedia):
#include <stdarg.h>
double average(int count, ...)
{
va_list ap;
int j;
double tot = 0;
va_start(ap, count); //使va_list指向起始的參數
for(j=0; j<count; j++)
tot+=va_arg(ap, double); //檢索參數,必須按需要指定類型
va_end(ap); //釋放va_list
return tot/count;
}
除此之外, 我们还需要注意一个陷阱,即va_arg宏的第2个参数不能被指定为char、short或者float类型。《C和C++经典著作:C陷阱与缺陷》 在可变参数函数传递时,因为char和short类型的参数会被提升为int类型,而float类型的参数会被提升为double类型 。
例如,以下的代码是错误的
a = va_arg(ap,char);
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
a = va_arg(ap,int);
还需要注意的一个问题是,即时我们知道在某种体系结构下C语言函数的参数都压在栈上,我们也应该避免直接去栈上取想要的参数,因为这样会降低程序的灵活性和可移植性,并带来一些安全上潜在的危险。上述的三个宏,包括va_list,在不同的体系结构下会有不同的实现方法,比如va_list,有的系统上直接指向栈;而有的系统却将其实现为一个指针数组。
2.printf函数的实现
//acenv.h
typedef char *va_list;
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)
#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))))
//start.c
static char sprint_buf[1024];
int printf(char *fmt, ...)
{
va_list args;
int n;
va_start(args, fmt);
n = vsprintf(sprint_buf, fmt, args);
va_end(args);
write(stdout, sprint_buf, n);
return n;
}
//unistd.h
static inline long write(int fd, const char *buf, off_t count)
{
return sys_write(fd, buf, count);
}
3.分析可变函数参数的实现
可变函数参数,实现的时候需要逐个调用传入的可变参数。想一想,我们需要完成哪些工作?
1)知道可变参数的起始地址
这个功能,我们是通过va_start(arg_ptr, argN)宏定义来实现的,具体如下:
#define
va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd
(A,_AUPBND))))
va_start(ap, A)
{
char *ap = ((char *)(&A)) + sizeof(A)并int类型大小地址对齐
}
这里,需要解释一下,函数调用时,参数入栈顺序是从右向左的,而栈的增长方向是从高地址到低地址。所以,如果一个函数形式如下:
int func(int a,int b,int c)那么,在栈中的情况如下:
c | 高地址 |
b | |
a | 低地址 |
这就是说,函数右边的参数占据着高地址。
另外,对于任何编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是
0x1ffc-->a (4字节)
0x2000-->b (4字节)
0x2004-->c (8字节)
0x200c-->d (4字节)
2)知道可变参数的个数以及每个参数的类型
如果知道了参数a的地址,则要取后续参数的值则可以通过a的地址计算a后面参数的地址,然后取对应的值,而后面参数的个数可以直接由变量a指定,当然也可以像printf一样根据第一个参数中的%模式个数来决定后续参数的个数和类型。如果参数的个数由第一个参数a直接决定,则后续参数的类型如果没有变化并且是已知的,则我们可以这样来取后续参数, 假定后续参数的类型都是double;
void fun1(int num, ...)
{
double *p = (double *)((&num)+1);
double Param1 = *p;
double Param2 = *(p+1);
...
double Paramn *(p+num);
}
三个与可变参数实现有关的宏定义如下:
#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 ) // 将指针置为无效
4.一个例子:printf系列函数
看了可变函数的实现,其实使用起来还是相当麻烦的。 而日常工作中,我们经常需要类似于printf的输出,所以,C语言库函数为我们实现了相应的封装。
SYNOPSIS
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
snprintf(), vsnprintf():
_BSD_SOURCE || _XOPEN_SOURCE >= 500 || _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L;
or cc -std=c99
简介:
printf系列函数根据格式控制产生相应的输出。printf和vprintf出处到stdout;fprintf和fprintf输出到指定的输出流;sprintf和snprintf输出到指定的字符串。n,控制输出个数(包含'\0')。
函数vprintf(), vfprintf(), vsprintf(), vsnprintf() 分别等同于 printf(),fprintf(), sprintf(), snprintf()——前者的函数参数是一个va_list,后者的参数是可变个数的。这些函数不调用va_end宏,因为它们调用 va_arg 宏, 详细的内容可以参考stdarg。
使用实例:
// 例1:格式化到一个文件流,可用于日志文件
FILE *logfile;
int WriteLog(const char * format, ...)
{
va_list arg_ptr;
va_start(arg_ptr, format);
int nWrittenBytes = vfprintf(logfile, format, arg_ptr);
va_end(arg_ptr);
return nWrittenBytes;
}
…
// 调用时,与使用printf()没有区别。
WriteLog("%04d-%02d-%02d %02d:%02d:%02d %s/%04d logged out.",
nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID); |
5.潜在的风险
从va的实现可以看出,指针的合理运用,把C语言简洁、灵活的特性表现得淋漓尽致,叫人不得不佩服C的强大和高效。不可否认的是,给编程人员太多自由空间必然使程序的安全性降低。va中,为了得到所有传递给函数的参数,需要用va_arg依次遍历。其中存在两个隐患:
1)如何确定参数的类型。 va_arg在类型检查方面与其说非常灵活,不如说是很不负责,因为是强制类型转换,va_arg都把当前指针所指向的内容强制转换到指定类型;
2)结束标志。如果没有结束标志的判断,va将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。例2中SqSum()求的是自然数的平方和,所以我把负数和0作为它的结束标志。例如scanf把接收到的回车符作为结束标志,大家熟知的printf()对字符串的处理用'\0'作为结束标志,无法想象C中的字符串如果没有'\0', 代码将会是怎样一番情景,估计那时最流行的可能是字符数组,或者是malloc/free。
允许对内存的随意访问,会留给不怀好意者留下攻击的可能。当处理cracker精心设计好的一串字符串后,程序将跳转到一些恶意代码区域执行,以使cracker达到其攻击目的。(常见的exploit攻击)所以,必需禁止对内存的随意访问和严格控制内存访问边界。