【发布时间】:2019-06-11 19:19:48
【问题描述】:
我认为这段代码应该警告越界数组访问:
int foo() {
int x[10] = {0};
int *p = &x[5];
return p[~0LLU];
}
我知道标准不需要越界警告,但编译器确实提供了它们。我在问编译器在这里给出这样的警告是否正确。
有什么理由认为该代码格式正确?
【问题讨论】:
-
评论不用于扩展讨论;这个对话是moved to chat。
我认为这段代码应该警告越界数组访问:
int foo() {
int x[10] = {0};
int *p = &x[5];
return p[~0LLU];
}
我知道标准不需要越界警告,但编译器确实提供了它们。我在问编译器在这里给出这样的警告是否正确。
有什么理由认为该代码格式正确?
【问题讨论】:
我认为这段代码应该警告超出范围的数组访问:
当您在非 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 变为指针)
之后,编译器无法判断数据的来源,因此它无法控制边界(即使在您的情况下,偏移量大得离谱/负数/其他)。专用的静态分析工具可能会发现问题。
【讨论】:
-1 是一个错误。或者一些实现定义的东西。 -1 也不能“超过数组末尾”。看起来像一些只显示的东西
~0LLU 在unsigned long long 中。它的值不能为 -1。
p[-1] 很好,因为没有什么可以说p[0] 是数组的开始。检查边界的编译器部分可能会转换为带符号的数字,只是为了避免有 2 个代码路径 - 任何超过 0x8000000000000000 的内容在我们有生之年制造的任何计算机上都将超出范围,因此它没有实际区别。跨度>
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] 越界,此代码也不会产生警告。
因此,是否要执行越界检查以及如何执行取决于实现。
【讨论】:
~0LLU 被转换为有符号值以用于指针算术并被视为 -1”,但 C 不会强制进行这种转换 - 它允许这样做。在另一个平台上 p[~0LLU] 尝试使用较大的正值进行数组访问 - 对于 x[] 来说太大了。
~0LLU 成为数组的有效索引——总有一天。即使在 2019 年的真正意义上,考虑到并非所有内存都需要物理访问一个元素。 unsigned long long 甚至没有被指定为最宽的可用整数类型。 OP 对~0LLU 的关注应该是UINTMAX_MAX,因为是(u)intmax_t 施加了一些限制,而不是unsigned long long。
编辑:完全重写,使用标准引号:
[dcl.array] [ 注意:除了已经为类声明的地方,下标运算符 [] 被解释为
E1[E2]等同于*((E1)+(E2))[expr.add] 当一个整数类型的表达式被添加到指针或从指针中减去时,结果的类型为 的指针操作数。如果表达式
P指向带有n元素的数组对象x的元素x[i], 表达式P + J和J + 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]。但是,标准没有在描述行为的表达式中明确定义 i 和 j 的类型。它应该被解释为纯数学表达式,在这种情况下,结果将是long long unsigned 无法表示的索引,并且肯定大于n,因此是未定义的行为。
鉴于行为未定义的解释,编译器发出警告不会是不正确的。无论如何,编译器不需要警告。
【讨论】:
5 的等级低于unsigned long long 的类型,则添加5 + ~0LLU 是有意义的,因为它在整数表达式中。然而,这里的5 没有定义为较低级别的类型——它根本没有规定的类型。就指针数学而言,它可以是比unsigned long long 更宽的“类型”,因此5 + ~0LLU 是一个很大的正值。
p == x + 5并不一定意味着p + ~0LLU == x + 5 + ~0LLU。每个子表达式都需要在整体表达式之前自行定义行为,所以你在乞求原始问题。
x + 5 + ~0LLU 将被评估为(x + 5) + ~0LLU,这与@ 具有相同的不确定性问题987654353@ 可以。 x + (5 + ~0LLU) 的行为不需要等价,在这种情况下,后一个表达式已经定义了行为是无关紧要的。