如何规避GCC中的格式截断警告?

59

我收到了以下gcc格式截断警告:

test.c:8:33: warning: ‘/input’ directive output may be truncated writing 6 bytes into a region of size between 1 and 20 [-Wformat-truncation=]
snprintf(dst, sizeof(dst), "%s-more", src);
                             ^~~~~~
test.c:8:3: note: ‘snprintf’ output between 7 and 26 bytes into a destination of size 20
snprintf(dst, sizeof(dst), "%s-more", src);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

在这样的代码上:

char dst[20];
char src[20];
scanf("%s", src);
snprintf(dst, sizeof(dst), "%s-more", src);
printf("%s\n", dst);

我知道它可能会被截断 - 但这正是我首先使用snprintf的原因。有没有一种方法可以让编译器清楚地知道这是有意为之(而不使用pragma或-Wno-format-truncation)?


3
@aicastell,这个警告是在gcc 8.1中引入的。 - Jabberwocky
如果这是特定于给定gcc版本的问题,我认为应该在原始问题中包含这些信息。 - aicastell
1
@WeatherVane 或许有充分的理由指定这个目标大小。如果没有,他就不会使用snprintf了吧? - fralbo
@fralbo也许他想限制的是字符串长度,而不是其存储大小。 - Weather Vane
@WeatherVane 可能是可以的,但如果我在 ret = snprintf() 调用后跟着 if (ret < 0),我们将不会收到任何警告,这是错误的!在截断时,ret 将为正数。增加目标数组并不能解决任何问题。GCC 不关心它,它只验证返回值是否被检查,但从我的角度来看,这样做很糟糕。 - fralbo
显示剩余4条评论
5个回答

53
  1. 该警告在gcc7.1中添加,详见gcc7.1发布变更
  2. gcc文档中:

-Wformat-truncation的级别1[...]只会对返回值未使用且很可能导致输出截断的有界函数调用发出警告。

  1. 该问题是一个错误报告,并被标记为NOTABUG:

未处理的输出截断通常是程序中的错误。[...]
在截断被期望的情况下,调用者通常会检查从函数返回的值并以某种方式进行处理(例如,通过分支)。 在这些情况下,不会发出警告。 警告打印的源行表明这不是这些情况之一。 该警告正在执行其设计的功能。

  1. 但我们可以仅检查snprintf的返回值,该函数在错误时返回负值。
#include <stdio.h>
#include <stdlib.h>
int main() {
    char dst[2], src[2] = "a";

    // snprintf(dst, sizeof(dst), "%s!", src); // warns

    int ret = snprintf(dst, sizeof(dst), "%s!", src);
    if (ret < 0) {
         abort();
    }

    // But don't we love confusing one liners?
    for (int ret = snprintf(dst, sizeof(dst), "%s!", src); ret < 0;) exit(ret);
    // Can we do better?
    snprintf(dst, sizeof(dst), "%s!", src) < 0 ? abort() : (void)0;
    // Don't we love obfuscation?
#define snprintf_nowarn(...) (snprintf(__VA_ARGS__) < 0 ? abort() : (void)0)
    snprintf_nowarn(dst, sizeof(dst), "%s!", src);
}

https://godbolt.org/上使用gcc7.1、gcc7.2、gcc7.3和gcc8.1进行测试,使用-O{0,1,2,3} -Wall -Wextra -pedantic编译选项,没有警告。使用大于-O1的优化时,gcc8.1会优化/删除对abort()的调用。

奇怪的是,当编译为C++源文件时,即使检查返回值,警告仍然存在。在C中一切正常。在C++中最好使用std::format_to。所以:

  1. 我们可以使用特定于编译器的语法来禁用警告。
#include <stdio.h>    
#include <stdlib.h>
int main() {
    char dst[2];

    char src[2] = "a";
    // does not warn in C
    // warns in C++ with g++ newer than 10.1 with optimization -O2
    int ret = snprintf(dst, sizeof(dst), "%s!", src);
    if (ret < 0) {
         abort();
    }

    // does not warn in C
    // still warns in C++
    ret = snprintf(dst, sizeof(dst), "%s!", "a");
    if (ret < 0) {
         abort();
    }

    // use compiler specific pragmas to disable the warning
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
    snprintf(dst, sizeof(dst), "%s!", "a");
#pragma GCC diagnostic pop

// wrapper macro with compiler specific pragmas
// works for any gcc
// works from g++ 10.1
#ifndef __GNUC__
#define snprintf_nowarn  snprintf
#else
#define snprintf_nowarn(...) __extension__({ \
    _Pragma("GCC diagnostic push"); \
    _Pragma("GCC diagnostic ignored \"-Wformat-truncation\""); \
    const int _snprintf_nowarn = snprintf(__VA_ARGS__); \
    _Pragma("GCC diagnostic pop"); \
    _snprintf_nowarn; \
})
#endif
    snprintf_nowarn(dst, sizeof(dst), "%s!", "a");
}

19
请注意,snprintf() 仅在出现错误时返回负数。当截断发生时,它会返回它本应写入的字符数,如果缓冲区足够长的话。 - nafmo
这个解决方案在我的情况下不起作用。请看一下:https://godbolt.org/z/4r7sT49hs - Tharindu Sathischandra
你正在使用C++编译器。 - KamilCuk
我更喜欢 #define saprintf(array, ...) (void)(snprintf(array, sizeof(array), __VA_ARGS__)<0?abort():0) 这样我可以避免重复输入第一个参数。 - user3710044
1
@user3710044 我建议不要偏好那种写法,对我来说听起来很混乱、危险且有限制 - 考虑使用 char *buf = (char[200]){}; saprintf(buf, "");snprintf 有着完善的API,通过传递指针和大小是在C语言中更易读且更受推崇的方式。 - KamilCuk
你觉得这个令人困惑,因为GCC在对待数组(已知大小)与指针(需要告诉它大小)时有所不同。在我看来,这反而改善了问题,因为它明确指出这个printf是用于数组而不是指针,并且我们知道它有一个固定的大小,我们希望编译器将其截断。这就是snprintf应该工作的方式,不是总是期望它是"asprintf"的奇怪变体。另外,如果你希望它对指针进行投诉,我建议你添加某种静态断言,比如({enum{_x=1/(sizeof(array)-sizeof(char*))};}) - user3710044

14

只有在调用长度限制的*printf函数时(例如snprintfvsnprintf),才会触发此错误。换句话说,它并不表示您可能会溢出缓冲区,就像使用sprintf时可能会发生的那样;它只会通知您,您没有检查snprintf是否在执行其工作并进行截断。(附注: snprintf始终以空字符结尾,因此这不会导致非终止字符串。)

了解了这一点,我对全局禁用它使用-Wno-format-truncation更加乐观,而不是试图说服gcc忽略特定实例。


我认为测试 snprintf 的返回值以避免警告是没有问题的。此外,这意味着您可以确保代码按预期工作。您还可以在格式字符串中使用限制(%.123s)。 - Alexis Wilke
2
如果我不在意它是否被截断,为什么我还需要测试呢?这就像使用max()函数,然后被迫测试是否实际达到了限制一样。 - Daniel Griscom
@DanielGriscom 如果你真的不在意,为什么不像@KamilCuk提供的答案中那样使用snprintf_nowarn宏呢? - Johan Bezem

6

这个页面对我很有用:
https://www.fluentcpp.com/2019/08/30/how-to-disable-a-warning-in-cpp/

您可以通过以下方式解决gcc / clang编译器的问题:

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
    snprintf(dst, sizeof(dst), "%s-more", src);
#pragma GCC diagnostic pop

上述网页还提供了Visual Studio编译器警告的解决方案。

这是最好的解决方案。感谢提供链接。 - Lehrian
2
他们可以通过从gcc中删除那些无意义的内容来解决问题。如果我们在sprintf中截断某些东西,那是因为我们希望它被截断(例如小时和日期,我们知道它永远不会是“数十亿”,而只是最多“31”;)而且,当我们的代码应该与其他所有东西一起编译时,我们不应该仅仅为了“GCC”而在其中包含“pragmas”。以前从未出现过。现在可以再次删除它。顺便说一句,如果我没有指定-Wall,我不想看到警告。尤其是关于我如何将日期放入%d字段;) 当一个月有20亿天时,我会回来处理它们。 - HRH Sven Olaf of CyberBunker

2

此外,在这里引入一个volatile临时变量作为目标大小的解决方法也是可行的。

char dst[20];
char src[20];
volatile int dst_size = sizeof(dst);
snprintf(dst, dst_size, "%s-more", src);

正如Martin Sebor所建议的那样,您也可以使用这个snprintf_trunc宏;

#define snprintf_trunc(dst, size, ...)   \
    do {                                 \
      volatile size_t n = size;          \
      snprintf (dst, n, __VA_ARGS__);    \
    } while (0)

0

没有定义也是可能的

//GCC 8.4 check
snprintf(str, (volatile size_t){sizeof(str)}, "42")

//as an option
#define WARN_SUPPRESS_VOLATILE(val) (volatile typeof(val)){(val)}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接