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 推断不出来的副作用, 例如

  1. 依赖 input 之外的输入
  2. 产生 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 中去找

Author: [email protected]
Date: 2023-09-14 Thu 13:28
Last updated: 2024-08-18 Sun 22:08

知识共享许可协议