“ struct hack”在技术上有未定义行为吗?

我要问的是众所周知的“ struct 的最后一个成员具有可变长度”的技巧。大概是这样的:

struct T {
int len;
char s[1];
};


struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

由于 struct 在内存中的布局方式,我们可以将 struct 覆盖在一个大于必要的块上,并将最后一个成员视为大于指定的 1 char

所以问题是: 这个技术在技术上有未定义行为吗?。我希望是这样,但是我很好奇这个标准是怎么说的。

PS: 我知道 C99的方法,我想要的答案坚持具体的版本的把戏,如上所列。

11113 次浏览

正如 常见问题所言:

It's not clear if it's legal or portable, but it is rather popular.

以及:

... 官方解释认为它不完全符合 C 标准,尽管它似乎在所有已知的实施中都有效。(仔细检查数组边界的编译器可能会发出警告。)

“严格一致性”这一概念背后的基本原理在规范的 J2未定义行为章节中,其中包括了一系列未定义行为:

  • 数组下标超出了范围,即使对象显然可以通过给定的下标访问(如在给定声明 int a[4][5]的左值表达式 a[1][7]中)(6.5.6)。

6.5.6加法运算符节第8段还提到,超出定义的数组界限的访问是未定义的:

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

我相信严格来说这是未定义行为。这个标准(可以说)并没有直接解决这个问题,所以它属于“或者由于遗漏了任何明确的行为定义”的范畴条款(4/2 of C99,3.16/2 of C89)说这是未定义行为。

上面的“可以说”取决于数组下标运算符的定义。具体来说,它说: “后缀表达式后跟方括号[]中的表达式是数组对象的下标指定。”(C89,6.3.2.1/2).

You can argue that the "of an array object" is being violated here (since you're subscripting outside the defined range of the array object), in which case the behavior is (a tiny bit more) explicitly undefined, instead of just undefined courtesy of nothing quite defining it.

理论上,我可以想象一个编译器进行数组边界检查,并且(例如)当/如果您试图使用一个超出范围的下标时会中止程序。事实上,我不知道有这样的东西存在,而且考虑到这种代码风格的流行,即使编译器试图在某些情况下强制执行下标,很难想象有人会忍受它在这种情况下这样做。

不管别人怎么说,它都不是强未定义行为,因为它是由标准定义的。除非使用左值,否则 p->s的计算结果为与 (char *)p + offsetof(struct T, s)相同的指针。特别是,这是 malloc’d 对象内部的一个有效的 char指针,紧随其后的有100个(或更多,取决于对齐考虑)连续地址,这些地址也作为分配对象内部的 char对象有效。指针是通过使用 ->而不是显式地将偏移量添加到 malloc返回的指针(强制转换为 char *)得到的,这一事实是无关紧要的。

从技术上讲,p->s[0]是结构内部 char数组的单个元素,接下来的几个元素(例如 p->s[1]p->s[3])可能是结构内部的填充字节,如果你对整个结构执行赋值操作,这些元素可能会被破坏,但如果你仅仅访问单个成员,其余的元素是分配对象中的额外空间,只要你遵守对齐要求(而且 char没有对齐要求) ,你可以自由地使用它们。

如果您担心在结构中与填充字节重叠的可能性可能以某种方式引起鼻腔恶魔,您可以通过将 [1]中的 1替换为一个值来避免这种情况,该值确保在结构的末尾没有填充。实现这一点的一个简单但是浪费的方法是创建一个只有末尾没有数组的成员相同的结构,并对数组使用 s[sizeof struct that_other_struct];。然后,将 p->s[i]明确定义为 i<sizeof struct that_other_struct结构中的数组元素,以及位于 i>=sizeof struct that_other_struct结构结束后的地址的 char 对象。

编辑: 实际上,在以上获得正确大小的技巧中,您可能还需要在数组前放置一个包含所有简单类型的联合,以确保数组本身以最大对齐开始,而不是在其他元素的填充中间。再说一遍,我认为这些都没有必要,但是我要把它提供给最偏执的语言律师。

编辑2: 由于标准的另一部分,与填充字节的重叠肯定不是问题。C 语言要求,如果两个结构在它们的元素的初始子序列中一致,则可以通过指向任一类型的指针访问公共初始元素。因此,如果声明了一个与 struct T相同但具有更大的最终数组的结构,则元素 s[0]必须与 struct T中的元素 s[0]重合,并且这些额外元素的存在不会影响或者通过使用指向 struct T的指针访问更大结构的公共元素而受到影响。

是的严格来说是未定义行为。

Note, that there are at least three ways to implement the "struct hack":

(1)使用大小为0 (遗留代码中最“流行”的方法)声明尾随数组。这显然是 UB,因为零大小的数组声明在 C 中总是非法的,即使它进行了编译,该语言也不能保证任何违反约束的代码的行为。

(2)使用最小合法大小声明数组-1 (您的情况)。在这种情况下,任何尝试获取指向 p->s[0]的指针并将其用于超越 p->s[1]的指针算法的未定义行为都是没有意义的。例如,允许调试实现生成具有嵌入范围信息的特殊指针,这将在每次尝试创建 p->s[1]之外的指针时陷入陷阱。

(3) Declaring the array with "very large" size like 10000, for example. The idea is that the declared size is supposed to be larger than anything you might need in actual practice. This method is free of UB with regard to array access range. However, in practice, of course, we will always allocate smaller amount of memory (only as much as really needed). I'm not sure about the legality of this, i.e. I wonder how legal it is to allocate less memory for the object than the declared size of the object (assuming we never access the "non-allocated" members).

这种特殊的方法在任何 C 标准中都没有明确定义,但是 C99确实将“ struct hack”作为语言的一部分。在 C99中,struct 的最后一个成员可能是一个“灵活的数组成员”,声明为 char foo[](使用您希望的任何类型取代 char)。

如果编译器接受

typedef struct {
int len;
char dat[];
};

我认为很明显,它必须准备好接受超出其长度的“ dat”下标。另一方面,如果有人编写这样的代码:

typedef struct {
int whatever;
char dat[1];
} MY_STRUCT;

然后再访问一些 struct-> dat [ x ] ; 我不认为编译器有任何义务使用地址计算代码,这些代码可以处理大的 x 值。我认为如果一个人想要真正安全,适当的范例应该是这样的:

#define LARGEST_DAT_SIZE 0xF000
typedef struct {
int whatever;
char dat[LARGEST_DAT_SIZE];
} MY_STRUCT;

and then do a malloc of (sizeof(MYSTRUCT)-LARGEST_DAT_SIZE + desired_array_length) bytes (bearing in mind that if desired_array_length is larger than LARGEST_DAT_SIZE, the results may be undefined).

顺便说一句,我认为禁止零长度数组的决定是一个不幸的决定(一些较老的方言,如 Turbo C 支持它) ,因为零长度数组可以被视为一个迹象,表明编译器必须生成代码,将工作在更大的索引。

标准非常明确,您不能访问数组末尾以外的内容。(通过指针也没有帮助,因为你甚至不允许在数组结束后递增指针)。

还有“在实践中工作”。我见过 gcc/g + + 优化器使用这部分标准,因此在遇到这个无效的 C 时生成了错误的代码。