“struct hack”在技术上是不确定的行为吗?

时间:2010-09-14 17:10:39

标签: c undefined-behavior c89

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

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

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

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

所以问题是:这种技术技术上是未定义的行为吗?。我希望它是,但很好奇标准对此有何看法。

PS:我知道C99的方法,我希望答案能够专门针对上面列出的技巧版本。

8 个答案:

答案 0 :(得分:51)

正如C FAQ所说:

  

目前尚不清楚它是合法的还是便携式的,但它很受欢迎。

  

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

“严格符合”位背后的基本原理在规范中, J.2未定义行为,其中包含未定义行为列表:

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

6.5.6添加运算符一节的第8段另外提到超出定义的数组边界的访问是未定义的:

  

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

答案 1 :(得分:34)

我认为技术上它是未定义的行为。该标准(可论证)并未直接解决,因此它属于“或遗漏任何明确的行为定义”。条款(C99第4/2条,C89第3.16 / 2条)表示它是未定义的行为。

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

你可以说这里违反了“数组对象”(因为你是在数组对象的定义范围之外进行了预订),在这种情况下,行为是(稍微多一点)显式未定义,而是只是未定义的礼貌没有任何明确的定义。

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

答案 2 :(得分:12)

是的,这是未定义的行为。

C语言缺陷报告#051给出了这个问题的明确答案:

  

这个成语虽然很常见,但并不严格遵守

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

在C99理由文件中,C委员会补充说:

  

这种结构的有效性一直是值得怀疑的。在对一个缺陷的回应中   报告中,委员会认为它是未定义的行为因为数组p->项目   只包含一个项目,无论空间是否存在。

答案 3 :(得分:11)

这种特殊的做法并没有在任何C标准中明确定义,但C99确实包含了“struct hack”作为语言的一部分。在C99中,结构的最后一个成员可以是“灵活的数组成员”,声明为char foo[](用你想要的任何类型代替char)。

答案 4 :(得分:7)

这不是未定义的行为,无论是谁,官方或其他,都说,因为它是由标准定义的。 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)可能是填充字节struct,如果你作为一个整体执行对结构的赋值可能会被破坏,但如果你只是访问单个成员则不会,并且其余的元素是分配对象中的额外空间,你可以随意使用,只要你喜欢,当你服从对齐要求时(1没有对齐要求)。

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

编辑:实际上,在上面获得正确大小的技巧中,你可能还需要在数组之前放置一个包含每个简单类型的联合,以确保数组本身以最大对齐开始而不是在其他元素的填充中间。再说一次,我不相信任何这些是必要的,但我会为那里最偏执的语言律师提供它。

编辑2:由于标准的另一部分,与填充字节的重叠绝对不是问题。 C要求如果两个结构在其元素的初始子序列中一致,则可以通过指向任一类型的指针来访问公共初始元素。因此,如果声明了与s[0]相同但最终数组较大的结构,则元素s[0]必须与struct T中的元素struct T重合,并且使用指向{{1}}的指针访问较大结构的公共元素时,这些附加元素的存在不会影响或受其影响。

答案 5 :(得分:7)

是的,这是技术上未定义的行为。

请注意,至少有三种方法可以实现“struct hack”:

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

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

(3)例如,声明像“非常大”的数组,例如10000。我们的想法是,声明的大小应该大于实际操作中可能需要的大小。关于阵列访问范围,该方法没有UB。然而,在实践中,当然,我们总是会分配更少的内存(只有真正需要的内存)。我不确定这是否合法,即我想知道为对象分配的内存比声明的对象大小更合法(假设我们从不访问“未分配”的成员)。

答案 6 :(得分:3)

标准很清楚,你不能访问数组末尾旁边的东西。 (并且通过指针也无济于事,因为在数组结束后你甚至不允许在指针之前增加指针)。

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

答案 7 :(得分:1)

如果编译器接受类似

的内容
typedef struct {
  int len;
  char dat[];
};

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

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

然后访问somestruct-&gt; dat [x];我不认为编译器有义务使用地址计算代码,它可以处理大的x值。我想如果一个人想要真正安全,那么正确的范例将更像是:

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

然后执行malloc(sizeof(MYSTRUCT)-LARGEST_DAT_SIZE + desired_array_length)字节(请记住,如果desired_array_length大于LARGEST_DAT_SIZE,结果可能未定义)。

顺便说一句,我认为禁止零长度数组的决定是不幸的(一些较旧的方言,如Turbo C支持它),因为零长度数组可以被视为编译器必须生成可以工作的代码的标志指数较大。