你如何安全地调用vsnprintf()?

时间:2016-06-13 11:12:49

标签: c printf glibc

我将一些非常旧的(> 10y)C代码移植到现代Linux中。我在自定义编写的vsnprintf()包装器中得到了分段错误(显然它的任务是检测重复的输出字符串并实习它们):

char* strVPrintf(const String fmt, va_list ap)
{
  /* Guess we need no more than 50 bytes. */
  int n, size = 50;
  char* p = (char*)memMalloc(size), q;

  while (1) {
    /* Try to print in the allocated space. */
    n = vsnprintf(p, size, fmt, ap);
    /* If that worked, return the string. */
    if (n > -1 && n < size) {
      break;
    }
    /* Else try again with more space. */
    if (n > -1)                /* glibc 2.1 */
      size = n + 1;            /* precisely what is needed */
    else                   /* glibc 2.0 */
      size *= 2;               /* twice the old size */
    p = memRealloc(p, size);
  }

  q =  strRegister(p);
  memFree(p);
  return q;
}

作者似乎假设标准vsnprintf()函数返回写入的字符数,如果没有足够的空间来格式化所有args,则只返回一个sentinel值。这意味着您可以猜测缓冲区大小并在必要时增加它。

但是在我的系统上(Ubuntu 14.04,glibc 2.19)vnprintf在为所提供的空间调用太多参数时会导致分段错误。 snprintf()家族的语义在此期间是否发生了巨大变化?什么是确保你给它足够的缓冲空间的现代方法?

3 个答案:

答案 0 :(得分:7)

这是在除SunOS 4(已经过时20年)之后的每个操作系统上使用snprintfvsnprintf的正确方法,因此您的问题就在其他地方。

我会做出一个纯粹的猜测并说我几乎可以肯定你的问题是你将va_list ap传递给消耗它的vsnprintf然后你希望它是下次通话时重置。这是不正确的,并且多年前在gcc中停止工作(因为它只适用于某些架构)。

变化:

n = vsnprintf(p, size, fmt, ap);

要:

va_list apc;
va_copy(apc, ap);
n = vsnprintf(p, size, fmt, apc);
va_end(apc);

看看是否有帮助。

这是一个简单的测试,看看发生了什么:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

void
foo(const char *fmt, va_list ap)
{
#ifdef BAD
    vprintf(fmt, ap);
#else
    va_list apc;
    va_copy(apc, ap);
    vprintf(fmt, apc);
    va_end(apc);
#endif
    vprintf(fmt, ap);
}

void
bar(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    foo(fmt, ap);
    va_end(ap);
}

int
main(int argc, char **argv)
{
    bar("foo %s\n", "bar");
    return 0;
}

跑步时我明白了:

$ cc -o foo foo.c && ./foo
foo bar
foo bar
$ cc -DBAD -o foo foo.c && ./foo
foo bar
foo ����

答案 1 :(得分:2)

据我了解代码,其目的是检测sprintf所需的大小,以便在缓冲区中完全写入输出字符串。有一个功能可以为您执行此操作:asprintf(或vasprintf此处)。

原型:

int vasprintf(char **strp, const char *fmt, va_list ap);

请按以下方式使用:

String strVPrintf(const String fmt, va_list ap)
{
    char *ans;
    int n;
    n = vasprintf(&ans, fmt, ap);
    // do the checks
    return ans;
}

使用此功能,我认为你不再需要这个包装器了。

答案 2 :(得分:2)

不确定你的,但我在变量参数列表上的手册页说:

  

兼容性
       这些宏与它们替换的历史宏不兼容。一个        向后兼容版本可以在包含文件&lt; varargs.h&gt;。

中找到

正如您所说,这是非常古老的代码,也许此例程中收到的va_list不是vsnprintf所期望的va_list。您应该首先尝试使用一个标头提取所有参数,然后确保另一个标头提取(通常vsnprintf与stdarg.h兼容)