【问题标题】:How does this code cause undefined behavior in memory alignment?此代码如何导致内存对齐中的未定义行为?
【发布时间】:2014-06-22 18:00:47
【问题描述】:

我正在使用 IAR(一种 C 编译器)为 TI 芯片(16 位 MCU)编程。

我有以下结构,

//I use union mainly because sometimes I use the 2 bytes word value
// and sometimes I only use one byte (either a or b)
typedef union {
  uint16_t address;
  struct
  {
    uint8_t parta;
    uint8_t partb;
  } details;
} address_t;

那我有如下mac地址定义,

typedef struct
{
  uint8_t frame_type;
  uint8_t sequence_number;  
  address_t source_address;
} mac_header_t;

到目前为止一切顺利。

当我通过无线电接收数据包时,它会存储在缓冲区数组中。

uint8_t buffer[MAX_PACKET_LEN];
//the first byte is packet length, mac address follows
mac_header_t *header = (mac_header_t *)(buffer + 1);

奇怪的事情发生了,

//The packet is say 
//   0x07 (length)
//   0x07 (frame_type)
//   0x04 (sequence_number)
//   0x00 (source address parta)
//   0x00 (source address partb)
//The source address is indeed 0x00 0x00 (2 bytes)

assert(header->source_address.details.parta == 0); //correct! there's no problem
assert(header->source_address.details.partb == 0); //correct! there's no problem

//assignment from header->source_address to another object
address_t source_address = header->source_address;

assert(source_address.details.parta == 0); //no! it's 0x04!
assert(source_address.details.partb == 0); //this is right

所以奇怪的是,从 header->source_address 分配到另一个对象后,对齐方式从 0x00 0x00 变为 0x04 0x00(注意缓冲区,这实际上将指针向前移动了 1 个字节)!

在我使用#pragma pack(1) 之后,事情就解决了。

但是,我不确定为什么这实际上会导致问题。在不同对齐边界处分配 2 个对象会导致两个完全不同的值? (右手边是0x00 0x00,左手边是0x04 0x00)

这段代码是否在 C 中未定义?还是IAR的bug?

谢谢。

【问题讨论】:

  • 这看起来像是一个非常明显的混叠违规。您可能应该从接收到的内存中手动填充 mac_header_t 对象。
  • header = (mac_header_t *)(buffer + 1) 之后,对非uint8_t 结构成员的任何访问都将违反对齐要求,因为+ 1
  • 您应该真正使用正确的反序列化代码,逐字节处理每条消息。

标签: c embedded memory-alignment iar


【解决方案1】:

您不能使用 C 结构/联合来存储数据协议或创建精确的内存映射。

这是因为 C 编译器可能会在结构/联合内的任何位置插入填充字节,但最开始除外。这是如何完成的,或者填充字节得到什么值,是实现定义的行为。

这就是导致您的问题的原因。当您尝试将原始数据缓冲区“别名”为对应于您的结构时,您会调用未定义的行为,因为结构内存映射不对应于原始数据。

有一些方法可以解决这个问题:

以安全、确定的方式使用结构/联合。您始终需要使用静态断言,以确保您的结构不包含填充。你可以这样写一个断言:

    static_assert (sizeof(my_struct) == (sizeof(my_struct.member1) + 
                                         sizeof(my_struct.member2) + 
                                         ...), 
                   "Padding detected!");

一旦你有了这个地方,你就阻止了错误的发生。但要真正解决这个问题,您必须以某种编译器特定的方式删除填充,例如#pragma pack(1)

如果您的编译器无法删除填充,您必须按照评论中的建议编写序列化/反序列化函数。它本质上只是一个像这样的数据挖掘函数:

void mac_serialize (mac_header_t* dest, const uint8_t* source)
{
  dest->details.parta = source[BYTE_PARTA];
  dest->details.partb = source[BYTE_PARTB];
  ...
}

另外请注意,您创建地址联合的方式是依赖于字节序的。这也可能是另一个与填充无关的问题。

【讨论】:

    【解决方案2】:

    许多微控制器对多字节值都有对齐要求。例如,MSP430 系列要求 2 字节字必须在偶数地址上对齐(低字节位于偶数地址,高字节位于下一个奇数地址)。当微控制器尝试从未对齐的地址访问多字节值时,您将获得未定义的行为或数据中止。

    编译器制造商知道这一点,他们设计编译器将填充字节插入到您声明的结构中,以保持每个成员正确对齐。当您使用编译器指令打包结构时,您是在告诉编译器不要插入填充字节。但这并不能消除微控制器的对齐限制。如果您访问未对齐的结构成员,您仍然会遇到问题。

    当您将序列化消息接收到缓冲区中时,其中填充字节可能在传输过程中被删除,并且数据之前可能有各种标头字节,很有可能数据中的多字节值未正确对齐. (多字节值甚至可能不是正确的字节序。)这就是为什么最好通过手动将每个字节复制到解压缩的结构(具有正确的字节序)来手动反序列化消息的原因。

    在您的情况下,我猜buffer 从偶数地址开始。然后分配header 指向一个奇数地址(buffer + 1)。这意味着address_taddress 字段将落在一个奇数地址上。由于address 是一个多字节值,因此在访问它时会出现未定义的行为。 (我不确定为什么打包在你的情况下解决了这个问题,所以我可能猜错了,但这应该会给你一个大致的想法。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-01-18
      • 2020-01-16
      • 2017-01-17
      • 2017-12-05
      • 1970-01-01
      • 1970-01-01
      • 2020-01-29
      • 1970-01-01
      相关资源
      最近更新 更多