zoukankan      html  css  js  c++  java
  • [翻译]类型双关不好玩:C中使用指针重新解释是坏的

    原文地址

    Type punning isn't funny: Using pointers to recast in C is bad.

    C语言中一个重新解释(reinterpret)数据类型的技巧有可能造成严重的bug。Apple知道,这也是为什么NSRectToCGRect的实现并没有按照文档的声明执行。我在这里展示一种安全的在你的代码中重新解释数据的技术。

    Apple的NSRectToCGRect文档称函数如此定义:

    CGRect NSRectToCGRect(NSRect nsrect) {
       return (*(CGRect *)&(nsrect));
    }

    如果你看过许多C代码,那么你可能已经见过此类实现。你不能直接将一个结构声明为另一个来重新解释——即使他们有相同的域——所以一种普遍的做法是声明一个指针然后类型转换该指针。

    虽然函数的功能是确定的,但这种实现并非如此。实际上,函数的实现应该像这样:

    NS_INLINE CGRect NSRectToCGRect(NSRect nsrect) {
        union _ {NSRect ns; CGRect cg;};
        return ((union _ *)&nsrect)->cg;
    }

    区别在哪?为什么要创建一个union?为什么不直接通过指针进行类型转换?

    类型双关(type punning)

    尽管通过指针进行类型转换是一种常用的方法,但它实际上是一种错误实践,有潜在的风险。造成错误的原因就是类型双关。

    类型双关

    是一种指针重叠(pointer aliasing),两个指针指向内存同一位置但将其解释为不同的数据类型。编译器会将两个指针视为不相干的指针。对于任何可以通过两个指针访问的数据,类型双关会产生依赖问题。

    大多数时候,类型双关不会导致问题,它是C标准中的未定义行为,但通常会按照我们的预期运行。

    然而,当你想要通过优化选项提高程序性能时,就会出现问题。例如,当你打开XCode中的"Enforce Strict Aliasing"选项(或者GCC中的-fstrict_aliasing),就会出现不可预测的结果。在强重叠/严格别名(strict aliasing)下,编译器可能会按照错误顺序处理事务,甚至将指令完全漏过。

    具体来说,错误只会出现在当你当你想要在同一个函数域内间接引用两个指针(或者想访问它们的共享数据)时,仅仅创建指针是安全的。

    双关错误的例子

    NSRectToCGRect函数存在之前,有其它的一些代码:

    NSRect ellipseBounds;
    ellipseBounds.origin.x = 0;
    ellipseBounds.origin.y = 0;
    ellipseBounds.size.width = WIDGET_SIZE - 1.0;
    ellipseBounds.size.height = WIDGET_SIZE - 1.0;
    ellipseBounds = NSInsetRect(ellipseBounds, 4, 4);
    
    CGContextAddEllipseInRect(context, *(CGRect *)&ellipseBounds);
    CGContextFillPath(context);

    这段代码创建了一个NSRect类型的数据,并在使用它之前将其转换为了一个CGRect类型。

    在这个例子中,如果打开-fstrict_aliasing选项,GCC会将NSInsetRect操作放到CGContextAddEllipseInRect之后执行,因为当使用另一种数据类型间接引用ellipseBounds时,类型双关破坏了两个操作之间的依赖关系。

    使用Union解决问题

    传统的解决方法是使用union,像之前的代码中展示的,union需要包含源类型和目标类型,只需要在从目标类型中读数据之前将其声明为源类型即可。

    根据C标准,任何包括类型双关的行为都是具体实现(implementation specific)的。所以在“标准”层面,使用union并不解决问题。根据标准,如果你在union的一个域中定义了数据,你就必须从同一个域中读回数据。

    幸运的是,GCC明确地允许了不同的做法。根据GCC的文档:

    从不同的union成员中读取数据,而不是从最近写入的成员中读取,(被称为类型双关),这种行为是普遍的。即使使用-fstrict-aliasing选项,类型双关也是允许的,只要内存是通过union类型访问的。

    完美。

    一个安全地重新解释数据的宏

    很简单:

    #define UNION_CAST(x, destType) 
        (((union {__typeof__(x) a; destType b;})x).b)

    所以你可以使用如下代码将一个float变量转换为int:

    int myInt = UNION_CAST(myFloat, int);

    你会发现我并不排斥使用内联函数,没有给union命名,也没有在类型转换前声明指针。Apple的NSRectToCGRect函数做了这些,但实际上都是不必要的。编译器会抛弃这些额外工作,无需在意。

    结论

    创建一个指针,然后通过指针类型转换是重新解释数据的最普遍方法,尽管流行,但你不该使用它。总是通过union来重新解释数据,这会在优化时防止大量的错误。

  • 相关阅读:
    三级指针
    外挂指针
    内存四区(1)(转载)
    内存四区(3)(转载)
    劫持(1)
    过滤劫持和函数回调(2)
    劫持程序(3)
    dll注入实现MFC程序劫持(4)
    virtual hust 2013.6.20 数论基础题目 I
    virtual hust 2013.6.20 数论基础题目 D
  • 原文地址:https://www.cnblogs.com/giddens/p/6059656.html
Copyright © 2011-2022 走看看