You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
  • 手动修改二进制文件中操作码的寄存器字段后,指令可正常运行

问题原因与正确写法

你的约束写法存在三个核心错误:

  1. 没有告知编译器ldrd需要偶数起始的连续寄存器对:普通的"r"/"X"约束只会让编译器随机分配两个独立寄存器,完全可能分配出r1、r2这种不满足寄存器对要求的组合,Clang报的奇偶寄存器错误就是这个原因。ARM GCC专门提供了"l"约束,用于分配ARMv6+要求的偶数起始连续通用寄存器对,正好匹配ldrd/strd的寄存器要求。
  2. 没有告知编译器基址寄存器会被写回修改:你用了后变址寻址[%[addr]], #8,执行完ldrd后addr寄存器的值会自增8,但你把addr放在输入操作数列表里,编译器不知道这个寄存器被修改了,第二次内联汇编块里直接复用原来ptr对应的寄存器,甚至可能把addr寄存器分配成和目标寄存器重叠的编号,就出现了GCC警告的基址寄存器和传输寄存器重叠的问题。
  3. 两个独立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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:24:42