“结构骇客”在技术上是不确定的行为吗?


111

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

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的方法,我希望答案特别遵循上面列出的技巧。


33
这似乎是一个相当明确的,合理的,首先回答的问题。没有看到密切投票的理由。
cHao 2010年

2
如果您引入了不支持struct hack的“ ansi c”编译器,那么我认识的大多数c程序员都不会接受您的编译器“工作正常”。尽管他们会接受对该标准的严格阅读。委员会只是错过了一个。
dmckee ---前主持人小猫,2010年

4
@james hack的工作原理是为声明的数组分配了足够大的对象,尽管已声明了最小数组。因此,您要在结构的严格定义之外访问分配的内存。超越分配而写无疑是一个错误,但这不同于在分配中但在“结构”之外的情况。
dmckee ---前主持人小猫

2
@James:这里的malloc过大至关重要。它确保存在内存-具有合法地址并由该结构“拥有”的内存(即,任何其他实体使用它都是违法的)-通过该结构的名义末端。请注意,这意味着您不能对自动变量使用struct hack:必须动态分配它们。
dmckee ---前主持人小猫,2010年

5
@detly:分配/解除分配一件事比分配/解除分配两件事更简单,尤其是因为后者有两种失败的方式需要处理。这对我来说比边际成本/速度节省更为重要。
jamesdlin

Answers:


52

正如C常见问题解答所说:

尚不清楚它是合法的还是可移植的,但它很受欢迎。

和:

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

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

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

6.5.6节的第8段又提到了未定义数组界限之外的访问是未定义的:

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


1
在OP的代码中,p->s从不用作数组。它传递给strcpy,在这种情况下,它衰减为一个Plain char *,它恰好指向一个对象,该对象可以合法地解释为char [100];所分配的对象内部。
R .. GitHub停止帮助ICE,2010年

3
也许另一种看待方式是该语言可能会像J.2中所描述的那样限制您访问实际数组变量的方式,但是malloc当您仅转换了返回值时,就无法对分配的对象进行此类限制。void *指向[包含数组的结构的指针]。使用指向char(或最好是unsigned char)的指针访问分配的对象的任何部分仍然有效。
R .. GitHub停止帮助ICE,2010年

@R。-我可以看到J2可能不会涵盖此内容,但是6.5.6也不涵盖吗?
不错,2010年

1
当然可以!类型和大小信息可以嵌入每个指针中,然后可以使任何错误的指针算法陷入陷阱,例如参见CCured。从更哲学的角度来看,没有任何可能的实现方式可以抓住您并不重要,它仍然是未定义的行为(iirc,存在未定义行为的情况,这需要使用“预言”来结清“停顿问题”,这就是为什么)它们是未定义的)。
zwol

4
该对象不是数组对象,因此6.5.6是无关紧要的。对象是分配的内存块malloc。喷出bs之前在标准中查找“对象”。
R .. GitHub停止帮助ICE,2010年

34

我认为从技术上讲这是未定义的行为。该标准(可以说)没有直接解决,因此属于“或遗漏了任何明确的行为定义”。条款(C99的第4/2条,C89的第3.16 / 2条)指出这是未定义的行为。

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

您可以争辩说这里违反了“数组对象”(因为您正在数组对象的定义范围之外下标),在这种情况下,行为(稍微多了)是显式未定义的,而不是未定义的没有任何东西可以完全定义它。

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


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

如果该标准将数组访问范围之外的数组访问定义为未定义的行为,那么struct hack也是如此。但是,如果该标准将数组访问定义为指针算术(a[2] == a + 2)的语法糖,则不会。如果我是正确的,那么所有C标准都将数组访问定义为指针算术。
yyny

13

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

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

这个习语虽然很常见,但并没有严格遵循

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

C委员会在C99基本原理文档中添加:

这种结构的有效性一直令人怀疑。在对一份缺陷报告的答复中,委员会认为这是未定义的行为,因为数组p-> item仅包含一项,而与是否存在空格无关。


2
+1可以找到这个,但我仍然认为这是矛盾的。另外,指向同一个对象的两个指针(在这种情况下,给定的字节)相等,并且指向它的一个指针(由所获得的整个对象的表示数组的指针malloc)在加法中是有效的,因此相同的指针怎么能,是通过另一条路线获得的,除此无效之外?即使他们想要声明它是UB,也没有任何意义,因为在计算上,实现没有办法区分定义良好的用法和假定的未定义用法。
R .. GitHub停止帮助ICE 2012年

C编译器开始禁止声明零长度数组,这是非常糟糕的。如果不是这个禁止,那么许多编译器就不必按照“应该”的方式进行任何特殊处理,但是仍然可以对单元素数组进行特殊情况的编码(例如,如果*foo包含一个单元素数组boz,表达式foo->boz[biz()*391]=9;可以简化为biz(),foo->boz[0]=9;)。不幸的是,编译器拒绝使用零元素数组,这意味着许多代码改为使用单元素数组,并且这种优化会破坏它们。
2012年

11

任何C标准都没有明确定义这种特定的实现方式,但是C99确实包括了“ struct hack”作为该语言的一部分。在C99中,结构的最后一个成员可能是“柔性数组成员”,声明为char foo[](用您想要的任何类型代替char)。


要学究一点,这不是struct hack。结构黑客使用固定大小的数组,而不是灵活的数组成员。被问及的结构黑客是UB。灵活的数组成员似乎只是在试图安抚在此线程中看到的抱怨这一事实的那种人。
underscore_d

7

不管官方或其他人怎么说,这都不是未定义的行为,因为它是由标准定义的。p->s,除了用作左值时,其结果等于的指针(char *)p + offsetof(struct T, s)。特别是,这是charmalloc对象内部的有效指针,紧随其后的有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]在for中明确定义为struct中数组的元素,并在struct 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


4
没错,指针算术的性质无关紧要,但是超出数组声明大小的访问是错误的。请参阅N1494(最新的公共C1x草案)第6.5.6节第8段-您甚至不可以进行添加操作,以使指针超出数组声明的大小超过一个元素,并且即使取消引用也不能取消引用这只是过去的一个要素。
zwol

1
@Zack:如果对象是数组,则为true。如果对象是malloc作为数组访问的分配对象,或者是通过指向较小结构的指针访问的较大结构,则该结构的元素是较大结构的元素的初始子集,这是不正确的案件。
R .. GitHub停止帮助ICE 2010年

6
+1如果malloc未分配可使用指针算术访问的内存范围,它将有什么用?并且如果该标准p->s[1]其定义为指针算术的语法糖,则此答案仅会重新声明malloc是有用的。还有什么要讨论?:)
Daniel Earwicker 2010年

3
您可以争辩说它定义得很好,但是并不能改变事实并非如此。该标准对访问超出数组界限非常清楚,并且此数组的界限为1。就这么简单。
Lightness Races in Orbit

3
@R ..,我认为,您认为两个相等的指针必须表现相同的假设是错误的。考虑int m[1]; int n[1]; if(m+1 == n) m[1] = 0;假设if已进入分支。n在我阅读的时候,这是UB(并且不能保证初始化),按照6.5.6 p8(最后一句话)。相关:6.5.9 p6,带脚注109。(参考C11 n1570。)
mafso 2014年

7

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

请注意,至少有三种方法可以实现“结构修改”:

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

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

(3)例如,声明大小 10000 ”的“非常大”的数组。这个想法是声明的大小应该大于实际实践中可能需要的任何大小。就阵列访问范围而言,此方法没有UB。但是,在实践中,当然,我们总是会分配较少的内存(仅实际需要的数量)。我不确定这样做的合法性,即我想知道为对象分配比声明的对象大小少的内存是否合法(假设我们从不访问“未分配”成员)。


1
在(2)中,s[1]不是未定义的行为。与相同*(s+1),与相同*((char *)p + offsetof(struct T, s) + 1),后者是指向char已分配对象中a的有效指针。
R .. GitHub停止帮助ICE,2010年

另一方面,我几乎可以确定(3)是未定义的行为。每当执行依赖于位于该地址的此类结构的任何操作时,编译器都可以自由生成从该结构的任何部分读取的机器代码。它可能没有用,或者可能是严格的分配检查的安全功能,但是没有理由没有实现可以做到。
R .. GitHub停止帮助ICE,2010年

R:如果一个数组被声明为具有大小(不仅仅是的foo[]语法糖*foo),那么任何超出其声明大小和分配大小中较小者的访问都是UB,而不管指针算术如何完成。
zwol

1
@Zack,您在几件事上错了。foo[]在结构中不是语法糖*foo; 它是C99灵活阵列成员。其余的,请参阅我的答案和对其他答案的评论。
R .. GitHub停止帮助ICE 2010年

6
问题在于委员会的某些成员迫切希望将此“ hack”成为UB,因为他们设想到一些仙境,C实现可以强制执行指针界限。然而,不管好坏,这样做都会与标准的其他部分发生冲突,例如比较指针是否相等的能力(如果边界是在指针本身中编码的)或要求任何对象都可以通过虚构的覆盖unsigned char [sizeof object]数组访问的事情。。我坚持认为C99之前的灵活阵列成员“ hack”具有明确定义的行为。
R .. GitHub停止帮助ICE 2010年

3

该标准非常清楚,您无法访问数组末尾之外的内容。(并且通过指针进行操作无济于事,因为您甚至不允许在数组结束后将指针增加到一个以上)。

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


能给我举个例子吗?
塔尔(Tal)

1

如果编译器接受类似

typedef struct {
  英伦
  char dat [];
};

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

typedef struct {
  诠释什么;
  char dat [1];
} MY_STRUCT;

然后稍后访问somestruct-> dat [x]; 我认为编译器没有义务使用适用于x较大值的地址计算代码。我认为,如果您想真正地安全,那么正确的范例将更像是:

#定义LARGEST_DAT_SIZE 0xF000
typedef struct {
  诠释什么;
  char dat [LARGEST_DAT_SIZE];
} MY_STRUCT;

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.