代码之家  ›  专栏  ›  技术社区  ›  Kirk Strauser

在C中安全地双关字符*

  •  11
  • Kirk Strauser  · 技术社区  · 17 年前

    program I wrote ,我从一个文件中读取二进制数据(由另一个程序编写),并输出整数,双精度, 和其他分类数据类型。挑战之一是它需要 最后不得不做一些低级的琐事。我知道一个(非常) 关于类型双关和严格的别名,我想确保

    基本上,很容易将char*转换为各种大小的int:

    int64_t snativeint64_t(const char *buf) 
    {
        /* Interpret the first 8 bytes of buf as a 64-bit int */
        return *(int64_t *) buf;
    }
    

    作为:

    int64_t swappedint64_t(const int64_t wrongend)
    {
        /* Change the endianness of a 64-bit integer */
        return (((wrongend & 0xff00000000000000LL) >> 56) |
                ((wrongend & 0x00ff000000000000LL) >> 40) |
                ((wrongend & 0x0000ff0000000000LL) >> 24) |
                ((wrongend & 0x000000ff00000000LL) >> 8)  |
                ((wrongend & 0x00000000ff000000LL) << 8)  |
                ((wrongend & 0x0000000000ff0000LL) << 24) |
                ((wrongend & 0x000000000000ff00LL) << 40) |
                ((wrongend & 0x00000000000000ffLL) << 56));
    }
    

    在运行时,程序检测机器的末端并分配 上面的其中一项指向函数指针:

    int64_t (*slittleint64_t)(const char *);
    if(littleendian) {
        slittleint64_t = snativeint64_t;
    } else {
        slittleint64_t = sswappedint64_t;
    }
    

    现在,当我试图将char*转换为double时,棘手的部分出现了。我愿意

    union 
    {
        double  d;
        int64_t i;
    } int64todouble;
    
    int64todouble.i = slittleint64_t(bufoffset);
    printf("%lf", int64todouble.d);
    

    然而,一些编译器可以优化掉“int64todouble.i”赋值 破坏程序。是否有一种更安全的方法来做到这一点,同时考虑 这个程序必须保持性能优化,而且我会 不希望编写一组并行转换来将char*强制转换为 重新编写snativeint64\t之类的函数来使用它?


    Steve Jessop's 答案是因为转换函数被重新编写为使用memcpy,如下所示:

    int64_t snativeint64_t(const char *buf) 
    {
        /* Interpret the first 8 bytes of buf as a 64-bit int */
        int64_t output;
        memcpy(&output, buf, 8);
        return output;
    }
    

    snativeint64_t:
            movq    (%rdi), %rax
            ret
    

    亚当,你的回答也很好,我从中学到了很多。谢谢你的发帖!

    5 回复  |  直到 9 年前
        1
  •  12
  •   Adam Rosenfield    16 年前

    我强烈建议你阅读 Understanding Strict Aliasing . 具体请参见标有“通过活接头铸造”的章节。它有很多很好的例子。虽然这篇文章是在一个关于Cell处理器的网站上发表的,并使用了PPC汇编示例,但几乎所有这些都同样适用于其他体系结构,包括x86。

        2
  •  2
  •   Steve Jessop    17 年前

    由于您似乎对实现非常了解,可以确保int64_t和double的大小相同,并且具有合适的存储表示形式,因此可能会使用memcpy。然后你甚至不必考虑别名。

    由于您使用的函数指针可能很容易内联,如果您愿意发布多个二进制文件,那么性能肯定不是一个大问题,但您可能想知道,一些编译器可以非常凶猛地优化memcpy-对于小整数大小,可以内联一组加载和存储,您甚至可能会发现变量被完全优化了,编译器的“复制”只是重新分配用于变量的堆栈槽,就像一个并集。

    int64_t i = slittleint64_t(buffoffset);
    double d;
    memcpy(&d,&i,8); /* might emit no code if you're lucky */
    printf("%lf", d);
    

    检查生成的代码,或者只是对其进行概要分析。即使在最坏的情况下,它也不会慢下来。

    不过,总的来说,对byteswapping做任何过于巧妙的事情都会导致可移植性问题。存在具有中端双精度的ABI,其中每个单词都是小端,但大词排在第一位。

    通常你可以考虑使用SaveTF和SCASNF存储双打,但是对于你的项目来说,文件格式不在你的控制之下。但是,如果您的应用程序只是将IEEE double从一种格式的输入文件转移到另一种格式的输出文件(不确定是否是,因为我不知道有问题的数据库格式,但如果是),那么您可能会忘记它是一个double的事实,因为您无论如何都不会将它用于算术。只需将其视为不透明字符[8],仅当文件格式不同时才需要字节切换。

        3
  •  2
  •   Pramod    17 年前

    该标准规定,向工会的一个领域写入数据并立即从中读取数据是未定义的行为。因此,如果你按规则行事,基于联合的方法就行不通了。

    宏通常是个坏主意,但这可能是一个例外。在C中,应该可以使用一组使用输入和输出类型作为参数的宏来获得类似模板的行为。

        4
  •  0
  •   unwind    17 年前

    0xff

    简言之,改变这一点:

    (((wrongend & 0xff00000000000000LL) >> 56)
    

    为此:

    ((wrongend >> 56) & 0xff)
    

        5
  •  -1
  •   Mecki    17 年前

    编辑:



    关于double/float,只需通过内存强制转换将它们存储到ints即可:

    double d = 3.1234;
    printf("Double %f\n", d);
    int64_t i = *(int64_t *)&d;
    // Now i contains the double value as int
    double d2 = *(double *)&i;
    printf("Double2 %f\n", d2);
    

    int64_t doubleToInt64(double d)
    {
        return *(int64_t *)&d;
    }
    
    double int64ToDouble(int64_t i)
    {
        return *(double *)&i;
    }
    

    提问者提供了以下链接:

    http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

    为了证明铸造是不好的。。。不幸的是,我只能强烈反对这一页的大部分内容。引述和评论:

    事实上,这是一种糟糕的做法 潜在风险代码。铸造

    这一点都不危险,也不是一种坏习惯。如果你做得不正确,它才有可能导致错误,就像用C语言编程如果你做得不正确,它有可能导致错误一样,任何语言的编程也是如此。根据这个论点,你必须完全停止编程。

    类型双关
    到内存中的相同位置,但 类型。编译器将同时处理这两个问题 “双关语”是不相关的指针。类型 双关语有可能导致 通过两个指针访问。

    这是事实,但不幸的是 与我的代码完全无关

    int64_t * intPointer;
    :
    // Init intPointer somehow
    :
    double * doublePointer = (double *)intPointer;
    

    现在,doublePointer和intPointer都指向相同的内存位置,但将其视为相同的类型。这是你应该通过工会来解决的情况。事实上,其他任何事情都是非常糟糕的。糟糕,这不是我的代码所做的!

    我的代码由 价值 ,而不是 参考 . 我将一个双精度指针转换为int64(或者反过来),然后 立即服从 信息技术一旦函数返回,就没有指向任何对象的指针。有一个int64和一个double,它们与函数的输入参数完全无关。我从不将任何指针复制到不同类型的指针(如果您在我的代码示例中看到了这一点,您会严重误读我编写的C代码),我只是将值转移到不同类型的变量(在自己的内存位置)。因此,类型双关的定义根本不适用,因为它说“引用内存中的相同位置”,这里没有任何内容引用相同的内存位置。

    int64_t intValue = 12345;
    double doubleValue = int64ToDouble(intValue);
    // The statement below will not change the value of doubleValue!
    // Both are not pointing to the same memory location, both have their
    // own storage space on stack and are totally unreleated.
    intValue = 5678;
    

    我的代码只不过是一个内存拷贝,只是用C编写的,没有外部函数。

    int64_t doubleToInt64(double d)
    {
        return *(int64_t *)&d;
    }
    

    可以写成

    int64_t doubleToInt64(double d)
    {
        int64_t result;
        memcpy(&result, &d, sizeof(d));
        return result;
    }