Inline ASM
Table of Contents
1. Inline ASM
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
asm volatile("xxx" :output :input :clobber :goto_labels);
1.1. 没有指明输入变量
#include <stdio.h> int main(int argc, char *argv[]) { int x = 0; asm("addi %0,%0,1" : "+r"(x)); asm("addi %0,%0,2" : "=r"(x)); printf("%d\n", x); return 0; }
$> riscv-gcc test.c -O2 -g -c $> riscv-objdump -d test.o 0000000000000000 <main>: 0: 00000537 lui a0,0x0 0000000000000004 <.LVL1>: 4: 1141 addi sp,sp,-16 6: 00050513 mv a0,a0 a: 0589 addi a1,a1,2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 000000000000000c <.LVL2>: c: 2581 sext.w a1,a1 e: e406 sd ra,8(sp) 10: 00000097 auipc ra,0x0 14: 000080e7 jalr ra # 10 <.LVL2+0x4> 0000000000000018 <.LVL3>: 18: 60a2 ld ra,8(sp) 1a: 4501 li a0,0 1c: 0141 addi sp,sp,16 1e: 8082 ret
源码中的
int x = 0; asm("addi %0,%0,1" : "+r"(x));
被优化掉了, 导致 x 没有初始化. 被优化掉的原因是 `asm("addi %0,%0,2" : "=r"(x))` 没有指明 x 是它的输入, 导致 gcc 认为之前对 x 赋值的语句没有用的.
即使加上 volatile 也无法保证程序正确:
#include <stdio.h> int main(int argc, char *argv[]) { int x = 0; asm volatile("addi %0,%0,1" : "+r"(x)); asm("addi %0,%0,2" : "=r"(x)); printf("%d\n", x); return 0; }
0000000000000000 <main>: 0: 1141 addi sp,sp,-16 2: e406 sd ra,8(sp) 4: 4781 li a5,0 6: 0785 addi a5,a5,1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0000000000000008 <.LVL1>: 8: 0589 addi a1,a1,2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 000000000000000a <.LVL2>: a: 00000537 lui a0,0x0 000000000000000e <.LVL3>: e: 2581 sext.w a1,a1 10: 00050513 mv a0,a0 14: 00000097 auipc ra,0x0 18: 000080e7 jalr ra # 14 <.LVL3+0x6> 000000000000001c <.LVL4>: 1c: 60a2 ld ra,8(sp) 1e: 4501 li a0,0 20: 0141 addi sp,sp,16 22: 8082 ret
加上 volatile 后 gcc 不会把 `addi %0,%0,1` 优化掉了, 但 `printf("%d\n", x)` 仍然认为 x 只取决了 `addi %0,%0,2`
正确修改后的代码为:
#include <stdio.h> int main(int argc, char *argv[]) { int x = 0; asm("addi %0,%0,1" : "+r"(x)); asm("addi %0,%0,2" : "+r"(x)); printf("%d\n", x); return 0; }
1.2. 没有指明输出变量
#include <stdio.h> int main(int argc, char *argv[]) { int x = 0; asm("addi %0,%0,1" ::"r"(x)); printf("%d\n", x); return 0; }
$> riscv-gcc test.c -O2 -g -c $> riscv-objdump -d test.o 0000000000000000 <main>: 0: 1141 addi sp,sp,-16 2: e406 sd ra,8(sp) 4: 4781 li a5,0 6: 0785 addi a5,a5,1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8: 00000537 lui a0,0x0 000000000000000c <.LVL1>: c: 4581 li a1,0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 000000000000000e <.LVL2>: e: 00050513 mv a0,a0 12: 00000097 auipc ra,0x0 16: 000080e7 jalr ra # 12 <.LVL2+0x4> 000000000000001a <.LVL3>: 1a: 60a2 ld ra,8(sp) 1c: 4501 li a0,0 1e: 0141 addi sp,sp,16 20: 8082 ret
没有 ouput 的 asm 默认自带 volatile 属性. 虽然针对 `addi %0,%0,1` 生成了指令, 但 `printf` 认为 x 只取决于 `int x=0`, 所以结果仍然是错误的.
正确修改的代码为:
#include <stdio.h> int main(int argc, char *argv[]) { int x = 0; asm("addi %0,%0,1" : "+r"(x)); printf("%d\n", x); return 0; }
1.3. 没有指明 clobber register
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; asm("li %0, 1\n\t" : "=r"(a) :); asm("li a1, 2\n\t"); printf("%d\n", a); return 0; }
0000000000000000 <main>: ... 4: 4585 li a1,1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 0000000000000008 <.LVL2>: 8: 4589 li a1,2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ a: 00000537 lui a0,0x0 000000000000000e <.LVL3>: e: 00050513 mv a0,a0 12: 00000097 auipc ra,0x0 16: 000080e7 jalr ra # 12 <.LVL3+0x4> ...
`asm("li a1, 2\n\t")` 修改了 `a1` 但没有通过 clobber 声明, 导致 printf 时直接使用了 a1.
修改后的代码为:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; asm("li %0, 1\n\t" : "=r"(a) :); asm("li a1, 2\n\t" ::: "a1"); printf("%d\n", a); return 0; }
... 4: 4785 li a5,1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6: 2781 sext.w a5,a5 0000000000000008 <.LVL1>: 8: 4589 li a1,2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 000000000000000e <.LVL3>: e: 85be mv a1,a5 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10: 00050513 mv a0,a0 14: 00000097 auipc ra,0x0 18: 000080e7 jalr ra # 14 <.LVL3+0x6> ...
1.4. 没有指明 clobber memory
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; int *pa = &a; asm( "ld a0, %0\n\t" "li a1, 10\n\t" "sd a1, 0(a0)\n\t" : : "m"(pa) : "a0", "a1"); printf("%d\n", a); return 0; }
0000000000000000 <main>: 0: 1101 addi sp,sp,-32 2: 005c addi a5,sp,4 4: ec06 sd ra,24(sp) 6: e43e sd a5,8(sp) 8: c202 sw zero,4(sp) a: 6522 ld a0,8(sp) c: 45a9 li a1,10 e: e10c sd a1,0(a0) 0000000000000010 <.LVL1>: 10: 00000537 lui a0,0x0 14: 4581 li a1,0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 16: 00050513 mv a0,a0 1a: 00000097 auipc ra,0x0 1e: 000080e7 jalr ra # 1a <.LVL1+0xa> 0000000000000022 <.LVL2>: 22: 60e2 ld ra,24(sp) 24: 4501 li a0,0 26: 6105 addi sp,sp,32 28: 8082 ret
clobber memory 表示 pa 指向的内存 (a) 被 inline asm 修改了, printf 时需要重新从内存 load a (而不是直接 li a1,0)
修改后的代码:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; int *pa = &a; asm( "ld a0, %0\n\t" "li a1, 10\n\t" "sd a1, 0(a0)\n\t" : : "m"(pa) : "a0", "a1", "memory"); printf("%d\n", a); return 0; }
0000000000000000 <main>: 0: 1101 addi sp,sp,-32 2: 005c addi a5,sp,4 4: ec06 sd ra,24(sp) 6: e43e sd a5,8(sp) 8: c202 sw zero,4(sp) a: 6522 ld a0,8(sp) c: 45a9 li a1,10 e: e10c sd a1,0(a0) 0000000000000010 <.LVL1>: 10: 4592 lw a1,4(sp) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12: 00000537 lui a0,0x0 16: 00050513 mv a0,a0 1a: 00000097 auipc ra,0x0 1e: 000080e7 jalr ra # 1a <.LVL1+0xa> 0000000000000022 <.LVL2>: 22: 60e2 ld ra,24(sp) 24: 4501 li a0,0 26: 6105 addi sp,sp,32 28: 8082 ret
注意 clobber memory 只会影响通过 input (pa) 指向的 a, 因为正常的 inline asm 要修改局部变量的内存只能通过传入 pa 这种形式.
例如:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; int b = 0; int *pa = &a; asm( "ld a0, %0\n\t" "li a1, 10\n\t" "sd a1, 0(a0)\n\t" : : "m"(pa) : "a0", "a1", "memory"); printf("%d %d\n", a, b); return 0; }
0000000000000000 <main>: 0: 1101 addi sp,sp,-32 2: 005c addi a5,sp,4 4: ec06 sd ra,24(sp) 6: e43e sd a5,8(sp) 8: c202 sw zero,4(sp) a: 6522 ld a0,8(sp) c: 45a9 li a1,10 e: e10c sd a1,0(a0) 0000000000000010 <.LVL2>: 10: 4592 lw a1,4(sp) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12: 00000537 lui a0,0x0 16: 4601 li a2,0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18: 00050513 mv a0,a0 1c: 00000097 auipc ra,0x0 20: 000080e7 jalr ra # 1c <.LVL2+0xc> 0000000000000024 <.LVL3>: 24: 60e2 ld ra,24(sp) 26: 4501 li a0,0 28: 6105 addi sp,sp,32 2a: 8082 ret
如果 inline asm 通过其它非正常的手段修改了变量, 那只能通过变量加 volatile 的方式避免出错:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; int b = 0; int *pa = &a; asm( "ld a0, %0\n\t" "li a1, 10\n\t" "sd a1, 4(a0)\n\t" : : "m"(pa) : "a0", "a1", "memory"); printf("%d\n", b); return 0; }
inline asm 中传入了 pa, 但修改了 b (4(a0)), 最后输出是 `0` 而不是 `10`.
修改后的代码为:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; volatile int b = 0; int *pa = &a; asm( "ld a0, %0\n\t" "li a1, 10\n\t" "sd a1, 4(a0)\n\t" : : "m"(pa) : "a0", "a1", "memory"); printf("%d\n", b); return 0; }
1.5. reorder
和普通代码一样, inline asm 也会有 reorder 的问题
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; int b = 0; int c = 0; asm("li %0, 1\n\t" : "=r"(a) :); asm("li %0, 2\n\t" : "=r"(b) :); asm("li %0, 3\n\t" : "=r"(c) :); printf("%d\n", c); printf("%d\n", b); printf("%d\n", a); return 0; }
0000000000000000 <main>: ... c: 458d li a1,3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 16: 4409 li s0,2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18: 00000097 auipc ra,0x0 1c: 000080e7 jalr ra # 18 <.LVL2+0xa> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 22: 85a2 mv a1,s0 24: 00048513 mv a0,s1 28: 00000097 auipc ra,0x0 2c: 000080e7 jalr ra # 28 <.LVL3+0x8> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 30: 4905 li s2,1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 32: 2901 sext.w s2,s2 ... 34: 85ca mv a1,s2 36: 00048513 mv a0,s1 3a: 00000097 auipc ra,0x0 3e: 000080e7 jalr ra # 3a <.LVL5+0x6> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ...
通过 volatile 以及 compiler fence 可以阻止 reorder
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; int b = 0; int c = 0; asm volatile("li %0, 1\n\t" : "=r"(a) :); asm volatile("li %0, 2\n\t" : "=r"(b) :); asm volatile("li %0, 3\n\t" : "=r"(c) :); /* NOTE: compiler fence */ asm("" ::: "memory"); printf("%d\n", c); printf("%d\n", b); printf("%d\n", a); return 0; }
... a: 4485 li s1,1 c: 2481 sext.w s1,s1 e: 4909 li s2,2 10: 2901 sext.w s2,s2 12: 458d li a1,3 ... 22: 000080e7 jalr ra # 1e <.LVL6> 26: 85ca mv a1,s2 ... 30: 000080e7 jalr ra # 2c <.LVL7+0x6> ... 34: 85a6 mv a1,s1 ... 3e: 000080e7 jalr ra # 3a <.LVL8+0x6>
reorder 一般不是问题, 因为 gcc 会根据 input/output 推断合适的顺序, 且没有 output 的 inline asm 会自带 volatile 避免 reorder. 但对于带 output 又有其它副作用的 inline asm 则需要考虑 reorder 问题.
1.6. volatile
gcc 把 inline asm 看做一个没有副作用 (side effect) 的黑盒 `output=f(input)`, 它不关注 f 中的具体代码, 只会根据 input/output/clobber 进行优化, 例如:
- inline asm 可以在不影响 input/output 的前提下被 reorder
- 若 output 的结果没有用 (没有使用或者被其它赋值覆盖), 则 asm 会被优化掉.
若两次调用的 input 不变(包含没有 input), 则可能会复用上一次的结果, 例如:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 0; for (int i = 0; i < 10; i++) { asm("li a0, 1\n\t" : "=r"(a) :); } printf("%d\n", a); return 0; }
0000000000000002 <.LBB3>: 2: 4505 li a0,1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 000000000000000a <.L2>: a: 37fd addiw a5,a5,-1 c: fffd bnez a5,a <.L2> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ...
如果 inline asm 有根据 input/output 推断不出来的副作用, 例如
- 依赖 input 之外的输入
- 产生 output 之外的输出
则需要加上 volatile 关键字. 另外, 若 output 为空, gcc 会自动加上 volatile, 因为没有 output 的 inline asm 显然是有副作用.
如果 inline asm 没有副作用, 盲目的添加 volatile 只会阻止 gcc 有益的优化.
1.7. 其它
inline asm 中的 constraints (r, m, …) 的意义需要到后端的 constraints.md 中去找. 模板中的 modifier 的用法 (%z0, …) 需要到后端的 TARGET_PRINT_OPERAND hook 中去找