此代码如何在内存对齐中导致未定义的行为?

时间:2014-05-06 08:13:14

标签: c embedded memory-alignment iar

我正在使用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的错误?

感谢。

2 个答案:

答案 0 :(得分:5)

您无法使用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];
  ...
}

另请注意您创建地址联合的方式,它取决于endianess。这也可能是另一个与填充无关的问题。

答案 1 :(得分:4)

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

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

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

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