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

Eva*_*ran 109 c c89 undefined-behavior

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

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

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

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

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

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

Car*_*rum 51

正如C FAQ所说:

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

和:

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

背后的"严格符合"位的基本原理是在规范中,部分J.2未定义行为,其中包括未定义行为的列表:

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

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

如果指针操作数和结果指向相同的数组对象,或一个过去的数组对象的最后一个元素的元素两者,所述评估也不得产生溢出; 否则,行为未定义.

  • 该对象不是数组对象,因此6.5.6无关紧要.对象是`malloc`分配的内存块.在你喷出bs之前在标准中查找"对象". (4认同)
  • 或许另一种看待这种情况的方法是,语言可以设想限制你如何访问实际的**数组变量**,如J.2所述,但是它无法对由`malloc`分配的对象进行这样的限制,当你只是将返回的`void*`转换为指向[包含]数组的结构的指针时.使用指向`char`(或最好是`unsigned char`)的指针访问已分配对象的任何部分仍然有效. (3认同)
  • 在OP的代码中,`p​​->s`从未用作数组。它被传递给“strcpy”,在这种情况下,它会衰减为普通的“char *”,它恰好指向一个可以在分配的对象内合法解释为“char [100];”的对象。 (2认同)

Jer*_*fin 34

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

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

您可以认为这里违反了"数组对象"(因为您在数组对象的定义范围之外进行了预订),在这种情况下,行为(更多一点)明确地未定义,而不仅仅是未定义礼貌没有完全定义它.

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

  • 我还可以想象一个编译器可能会决定,如果一个数组碰巧大小为 1,那么 `arr[x] = y;` 可能会被重写为 `arr[0] = y;`; 对于大小为 2 的数组,`arr[i] = 4;` 可以重写为 `i ? arr[1] = 4 : arr[0] = 4;` 虽然我从未见过编译器执行这样的优化,但在某些嵌入式系统上它们可能非常高效。在 PIC18x 上,使用 8 位数据类型,第一个语句的代码为 16 个字节,第二个,两个或四个字节,第三个,八个或十二个字节。如果合法的话,这是一个不错的优化。 (2认同)

oua*_*uah 12

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

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

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

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

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

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

  • 找到这个+1,但我仍然认为这是矛盾的.指向同一个对象的两个指针(在这种情况下,给定的字节)是相等的,并且指向它的一个指针(指向由`malloc`获得的整个对象的表示数组的指针)在添加中是有效的,那么怎么能通过另一条路线获得的相同指针在添加中是无效的?即使他们想要声称它是UB,这也是毫无意义的,因为在计算上没有办法让实现区分明确定义的用法和所谓的未定义的用法. (2认同)

Chu*_*uck 11

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


R..*_*R.. 7

它不是未定义的行为,无论官方或其他人说什么,因为它是由标准定义的.p->s,除了用作左值时,计算与指针相同的指针(char *)p + offsetof(struct T, s).特别是,这是charmalloc'd对象内部的有效指针,并且紧跟其后的连续地址有100个(或更多,取决于对齐注意事项),这些地址也作为char分配对象内的对象有效.该指针是通过使用得到的事实->,而不是明确地将偏移到返回的指针malloc,转换为char *,是无关紧要的.

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

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

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

编辑2:由于标准的另一部分,与填充字节的重叠绝对不是问题.C要求如果两个结构在其元素的初始子序列中一致,则可以通过指向任一类型的指针来访问公共初始元素.因此,如果相同的结构struct T,但具有较大的阵列天线的最终被宣布,该元件s[0]将不得不与元件重合s[0]struct T,并且这些附加的元件的存在不能影响或通过访问较大结构的共同要素的影响使用指针struct T.

  • +1如果`malloc`没有分配一系列可以用指针算法访问的内存,它会有什么用处?如果`p-> s [1]`被标准定义*作为指针算术的语法糖,那么这个答案只是重申`malloc`是有用的.剩下要讨论的是什么?:) (6认同)
  • 你是对的,指针算术的本质是无关紧要的,但你对于超出声明的数组大小的访问是错误的.见[N1494](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1494.pdf)(最新的公共C1x草案)第6.5.6节第8段 - 你甚至不被允许执行*addition*,它使一个指针超过一个元素超过数组的声明大小,即使它只是一个元素,你也不能取消引用它. (4认同)
  • 你可以认为它的定义很好,但是这并没有改变事实并非如此.关于超出数组边界的访问的标准是非常明确的,并且该数组的边界是"1".这正是那么简单. (3认同)
  • @R ..,我认为,你的假设是两个指针比较相等必须表现相同是错误的.考虑`int m [1]; int n [1]; if(m + 1 == n)m [1] = 0;`假设输入`if`分支.按照6.5.6 p8(最后一句),这是UB(并不保证初始化`n`),正如我读到的那样.相关:6.5.9 p6,脚注109.(参考文献C11 n1570.)[...] (3认同)
  • @Zack:如果对象是数组,那是对的。如果对象是由`malloc` 分配的对象作为数组访问,或者它是一个较大的结构,通过指向较小结构的指针访问其元素是较大结构的元素的初始子集,则不正确,在其他情况下。 (2认同)
  • 我还没有看到一个有效的论点,即`p-&gt;s` 不是指向`char` 数组元素的指针,该元素是分配对象的*表示数组*。如果有这样的争论,那就会改变事情。关于构建一个的任何想法? (2认同)

AnT*_*AnT 7

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

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

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

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

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

  • 问题在于委员会的一些成员迫切地希望**这个"黑客"成为UB,因为他们设想了一些C实现可以强制指针界限的仙境.然而,无论好坏,这样做会与标准的其他部分发生冲突 - 例如比较指针的相等性(如果边界在指针本身中编码)或者要求任何对象可以通过虚构覆盖进行访问. unsigned char [sizeof object]`array.我坚持认为灵活的阵列成员"破解"前C99具有明确定义的行为. (6认同)

小智 5

标准非常明确,您无法访问数组末尾之外的内容。(并且通过指针没有帮助,因为您甚至不允许在数组结束后将指针增加到超过一)。

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