我正在编写需要格式化字符串的代码,并且我想避免缓冲区溢出。
我知道,如果vsnprintf
可用(从C99开始),我们可以做到:
char* formatString(const char *format, ...)
{
char* result = NULL;
va_list ap;
va_start(ap, format);
/* Get the size of the formatted string by getting vsnprintf return the
* number of remaining characters if we ask it to write 0 characters */
int size = vsnprintf(NULL, 0, format, ap);
if (size > 0)
{
/* String formatted just fine */
result = (char *) calloc(size + 1, sizeof(char));
vsnprintf(result, size + 1, format, ap);
}
va_end(ap);
return result;
}
我想不出一种在C90中进行类似操作的方式(没有vsnprintf
)。如果事实证明,如果不编写极其复杂的逻辑是不可能的,那么我很乐意为结果设置最大长度,但是我不确定在不冒缓冲区溢出风险的情况下如何实现这一目标。
答案 0 :(得分:2)
Pre-C99无法简单地解决格式化字符串的问题,并且具有防止缓冲区溢出的高度安全性。
正是那些令人讨厌的"%s"
,"%[]"
,"%f"
格式说明符需要对潜在的长输出量进行仔细的考虑。因此需要这种功能。 @Jonathan Leffler
为此,那些早期的编译器必须执行代码来分析format
并使用参数来查找所需的大小。到那时,几乎可以使用代码来拥有完整的my_vsnprintf()
。我会为此寻求现有的解决方案。 @user694733。
即使使用C99,*printf()
也有环境限制。
任何一次转换可以产生的字符数至少应为4095。C11dr§7.21.6.115
因此,即使有足够的char buf[10000]; snprintf(buf, sizeof buf, "%s", long_string);
且buf[]
,任何试图strlen(long_string) > 4095
的代码都可能会带来问题。
这意味着快速而肮脏的代码可以计算%
和格式长度,并合理地假设所需的大小不超过:
size_t sz = 4095*percent_count + strlen(format) + 1;
当然,对说明符的进一步分析可能会导致更为保守的sz
。继续path,我们end继续编写自己的my_vsnprintf()
。
即使使用您自己的my_vsnprintf()
,安全也是如此。没有运行时检查format
(可能是动态的)是否与以下参数匹配。这样做需要一种新方法。
C99解决方案的厚脸皮自我广告,以确保匹配的说明符和参数:Formatted print without the need to specify type matching specifiers using _Generic。
答案 1 :(得分:1)
转移comments来回答。
将
vsnprintf()
添加到C99的主要原因是,很难保护vsprintf()
或类似的东西。一种解决方法是打开/dev/null
,使用vfprintf()
格式化数据,注意需要多大的结果,然后确定是否安全进行。麻烦,尤其是在每次通话中打开设备时。
这意味着您的代码可能会变成:
#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
extern char *formatString(const char *format, ...);
char *formatString(const char *format, ...)
{
static FILE *fp_null = NULL;
if (fp_null == NULL)
{
fp_null = fopen("/dev/null", "w");
if (fp_null == NULL)
return NULL;
}
va_list ap;
va_start(ap, format);
int size = vfprintf(fp_null, format, ap);
va_end(ap);
if (size < 0)
return NULL;
char *result = (char *) malloc(size + 1);
if (result == NULL)
return NULL;
va_start(ap, format);
int check = vsprintf(result, format, ap);
va_end(ap);
assert(check == size);
return result;
}
int main(void)
{
char *r1 = formatString("%d Dancing Pigs = %4.2f%% of annual GDP (grandiose dancing pigs!)\n",
34241562, 21.2963);
char *r2 = formatString("%s [%-13.10s] %s is %d%% %s\n", "Peripheral",
"sub-atomic hyperdrive", "status", 99, "of normality");
if (r1 != NULL)
printf("r1 = %s", r1);
if (r2 != NULL)
printf("r2 = %s", r2);
free(r1);
free(r2);
return 0;
}
如在函数内部用fp_null
编写一个静态变量,则无法关闭文件流。如果这很麻烦,请将其设置为文件内的变量,并为if (fp_null != NULL) { fclose(fp_null); fp_null = NULL; }
提供一个函数。
我毫不犹豫地假设一个带有/dev/null
的类Unix环境;如果您使用的是Windows,则可以将其翻译为NUL:
。
请注意,问题中的原始代码没有两次使用va_start()
和va_end()
(不同于此代码);那会导致灾难。我认为,最好将va_end()
放在va_start()
之后,如代码所示。显然,如果您的函数本身正在逐步通过va_list
,则差距将比此处显示的要大,但是,当您只是将变量参数中继到另一个函数(如此处)时,应该只有一行在两者之间。
代码使用GCC 8.2.0(在macOS 10.13 High Sierra上编译)通过命令行在运行macOS 10.14 Mojave的Mac上干净地编译:
$ gcc -O3 -g -std=c90 -Wall -Wextra -Werror -Wmissing-prototypes \
> -Wstrict-prototypes vsnp37.c -o vsnp37
$
运行时会产生:
r1 = 34241562 Dancing Pigs = 21.30% of annual GDP (grandiose dancing pigs!)
r2 = Peripheral [sub-atomic ] status is 99% of normality