添加到固定长度数组指针时行为是否未定义,从而导致溢出?

And*_*eas 9 c

看一下以下代码,取自旧版本的 ffmpeg:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

struct foo
{
    int16_t (*ac_val_base)[16];
    int16_t (*ac_val[3])[16];
};

int main(int argc, char *argv[])
{
    struct foo bar;
    int16_t *ac_val, *ac_val1;

    bar.ac_val_base = malloc(4639 * 16 * sizeof(int16_t));
    bar.ac_val[0] = bar.ac_val_base + 66;

    ac_val = bar.ac_val[0][0] + 3780 * 16;
    ac_val1 = ac_val;
    
    printf("Result: %d\n", (int) (((char *) ac_val1) - ((char *) bar.ac_val[0][0])));

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

当使用 gcc 或 Visual C 等已建立的编译器对其进行编译时,结果是 120960。这对我来说很有意义,因为我要添加3780 * 16int16_t数组指针,因此我希望生成的指针比源指针高 120960 字节。

然而,当使用 vbcc 编译代码时,结果是 -8000,因为编译器执行了一些优化。vbcc 编译器的作者确信 C99 标准的 6.5.6/8 涵盖了优化,该标准表示在这种情况下行为未定义,引用:

如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则求值不会产生溢出;否则,行为是未定义的。

那么上面的代码真的依赖于未定义的行为吗?我有点怀疑,因为代码适用于除 vbcc 之外的所有编译器。

Kaz*_*Kaz 2

简短的答案是表达式的类型bar.ac_val[0][0]是“array of 16 int16_t”。尽管此数组对象位于较大的 malloc 块内,并且表达式的计算结果为块内的指针,但该指针源自数组

从数组维度为 N 的数组表达式获得的指针最多可以移位 N(超出数组末尾一个字节),同时保持在定义的行为范围内。(如果一直移位到 N,则不得取消对指针的引用。)

一个更简单的例子是这样的:

struct obj {
  int arr[32];
  int other_member;
};
Run Code Online (Sandbox Code Playgroud)

假设您有一个malloc指向 this 的 -ed 指针,但使用ptr->arr[32]来访问other_member, this 没有明确定义,即使所有内容都在malloc-ed 对象中。

编译器可以执行的一种可能的优化是使用某种仅适用于该大小的数组的寻址模式。假设ptr->arr[i]转换为某个指令,该指令具有一个五位字段来编码从 0 到 31 的缩放位移值。编译器可以自由地忽略位移 [32] 无法适合该指令,并将其截断为最低的 5 位位,为零,有效地将含义更改为ptr->arr[0]

或者,规则可以启用有用的诊断工具。编译器可能会在编译时警告您存在数组溢出,并且由于这是未定义的行为,因此可能会导致转换失败,同时保持一致。可以使用一些工具来编译代码,以便您在运行时进行详细的数组边界检查(而不仅仅是检查 -edmalloc块的溢出)。访问超出数组末尾可能会发生意外,从而导致难以发现的错误,特别是在访问未超出分配范围的情况下。