我最近了解到GCC新的-fanalyzer功能,并决定在我们的一些代码库中试用它。结果相当有趣,但有一个函数我怀疑是假阳性。
这是有问题的函数,GCC 报告了一个 malloc 泄漏 (见此):
char** va_to_argv(va_list args, int32_t* argc)
{
va_list a;
char* arg;
int32_t n;
int32_t l;
int32_t sz = 0;
int32_t cnt = 0;
va_copy(a, args);
while ((arg = va_arg(a, char*)) != NULL)
{
sz += strlen(arg) + 1;
++cnt;
}
va_end(a);
struct s
{
char* argv[cnt + 1];
char data[sz];
};
struct s* tmp = calloc(1, sizeof(*tmp));
for (n=0, l=0; n<cnt; ++n)
{
tmp->argv[n] = &tmp->data[l];
strcpy(tmp->argv[n], va_arg(args, char*));
l += strlen(tmp->argv[n]) + 1;
}
tmp->argv[cnt++] = NULL;
if (argc)
{
*argc = cnt;
}
return &(tmp->argv[0]);
}
它应该是转换一个 va_list
(仅含 char*
,与 NULL
作为最后一个参数)到等价的argcargv表示。据我所知,这个函数从来没有出现过任何问题,所以当GCC报告说 tmp
在回报表时被泄露。
是的。calloc()
存储在tmp中,而函数并没有回到 tmp
的第一个元素,所以乍一看,这个警告是有道理的。然而,返回的值是一个指向 tmp
,而且结构的第一个元素的地址与结构本身的地址相同(所以 free(&(tmp->argv[0]))
应该是合法的)。)
ISOIEC 9899,第 6.7.2.1 节。
指向结构对象的指针,经过适当的转换后,会指向它的初始成员(如果该成员是一个位字段,则指向它所在的单位),反之亦然。在结构对象中可能会有未命名的填充,但不是在结构对象的开头。
不幸的是,这段代码依赖于GCC特有的扩展(结构体中的VLA),而clang并不支持。否则我想交叉检查一下clang的静态分析器是怎么说的。
EDIT:
即使类型不匹配(正如Lundin所指出的),警告仍然存在,即使在这个版本的函数中也是如此(当然,它不再做任何有意义的事情)。
char** va_to_argv(va_list args, int32_t* argc)
{
struct s
{
char** argv;
char data[10];
};
struct s* tmp = calloc(1, sizeof(*tmp));
if (tmp == NULL) return NULL;
return tmp->argv;
}
没有VLA的简化版本。
char **va_to_argv(va_list args, int32_t *argc)
{
va_list a;
char *arg;
char *data;
char **arr;
int32_t n;
int32_t ii;
int32_t sz = 0;
int32_t cnt = 0;
va_copy(a, args);
for(cnt=0; (arg = va_arg(a, char*)) ; cnt++ )
{
sz += 1+ strlen(arg) ;
}
va_end(a);
data = malloc(sz+1);
arr = calloc(cnt+1, sizeof *arr);
for (n=ii=0; n<cnt; n++)
{
arr[n] = &data[ii];
strcpy(arr[n], va_arg(args, char*));
ii += 1 + strlen(arr[n]) ;
}
arr[n++] = NULL;
if (argc) *argc = n;
return arr;
}
EDIT: 如果你只想分配一个对象, 你可以把char[]数组放在*char[]数组之后, 然后只返回指针数组:
arr = malloc(sz+1 + (cnt+1) * sizeof *arr);
data = (char*) ( &arr[cnt+1] ); // Put data after the last arr[] element