跳到主要内容

1. 概述

在Linux和opensbi源码中经常可以看到C语言中嵌入了汇编代码,这些汇编代码被称为Inline ASM,翻译过来就是内联汇编,也被称为嵌入式汇编。

2. 使用内联汇编的目的

使用内联汇编的目的有以下几个:

  1. 提高执行效率。
  2. 部分指令无法通过C语言编写,例如ecall等指令。
  3. 减少.S汇编文件的使用。

3. 内联汇编的格式

内联汇编的基本格式如下,共四个部分:汇编部分、输出部分、输入部分、修改部分。

各部分之间使用冒号隔开,汇编部分是必须存在的,其他三部分是可以为空的,但纵使为空,也需要用冒号隔开。

__asm__ __volatile__("汇编部分"

: 输出部分

: 输入部分

: 修改部分);

4. 嵌入式汇编应用

4.1 应用一

static inline void wait_for_interrupt(void)
{
__asm__ __volatile__ ("wfi");
}

上述示例摘自linux内核源码arch/riscv/include/asm/processor.h,作用为让CPU进入等待中断的状态。

由于wfi指令的作用比较单纯,所以括号内只存在汇编部分。

4.2 应用二

static inline void local_flush_icache_all(void)
{
asm volatile ("fence.i" ::: "memory");
}

上述示例摘自linux内核源码arch/riscv/include/asm/cacheflush.h,作用为刷指令cache。

可以看到括号内包含了汇编部分和修改部分,由于fence.i会修改内存相关(指令cache)的状态,所以在修改部分添加了“memory”关键字。

4.3 应用三

#define csr_read(csr) \
({ \
register unsigned long __v; \
__asm__ __volatile__ ("csrr %0, " __ASM_STR(csr) \
: "=r" (__v) : \
: "memory"); \
__v; \
})

上述示例摘自linux内核源码arch/riscv/include/asm/csr.h,用于RISC-V架构的控制系统和状态寄存器(CSR)的定义和声明。

%0:在内联汇编中,%0 表示输出操作数的第一个(也是唯一一个在这个例子中)占位符。GCC 使用这种格式来引用 C 代码中定义的变量,这些变量将用于汇编指令中。在这个宏里,%0 对应于 __v,这是一个 register unsigned long 类型的变量,用于存储从 CSR 读取的值。

=r:是一个约束字符串,用于描述输出操作数的约束条件。

  • =:表示这是一个输出操作数。这意味着该操作数在汇编指令执行后会被修改,并且其值会返回到 C 代码中。

  • r:表示该操作数应该使用一个通用寄存器来存储。GCC 会选择一个合适的寄存器来存放 __v 的值,并在汇编指令中使用这个寄存器。

4.3 应用四

struct sbiret __sbi_ecall(unsigned long arg0, unsigned long arg1,
unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5,
int fid, int ext)
{
struct sbiret ret;

trace_sbi_call(ext, fid);

register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0);
register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1);
register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2);
register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3);
register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4);
register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5);
register uintptr_t a6 asm ("a6") = (uintptr_t)(fid);
register uintptr_t a7 asm ("a7") = (uintptr_t)(ext);
asm volatile ("ecall"
: "+r" (a0), "+r" (a1)
: "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7)
: "memory");
ret.error = a0;
ret.value = a1;

trace_sbi_return(ext, ret.error, ret.value);

return ret;
}

上述示例摘自linux内核源码arch/riscv/kernel/sbi_ecall.c,调用SBI(Supervisor Binary Interface)接口的桥梁。

register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0):

  • 声明一个寄存器变量:通过 register 关键字,声明了一个名为 a0 的变量,这个变量将会存储在 CPU 的寄存器中,而不是内存里。这通常用于需要快速访问的变量,比如传递给函数的参数或者循环计数器。

  • 指定寄存器类型:uintptr_t 是一个无符号整数类型,其大小足以存储一个指针的值。这里用它来确保 a0 能够存储任何有效的内存地址。

  • 指定具体的寄存器:asm ("a0") 是一个 GCC 特定的扩展,用于告诉编译器 a0 变量应该存储在 RISC-V 架构的 a0 寄存器中。这是因为在 RISC-V 架构中,a0 到 a7 寄存器通常用于传递函数参数。

  • 初始化变量:(uintptr_t)(arg0) 将函数参数 arg0 的值转换为 uintptr_t 类型,并初始化 a0 变量。

r:表示该输入操作数可以放在任何通用寄存器中。编译器会选择一个合适的寄存器来存储这个操作数的值。

+r:与 r 类似,但多了一个加号(+),表示这个操作数既是输入也是输出。也就是说,这个寄存器在汇编指令执行前后都会被使用,并且其值可能会发生变化。编译器需要确保在执行汇编指令后,这个寄存器的值能够正确地反映回 C 代码中的变量。

5. 示例

// 引入必要的头文件,这些文件可能定义了CSR寄存器的访问方法、中断处理以及SBI(Supervisor Binary Interface)的相关函数
#include "asm/csr.h"
#include "asm/irq.h"
#include "asm/sbi.h"
#include "printk.h"

// 声明一个外部函数trap_init,它可能用于初始化异常或中断陷阱
extern void trap_init(void);

// 定义CSR_SCAUSE,这是导致最近一次异常或中断的原因的CSR寄存器地址
#define CSR_SCAUSE 0x142

// 定义一个宏__ASM_STR,用于将宏参数转换为字符串字面量
// 这在编写内联汇编代码时非常有用,因为它允许我们动态地插入寄存器名或指令
#define __ASM_STR(x) #x

// 定义一个宏csr_read,用于读取指定的CSR寄存器值
// 它使用GCC的内联汇编功能来实现
#define csr_read(csr) \
({ \
register unsigned long __v; \
__asm__ __volatile__ ("csrr %0, " __ASM_STR(csr) \
: "=r" (__v) : \
: "memory"); \
__v; \
})

// 定义一个静态内联函数local_flush_icache_all,用于刷新指令缓存
// 在RISC-V中,使用fence.i指令可以确保之前的所有加载和存储操作在指令缓存被刷新之前完成
static inline void local_flush_icache_all(void)
{
asm volatile ("fence.i" ::: "memory");
}

// 主函数入口
int main(void)
{
// 调用trap_init函数来初始化异常或中断陷阱
trap_init();

// 启用本地中断
// 注意:arch_local_irq_enable可能是一个在asm/irq.h中定义的宏或函数
// 它可能会修改sstatus寄存器来启用中断
arch_local_irq_enable();

// 使用csr_read宏读取scause寄存器的值
// scause寄存器包含了导致最近一次异常或中断的原因
unsigned long scause = csr_read(CSR_SCAUSE);

// 使用printk函数打印scause寄存器的值
// printk可能是一个类似于printf的函数,但它是为嵌入式系统或内核环境设计的
printk("Inline ASM example: csr read scause 0x%lu\n", scause);

// 打印一条消息表示即将刷新指令缓存
printk("Inline ASM exmaple: flush icache\n");

// 调用local_flush_icache_all函数来刷新指令缓存
local_flush_icache_all();

// 在嵌入式系统中,主函数通常包含一个无限循环,以等待事件或中断
while (1) {}

return 0;
}

5.1 进入示例

下载实验代码-Embedded_assembly

下载解压并进入示例目录

5.2 编译

make

5.3 运行

make run

运行LOG如下

qemu-system-riscv64 -nographic -machine virt -m 128M -bios m_mode.bin -device loader,file=s_mode.bin,addr=0x80200000 -kernel s_mode.elf
_____ ____ _____
/ ____| _ \_ _|
| (___ | |_) || |
\ \___ \| _ < | |
____) | |_) || |_
|_|_____/|____/_____|
stvec=0x8020097c, 0x8020097c
Inline ASM example: csr read scause 0x0
Inline ASM exmaple: flush icache

详见请参考文档:实验环境搭建。