GNU C内联汇编中ARM ldrd指令的约束使用问题
简述:尝试使用内联汇编时遇到问题,需要确认使用的约束是否合理。
正在进行内存操作相关实验,希望在ARM CPU(Cortex A9)上手动测试内存读取功能。
声明:本实验仅用于学习目的,我清楚99.999%的场景下依赖编译器优化是正确选择,仅希望搞清楚当前代码出错的根本原因。
目标硬件特性
- CPU与内存间总线宽度为64位,因此尝试使用
ldrd指令一次性加载两个32位字。 - 待读取的内存数据按128位对齐,计划连续执行两次
ldrd指令完成读取。
问题现象
编译器生成的汇编代码不符合指令要求,且该问题不受以下变量影响:
- 编译器类型:已测试GCC和Clang
- 优化等级:已测试
-O0、-Og、-O2、-O3 - 编译方式:已测试arm-linux-gnueabihf-gcc交叉编译与本地gcc编译
复现代码
#include <stdint.h> // 自定义结构体:表示128位数据 typedef struct __attribute__ ((packed)) u128 { uint32_t a; uint32_t b; uint32_t c; uint32_t d; } u128; int main(void) { uint32_t *ptr = (uint32_t*) 0xdeadbeef; // 测试用:指向任意内存地址 u128 words; // 第一次读取:64位 asm volatile inline ( "ldrd %[high_32b], %[low_32b], [%[addr]], #8" : [high_32b] "=X" (words.a), [low_32b] "=X" (words.b) : [addr] "r" (ptr)); // 第二次读取:64位 asm volatile inline ( "ldrd %[high_32b], %[low_32b], [%[addr]], #8" : [high_32b] "=X" (words.c), [low_32b] "=X" (words.d) : [addr] "r" (ptr)); return 0; }
GCC编译结果
使用命令arm-linux-gnueabihf-gcc -Wall -Wextra -O3 -g -ggdb broken_asm.c -o broken_asm编译时,汇编器输出警告:
/tmp/ccIaxiTz.s:51: Warning: base register written back, and overlaps one of transfer registers
反汇编结果(执行radare2 -A -c 's sym.main; pdf' broken_asm):
│ 0x000003da f3e80221 ldrd r2, r1, [r3], 8 | 0x000003de f3e80232 ldrd r3, r2, [r3], 8
该警告合理:ldrd r3, r2, [r3], 8不符合指令要求,基址寄存器不能与目标传输寄存器重叠,正确形式应为ldrd r3, r2, [r4], 8这类基址与目标寄存器不重叠的写法。
Clang编译结果
使用命令clang -mtune=cortex-a9 --target=arm-linux-gnueabihf -isystem /usr/arm-linux-gnueabihf/include -Wall -Wextra -O3 -g -ggdb broken_asm.c -o broken_asm编译时,输出以下错误:
broken_asm.c:22:5: error: Rt must be even-numbered "ldrd %[high_32b], %[low_32b], [%[addr]], #8" ^ :1:11: note: instantiated into assembly here ldrd r1, r2, [r0], #8 ^ broken_asm.c:28:5: error: base register needs to be different from destination registers "ldrd %[high_32b], %[low_32b], [%[addr]], #8" ^ :1:11: note: instantiated into assembly here ldrd r0, r1, [r0], #8 ^ 2 errors generated.
错误分析:
base register needs to be different from destination registers:与GCC警告本质一致,基址寄存器不能与目标寄存器重叠。error: Rt must be even-numbered:根据ARM指令集参考,ldrd指令的第一个目标寄存器Rt必须为偶数编号且不能是R14,第二个目标寄存器Rt2必须为Rt+1,即连续的奇偶寄存器对,但编译器生成的ldrd r1, r2 ...不符合该要求。
当前对约束的理解
- 输出操作数约束:输出寄存器是只写的,
=修饰符即标识只写操作数。最初使用"g"约束,后来换成"X"约束希望给编译器更多分配自由度,根据文档'X'代表允许任意操作数。 - 输入操作数约束:使用
"r"约束,希望两次ldrd指令都使用同一个寄存器存放地址指针,也曾尝试过"X"约束但问题依旧,根据文档'r'代表操作数存放在通用寄存器中。
测试环境
- 主机环境:Linux (Debian)
- 目标平台:Zynq 7000(PS端为Cortex A9)
- Clang版本:Debian clang version 11.0.1-2
- 交叉GCC版本:arm-linux-gnueabihf-gcc (Debian 10.2.1-6) 10.2.1 20210110
- 本地GCC版本:gcc (Debian 10.2.1-6) 10.2.1 20210110
- 手动修改二进制文件中操作码的寄存器字段后,指令可正常运行
你的约束写法存在三个核心错误:
- 没有告知编译器
ldrd需要偶数起始的连续寄存器对:普通的"r"/"X"约束只会让编译器随机分配两个独立寄存器,完全可能分配出r1、r2这种不满足寄存器对要求的组合,Clang报的奇偶寄存器错误就是这个原因。ARM GCC专门提供了"l"约束,用于分配ARMv6+要求的偶数起始连续通用寄存器对,正好匹配ldrd/strd的寄存器要求。 - 没有告知编译器基址寄存器会被写回修改:你用了后变址寻址
[%[addr]], #8,执行完ldrd后addr寄存器的值会自增8,但你把addr放在输入操作数列表里,编译器不知道这个寄存器被修改了,第二次内联汇编块里直接复用原来ptr对应的寄存器,甚至可能把addr寄存器分配成和目标寄存器重叠的编号,就出现了GCC警告的基址寄存器和传输寄存器重叠的问题。 - 两个独立asm块之间没有做寄存器状态衔接:你希望第一次
ldrd后ptr自增8,第二次ldrd直接用更新后的ptr,但两个asm块是独立的,编译器不知道第一个块修改了ptr的值,第二个块还是会拿原始ptr值做输入。
修正后的可运行代码
#include <stdint.h> typedef struct __attribute__ ((packed, aligned(16))) u128 { uint32_t a; uint32_t b; uint32_t c; uint32_t d; } u128; int main(void) { uint32_t *ptr = (uint32_t*) 0xdeadbeef; u128 words; uint64_t tmp0, tmp1; // 用64位变量匹配寄存器对约束,避免硬编码寄存器 asm volatile ( "ldrd %[val0], %[addr], #8\n" "ldrd %[val1], %[addr], #8\n" : [val0] "=l" (tmp0), [val1] "=l" (tmp1), [addr] "+r" (ptr) : : "memory" // 告知编译器内存内容被访问,不要乱优化内存访问顺序 ); // 把读取到的两个64位值拆分到u128结构体 words.a = (uint32_t)tmp0; words.b = (uint32_t)(tmp0 >> 32); words.c = (uint32_t)tmp1; words.d = (uint32_t)(tmp1 >> 32); return 0; }
关键修正点说明
- 用
"=l"约束输出64位值,编译器会自动分配满足ldrd要求的偶数起始连续寄存器对,不会再出现Rt为奇数、寄存器不连续的问题。 - 把addr的约束从输入
"r"改成读写型"+r",明确告诉编译器这个寄存器的值会在asm块内被修改,编译器会自动避免把addr分配成和输出寄存器重叠的编号,同时后续代码能拿到更新后的ptr值。 - 把两次
ldrd放到同一个asm块里,避免跨块的寄存器状态不同步问题,额外加"memory"标记告知编译器这段汇编会访问内存,防止编译器做违反预期的内存重排优化。 - 给u128结构体加了
aligned(16)属性,和你提到的128位对齐的硬件前提匹配,避免后续出现非对齐访问问题。
如果不想用临时64位变量,也可以直接通过操作数修饰符绑定结构体成员,%M0对应偶数寄存器(低32位)、%N0对应奇数寄存器(高32位),写法如下:
asm volatile ( "ldrd %M[val0], %N[val0], [%[addr]], #8\n" "ldrd %M[val1], %N[val1], [%[addr]], #8\n" : [val0] "=l" (*(uint64_t*)&words.a), [val1] "=l" (*(uint64_t*)&words.c), [addr] "+r" (ptr) : : "memory" );
这种写法依赖结构体的内存布局,加了packed属性的情况下是安全的,但用临时变量的写法可移植性更好,开O2以上优化时两种写法生成的汇编完全一致,没有额外拷贝开销。
内容的提问来源于stack exchange,提问作者Zermingore

