我在裸机项目中使用 ARM GCC 版本 aarch64-none-elf-gcc-11.2.1 已经有一段时间了,在一个大型项目中,该项目已成功使用 libc 函数(malloc/memcpy)多次,使用这些选项没有问题:
-L$AARCH64_GCC_PATH/aarch64-none-elf/lib -lc -lnosys -lg
我最近看到一个异常,尽管使用 -mstrict-align 编译,但在 memcpy 期间访问未对齐。
隔离问题并创建单元测试后,我相信我发现了一个错误,请忽略 objdump 和 memcpy 调用的地址,只是为这次测试弥补了它们。
//unit test
#include <stdlib.h>
#include <string.h>
volatile int bssTest;
void swap(int a, int b) {
memcpy((void*)0x500,(void*)0x1000,0xc);
}
0000000000060040 <memcpy>:
60040: f9800020 prfm pldl1keep, [x1]
60044: 8b020024 add x4, x1, x2
60048: 8b020005 add x5, x0, x2
6004c: f100405f cmp x2, #0x10
60050: 54000209 b.ls 60090 <memcpy+0x50> // b.plast
60054: f101805f cmp x2, #0x60
60058: 54000648 b.hi 60120 <memcpy+0xe0> // b.pmore
6005c: d1000449 sub x9, x2, #0x1
60060: a9401c26 ldp x6, x7, [x1]
60064: 37300469 tbnz w9, #6, 600f0 <memcpy+0xb0>
60068: a97f348c ldp x12, x13, [x4, #-16]
6006c: 362800a9 tbz w9, #5, 60080 <memcpy+0x40>
60070: a9412428 ldp x8, x9, [x1, #16]
60074: a97e2c8a ldp x10, x11, [x4, #-32]
60078: a9012408 stp x8, x9, [x0, #16]
6007c: a93e2caa stp x10, x11, [x5, #-32]
60080: a9001c06 stp x6, x7, [x0]
60084: a93f34ac stp x12, x13, [x5, #-16]
60088: d65f03c0 ret
6008c: d503201f nop
60090: f100205f cmp x2, #0x8
60094: 540000e3 b.cc 600b0 <memcpy+0x70> // b.lo, b.ul, b.last
60098: f9400026 ldr x6, [x1]
6009c: f85f8087 ldur x7, [x4, #-8]
600a0: f9000006 str x6, [x0]
600a4: f81f80a7 stur x7, [x5, #-8]
600a8: d65f03c0 ret
600ac: d503201f nop
600b0: 361000c2 tbz w2, #2, 600c8 <memcpy+0x88>
600b4: b9400026 ldr w6, [x1]
600b8: b85fc087 ldur w7, [x4, #-4]
600bc: b9000006 str w6, [x0]
600c0: b81fc0a7 stur w7, [x5, #-4]
600c4: d65f03c0 ret
600c8: b4000102 cbz x2, 600e8 <memcpy+0xa8>
600cc: d341fc49 lsr x9, x2, #1
600d0: 39400026 ldrb w6, [x1]
600d4: 385ff087 ldurb w7, [x4, #-1]
600d8: 38696828 ldrb w8, [x1, x9]
600dc: 39000006 strb w6, [x0]
600e0: 38296808 strb w8, [x0, x9]
600e4: 381ff0a7 sturb w7, [x5, #-1]
600e8: d65f03c0 ret
600ec: d503201f nop
600f0: a9412428 ldp x8, x9, [x1, #16]
600f4: a9422c2a ldp x10, x11, [x1, #32]
600f8: a943342c ldp x12, x13, [x1, #48]
600fc: a97e0881 ldp x1, x2, [x4, #-32]
60100: a97f0c84 ldp x4, x3, [x4, #-16]
60104: a9001c06 stp x6, x7, [x0]
60108: a9012408 stp x8, x9, [x0, #16]
6010c: a9022c0a stp x10, x11, [x0, #32]
60110: a903340c stp x12, x13, [x0, #48]
60114: a93e08a1 stp x1, x2, [x5, #-32]
60118: a93f0ca4 stp x4, x3, [x5, #-16]
6011c: d65f03c0 ret
60120: 92400c09 and x9, x0, #0xf
60124: 927cec03 and x3, x0, #0xfffffffffffffff0
60128: a940342c ldp x12, x13, [x1]
6012c: cb090021 sub x1, x1, x9
60130: 8b090042 add x2, x2, x9
60134: a9411c26 ldp x6, x7, [x1, #16]
60138: a900340c stp x12, x13, [x0]
6013c: a9422428 ldp x8, x9, [x1, #32]
60140: a9432c2a ldp x10, x11, [x1, #48]
60144: a9c4342c ldp x12, x13, [x1, #64]!
60148: f1024042 subs x2, x2, #0x90
6014c: 54000169 b.ls 60178 <memcpy+0x138> // b.plast
60150: a9011c66 stp x6, x7, [x3, #16]
60154: a9411c26 ldp x6, x7, [x1, #16]
60158: a9022468 stp x8, x9, [x3, #32]
6015c: a9422428 ldp x8, x9, [x1, #32]
60160: a9032c6a stp x10, x11, [x3, #48]
60164: a9432c2a ldp x10, x11, [x1, #48]
60168: a984346c stp x12, x13, [x3, #64]!
6016c: a9c4342c ldp x12, x13, [x1, #64]!
60170: f1010042 subs x2, x2, #0x40
60174: 54fffee8 b.hi 60150 <memcpy+0x110> // b.pmore
60178: a97c0881 ldp x1, x2, [x4, #-64]
6017c: a9011c66 stp x6, x7, [x3, #16]
60180: a97d1c86 ldp x6, x7, [x4, #-48]
60184: a9022468 stp x8, x9, [x3, #32]
60188: a97e2488 ldp x8, x9, [x4, #-32]
6018c: a9032c6a stp x10, x11, [x3, #48]
60190: a97f2c8a ldp x10, x11, [x4, #-16]
60194: a904346c stp x12, x13, [x3, #64]
60198: a93c08a1 stp x1, x2, [x5, #-64]
6019c: a93d1ca6 stp x6, x7, [x5, #-48]
601a0: a93e24a8 stp x8, x9, [x5, #-32]
601a4: a93f2caa stp x10, x11, [x5, #-16]
601a8: d65f03c0 ret
601ac: 00000000 udf #0
在大小 = 0x8 + 0x4n 的设备类型内存上执行 memcpy 时,其中 n 是任何自然数,将抛出异常,因为即使注意对齐 src/dst 指针,6009c 上的指令来自以下 aarch64 上 memcpy 的 objdump 导致 ldur x7,[x4,#-8]。在大小为 0xc 的情况下,副本将对以 0x4 结尾的 32 位对齐地址执行 LDUR 到 64 位 x 寄存器,这会导致系统类型内存上的数据中止。
虽然我知道在裸机应用程序中使用 stdlib 函数时必须小心,但由于我们代码库的性质,很难确保每次调用 memcpy 的大小都是 64 位对齐的。 newlib/compiler 不应该注意确保 memcpy 将对任何 32 位对齐的 memcpy 使用 32 位 w 寄存器吗?特别是 -mstrict-align?
在此期间,就提供即时修复而言,我的选择是什么,我想我可以尝试覆盖 memcpy 的定义,但在这种情况下,我应该基于什么来源进行替换实现。
感谢任何帮助,谢谢。
其实我觉得更大的“bug”在你的意料之中。您根本无法在设备内存上使用
memcpy
或任何其他库函数。
现代优化编译器和库的默认假设是它们在普通内存上运行,其访问没有副作用并且不会被任何其他软件或硬件同时访问(*)。所以未对齐的访问(默认情况下 gcc 和 newlib 假设是可以的)是你最不担心的。
memcpy
使用任何负载或存储的任何组合来完成它的工作是完全公平的游戏。其中:
三个 4 字节访问
8 字节和 4 字节访问
十二次单字节访问
两次重叠的八字节访问
超出源缓冲区边界的 16 字节加载,如果它能证明它不会跨越页面边界
同一地址多次加载
多个存储到同一个地址,其中除最后一个之外的任何一个都可能是错误的值
使用
-mstrict-align
并没有真正的帮助。首先,正如您已经注意到的,它只会影响您实际用它编译的代码;它对已经构建的库代码没有任何作用。您将不得不使用此选项重建所有 newlib,然后分别审核 newlib 中的所有汇编代码。但这对上述任何其他问题都没有帮助,所有这些问题都可能对设备内存造成灾难性影响。 (正如 amonakov 指出的那样,由于 -mstrict-align
很少使用,它很容易出现编译器错误。)
对于设备内存,您需要精确控制加载和存储的数量、地址、大小以及顺序。在 C/C++ 中只有一种机制可以做到这一点,即
volatile
。因此,所有对设备内存的访问都需要通过 volatile
指针或使用程序集显式完成。
如果您需要完成 32 位访问,我认为编写示例代码的唯一安全方法是:
volatile uint32_t *dest = (volatile uint32_t *)0x500;
volatile uint32_t *src = (volatile uint32_t *)0x1000;
for (int i = 0; i < 3; i++)
dest[i] = src[i];
如果你对所有设备内存都这样做,那么你可以安全地在你的普通内存上使用编译代码和库函数,而不需要
-mstrict-align
。 (前提是您在页表中正确标记了所有正常内存,并且清除了SCTLR_ELx.A
位。)
(*) C/C++ 数据竞争规则确实允许多个 reader 同时访问同一内存。所以你可以假设你没有明确写入的内存根本不会被写入。除此之外,编译器几乎可以完全自由地以任何方式发明/丢弃/组合/重新排序加载和存储。