在一个旧程序中,我将数据结构序列化为字节,方法是分配一个unsigned char数组,然后通过以下方式转换为int:
*((*int)p) = value;
(其中p
是unsigned char*
,value
是要存储的值。
这个工作正常,除非在Sparc上编译时由于访问内存不正确的对齐而触发异常。这是完全合理的,因为数据元素的大小不同,因此p
很快变得不对齐,并在用于存储int值时触发错误,其中底层的Sparc指令需要对齐。
这很快被修复了(通过逐字节写出char-array的值)。但我对此有点担心,因为多年来我在很多程序中都使用过这种结构而没有问题。但显然我违反了一些C规则(严格别名?),虽然很容易发现这种情况,但是由于优化编译器等原因,违规行为可能导致其他类型的未定义行为更加微妙。我也有点疑惑,因为我相信多年来我在很多C代码中都看到了这样的结构。我正在考虑硬件驱动程序,它描述了由硬件交换的数据结构(当然是使用pack(1)),并将它们写入h / w寄存器等。所以它似乎是一种常见的技术。
所以我的问题是,上面违反了什么规则,以及实现用例的正确C方法(即将数据序列化为unsigned char数组)。当然,可以为所有函数编写自定义序列化函数,以逐字节写出来,但这听起来很麻烦而且效率不高。
最后,一般可以通过违反此别名规则来预期不良影响(对齐问题等)吗?
答案 0 :(得分:1)
是的,您的代码违反了strict aliasing rule。在C语言中,只有char*
及其signed
和unsigned
对应词被假定为其他类型的别名。
因此,执行此类原始序列化的正确方法是在ints
上创建一个数组,然后将其视为unsigned char
缓冲区。
int arr[] = { 1, 2, 3, 4, 5 };
unsigned char* rawData = (unsigned char*)arr;
您可以memcpy
,fwrite
或进行rawData
的其他序列化,这绝对有效。
反序列化代码可能如下所示:
int* arr = (int*)calloc(5, sizeof(int));
memcpy(arr, rawData, 5 * sizeof(int));
当然,您应该关注endianness
,padding
和其他问题以实现可靠的序列化。
答案 1 :(得分:0)
特定于编译器和平台,关于如何在内存中表示(布局)结构以及结构的起始地址是否与1,2,4,8,...字节边界对齐。因此,您不应对结构成员的布局进行任何假设。
在您的成员类型需要特定对齐的平台上,填充字节被添加到结构(等于我上面的语句,sizeof(struct Foo)> =其数据成员大小的总和)。填充...
现在,如果您fwrite()
或memcpy()
从一个实例到另一个实例的结构,在具有相同编译器和设置的同一台机器上(例如,在您的相同程序中),您将同时编写数据内容和填充字节,由编译器添加。只要你处理整个结构,你就可以成功地往返(只要结构中没有指针成员,至少)。
你不能假设的是,你可以将较小的类型(例如unsigned char )转换为“较大的类型”(例如unsigned int )和那些方向的memcpy,因为unsigned int可能需要在该目标平台上正确对齐。通常如果你做错了,你会看到总线错误或类似错误。
在最一般的情况下, malloc()
是获取任何类型数据的堆内存的通用方法。无论是字节数组还是某些结构,都与其对齐要求无关。没有系统存在,您无法struct Foo *ps = malloc(sizeof(struct Foo))
。在对齐至关重要的平台上,malloc不会返回未对齐的地址,因为它会破坏任何代码,尝试为结构分配内存。由于malloc()
不是通灵的,如果你用它来分配字节数组,它也会返回“struct compatible aligned”指针。
任何形式的“临时”序列化(如编写整个结构)只是一种很有前途的方法,只要您不需要将序列化数据与其他机器或其他应用程序(或某些人可能已修改的同一应用程序的未来版本)进行交换使用与对齐相关的编译器设置。
如果您寻找可移植且更可靠,更强大的解决方案,则应考虑使用其中一个主流序列化软件包,其中一个是上述Google协议缓冲区。