如何停止GCC在snprintf()调用中抱怨“指令输出可能被截断”?

8

我在古老的Linux(RedHat 5.2)和现代的macOS 10.14.6 Mojave上都使用GCC 9.2.0,并且两者都出现了同样的问题。

#include <stdio.h>
#include <time.h>

struct Example
{
    /* ... */
    char mm_yyyy[8];    /* Can't be changed */
    /* ... */
};

extern void function(struct tm *tm, struct Example *ex);

void function(struct tm *tm, struct Example *ex)
{
    snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
             tm->tm_mon + 1, tm->tm_year + 1900);
}

当使用-Wall和任何优化(因此不是使用-O0和没有任何优化选项)进行编译时,编译器会提示:
$ gcc -O -Wall -c so-code.c 
so-code.c: In functionfunction’:
so-code.c:15:49: warning: ‘%d’ directive output may be truncated writing between 1 and 11 bytes into a region of size 8 [-Wformat-truncation=]
   15 |     snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
      |                                                 ^~
so-code.c:15:48: note: directive argument in the range [-2147483647, 2147483647]
   15 |     snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
      |                                                ^~~~~~~
so-code.c:15:48: note: directive argument in the range [-2147481748, 2147483647]
so-code.c:15:5: note: ‘snprintf’ output between 4 and 24 bytes into a destination of size 8
   15 |     snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   16 |              tm->tm_mon + 1, tm->tm_year + 1900);
      |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$

在某种程度上,很好;如果tm->tm_mon包含超出范围0到99(或-1到-9)的值,则会将超过两个字节写入输出缓冲区,或者如果tm->tm_year + 1900需要超过4位数字,则会发生截断/溢出。然而,时间值已知是有效的(月份011;年份+1900在范围19702100内,为了具体起见;实际年份范围较小——大约是2019..2025),因此实际上并不需要担心这个问题。
有没有一种方法可以抑制警告,而不必诉诸于以下代码:
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignore "-Wformat-overflow" /* Or "-Wformat-truncation" */
#endif

    snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
             tm->tm_mon + 1, tm->tm_year + 1900);

#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#ifdef行是必要的,因为代码必须由其他编译器(尤其是AIX上的XLC 13.x)编译,这些编译器会抱怨未知的pragma,尽管它们实际上不应该。更正式地说,它们不需要抱怨并且应该接受忽略未知的pragma的代码,但是它们会发出有关无法识别pragma的评论,这破坏了干净编译的目标。

仅仅为了好玩;如果将函数从返回void改为返回int,然后再return snprintf(…);,则不会生成错误。(这让我感到惊讶-我不确定为什么这也不是一个问题。我想这与“从snprintf()返回值被返回以便可以检查并发现溢出”有关,但这有点令人惊讶。)

不幸的是,这是一个MCVE(Minimal, Complete, Verifiable Example; 它所提取的代码要大得多,并且更改数据结构不是一个选项-而这只是出现在其中的函数中的许多步骤之一。

我想我可以编写一个微小的函数来调用snprintf()并返回值(该值将被忽略),但是:

  • 是否有其他可行的替代方法?
  • 是否有一种方法告诉GCC传递给snprintf()等的变量范围比最坏情况更安全?

3
你尝试过使用(tm->tm_mon + 1)%100u, (tm->tm_year + 1900)%10000u吗?这是对原文的翻译,没有改变原意,也没有添加解释或其他额外内容。 - chux - Reinstate Monica
1
@chux:看起来这确实解决了问题——谢谢!当你自己遇到瓶颈时,有人与你一起思考(或替你思考)总是很有帮助的。将其转化为答案,你将得到我的感激之情。 - Jonathan Leffler
@JonathanLeffler:这种行为只发生在GCC 9.2.0上吗? - l'L'l
使用(void)强制转换显式忽略snprintf的返回值是否可以停止警告? - Chris Dodd
@LorinczyZsigmond:有趣的是,POSIX strftime()没有提供一种获取1-9月份的月份数字的方法,据我所知。而且它提供的选项比标准C strftime()更多。 - Jonathan Leffler
显示剩余3条评论
2个回答

8

使用检查可能值范围的编译器,可以使用%快速限制范围。

% some_unsigned_N 确保输出位于[0 ...N-1]

请注意,% some_pos_int_N 输出在(-N ... N) 范围内,因此建议使用 无符号数 进行运算,以避免出现 '-' 符号。

snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d", 
    //  tm->tm_mon + 1, tm->tm_year + 1900);
    (tm->tm_mon + 1)%100u, (tm->tm_year + 1900)%10000u);

some_unsigned_N 接近 INT_MAX 时,可能需要使用 "%u"


2
谢谢。这样可以停止投诉,并确保值在预期范围内,没有理由认为其他编译器会拒绝它,因此它符合我的标准。(可能有另一个编译器集会挑剔,不接受这样做的安全性 - 如果我需要,我会解决这个问题。我可能需要处理关于混合有符号和无符号算术的投诉,但如果必要,也可以以后再处理。) - Jonathan Leffler

0
还有几个其他选择:
在运行时进行范围检查,并在值超出范围时调用`abort()`。这样做的优点是完全符合标准,无论输入数据是否有效,都不会生成错误的字符串。实际上,它可能比计算余数更快。
if (tm->tm_year < -1900 || tm->tm_year > 9999 - 1900)
    abort();

使用`unreachable()`(从C23开始)或`__builtin_unreachable()`(GCC扩展)告诉编译器值在范围内。这没有额外开销,但传递无效日期不仅会生成意外的字符串,还会导致未定义行为。如果在日期验证之前意外调用了该函数作为日志代码的一部分,则日期验证可能会被优化掉。
if (tm->tm_mon < 0 || tm->tm_mon > 11)
    __builtin_unreachable();

使用位掩码来缩小输入的范围。这比使用余数更便宜,但在某些情况下不可行,并且如果值超出范围,结果将不太直观。
snprintf(..., (tm->tm_mon & 0xF) + 1, (tm->tm_year & 0xFFF) + 1900);

在运行时进行范围检查,并在值超出范围时调用abort()。哦,绝对不行!!!因为有一个错误的数据就导致整个进程崩溃?!?!使用位掩码来缩小输入范围。这比使用取余更便宜。你怎么知道它更"便宜"呢?即使是真的(而且在执行多个指令的深度流水线CPU上,%甚至可能更快...),在昂贵的调用(如snprintf())的背景下,失去清晰度并不值得这种过早的微观优化带来的无穷小好处。 - Andrew Henle

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