C语言中可变长度参数极大地方便了我们编程的同时,也非常容易由于使用不慎导致及其隐蔽的错误。以下面的这段函数为例,运行一段时间后会随机出现段错误,而且出错的位置一直稳定在vsprintf()函数里面。
……………...
a_list
ap;
va_start(ap,
cmd);
……………...
rep
= (redisReply *)redisvCommand(rc, cmd, ap);
vsprintf(str,
cmd, ap);
……………...
va_end(ap);
为了深入探究原因,我们下载了redis源代码和glibc源代码。看redis源代码中 redisvCommand的实现:
void
*redisvCommand(redisContext *c, const char *format, va_list ap) {
if
(redisvAppendCommand(c,format,ap) != REDIS_OK)
return
NULL;
return
__redisBlockForReply(c);
}
它主要调用了redisvAppendCommand:
int
redisvAppendCommand(redisContext *c, const char *format, va_list ap)
{
char
*cmd;
int
len;
len
= redisvFormatCommand(&cmd,format,ap);
if
(len == -1) {
__redisSetError(c,REDIS_ERR_OOM,"Out
of memory");
return
REDIS_ERR;
}
else if (len == -2) {
__redisSetError(c,REDIS_ERR_OTHER,"Invalid
format string");
return
REDIS_ERR;
}
if
(__redisAppendCommand(c,cmd,len) != REDIS_OK) {
free(cmd);
return
REDIS_ERR;
}
free(cmd);
return
REDIS_OK;
}
而redisvAppendCommand()函数中使用了va_arg,比如下面的部分代码:
256
/* Set newarg so it can be checked even if it is not
touched. */
257
newarg = curarg;
258
259
switch(c[1]) {
260
case ‘s‘:
261
arg = va_arg(ap,char*);
262
size = strlen(arg);
263
if (size > 0)
264
newarg = sdscatlen(curarg,arg,size);
265
break;
266
case ‘b‘:
267
arg = va_arg(ap,char*);
268
size = va_arg(ap,size_t);
269
if (size > 0)
270
newarg = sdscatlen(curarg,arg,size);
271
break;
272
case ‘%‘:
273
newarg = sdscat(curarg,"%");
274
break;
275
default:
乍一看,ap传进去都是形式参数,不会改变,但仔细看va_arg的帮助文档可以看到,其实每次调用va_arg()都会改变ap的值:
va_arg()
The
va_arg() macro expands to an expression that has the type and value
of the next argument in the call. The argument ap is the va_list ap
initialized by va_start(). Each call to va_arg() modifies ap so
that the next call returns the next argument. The argument type
is a
type
name specified so that the type of a pointer to an object that has
the specified type can be obtained simply by adding a * to type.
The
first use of the va_arg() macro after that of the va_start() macro
returns the argument after last. Successive invocations return the
values
of the remaining arguments.
而ap又是作为指针的指针传递进来的,因此上层调用函数里的可变长度参数ap也会改变,着就导致后面对可变长度参数的使用出现段错误。因为前面已经遍历一遍了,ap到末尾了。
理解了这一点,针对一个函数中要调用多个可变长度的参数的用法,安全的用法就是为每一个被调用的函数单独分配一个可变长度参数va_list。据此,上面的代码就应该改写成这样:
a_list
ap;
va_list
aq;
va_start(ap,
cmd);
va_copy(aq,
ap);
………
rep
= (redisReply *)redisvCommand(conn, cmd, ap);
vsprintf(str,
cmd, aq);
va_end(ap);
va_end(aq);
………