是“结构黑客”技术上未定义的行为?

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

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的方法,我希望答案专门针对上面列出的技巧的版本。

正如C FAQ所说:

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

和:

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

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

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

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

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

我相信在技术上它是未定义的行为。 这个标准(可以说)并没有直接解决这个问题,所以这个标准属于“或者没有任何明确的行为定义”。 (C99的§4/ 2,C89的§3.16/ 2),说它是未定义的行为。

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

你可以争辩说,“数组对象”在这里被违反了(因为你在数组对象的定义范围之外),在这种情况下,行为是(明显更不明显),而不是仅仅是undefined礼貌没有任何相当的定义。

从理论上讲,我可以想象一个编译器会执行数组边界检查,例如,如果您尝试使用超出范围的下标,则会中止程序。 事实上,我不知道这样的东西是存在的,而且由于这种代码风格的普及,即使编译器在某些情况下试图强制下标,也很难想象有人会忍受这样做这个情况。

这样做的具体方式并没有在C标准中明确定义,但是C99确实包含了“struct hack”作为语言的一部分。 在C99中,结构的最后一个成员可能是一个“灵活的数组成员”,声明为char foo[] (无论你想要什么类型的char )。

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

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

成语虽然常见,但并不完全符合

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

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

这个构造的有效性一直是有问题的。 针对一个缺陷报告,委员会认为这是未定义的行为,因为数组p->项只包含一个项目,而不管该空间是否存在。

不管什么人, 官方或其他方式这都是不明确的行为 ,因为它是由标准定义的。 除了作为左值使用时,除了(char *)p + offsetof(struct T, s)之外的其他指针。 特别的,这是malloc对象内的一个有效的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相同的struct T但声明了更大的最终数组,则元素s[0]必须与struct T的元素s[0]重合,并且这些附加元素的存在不能影响或受到使用指向struct T的指针访问较大结构的公共元素的影响。

是的,这在技术上是不确定的行为。

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

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

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

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

标准很清楚,你不能访问数组末尾的东西。 (并且通过指针进行操作并没有帮助,因为在数组结束之后,您甚至不允许增加指针)。

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

如果编译器接受类似的东西

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

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

 typedef struct {
  无论如何
   char dat [1];
 } MY_STRUCT; 

然后再访问somestruct-> dat [x]; 我不认为编译器有任何义务使用地址计算代码,这将与大的值x的工作。 我想如果一个人想要真正安全,那么适当的范式更像是:

 #define LARGEST_DAT_SIZE 0xF000
 typedef struct {
  无论如何
   char dat [LARGEST_DAT_SIZE];
 } MY_STRUCT; 

然后做一个malloc(sizeof(MYSTRUCT)-LARGEST_DAT_SIZE + desired_array_length)字节(注意,如果desired_array_length大于LARGEST_DAT_SIZE,结果可能是未定义的)。

顺便说一句,我认为禁止零长度数组的决定是一个不幸的(一些像Turbo C这样的老式方言支持它),因为零长度数组可以被看作是编译器必须生成代码的标志, 。