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

Posted

技术标签:

【中文标题】为啥通过直接赋值复制结构会失败?【英文标题】:Why does copying a structure by a direct assignment fail?为什么通过直接赋值复制结构会失败? 【发布时间】:2018-07-22 13:01:14 【问题描述】:

我在将微控制器上的一些数据从一个结构复制到另一个结构时遇到硬故障异常。我尝试了不同的实现,它们应该做的都是一样的。查看我的代码行:

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 Embedded Toolchain 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);

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

【问题讨论】:

可能是对齐问题。 msg的类型是什么? @alk 如果data 没有正确对齐,UB。 显示所涉及变量的声明和初始化。 您需要告诉我们您是如何获得data的。即,它指向什么,尤其是在涉及任何指针转换的情况下。 任何#packing 参与? 【参考方案1】:

当你使用非标准的#pragma pack 时,你会强制编译器存储没有任何填充的结构。 data 之前的结构成员以 4+4+3 为一组,然后是第 11 个字节的 data,这是未对齐的。

因此,您强制data 始终分配未对齐,如果将其作为字(32 位)访问,这可能会导致某些 CPU 上出现硬件异常。编译器生成的代码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 因为编译器生成的任何复制代码似乎都是以利用 32 位读/写的优化方式编写的。 为答案添加了一些说明。 @Lundin 另一个问题 - 如果结构被打包,编译器应该像 gcc 那样为其生成效率较低但安全的代码 - 改用字节指令。这个没有,这太荒谬了..顺便说一句,这个编译器是什么? 是的,你是对的。我查看了 CAN API,它表明该结构不是基于硬件的。似乎更多的是基于 ST-Standard-Library 的作者。【参考方案2】:

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

位 24 UNALIGNED:未对齐访问使用错误。启用陷印 通过将 CCR 中的 UNALIGN_TRP 位设置为 1 来进行非对齐访问,请参见 第 214 页的配置和控制寄存器 (CCR)。未对齐的 LDM, STM、LDRD 和 STRD 指令总是出错,而与 UNALIGN_TRP 的设置。

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

1: 处理器做了一个未对齐的 内存访问。

这确实与@Lundin 的回答相符。

【讨论】:

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

以上是关于为啥通过直接赋值复制结构会失败?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在直接初始化和赋值中传递 lambda 而不是复制初始化时会编译?

为啥通过引用传递数组元素会显式导致 IL 中的赋值操作?

Vue数组更新,为啥不能通过索引直接设置一个值

Golang复制结构体

Golang复制结构体

为啥变量赋值中的空格会在 Bash 中出错? [复制]