如何正确编写asnprintf以避免格式字符串编译器警告?
我想要编写一个asnprintf函数——它是snprintf的包装函数,会根据输出大小动态分配内存。但编译时我遇到了一个警告(在我的系统中已升级为错误):format string is not a string literal [-Werror,-Wformat-nonliteral]。
我查阅了该警告的相关信息,了解到向printf类函数传递非字面量格式字符串存在安全隐患,但在我的场景中,需要接收一个格式指针并将其传递下去。有没有既能避开该警告又不会引入相同安全漏洞的合适方法?
我的现有函数如下:
int asnprintf(char **strp, int max_len, const char *fmt, ...) { int len; va_list ap,ap2; va_start(ap, fmt); va_copy(ap2, ap); len = vsnprintf(NULL, 0, fmt, ap); if ( len > max_len) len = max_len; *strp = malloc(len+1); if (*strp == NULL) return -1; len = vsnprintf(*strp, len+1, fmt, ap2); va_end(ap2); va_end(ap); return len; }
针对这个问题,有几种安全且合规的处理方式,既能绕过警告,又不会引入额外的安全风险:
1. 添加printf风格函数属性
GCC和Clang支持通过函数属性标记printf风格的包装函数,让编译器识别你的函数属于printf家族,从而不再对内部传递的fmt参数触发警告,同时还能在调用asnprintf时自动检查格式字符串与参数的匹配性。
修改函数声明,添加__attribute__((format(printf, 3, 4)))属性:
#include <stdio.h> #include <stdlib.h> #include <stdarg.h> // 3表示fmt是第3个参数,4表示可变参数从第4个开始 int asnprintf(char **strp, int max_len, const char *fmt, ...) __attribute__((format(printf, 3, 4))); int asnprintf(char **strp, int max_len, const char *fmt, ...) { int len; va_list ap,ap2; va_start(ap, fmt); va_copy(ap2, ap); len = vsnprintf(NULL, 0, fmt, ap); if ( len > max_len) len = max_len; *strp = malloc(len+1); if (*strp == NULL) return -1; len = vsnprintf(*strp, len+1, fmt, ap2); va_end(ap2); va_end(ap); return len; }
这个属性会让编译器将你的函数视为标准printf系列函数的扩展,既解决了警告问题,还保留了格式字符串的安全性检查。
2. 局部禁用特定警告
如果不想修改函数属性,可以在调用vsnprintf的代码块附近临时禁用-Wformat-nonliteral警告,确保仅影响必要的代码,不掩盖其他潜在问题。
修改后的代码示例:
#include <stdio.h> #include <stdlib.h> #include <stdarg.h> int asnprintf(char **strp, int max_len, const char *fmt, ...) { int len; va_list ap,ap2; va_start(ap, fmt); va_copy(ap2, ap); // 保存当前警告状态,禁用目标警告 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" len = vsnprintf(NULL, 0, fmt, ap); #pragma GCC diagnostic pop // 恢复警告状态 if ( len > max_len) len = max_len; *strp = malloc(len+1); if (*strp == NULL) return -1; // 再次临时禁用警告 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" len = vsnprintf(*strp, len+1, fmt, ap2); #pragma GCC diagnostic pop va_end(ap2); va_end(ap); return len; }
注意这个语法是GCC/Clang特有的,若需兼容其他编译器,可能需要调整对应的预处理指令。
3. 补充格式字符串安全检查(可选)
即使允许传递非字面量格式字符串,也可以在函数内部添加额外的安全验证,比如在调试模式下检查格式字符串的合法性:
#ifdef DEBUG // 调试模式下验证格式与参数是否匹配,仅作调试用 va_list ap_debug; va_start(ap_debug, fmt); // 输出到/dev/null,仅检查格式合法性 int ret = vfprintf(fopen("/dev/null", "w"), fmt, ap_debug); va_end(ap_debug); if (ret < 0) { // 格式字符串非法,返回错误码 return -1; } #endif
内容的提问来源于stack exchange,提问作者HardcoreHenry

