为什么通过直接赋值复制结构体会失败?

11

当我将 micro-controller 上的某些数据从一个结构体复制到另一个结构体时,我遇到了 Hard Fault 异常。 我尝试了不同的实现方式,它们应该都是相同的。请参阅我的代码行:

memcpy(&msg.data, data, 8);
memcpy(&msg.data, data, sizeof(*data));
memcpy(&msg.data, data, sizeof(msg.data));
msg.data = *data;  // Hard Fault

前三行工作得很好。最后一行以一个硬件错误异常结束。使用 memcpy 的行的汇编代码相同。直接赋值操作的汇编代码不同:

  memcpy(&msg.data, data, sizeof(msg.data));
 800c480:   f107 030c   add.w   r3, r7, #12
 800c484:   330b        adds    r3, #11
 800c486:   2208        movs    r2, #8
 800c488:   6879        ldr r1, [r7, #4]
 800c48a:   4618        mov r0, r3
 800c48c:   f7f4 f82e   bl  80004ec <memcpy>
  msg.data = *data;                  // Hard Fault
 800c490:   687b        ldr r3, [r7, #4]
 800c492:   f107 0217   add.w   r2, r7, #23
 800c496:   cb03        ldmia   r3!, {r0, r1}
 800c498:   6010        str r0, [r2, #0]
 800c49a:   6051        str r1, [r2, #4]

我正在使用GNU Arm嵌入式工具链5.4.1 20160919

这里是一个最简代码示例,它(希望)展示了问题。数据结构msg_t必须使用packed属性来匹配一些硬件寄存器。在微控制器上,这段代码将在msg.data = *data;这一行中导致硬件错误。

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

typedef struct canData_s {
  uint8_t d1;
  uint8_t d2;
  uint8_t d3;
  uint8_t d4;
  uint8_t d5;
  uint8_t d6;
  uint8_t d7;
  uint8_t d8; 
} canData_t;

#pragma pack(push, 1)
typedef struct msg_s {
  uint32_t stdId;
  uint32_t extId;
  uint8_t ide;
  uint8_t rtr;
  uint8_t dlc;
  canData_t data;  // 8 Bytes
  uint8_t navail;  // not available
  uint32_t timestamp;
} msg_t;
#pragma pack(pop)

void setData(canData_t *data) {
  msg_t msg;
  msg.data = *data;

  // Do something more ...
  printf("D1:%d", msg.data.d1);
  // ...
}

int main() {
  canData_t data;
  memset(&data, 0, 8);

  setData(&data);
}

为什么通过直接赋值复制结构会失败?


5
可能是对齐问题。msg 的类型是什么? - Jabberwocky
4
如果数据没有正确对齐,会产生未定义行为(UB)。 - Antti Haapala -- Слава Україні
3
是否涉及到任何打包 (#pack) 操作? - alk
1
问题很可能不在结构体的data字段中,因为通常编译器会使用适当的填充,但仍然尝试找出offsetof告诉你的内容。 - Jabberwocky
1
理论上,您编译器的 pragma pack 文档应该提到这个问题。 - M.M
显示剩余14条评论
2个回答

10
当你使用非标准的#pragma pack时,你会强制编译器在不进行任何填充的情况下存储结构体。在data之前的结构体成员分为4 + 4 + 3组,然后在第11个字节处是data,这是未对齐的。
因此,你强制data始终被分配为未对齐的,这可能会在某些CPU上访问它作为一个字(32位)时引起硬件异常。编译器生成的代码msg.data = *data;可能假定当你复制两个结构体时,它们总是正确对齐的,因为通常情况下是这样的。最有效的实现将使用32位数据块处理数据,例如上面的代码。
问题在于为什么要打包这个结构体,因为它既不可能是硬件寄存器映射,也不可能是数据协议映射。CAN-bus IDE和RTR这样的东西只是单个位;我非常怀疑任何CAN控制器都为此保留了整个8位寄存器。例如,ST的"bxCAN"控制器将它们放置在CAN_TIxR寄存器(CAN TX邮箱标识符寄存器)的各个位上。市场上的所有其他CAN控制器都会表现出类似的行为。
至于CAN帧本身,你不能直接对它进行内存映射。CAN控制器将抓取原始的CAN帧并将其放置在自己的内存映射寄存器中。
要么重新制作这个结构体而不填充,要么使用硬件提供的实际CAN控制器寄存器。

“struct” 结构体中充满了 “uint8_t” 类型,为什么会出现不对齐的情况?我一直以为打包会通过强制编译器发出更低效但更容错的输出来放宽对齐限制。 - user694733
@user694733 因为无论编译器生成的复制代码是什么,它似乎都是以优化的方式编写的,利用了32位读/写。 - Lundin
在答案中添加了一些澄清。 - Lundin
@Lundin 另一个问题 - 如果结构体是紧凑的,编译器应该生成更少效率但更安全的代码,就像gcc一样 - 使用字节指令。这个编译器没有这样做,这很荒谬... 顺便问一下,这个编译器是什么? - 0___________
是的,你说得对。我查看了CAN API,发现其结构并不基于硬件。似乎更多地是基于ST标准库的作者。 - eDeviser

0
我发现有一个CFSR寄存器,其中包含有关硬件故障异常类型的信息。该寄存器显示位号24已设置。ARM的编程手册PM0214在第221页上说:

位24 UNALIGNED:未对齐访问使用错误。通过将CCR中的UNALIGN_TRP位设置为1来启用捕获未对齐访问,请参见页面214上的配置和控制寄存器(CCR)。无论UNALIGN_TRP的设置如何,未对齐的LDM、STM、LDRD和STRD指令始终会出错。

0:没有未对齐访问错误,或未启用未对齐访问捕获

1:处理器进行了未对齐的内存访问。

这确实与@Lundin的答案相匹配。

1
更改(更新)编译器。一个好的编译器应该在分配结构时生成安全代码。例如,gcc会生成字节宽度的加载存储指令或在存在未对齐的结构数据访问风险时内部调用memcpy。 - 0___________
你好 @PeterJ_01。我刚刚将编译器更新到7.2.1,但这并没有解决问题。 :-( - eDeviser

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