GCC结构体内存对齐

16

我正在将一个C语言应用程序移植到ARM平台,该应用程序还可以在x86处理器上运行,并且必须向后兼容。

我现在遇到一些变量对齐的问题。我已经阅读了GCC手册中关于__attribute__((aligned(4),packed))的说明,我理解它的意思是结构体的开始会按照4字节边界对齐,而内部由于有packed声明会保持不变。

最初我用了这个方法,但是有时候它并没有按照4字节边界对齐。

typedef struct  
{  
 unsigned int code;  
 unsigned int length;  
 unsigned int seq;  
 unsigned int request;  
 unsigned char nonce[16];  
 unsigned short  crc;  
} __attribute__((packed)) CHALLENGE;

所以我把它改成了这样。

typedef struct  
{  
 unsigned int code;  
 unsigned int length;  
 unsigned int seq;  
 unsigned int request;  
 unsigned char nonce[16];  
 unsigned short  crc;  
} __attribute__((aligned(4),packed)) CHALLENGE;

我之前提到的理解似乎是错误的,因为结构体现在已经对齐到了4字节边界,并且内部数据也已经对齐到了四字节边界,但由于大小端的原因,结构体的大小从42字节增加到44字节。这个大小很关键,因为我们有其他依赖于结构体大小为42字节的应用程序。

请问有人能告诉我如何执行我需要的操作吗?非常感谢任何帮助。

6个回答

16

如果你依赖于sizeof(yourstruct)为42字节,那么你很可能会陷入一片不可移植的假设中。虽然你没有说明这是用来干什么的,但似乎结构体内容的字节序也很重要,所以你也可能与x86存在不匹配。

在这种情况下,我认为唯一确保稳定的方法是在关键部分使用unsigned char[42]。首先,精确地编写规范,确定这42字节块中的每个字段及其字节序,然后使用该定义编写代码来转换为您可以交互的结构体。这段代码通常是一个整体序列化代码(又名封送),或一堆获取器和设置器。


1
虽然我同意其他所有内容,但我不确定为什么你建议使用char数组。 - Roger Pate
@Roger:我假设OP需要以强制形式在内存中保存结构体,同时也需要以更容易操作的形式保存——除非你有其他我错过的观点? - crazyscot
@crazy:OP 显然对于使用紧凑结构体作为数据文件的内存表示感到满意,这使得使用 char 数组等同于将其转换为 char 指针并仅使用前 42 个字节的 &struct_obj 作为 char 数组。如果他想放弃紧凑性,则可能需要---暂时。但即使在这种情况下,我也只会使用缓冲操作(例如 FILE)并读取每个成员。 - Roger Pate
数据结构本质上是一个数据包,在发送之前,我会确保对相关成员使用htonl/htons。我认为编组将是正确的选择。由于有大约100个类似的结构体,我将研究其实现的难易程度。非常感谢您的回复。 - Mumbles
1
@Mumbles:如果你可以使用C++而不是C,那么你只需要为每个结构体编写一小段代码就可以完成它(类似于boost::serialize的工作方式)。否则(或者即使在C++中也是如此),我会为你的结构体生成代码,这样你就可以使用相同的输入文件来生成序列化函数,并始终知道它们是同步的。 - Roger Pate
显示剩余2条评论

6
这就是为什么读取整个结构体而不是逐个成员会失败,并且应该避免的一个原因。
在这种情况下,打包加上对齐到4意味着会有两个字节的填充。这是因为大小必须兼容将类型存储在数组中并且所有项仍然对齐到4。
我想你有类似这样的东西:
read(fd, &obj, sizeof obj)

因为您不想读取属于不同数据的那两个填充字节,所以必须明确指定大小:

read(fd, &obj, 42)

让你的代码易于维护:

typedef struct {
  //...
  enum { read_size = 42 };
} __attribute__((aligned(4),packed)) CHALLENGE;

// ...

read(fd, &obj, obj.read_size)

或者,如果您的C语言中无法使用C++的某些特性:

typedef struct {
  //...
} __attribute__((aligned(4),packed)) CHALLENGE;
enum { CHALLENGE_read_size = 42 };

// ...

read(fd, &obj, CHALLENGE_read_size)

在下一次重构机会中,我强烈建议您开始逐个阅读每个成员,这可以很容易地封装在一个函数中。

4

我一直在各种系统中移植结构,如Linux、Windows、Mac、C、Swift、Assembly等。

问题并不是不能完成,而是你不能偷懒,必须了解你的工具。

我不明白为什么你不能使用:

typedef struct  
{  
 unsigned int code;  
 unsigned int length;  
 unsigned int seq;  
 unsigned int request;  
 unsigned char nonce[16];  
 unsigned short  crc;  
} __attribute__((packed)) CHALLENGE;

您可以使用它,而且不需要任何特殊或聪明的代码。我编写了很多与ARM通信的代码。结构是使事情正常工作的关键。__attribute__ ((packed))是我的好帮手。

如果您了解两者的情况,那么处于“一团糟”状态的可能性为零。

最后,我无法理解您如何得出42或44. Int是4或8字节(取决于编译器)。假设它真的被打包了,那么数字将为16+16+2=34或32+16+2=50。

正如我所说,了解您的工具是解决问题的一部分。


1
最好使用uint32_t来表示无符号整数,使用uint16_t来表示无符号短整数。 - Garret Gang
4
毫无疑问,您指的是 uint16_t 表示无符号短整型。 - Cat Sargent
对“更安全”感到困惑。你的意思是它不会让人迷惑字节数量。关键是如果你不了解你的工具,不知道字节数量等等,你就会崩溃和失败。至于int32_t,是的,它比int更好。int16_t比short更好。(或uintxx_t,这取决于符号是否是一个问题) - Cat Sargent
是的,我指的是无符号短整型 uint16_t。我所说的更安全是指,如果你在多台不同的计算机之间传递这个结构体(比如一个16位机器、一个32位机器和一个64位机器),它们中的每一个都可能对于无符号整型/无符号短整型有不同的长度。因为C++并不保证大小。这使得结构体在多台机器之间变得无用。 - Garret Gang

3

你的真正目标是什么?

如果你的目标是处理特定格式的文件或网络数据,那么你应该编写一些序列化程序来在编译器结构体和字符数组之间移动数据。编译器结构体用于表示程序内部如何处理数据,而字符数组则用于表示数据在网络或文件中的样子。

这样,你只需要仔细处理序列化程序,可能需要编写平台特定的代码。你可以编写一些良好的单元测试来确保序列化的数据能够正确地从结构体中传输到网络或文件中,并且不管你今后要移植到哪个平台,都能正常工作。


这个结构体的目标是成为网络数据包。我非常喜欢使用内部结构体,让编译器对齐以确保正确性,并且只在需要时构建该数据包。 - Mumbles

0
我猜问题在于42不能被4整除,因此如果将多个这些结构体放在一起(例如使用sizeof分配内存确定它们的大小),它们就会失去对齐。将大小设置为44可以强制在这些情况下进行对齐,正如您所请求的那样。但是,如果每个结构成员的内部偏移量保持不变,则可以将44字节的结构视为42字节(只要注意将任何后续数据对齐到正确的边界即可)。
一个值得尝试的技巧可能是将这两个结构体都放在单个联合类型中,并且仅从每个这样的联合中使用42字节版本。

请注意,这种“背靠背”分配在数组中会自动发生,这就是为什么类型的大小必须包括那些填充字节以保持对齐的原因。您不能通过任何技巧更改数组布局,我也不建议使用它们。 - Roger Pate

-3

由于我正在使用Linux,我发现通过 echo 3 > /proc/cpu/alignment 会发出警告,并解决对齐问题。 这是一个解决方法,但可以帮助定位结构失败的位置。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接