问题描述
我在某些代码中遇到了HardFault异常,该异常使用整数操作数计算浮点表达式。 操作数通过地址传递,然后被转换(隐式或显式强制转换)为浮点数。 当操作数不是32位对齐的(不在我的控制之下)时,我得到异常。
我试图在Godbolt here上重现该行为,并且生成的代码与我在设备上得到的一致。
基本上,以下反汇编代码
vldr.32 s0,[r0] @ int
直接使用vldr
指令中传递给函数的可能未对齐的地址,该地址需要对齐的地址。
我发现this question可以解决类似的问题,但是在那里他们谈论的是浮点指针。在那种情况下,我知道浮标不能对齐。
在我的情况下,我正在处理允许不对齐的整数,尽管如此,编译器认为它仍然可以使用vldr指令中的地址。更令我困惑的是这段代码
uint32_t pippo = *(uint32_t *)src;
float pippof = pippo * 10.0f;
在提供未对齐地址时可能会生成异常,也可能不会生成异常,具体取决于优化级别,因为例如使用-O0
会在堆栈上分配一个整数。
所以我的问题是:
- 对于编译器(或后端)来说,这是正确的行为吗?由于整数可以不对齐,所以我希望生成的代码从CPU寄存器传递。
- 即使通过临时int变量也不安全时,避免这种问题的正确策略是什么?
解决方法
C不是可移植的汇编语言,它有自己的规则
当操作数不是32位对齐的(不在我的控制之下)
alignof(uint32_t)
为4,因此允许编译器假定4字节对齐。取消引用未对齐4字节的uint32_t*
是C的未定义行为,因此,是的,允许编译器100%假定未发生这种情况。
在您的情况下,如果*(uint32_t *)src
未对齐,则src
是未定义的行为。这就是为什么允许为以后使用该数据而生成的代码假定它们 是对齐的。 ARM组件碰巧可以承受未对齐的整数负载,这一事实与无关,与之无关,除了为什么它在禁用优化的情况下可以正常工作。参见https://trust-in-soft.com/blog/2020/04/06/gcc-always-assumes-aligned-pointers/和
Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?以获得更多示例和有关目标ISA未对准行为的讨论/保证在C语言中不会使其安全。
如果您的数据对齐程度比此少,则需要某种方法来进行安全的未对齐加载。一种ISO-C标准方法是使用memcpy
。 (GCC会可靠地将其内联到它知道如何执行未对齐整数加载的目标上,例如使用足够新的-march=
或-mcpu=
的ARM。除非您使用过-fno-builtin-memcpy
或类似的代码,在这种情况下,这将是一个错误的选择,而且开销太大。)
另一种方法是像这样的GNU C typedef typedef uint32_t unaligned_u32 __attribute__((aligned(1)))
并使用unaligned_u32*
。
这使编译器知道它不是与ABI兼容的普通uint32_t
对象,并且必须发出即使无法对齐也可以工作的代码。这可能效率很低;我没有检查GCC的asm输出。
(您可以将此GNU C type属性用于任何类型,包括float
,如果您需要unaligned_float。)
__attribute__((may_alias,aligned(1)))
,则 uint32_t
可能很有用。 (许多嵌入式构建都是使用-fno-strict-aliasing编译的,其中每种类型都隐式地为may_alias,但是如果在所有实际需要的地方都严格使用此代码,则可以使代码严格别名是安全的。)