3

我发现了一个特定的使用模式,它看起来完全没问题,以前没有编译器抱怨过。现在它使用 gcc-11 发出警告:下面是一个接近最小的示例。另存为t.c并使用gcc-11 -O2 -Wall -c t.c.

#include <stdlib.h>
#include <string.h>

extern void f(const char *s);

void
t(int len, char *d)
{ char tmp[500];
  char *s, *o;
  int i;

  if ( len <= sizeof(tmp) )
    s = tmp;
  else if ( !(s=malloc(len)) )
    return;

  for(o=s,i=0; i < len; i++)
    *o++ = d[i]+1;

  f(s);
//  i = strlen(s);
  if ( s != tmp )
    free(s);
}

编译结果:

gcc-11 -O2 -Wall -c t.c
t.c: In function ‘t’:
t.c:20:3: warning: ‘s’ may be used uninitialized [-Wmaybe-uninitialized]
   20 |   f(s);
      |   ^~~~
t.c:4:13: note: by argument 1 of type ‘const char *’ to ‘f’ declared here
    4 | extern void f(const char *s);
      |             ^
t.c:20:3: warning: ‘tmp’ may be used uninitialized [-Wmaybe-uninitialized]
   20 |   f(s);
      |   ^~~~
t.c:4:13: note: by argument 1 of type ‘const char *’ to ‘f’ declared here
    4 | extern void f(const char *s);
      |             ^
t.c:8:8: note: ‘tmp’ declared here
    8 | { char tmp[500];
      |        ^~~

现在有一些观察

  • 调用 strlen(s) 而不是 f(s) 不会导致警告。请注意,两者都接受const char*(签入/usr/include/string.h
  • 如果我删除 的const声明中的f(),问题也会消失。
  • 调用 usingf((const char*)s)也无济于事。
  • 像 in 一样初始化 schar *s = NULL也无济于事(无论如何我都不希望这样做,因为它隐藏了有关未初始化变量的正确警告)。

我了解到,声称某事是 GCC 错误通常被正确地证明是错误的。所以我首先在这里检查我缺少什么。

编辑由于我无法在评论中添加代码,因此这里是反驳部分声明的代码。这编译得很好:

extern void f(const char *__s);

void
t(int len, char *d)
{ char tmp[500];
  char *s=tmp, *o;
  int i;

  for(o=s,i=0; i < len; i++)
    *o++ = d[i]+1;

  f(tmp);
}
4

2 回答 2

4

编译器是正确的,虽然英文措辞不完善。

假设len总是积极的,解决方法是插入if (len <= 0) __builtin_unreachable();函数。这告诉编译器len始终为正,这意味着必须将一些数据写入调用s之前指向的内存点。f

当编译器说“'s' may be used uninitialized”时,这并不意味着可以使用的值,s而是它所指向的内容可以被使用,并且指向的内存没有被初始化。请注意,它s被传递给一个接受 a 的函数const char *s,这表明该函数不会修改数据s点,因此希望它已经包含数据。C 标准并不严格要求这一点;只要未用 定义指向的内存constf就可以将指针重新转换为其原始指针char *并修改那里的数据,但参数声明的含义是它不会。

我们可以通过将函数的主体更改为:

char tmp[500];
f(tmp);

然后编译器抱怨“警告:'tmp'可能未初始化使用。” 很明显tmp,传递给函数的 不是未初始化的;它将是数组的地址。所以编译器必须警告它是可能未初始化使用的数组的内容。

请注意,虽然从for(o=s,i=0; i < len; i++)表面上开始的循环似乎初始化了指向的数据,但如果为零s,它不会。len并且由于s被传递给fwithout lenf因此无法知道其中没有任何内容s(通过某些侧通道(例如使用外部对象)除外)。所以大概在每次调用f中至少读取一些数据。s

这是一个较小的示例:

#include <stdlib.h>

extern void f(const char *s);

void t(int len)
{
    char *s = malloc(len);
    f(s);
    free(s);
}

想必,len总是积极的。要告诉 GCC,请在函数中插入这一行:

if (len <= 0) __builtin_unreachable();

这导致没有新的代码生成,但警告消失了。(实际上,生成的代码变小了,部分原因是编译器无需先测试就可以进入for循环i < len。)

于 2021-11-01T20:14:37.173 回答
0

它在 gcc-11 发行说明中进行了解释

https://www.gnu.org/software/gcc/gcc-11/changes.html

现在默认包含可能未初始化

https://gcc.gnu.org/onlinedocs/gcc-11.1.0/gcc/Warning-Options.html#index-Wmaybe-uninitialized

如果编译器无法证明未初始化的路径未在运行时执行,则编译器会发出警告。

即使作者很清楚,编译器有责任证明数据已初始化,否则会发出警告。

-Wmaybe-uninitialized被触发-Wall

真正的问题变成了——为什么不只是初始化数据呢?

这是示例代码,但鉴于 tmp 可以是任意非空内存,它看起来像是一个等待利用的漏洞,大概len是因为不能保证空终止符(或可能存在嵌入的空值),但len不是通过到f.


#include <stdlib.h>
#include <string.h>

extern void f(const char *s);

void
t(int len, char *d)
{ char tmp[500] = {0};  //ensure nulls
  char *s = tmp;
  char *o = s;
  int i;

  if ( len <= sizeof(tmp) )
    s = tmp;
  else if ( !(s=malloc(len)) )
    return;
  memset(s+len, 0, 1); // ensure a trailing null
  for(i=0; i < len-1; i++) // leave that null there at least
    *o++ = d[i]+1;

  f(s);
//  i = strlen(s);
  if ( s != tmp )
    free(s);
}

gcc 编译它没有警告

gcc -O2 -Wall -c t.c
于 2021-11-01T20:38:38.723 回答