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

yshpjwxd  于 2023-08-03  发布在  其他
关注(0)|答案(2)|浏览(340)

我在古老的Linux(RedHat 5.2)和现代的macOS 10.14.6莫哈韦上使用GCC 9.2.0,我在两者上都得到了同样的抱怨。

  1. #include <stdio.h>
  2. #include <time.h>
  3. struct Example
  4. {
  5. /* ... */
  6. char mm_yyyy[8]; /* Can't be changed */
  7. /* ... */
  8. };
  9. extern void function(struct tm *tm, struct Example *ex);
  10. void function(struct tm *tm, struct Example *ex)
  11. {
  12. snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
  13. tm->tm_mon + 1, tm->tm_year + 1900);
  14. }

字符串
当使用-Wall和任何优化进行编译时(所以不是使用-O0,也不是完全没有优化选项),编译器说:

  1. $ gcc -O -Wall -c so-code.c
  2. so-code.c: In function function’:
  3. 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=]
  4. 15 | snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
  5. | ^~
  6. so-code.c:15:48: note: directive argument in the range [-2147483647, 2147483647]
  7. 15 | snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
  8. | ^~~~~~~
  9. so-code.c:15:48: note: directive argument in the range [-2147481748, 2147483647]
  10. so-code.c:15:5: note: snprintf output between 4 and 24 bytes into a destination of size 8
  11. 15 | snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
  12. | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  13. 16 | tm->tm_mon + 1, tm->tm_year + 1900);
  14. | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  15. $


在一个层面上,足够公平;如果tm->tm_mon包含范围0到99(或-1到-9)之外的值,则将多于两个字节写入输出缓冲器,或者如果tm->tm_year + 1900需要多于4个数字,则将存在截断/溢出。但是,已知时间值是有效的(月011; year + 1900,范围为19702100,为了具体起见;年份的范围其实更小-- 2019年。2025年左右),所以这种担忧实际上是没有道理的。
是否有一种方法可以在不使用以下代码的情况下禁止显示警告:

  1. #ifdef __GNUC__
  2. #pragma GCC diagnostic push
  3. #pragma GCC diagnostic ignore "-Wformat-overflow" /* Or "-Wformat-truncation" */
  4. #endif
  5. snprintf(ex->mm_yyyy, sizeof(ex->mm_yyyy), "%d-%d",
  6. tm->tm_mon + 1, tm->tm_year + 1900);
  7. #ifdef __GNUC__
  8. #pragma GCC diagnostic pop
  9. #endif


#ifdef行是必要的,因为代码必须由其他编译器(特别是AIX上的XLC 13.x)编译,这些编译器会抱怨未知的杂注,即使它们实际上并不应该这样做。(更正式地说,它们不需要抱怨,并且应该接受忽略未知杂注的代码,但它们会传递关于不识别杂注的注解,这违背了干净编译的目标。
只是为了好玩;如果你将函数从返回void改为返回int,如果你再返回return snprintf(…);,则不会生成错误。(这让我很惊讶,我不知道为什么这不是一个问题。我猜这是因为snprintf()的返回值被返回,所以可以检查它并发现溢出,但这有点令人惊讶。)
不幸的是,这是一个MCVE(Minimal, Complete, Verifiable Example;它所提取的代码要大得多,并且改变数据结构不是一个选项-这只是它出现的函数中许多步骤中的一个步骤。
我想我可以写一个微观函数来调用snprintf()并返回值(将被忽略),但是:

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

pobjuy321#

对于检查可能值范围的编译器,可以使用%快速限制范围。
% some_unsigned_N确保输出在[0... N-1]中。
请注意,% some_pos_int_N输出在(-N ...N)范围内,因此建议使用 unsigned math来避免'-'符号。

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

字符串
可能还想用"%u"应该some_unsigned_N靠近INT_MAX

0aydgbwb

0aydgbwb2#

其他几个选项:

  • 在运行时进行范围检查,如果值超出范围,则调用abort()。这样做的优点是完全标准化,无论输入数据是否有效,都不会生成错误的字符串。它实际上可能比计算余数更快。
  1. if (tm->tm_year < -1900 || tm->tm_year > 9999 - 1900)
  2. abort();

字符串

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

  • 使用位掩码来缩小输入范围。它比使用余数更便宜,但不是在所有情况下都可行,如果值超出范围,则会产生不太直观的结果。
  1. snprintf(..., (tm->tm_mon & 0xF) + 1, (tm->tm_year & 0xFFF) + 1900);

展开查看全部

相关问题