通过联合和位移将平台字节序读为两倍,这安全吗?

时间:2018-10-03 16:21:28

标签: c decode endianness undefined-behavior decoding

我所见过的从缓冲区读取已知字节序的两倍到平台字节序的所有示例都涉及检测当前平台的字节序,并在必要时执行字节交换。

另一方面,除了使用位移位(one such example)的整数外,我还看到了另一种做相同事情的方法。

这让我想到可能可以使用联合和位移位技术从缓冲区读取双精度(和浮点数),并且似乎可以进行快速测试(至少对于x86_64上的clang而言):

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

double read_double(char * buffer, bool le) {
    union {
        double d;
        uint64_t i;
    } data;
    data.i = 0;

    int off = le ? 0 : 7;
    int add = le ? 1 : -1;
    for (int i = 0; i < 8; i++) {
        data.i |= ((uint64_t)(buffer[off] & 0xFF) << (i * 8));
        off += add;
    }
    return data.d;
}

int main() {
    char buffer_le[] = {0x6E, 0x86, 0x1B, 0xF0, 0xF9, 0x21, 0x09, 0x40};
    printf("%f\n", read_double(buffer_le, true)); // 3.141590

    char buffer_be[] = {0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E};
    printf("%f\n", read_double(buffer_be, false)); // 3.141590

    return 0;
}

我的问题是,这样做安全吗?还是这里涉及未定义的行为?或者,如果此方法和字节交换方法都涉及未定义的行为,那么一个方法比另一个方法安全吗?

2 个答案:

答案 0 :(得分:2)

通过工会重新诠释

C标准当然支持通过移位和ORing字节来构造uint64_t值。 (由于需要确保左操作数的大小和类型正确,以避免发生溢出和移位宽度问题,因此在进行移位时存在一定的危险,但是问题中的代码在移位之前会正确转换为uint64_t。)代码剩下的问题是C标准是否允许通过联合进行重新解释。答案是肯定的。

C 6.5.2.3 3说:

  

后缀表达式,后跟运算符和标识符,指定结构或联合对象的成员。该值是命名成员的值, 99)

注释99说:

  

如果用于读取联合对象的内容的成员与最后一次用于在对象中存储值的成员不同,则该值的对象表示的适当部分将重新解释为对象中的对象表示。 6.2.6中描述的新类型(有时称为“类型校正”的过程)…

当然,这种重新解释依赖于C实现中使用的对象表示。值得注意的是double必须使用预期的格式,与从输入流中读取的字节相匹配。

修改对象的字节

C允许通过修改对象的字节(例如使用指向unsigned char的指针)来修改对象。C 2018 6.5 7说:

  

只能通过具有以下类型之一的左值表达式访问对象的存储值:[各种类型的列表]或字符类型。

尽管其中一项评论指出您可以通过这种方式“访问”但不能“修改”对象的字节(显然将“访问”解释为仅读而不是写),C 2018 3.1将“访问”定义为:

  

读取或修改对象的值。

因此,允许通过字符类型读取或写入对象的字节。

答案 1 :(得分:0)

  

通过联合和位移将平台字节序读为两倍,这安全吗?

这种事情仅在处理程序外部的数据(例如来自文件或网络的数据)时才有意义;您对数据有严格格式(在文件格式的规范或网络协议的规范中定义)的数据,可能与C使用的格式无关,可能与CPU使用的无关,并且可能不是IEEE 754格式要么。

另一方面,C根本不提供任何保证。举一个简单的例子,即使CPU本身恰好支持“ IEEE 754”,对于float = 0x12345e78的{​​{1}}使用BCD格式也是完全合法的。

结果是您从程序外部获得了“规范说明的格式”,并且将其转换为供程序内部使用的其他“编译器希望的格式”。并且您所做的每个假设(包括1.2345 * 10**78)都可能是错误的。