RISC-V内联汇编实现SC.D条件存储指令执行失败问题
问题背景
参考RISC-V ISA规范v2.2第40页7.2节对Store-Conditional指令的定义:
SC指令仅当目标地址仍存在有效预留时,才会将rs2寄存器中的字写入rs1指向的内存地址;执行成功时向rd寄存器写入0,失败则写入非零错误码。
本次排查对象为64位条件存储指令SC.D,根据规范第106页定义,其指令编码格式如下:
00011 | aq<1> | rl<1> | rs2<5> | rs1<5> | 011 | rd<5> | 0101111
RISC-V共32个通用寄存器x0~x31,每个寄存器对应专属ABI名称,例如x16对应ABI名a6,5位编码值为10000。本次实现选定的寄存器分配方案:
- rs2:使用a6寄存器(x16,编码0b10000),存储待写入值
- rs1:使用a7寄存器(x17,编码0b10001),存储待写入目标地址
- rd:使用s4寄存器(x20,编码0b10100),存储SC.D指令返回值
将寄存器编码填入指令格式后得到:
00011 | aq<1> | rl<1> | 10000 | 10001 | 011 | 10100 | 0101111
其中aq、rl两位用于指定内存序约束(参考ISA规范第40页):两位同时置1时,原子内存操作满足顺序一致性,不会被观察到早于同硬件线程(hart)上的先前内存操作、或晚于后续内存操作执行,且仅能被其他硬件线程以同一地址域所有顺序一致原子操作的全局顺序观察到。为保证SC.D原子性将两位置1,最终得到完整指令编码:
00011 | 1 | 1 | 10000 | 10001 | 011 | 10100 | 0101111 -> 00011111|00001000|10111010|00101111 0x1f 0x08 0xba 0x2f
由于RISC-V采用小端序,最初通过如下内联汇编生成对应机器码:
__asm__ volatile(".byte 0x2f, 0xba, 0x08, 0x1f");
故障现象
实现时提前向rs1(a7)、rs2(a6)加载对应值,编写的实现与测试代码如下:
/** * rs2: holds the value to be written. I pick a6 register. * rs1: holds the address to be written to. I pick a7 register. * rd: holds the return value of SC.D instruction. I pick s4 register. * * @src: the value to be written. rs2. a6 register * @dst: the address to be written to. rs1. a7 register * @rd: the value that holds the return value of SC.D */ static inline void sc(void *src, void *dst, uint64_t *rd) { uint64_t *tmp_src = (uint64_t *)src; uint64_t src_val = *tmp_src; // 13 uint64_t dst_addr = (uint64_t)dst; uint64_t ret = 100; // first of all, need to prepare the registers a6 and a7. /* load value to be written into register a6 */ __asm__ volatile("ld a6, %0"::"m"(src_val)); /* load the address to be written to into register a7 */ __asm__ volatile("ld a7, %0"::"m"(dst_addr)); /* the actual SC.D: */ __asm__ volatile(".byte 0x2f, 0xba, 0x08, 0x1f"); // __asm__ volatile("sc.d s4, a6, (a7)"); // this does not work either. /* obtain the value in register s4 */ __asm__ volatile("sd s4, %0":"=m"(ret)); *rd = ret; return; } int main() { uint64_t *src = malloc(sizeof(uint64_t)); uint64_t *dst = malloc(sizeof(uint64_t)); uint64_t rd = 20; *src = 13; *dst = 3; sc(src, dst, &rd); // write value 13 into @dst, so @dst should be 13 afterwards // the expected output should be "dst: 13, rd: 0" // What I get: "dst: 3, rd: 1" printf("dst: %ld, rd: %ld\n", *src, *dst, rd); return 0; }
测试预期输出为dst: 13, rd: 0,实际运行输出为dst: 3, rd: 1,无论使用手动编码的机器码还是直接编写sc.d汇编指令,均无法得到正确结果。
根因排查
- 核心逻辑错误:缺失前置LR(Load-Reserved,预留加载)指令
RISC-V的LR/SC是必须配对使用的原子指令对:SC执行成功的必要前提是,当前hart此前已经对同一目标地址执行过LR指令,且LR执行后到SC执行前,没有其他hart对该预留地址执行写入、也没有异常、上下文切换、地址不匹配等事件导致预留失效。当前代码仅单独执行SC,没有任何前置LR为目标地址建立硬件预留,SC必然返回非零失败值,不会执行内存写入,这就是返回rd=1、dst值未修改的直接原因。
正确的原子操作流程必须是:先执行LR.D对目标地址建立预留、加载目标内存原值,校验值符合预期后再执行SC.D尝试写入,SC返回0才代表写入成功,返回非0则需要回到LR步骤重试整个操作。 - 内联汇编用法存在严重缺陷
拆分多个独立__asm__ volatile块操作寄存器的写法完全不符合GCC内联汇编规范:- 编译器不会感知到独立汇编块中对
a6/a7/s4寄存器的修改,可能在汇编块之间插入其他代码覆盖这些寄存器的值。其中s4是被调用者保存寄存器,直接修改却不告知编译器,会导致函数返回后上层代码的寄存器状态被破坏。 - 使用
"m"约束让汇编直接从栈上变量位置加载值,但没有告知编译器汇编块的输入输出依赖,编译器可能优化掉变量存储、调整变量位置,导致ld指令加载到错误值。 - 手动拼接
.byte机器码的写法无移植性,且编译器无法识别该指令对寄存器、内存的修改,会生成错误的优化代码。
- 编译器不会感知到独立汇编块中对
- 测试代码逻辑错误
printf格式化参数数量不匹配:格式串仅定义2个%ld占位符,实际传入了3个参数(*src、*dst、rd),会导致输出值错位。
修正参考
正确的64位原子比较交换实现(基于LR/SC)如下,通过内联汇编约束告知编译器寄存器使用规则,不需要手动硬编码指令、也不需要手动指定固定寄存器破坏ABI约定:
static inline uint64_t atomic_cas_d(uint64_t *target, uint64_t old_val, uint64_t new_val) { uint64_t ret; __asm__ volatile ( "lr.d.aqrl %0, (%1)\n" "bne %0, %2, 1f\n" "sc.d.aqrl %0, %3, (%1)\n" "bnez %0, -4\n" // SC失败则回到LR位置重试 "1:\n" : "=&r"(ret) : "r"(target), "r"(old_val), "r"(new_val) : "memory" ); return ret; }
如果仅测试SC指令的成功路径,必须先执行同地址的LR.D建立预留,且整个LR/SC流程要放在同一个汇编块中。
内容的提问来源于stack exchange,提问作者Ethan L.

