重叠、指针转换和gcc3.3

http://mail-index.netbsd.org/tech-kern/2003/08/11/0001.html

Subject: Aliasing, pointer casts and gcc 3.3.

To: None <tech-kern@netbsd.org>

From: Krister Walfridsson <cato@df.lth.se>

List: tech-kern

Date: 08/11/2003 23:16:48



我见过一些用来“修复”gcc 3.3 别名警告的提交,这些并没有给我貌似温存的感觉,同时我总是在不同的邮件列表看到很多关于别名(一般来说是ISO C)概念的混淆,所以我尝试解释一些已经/将要困扰我们的一些问题。



如果本文太浅显,请包涵。

重叠(ALIASING)

什么是重叠?



从硬件的角度来看指针的话,指针可以指向任何内存地址,所以通过指针写入可以更改程序中的任何变量:

int i = 23;

*f = 5;

/* 这时候我们不能确定i是什么值。 */

我们无法得知i的值,因为指针&i和f可能指向同一个地址(这就是当ISO C说&i与f可能重叠时所表达的意思)。



这样就使很多类型的优化无法进行,同时它也确实造成了实实在在的区别,因为现实情况中多数指针是不会指向同一个位置的。ISO C通过(大致)说“不同类型的指针不能指向同一个地址”来(针对编译器)改进了这个情况。

int

foo(float *f) {

        int i = 23;

        *f = 5.0;

        /* float* 不能和 int* 指向同一个地址。 */

        return i * 2;

}

这样,编译起可以将其优化为:

int

foo(float *f) {

        *f = 5.0;

        return 46;

}

The ISO标准并不完全阻止指针指向同一个地址——它指定了当你提领指向一个不同(不兼容)类型的对象的指针时,结果是不确定的。所以以下例子也是可行的:

int i = 23, *tmp;

tmp = (int*)f;

*tmp = 5;

/* 我们无法确定i现在的值。 */

不过注意,我们实际上在内存中按照“int”写入而非按照“float”。



然而以上规则有一个非常重要的例外: char* 可以重叠任何类型(如果ISO阻止这点,那么太多的代码就会坏掉……)



很多情况下你还是想以不同的类型来访问同一个地址:

float *f = 2.718;

printf(“The memory word has value 0x%08x\n”, *((int*)f));

在ISO C中,你不能这么做,但是gcc在这里会进行一个扩展,认为内存中存在一个有多个类型的联合体,所以以下代码可以在gcc中运行(但不能保证在其他编译器中也可以运行!)

union {

    int i;

    float f;

} u;

u.f = 2.718;

printf(“The memory word has value 0x%08x\n”, u.i);

有个副作用是当你使用很多联合体构造时,gcc可能会损失很多优化。


标准是如何说的[*]

第6.5条(表达式)陈述了重叠规则:



 7 一个对象只允许通过有以下类型之一的左值表达式来访问对象存储的值:{脚注73}



      一种与该对象的有效类型相兼容的类型,



     一种与该对象的有效类型相兼容的类型的正规版本,



     一种与该对象的有效类型相对应的有符号或无符号类型,



     一种与该对象的有效类型相兼容的类型的正规版本相对应的有符号或无符号类型,



     一种聚合或联合类型包含了上述类型之一成员(包括递归的子聚合或被包含的联合体),或者



     一种字符类型。

     

 {脚注73} 该列表的意图是指定哪些情况下,对象可能或可能不被重叠。



 7 An object shall have its stored value accessed only by an lvalue

   expression that has one of the following types: {footnote 73}



     a type compatible with the effective type of the object,



     a qualified version of a type compatible with the effective type of

     the object,



     a type that is the signed or unsigned type corresponding to the

     effective type of the object,



     a type that is the signed or unsigned type corresponding to a

     qualified version of the effective type of the object,



     an aggregate or union type that includes one of the aforementioned

     types among its members (including, recursively, a member of a

     subaggregate or contained union), or



     a character type.



 {footnote 73} The intent of this list is to specify those circumstances

 in which an object may or may not be aliased.

gcc的警告

gcc会警告某些可能会破坏重叠规则的构造,但不是所有(甚至大部分都没有警告),所以没有出现警告的源代码并不能给你任何担保。



最常见的警告可能是“提领类型双关的指针将破坏强重叠规则”(dereferencing type-punned pointer will break strict-aliasing rules)。它给出警告的位置一般来说是不会错的——gcc想告诉你的是当你提领了指针后,你可能会破坏重叠规则(除非你首先将它转换为原始类型)。该警告应该解释为,你的接口可能设计得很差,同时正确避免该警告的方式是重新以一种无须在相冲突的类型之间转换的方式重新设计他们。(即使你可以通过将void**改成void*来绕过这个警告)


指针转换和对齐

问题

很多架构都要求指针在访问大于一个字节的对象时能够正确对齐。然而系统代码中很多地方你只能接受到未对齐的数据(如,网络栈),所以你需要更正它:


    char* data;

    struct foo_header *tmp, header;



    tmp = data + offset;

    memcpy(&header, tmp, sizeof(header));



    if (header.len < FOO)

    […]



然而这是无效的……原因是当你将一个未对齐的值赋给指向需要进行对齐的类型的指针的时候,行为是不确定的。在以上例子中所发生的是,gcc注意到tmp和header必须被对齐,所以它可能会使用了一个内联的memcpy,它用到了假设数据已对齐的指令。



正确的修正方式是不使用foo_header指针。

    char* data;

    struct foo_header header;



    memcpy(&header, data + offset, sizeof(header));



    if (header.len < FOO)

    […]

上面的例子可能看上去比较傻,但确实是让我们好几次头疼……






标准是如何说的[*]

在第6.3.2.3条(指针)中陈述了指针对齐要求:



 7 […]指向某个对象或者不完全类型的指针可以被转换成指向另一个不同对象或者不完全类型的指针。如果得到的指针没有针对所指向的对象被正确地对齐{脚注57},行为是无法确定的。否则,当转换回去时,结果应该与原始指针相同。[…]



 {footnote 57} 一般来说,“被正确对齐”的概念是传递的:如果一个指向A类型的指针被正确为指向B类型的指针进行对齐,同时指向B类型的指针又是正确为一个指向C类型的指针对齐的,那么指向A类型的指针则是针对指向C类型的指针正确对齐的。



 7 A pointer to an object or incomplete type may be converted to a pointer

   to a different object or incomplete type. If the resulting pointer is

   not correctly aligned {footnote 57} for the pointed-to type, the

   behavior is undefined. Otherwise, when converted back again, the result

   shall compare equal to the original pointer. […]



 {footnote 57} In general, the concept “correctly aligned” is transitive:

 if a pointer to type A is correctly aligned for a pointer to type B,

 which in turn is correctly aligned for a pointer to type C, then a

 pointer to type A is correctly aligned for a pointer to type C.








总结

ISO C已经不是爷爷用的C了,同时把它认为是一种高级机器语言的想法也是错的……



指针转换非常不和谐(无论是显式还是隐式转换),同时你必须在给代码加入指针转换前三思。





[*] 标准引用来自ISO/IEC 9899:1999,不过老的ANSI/ISO标准基本上说的一样。





作者:Krister,翻译:ShiningRay






发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.