risc-v GCC内嵌汇编

risc-v GCC内嵌汇编

1 背景

有时候需要对比不同指令的性能,这时候如果会手撸汇编是最方便的,但汇编掌握起来太麻烦,我们可以使用内嵌汇编,在C代码中直接内嵌汇编语句,大大方便了程序的设计。

2 内嵌汇编语法

asm volatile (
	"Instruction_l;\
	 Instruction 2;\
	 ...\
	 Instruction_n;"
     :"=r"(valuel),
     "=r"(value2),
     "=r"(valuen)

     :"r"(valuel),
     "r"(value2),
     "r"(valuen)

     :"r0","r1",..."rn"
);

其中冒号:将括号中的内容分为4部分(用冒号分开):

asm volatile(汇编指令列表 
            ∶ 输出操作数(非必需)
            ∶ 输入操作数(非必需)
            ∶ 破坏描述部分(非必需));
  • 关键字 asm volatile或(__asm__ __volatile__)

    其中:asm 表示是内嵌汇编操作,必需的关键字;

    ​ volatile是可选的关键字,使用volatile表示gcc不进行任何优化。

括号中的4部分详细说明如下:

1. 汇编指令列表:

要嵌入的汇编指令,每条指令应该被双引号括起来作为字符串,指令以\n 或者;作为分隔符。

如下4中样式都OK

样式1:
	"Instruction_l;\
	 Instruction 2;\
	 ...\
	 Instruction_n;"
样式2:
	"Instruction_l\n\
	 Instruction 2\n\
	 ...\n\
	 Instruction_n\n"
样式3:
	"Instruction_l;"
	"Instruction 2;"
	"...;"
	"Instruction_n;"
样式4:
	"Instruction_l\n"
	"Instruction 2\n"
	"...\n"
	"Instruction_n\n"
	 

2. 输出操作数:

其格式如下:

:[out1]"=r"(valuel), [out2]"=r"(value2)...
  • 新段由冒号:开头,若有多个输出用逗号,间隔

  • 方括号[]中的符号名用于将内联汇编程序中使用的操作数。如[out1]"=r"(valuel) 输出是valuel,汇编代码中可以用%[out1]指代。除了 %[str] 中明确的符号命名指定外,还可以使用 %数字 的方式进行隐含指定。"数字"从0开始,依次表示输出操作数和输入操作数。

  • 双引号"=r" 字母r表示使用编译器自动分配的寄存器来存储该操作数变量。

    常见constraints 含义
    m 内存操作数
    r 寄存器操作数
    i 立即数(整数)
    f 浮点寄存器操作数
    F 立即数(浮点)

    对于输出操作数来说:“=”表示输出变量,用作输出, “+”表示输出变量不仅作为输出,也作为输入

  • 圆括号()中的C/C+变量或者表达式。

3. 输入操作数:

与输出操作数格式相同,差别是没有“=”约束

4. 破坏描述部分:

  • 新的段用:开头,若有多个寄存器用逗号,间隔

  • 在汇编代码中,我们用到了一些寄存器,需要告知GCC不再信任这些寄存器的值(输入输出操作数指定的寄存器外,这部分由编译器自动分配回收)。

3 ABI接口

ABI接口即二进制接口,其包含许多细节:

  • 数据类型的大小、布局和对齐。
  • 函数调用的方式,如参数传递是通过栈传递还是寄存器、参数入栈顺序、返回值如何保存等

在写内嵌汇编时,需要了解函数调用输入输出用到了哪些寄存器。

对于RISC-V汇编程序而言,在汇编程序中调用C/C+语言函数,必须遵照ABI所定义的函数调用规则。即:函数参数由寄存器a0-a7传递,函数返回由寄存器a0-a1指定.

4 内嵌汇编示例

4.1 简单形式的内嵌汇编

内嵌汇编共四个部分 汇编指令列表 ∶输出操作数(非必需)∶输入操作数(非必需)∶破坏描述部分(非必需)

各部分使用“:”格开,汇编指令列表必不可少,其他三部分可选。

asm volatile("wfi");  /* 若函数没有输入输出,可直接调用 */
asm volatile("cli": : :"memory") /* 如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空 */

4.2 扩展内嵌汇编

以下是一个Riscv平台累加和的例子,展示C语言->汇编->内嵌汇编的转换

C版本:

int32_t c_macc(const int32_t *pA, const int32_t *pB, int32_t colCnt)
{
    /* init the sum with bias */
    int32_t sum = 0;

    while (colCnt)
    {
        sum += *(pA++) * *(pB++);  
        colCnt--;
    }

    /* return the new output pointer with offset */
    return sum;
}

汇编:

新建一个文件:a_macc.S,将C语言版本的elf文件反汇编,得到汇编代码,加上文件头,稍加修改后贴到a_macc.S中

	.file	"a_macc.S"
	.option nopic
	.attribute unaligned_access, 0
	.attribute stack_align, 16
	.text
	.align	1
	.globl	a_macc
	.type	a_macc, @function
a_macc:
	mv	a5,a0
	beq	a2,zero,.L4
	slli	a4,a2,32
	srli	a2,a4,30
	add	a2,a0,a2
	li	a0,0
.L3:
	lw	a4,0(a5)
	lw	a3,0(a1)
	addi	a5,a5,4
	addi	a1,a1,4
	mulw	a4,a4,a3
	addw	a0,a4,a0
	bne	a2,a5,.L3
	ret
.L4:
	li	a0,0
	ret
	.size	a_macc, .-a_macc
	.ident	"GCC: (GNU) 10.2.0"

调用时,需在C代码里声明,然后使用:

extern int32_t a_macc(const int32_t *pA, const int32_t *pB, int32_t colCnt);
void main(void)
{
	int32_t a[100], b[100];
	int32_t sum;
	sum = a_macc(a, b, 100);
}

内嵌汇编:

int32_t inline_macc(const int32_t *pA, const int32_t *pB, int32_t colCnt)
{
    q31_t sum = 0;
    colCnt *= 4;
    asm volatile(
                "sub a7, a7, a7;\
                mv s0, %1;\
                mv s1, %2;\
                add	a2, s1, %3;\
                mv	a5, s1;\
                loop:;\
                lw	a4, 0(a5);\
                addi	a5,a5,4;\
                lw	a3,0(s0);\
                addi	s0,s0,4;\
                mulw	a4,a4,a3;\
                addw	a7,a7,a4;\
                mv	%0,a7;\
                bne	a2, a5, loop"
                :"=r" (sum) // %0
                :"r"(pA),   // %1
                "r"(pB),    // %2
                "r"(colCnt) // %3
                : "s0", "s1"
    );
    return sum;
}

以上几种写法是等价的。

参考:

  1. https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm
  2. GCC-Inline-Assembly-HOWTO
  3. riscv下的GCC内联汇编
  4. 关于Linux进程切换switch_to宏的一个细节(认识内联汇编)
posted @ 2022-05-18 22:04  sureZ_ok  阅读(5181)  评论(0编辑  收藏  举报