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

omqzjyyz  于 2023-01-29  发布在  其他
关注(0)|答案(2)|浏览(90)

看一下下面的代码,这些代码取自较旧版本的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;
}

当使用成熟的编译器(如gcc或Visual C)编译此函数时,结果为120960。这对我来说是有意义的,因为我将3780 * 16添加到int16_t数组指针,所以我希望结果指针比源指针高120960个字节。
然而,当使用vbcc编译代码时,结果是-8000,因为编译器执行了一些优化。vbcc编译器的作者确信C99标准的6.5.6/8涵盖了优化,该标准规定在这种情况下行为是未定义的,引用:
如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象最后一个元素之后的元素,则计算不应产生溢出;否则,该行为是未定义的。
那么上面的代码真的依赖于未定义的行为吗?我有点怀疑,因为这些代码可以在除了vbcc之外的所有编译器上工作。

kyxcudwk

kyxcudwk1#

简短的答案是表达式bar.ac_val[0][0]的类型是“16 int16_t的数组“。虽然这个数组对象位于一个更大的malloc块中,并且表达式的计算结果是块中的指针,但指针具有来自数组的 * 出处 *。
从数组表达式中获取的指针,其中数组维数为N,最多可以被置换N(超出数组末尾一个字节),同时保持在定义的行为范围内。(如果一直置换到N,则不能取消引用该指针。)
一个简单的例子是这样的:

struct obj {
  int arr[32];
  int other_member;
};

假设你有一个指向它的malloc-艾德指针,但是使用ptr->arr[32]来访问other_member,即使所有的东西都在malloc-ed对象中,这也不是很好定义的。
编译器可以执行的一个可能的优化是使用某种只适用于该数组大小的寻址模式。假设ptr->arr[i]转换为某个指令,该指令具有5位字段,用于编码从0到31的缩放位移值。编译器可以忽略位移[32]无法装入该指令,并将其截断为最低5位,即0。有效地将含义改变为X1 M7 N1 X。
或者,规则可以启用有用的诊断工具,编译器可以在编译时警告你有一个数组溢出,并且因为它是未定义的行为,它可以使转换失败。可以使用一些工具来编译代码,使您能够在运行时获得详细的数组边界检查(不仅仅是检查malloc-艾德块的溢出)。访问超过数组末尾可能是一个意外,导致难以发现的bug,特别是当访问没有超过分配时。

pdkcd3nj

pdkcd3nj2#

ac_val = bar.ac_val[0][0] + 3780 * 16;

bar.ac_val[0][0]int16_t[16],因此向其添加范围[0,16)以外的任何值都会导致未定义的行为。
未定义行为的原因是分段存储器模型(与现代的平面/线性存储器模型相反),当指针值由段描述符和段内的字节偏移量组成时,C语言仍与之兼容。在这种模型中,不同的数组可以驻留在不同的段中。段描述符单元不是字节偏移量,因此减去段描述符值不会产生字节距离。指向驻留在不同段中的不同数组的指针之间的差异最终会减去段描述符,从而导致未定义的行为。
您的特定数组是使用malloc分配的。它不可能跨越多个内存段。只要您的指针(包括表达式临时变量)不指向此堆分配数组之外,这些指针就是有效的且定义良好的。
数组元素类型int16_t[16]和索引超出了它的边界,这导致了未定义的行为。这种数组元素类型本质上是C编译器的一个转移注意力的对象。
如果将数组元素类型切换为普通的int16_t,并将2d数组索引转换为1d,例如将[row][column]转换为[row * n_columns + column],则此问题不再存在。
您还可以使用整数运算来避开指针运算所产生的未定义行为:

uintptr_t ac_val = (uintptr_t)bar.ac_val[0][0] + 3780 * 16 * sizeof(int16_t);
printf("Result: %zu\n", (size_t) ((ac_val - ((uintptr_t) bar.ac_val[0][0])));

这取决于以下事实:

  • 将指针转换为uintptr_t并返回是定义良好的。
  • 无符号整数加法和减法定义明确。

相关问题