【问题标题】:Shouldn't this give an out-of-bounds warning?这不应该给出越界警告吗?
【发布时间】:2019-06-11 19:19:48
【问题描述】:

我认为这段代码应该警告越界数组访问:

int foo() {
  int x[10] = {0};
  int *p = &x[5];
  return p[~0LLU];
}

我知道标准不需要越界警告,但编译器确实提供了它们。我在问编译器在这里给出这样的警告是否正确。

有什么理由认为该代码格式正确?

【问题讨论】:

标签: c++ c


【解决方案1】:

我认为这段代码应该警告超出范围的数组访问:

当您在非 VLA 数组 上执行此操作时,一个不错的编译器可以警告您(gcc 不会,但 clang 会:https://godbolt.org/z/lOvl5n

对于这个sn-p:

int foo() {
  int x[10] = {0};  
  return x[~0LLU];  // or x[40] to make it simpler, same thing
}

警告:

<source>:3:10: warning: array index -1 is past the end of the array (which contains 10 elements) [-Warray-bounds]

  return x[~0LLU];

         ^ ~~~~~

编译器知道这是一个数组,知道大小,因此可以检查所有内容是否都是文字的边界(非 VLA 数组和文字索引是先决条件)

在您的情况下,编译器“丢失”的是您分配给指针(数组 decays 变为指针)

之后,编译器无法判断数据的来源,因此它无法控制边界(即使在您的情况下,偏移量大得离谱/负数/其他)。专用的静态分析工具可能会发现问题。

【讨论】:

  • @NathanOliver 是的,但编译器诊断认为它无论如何都是-1 :)
  • 我认为这个-1 是一个错误。或者一些实现定义的东西。 -1 也不能“超过数组末尾”。看起来像一些只显示的东西
  • 不,我认为这是一个完全不同的问题。
  • @GoswinvonBrederlow 回复;weather or not ~0LLU is -1 or not~0LLUunsigned long long 中。它的值不能为 -1。
  • @GoswinvonBrederlow p[-1] 很好,因为没有什么可以说p[0] 是数组的开始。检查边界的编译器部分可能会转换为带符号的数字,只是为了避免有 2 个代码路径 - 任何超过 0x8000000000000000 的内容在我们有生之年制造的任何计算机上都将超出范围,因此它没有实际区别。跨度>
【解决方案2】:

C 语言对数组的边界检查没有任何要求。这是使它快速的部分原因。话虽如此,编译器可以并且确实在某些情况下执行检查。

例如,如果我在 gcc 中使用 -O3 编译并将 return p[~0LLU]; 替换为 return p[10];,我会收到以下警告:

x1.c: In function ‘foo’:
x1.c:6:10: warning: ‘*((void *)&x+60)’ is used uninitialized in this function [-Wuninitialized]
   return p[10];

如果我使用-10 作为索引,我会收到类似的警告:

gcc -g -O3 -Wall -Wextra -Warray-bounds -o x1 x1.c
x1.c: In function ‘foo’:
x1.c:6:10: warning: ‘*((void *)&x+-20)’ is used uninitialized in this function [-Wuninitialized]
   return p[-100];

所以它似乎可以警告数组索引的无效负值。

在您的情况下,似乎 对于这个编译器,值 ~0LLU 被转换为有符号值以用于指针算术并被视为 -1。

请注意,可以通过将其他初始化变量放在 x 周围来欺骗此检查:

int foo() {
  int y[10] = {0};
  int x[10] = {0};
  int z[10] = {0};
  int *p = &x[5];
  printf("&x=%p, &y=%p, &z=%p\n", (void *)x, (void *)y, (void *)z);
  return p[10] + y[0] + z[0];
}

即使p[10] 越界,此代码也不会产生警告。

因此,是否要执行越界检查以及如何执行取决于实现。

【讨论】:

  • 问题的很大一部分是天气,这是一个溢出,或者如果标准要求它评估为 p[-1]。
  • OP:“我认为这段代码应该警告数组访问越界”。 OP 不是在谈论运行时错误,而是在谈论编译时错误。
  • 同意在 OP 的情况下,“~0LLU 被转换为有符号值以用于指针算术并被视为 -1”,但 C 不会强制进行这种转换 - 它允许这样做。在另一个平台上 p[~0LLU] 尝试使用较大的正值进行数组访问 - 对于 x[] 来说太大了。
  • @chux,我认为 C 甚至 允许 这种解释,除非行为未定义,因此任何事情都可能发生。
  • @JohnBollinger 当然允许~0LLU 成为数组的有效索引——总有一天。即使在 2019 年的真正意义上,考虑到并非所有内存都需要物理访问一个元素。 unsigned long long 甚至没有被指定为最宽的可用整数类型。 OP 对~0LLU 的关注应该是UINTMAX_MAX,因为是(u)intmax_t 施加了一些限制,而不是unsigned long long
【解决方案3】:

编辑:完全重写,使用标准引号:

[dcl.array] [ 注意:除了已经为类声明的地方,下标运算符 [] 被解释为E1[E2] 等同于*((E1)+(E2))

[expr.add] 当一个整数类型的表达式被添加到指针或从指针中减去时,结果的类型为 的指针操作数。如果表达式P 指向带有n 元素的数组对象x 的元素x[i], 表达式P + JJ + P(其中J 的值为j)指向(可能是假设的)元素 x[i + j] 如果0 ≤ i + j ≤ n;否则,行为未定义。

因此,p[~0LLU]*(p + ~0LLU) 的解释相同(根据 [dcl.array]),其中括号表达式指向元素 x[5 + ~0LLU] - 如果索引在有效范围内 - (根据 [expr.array])添加])。如果索引不在范围内,则行为未定义。

5 + ~0LLU 是否在有效的索引范围内?给定语言的整数转换规则,如果 5 的类型是大小不大于 unsigned long long 的有符号类型,则显示的表达式似乎是明确定义的,在这种情况下,指向的元素将是 x[4]。但是,标准没有在描述行为的表达式中明确定义 ij 的类型。它应该被解释为纯数学表达式,在这种情况下,结果将是long long unsigned 无法表示的索引,并且肯定大于n,因此是未定义的行为。

鉴于行为未定义的解释,编译器发出警告不会是不正确的。无论如何,编译器不需要警告。

【讨论】:

  • 我说的是“应该”,而不是“必须”。 gcc、clang 和其他编译器确实会给出这样的警告。编辑问题以澄清。
  • 如果5 的等级低于unsigned long long 的类型,则添加5 + ~0LLU 是有意义的,因为它在整数表达式中。然而,这里的5 没有定义为较低级别的类型——它根本没有规定的类型。就指针数学而言,它可以是比unsigned long long 更宽的“类型”,因此5 + ~0LLU 是一个很大的正值。
  • @chux 确实。因此得出行为未定义的结论。
  • 仅仅因为p == x + 5并不一定意味着p + ~0LLU == x + 5 + ~0LLU。每个子表达式都需要在整体表达式之前自行定义行为,所以你在乞求原始问题。
  • 此外,加法运算符是从左到右关联的,但在数学意义上总体上是不关联的,因此x + 5 + ~0LLU 将被评估为(x + 5) + ~0LLU,这与@ 具有相同的不确定性问题987654353@ 可以。 x + (5 + ~0LLU) 的行为不需要等价,在这种情况下,后一个表达式已经定义了行为是无关紧要的。
猜你喜欢
  • 1970-01-01
  • 2010-12-16
  • 2016-08-08
  • 2018-08-02
  • 2010-11-28
  • 1970-01-01
  • 2016-08-27
  • 2011-01-20
  • 2019-07-13
相关资源
最近更新 更多