Post

RISCV操作系统-CH05-RISC-V-汇编

RISCV操作系统-CH05-RISC-V-汇编

第5章 RISC-V 汇编语言编程

目录

参考文献

  1. The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA, Document Version 20191213
  2. Using as: https://sourceware.org/binutils/docs/as/
  3. How to Use Inline Assembly Language in C Code: https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html

RISC-V 汇编语言入门

汇编语言概念简介

汇编语言(Assembly Language)是一种”低级”语言,具有以下特点:

  • 缺点
    • 难读:指令和寄存器操作抽象层次低,不如高级语言直观
    • 难写:需要手动管理寄存器和内存,没有自动内存管理
    • 难移植:特定于处理器架构,无法跨平台运行
  • 优点
    • 灵活:可以直接控制硬件资源和指令执行
    • 强大:能够实现高性能和底层控制
  • 应用场景
    • 需要直接访问底层硬件的地方(如操作系统内核、设备驱动)
    • 需要对性能执行极致优化的地方(如高性能计算、实时系统)
    • 资源受限的嵌入式系统

汇编语言语法介绍(GNU 版本)

.s, .S都是汇编文件的后缀。区别是S包含宏的汇编文件,需要预处理。

一个完整的 RISC-V 汇编程序由多条语句(statement)组成。

一条典型的 RISC-V 汇编语句由 3 部分组成:

1
[label:] [operation] [comment]
  • label(标号): GNU汇编中,任何以冒号结尾的标识符都被认为是一个标号。标号用于标记代码位置,供跳转指令引用。
  • operation 可以有以下多种类型:
    • instruction(指令): 直接对应二进制机器指令的字符串,如addlw
    • pseudo-instruction(伪指令): 为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instructions),如li(load immediate)实际会被转换为luiaddi的组合
    • directive(指示/伪操作): 通过类似指令的形式(以”.”开头),通知汇编器如何控制代码的产生等,不对应具体的指令,如.text.data
    • macro: 采用 .macro/.endm 自定义的宏,类似于C语言中的宏定义
  • comment(注释): 常用方式,”#” 开始到当前行结束。用于解释代码的用途和工作原理。

RISC-V 汇编指令总览

RISC-V 汇编指令操作对象

寄存器

  • 32个通用寄存器,x0 ~ x31(注意:本章节课程仅涉及 RV32I 的通用寄存器组)
  • 在 RISC-V 中,Hart(硬件线程)在执行算术逻辑运算时所操作的数据必须直接来自寄存器
  • 每个寄存器在RISC-V 32位架构中都是32位宽度(RV32I)
  • 特殊寄存器x0始终保持值为0,无论写入什么值

重要的寄存器命名约定(ABI名称)

  • x0: zero - 永远返回0
  • x1: ra - 返回地址(函数调用返回点)
  • x2: sp - 栈指针(指向栈顶)
  • x5-x7, x28-x31: t0-t6 - 临时寄存器
  • x8-x9, x18-x27: s0-s11 - 保存寄存器
  • x10-x11: a0-a1 - 函数参数/返回值
  • x12-x17: a2-a7 - 函数参数

内存

  • Hart 可以执行在寄存器和内存之间的数据读写操作
  • 读写操作使用字节(Byte)为基本单位进行寻址
  • RV32 可以访问最多 2^32 个字节(4GB)的内存空间
  • 内存访问必须通过加载(load)和存储(store)指令实现,没有直接操作内存的算术指令

RISC-V 汇编指令编码格式

  • 指令长度:ILEN1 = 32 bits (RV32I)
  • 指令对齐:IALIGN = 32 bits (RV32I),即所有指令必须在4字节边界上对齐
  • 32 个 bit 划分成不同的 “域(field)”,用于指定操作码、寄存器、立即数等
  • funct3/funct7 和 opcode 一起决定最终的指令类型
  • 指令在内存中按照小端序排列,即低字节存储在低地址

小端序的概念

  • 主机字节序(HBO - Host Byte Order)指一个多字节整数在计算机内存中存储的字节顺序
  • 不同类型 CPU 的 HBO 不同,这与 CPU 的设计有关,分为大端序(Big-Endian)和小端序(Little-Endian)
  • 大端序:数据的高位字节存放在内存的低地址,更符合人类的阅读习惯
  • 小端序:数据的低位字节存放在内存的低地址,RISC-V采用此方式

示例:存储32位整数0x12345678

  • 大端序:内存地址递增方向 [12][34][56][78]
  • 小端序:内存地址递增方向 [78][56][34][12]

RISC-V 汇编指令编码格式

RISC-V 指令格式有 6 种:

指令格式 说明 典型用途
R-type Register 格式,每条指令中有三个 fields,用于指定 3 个寄存器参数 寄存器间运算,如add、sub
I-type Immediate 格式,每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为 12 bits) 立即数运算和加载操作,如addi、lw
S-type Store 格式,每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为 12 bits,但 fields 的组织方式不同于 I-type) 存储指令,如sw、sb
B-type Branch 格式,每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为 12 bits,但取值为 2 的倍数) 条件分支,如beq、bne
U-type Upper 格式,每条指令含有一个寄存器参数再加上一个立即数参数(宽度为 20 bits,用于表示一个立即数的高 20 位) 加载高位立即数,如lui、auipc
J-type Jump 格式,每条指令含有一个寄存器参数再加上一个立即数参数(宽度为 20 bits) 无条件跳转,如jal

RISC-V 汇编指令分类

RISC-V 指令可以分为以下几类:

  1. 算术运算指令(如 ADD, SUB, ADDI 等):执行加减运算
  2. 逻辑运算指令(如 XOR, OR, AND 等):执行位操作
  3. 移位运算指令(如 SLL, SRL, SRA 等):执行位的左移和右移操作
  4. 内存读写指令(如 LB, LW, SB, SW 等):在内存和寄存器之间传输数据
  5. 分支与跳转指令(如 BEQ, BNE, JAL 等):控制执行流程

注:部分指令因篇幅原因未列出,如 Compare/Synch/Change Level 指令等。

RISC-V 汇编伪指令一览

伪指令是为了提高编程效率,由汇编器转换成一条或多条实际指令的指令。RISC-V 常用伪指令包括:

伪指令 等价指令 含义 示例使用
LI RD, IMM LUI 和 ADDI 的组合 将立即数 IMM 加载到 RD 中 li x5, 1000 将数值1000加载到x5
LA RD, LABEL AUIPC 和 ADDI 的组合 为 RD 加载一个地址值 la x5, data_start 加载标签地址到x5
NEG RD, RS SUB RD, x0, RS 对 RS 中的值取反并将结果存放在 RD 中 neg x5, x6 等价于 x5 = -x6
MV RD, RS ADDI RD, RS, 0 将 RS 中的值拷贝到 RD 中 mv x5, x6 将x6的值复制到x5
NOP ADDI x0, x0, 0 什么也不做 nop 空操作,常用于延时或对齐

RISC-V 汇编指令详解

算术运算指令

ADD 指令

1
ADD RD, RS1, RS2
  • 格式:R-type
  • 功能:RD = RS1 + RS2,将两个寄存器中的值相加,结果放入目标寄存器
  • 示例add x5, x6, x7 执行后 x5 = x6 + x7
    • 如果x6=10,x7=20,执行后x5=30

SUB 指令

1
SUB RD, RS1, RS2
  • 格式:R-type
  • 功能:RD = RS1 - RS2,用第一个源寄存器的值减去第二个源寄存器的值,结果放入目标寄存器
  • 示例sub x5, x6, x7 执行后 x5 = x6 - x7
    • 如果x6=30,x7=12,执行后x5=18

ADDI 指令

1
ADDI RD, RS1, IMM
  • 格式:I-type
  • 功能:RD = RS1 + IMM,将寄存器中的值与立即数相加,结果放入目标寄存器
  • 示例addi x5, x6, 1 执行后 x5 = x6 + 1
    • 如果x6=15,执行后x5=16
  • 注意:IMM 是 12 位有符号数,范围 [-2048, 2047],在运算前会进行符号扩展

LUI 指令(Load Upper Immediate)

1
LUI RD, IMM
  • 格式:U-type
  • 功能:将 20 位立即数左移 12 位(低 12 位置零)后写入 RD,即 RD = IMM « 12
  • 示例lui x5, 0x12345 执行后 x5 = 0x12345000
  • 用途:用于构建32位常数的高20位部分

AUIPC 指令(Add Upper Immediate to PC)

1
AUIPC RD, IMM
  • 格式:U-type
  • 功能:将 20 位立即数左移 12 位后与 PC 值相加,结果存入 RD,即 RD = PC + (IMM « 12)
  • 示例auipc x5, 0x12345 执行后 x5 = 0x12345000 + PC
  • 用途:用于创建PC相对地址,适合位置无关代码

基于算术运算指令实现的伪指令

伪指令 等价指令 功能 示例与解释
LI RD, IMM LUI+ADDI 组合 将任意 32 位立即数加载到寄存器 li x5, 0x12345678:如果立即数较大,会被拆分为加载高20位和低12位两步操作
MV RD, RS ADDI RD, RS, 0 寄存器间的值拷贝 mv x5, x6:将x6的值复制到x5
NEG RD, RS SUB RD, x0, RS 对值取反 neg x5, x6:x5 = -x6,即x5 = 0 - x6
NOP ADDI x0, x0, 0 空操作 nop:什么都不做,但占用一个指令周期

逻辑运算指令

RISC-V 提供了以下逻辑运算指令:

指令 格式 功能 示例与解释
AND R-type 按位与:RD = RS1 & RS2 and x5, x6, x7:x5中每个位是x6和x7对应位的逻辑与结果
例如,若x6=0b1010,x7=0b1100,则结果x5=0b1000
OR R-type 按位或:RD = RS1 | RS2 or x5, x6, x7:x5中每个位是x6和x7对应位的逻辑或结果
例如,若x6=0b1010,x7=0b1100,则结果x5=0b1110
XOR R-type 按位异或:RD = RS1 ^ RS2 xor x5, x6, x7:x5中每个位是x6和x7对应位的异或结果
例如,若x6=0b1010,x7=0b1100,则结果x5=0b0110
ANDI I-type 立即数按位与:RD = RS1 & IMM andi x5, x6, 20:x5 = x6 & 20
例如,若x6=0b11111,则结果x5=0b10100 (20的二进制)
ORI I-type 立即数按位或:RD = RS1 | IMM ori x5, x6, 20:x5 = x6 | 20
例如,若x6=0b01001,则结果x5=0b11101
XORI I-type 立即数按位异或:RD = RS1 ^ IMM xori x5, x6, 20:x5 = x6 ^ 20
例如,若x6=0b11011,则结果x5=0b01111

基于逻辑运算的伪指令:

伪指令 等价指令 功能 示例与解释
NOT XORI RD, RS, -1 按位取反:RD = ~RS not x5, x6:对x6的每一位取反,存入x5
例如,若x6=0b10101010,则结果x5=0b01010101

移位运算指令

RISC-V 提供以下移位运算指令:

逻辑移位

指令 格式 功能 示例与解释
SLL R-type 逻辑左移:RD = RS1 « RS2(低位补0) sll x5, x6, x7:将x6左移x7位,结果存入x5
例如,若x6=5(0b101),x7=2,则结果x5=20(0b10100)
SRL R-type 逻辑右移:RD = RS1 » RS2(高位补0) srl x5, x6, x7:将x6右移x7位,结果存入x5
例如,若x6=20(0b10100),x7=2,则结果x5=5(0b101)
SLLI I-type 立即数逻辑左移:RD = RS1 « IMM slli x5, x6, 3:将x6左移3位,结果存入x5
例如,若x6=2,则结果x5=16(2 * 2^3)
SRLI I-type 立即数逻辑右移:RD = RS1 » IMM srli x5, x6, 3:将x6右移3位,结果存入x5
例如,若x6=40,则结果x5=5(40 / 2^3)

算术移位

指令 格式 功能 示例与解释
SRA R-type 算术右移:RD = RS1 » RS2(保持符号位,高位补符号位) sra x5, x6, x7:将x6算术右移x7位,结果存入x5
例如,若x6=-16(0xFFFFFFF0),x7=2,则结果x5=-4(0xFFFFFFFC)
SRAI I-type 立即数算术右移:RD = RS1 » IMM(保持符号位) srai x5, x6, 3:将x6算术右移3位,结果存入x5
例如,若x6=-40,则结果x5=-5(-40 / 2^3)

注意

  1. 逻辑移位时补充的位为 0,无论原操作数是正数还是负数
  2. 算术右移时按照符号位值补足,保持数值的符号
  3. 没有算术左移指令,因为逻辑左移就可以实现等效功能(左移总是在低位补0)
  4. 移位常用于乘除2的幂:左移一位相当于乘2,右移一位相当于除2

内存读写指令

内存读(Load)指令

指令 格式 功能 示例与解释
LB I-type 从内存加载有符号字节到寄存器:RD = SignExt(MEM[RS1+offset]) lb x5, 40(x6):从地址x6+40读取一个字节,进行符号扩展后存入x5
例如,若内存中为0xFF,则x5=0xFFFFFFFF(-1)
LBU I-type 从内存加载无符号字节到寄存器:RD = ZeroExt(MEM[RS1+offset]) lbu x5, 40(x6):从地址x6+40读取一个字节,进行零扩展后存入x5
例如,若内存中为0xFF,则x5=0x000000FF(255)
LH I-type 从内存加载有符号半字(16位)到寄存器:RD = SignExt(MEM[RS1+offset]) lh x5, 40(x6):从地址x6+40读取两个字节,进行符号扩展后存入x5
例如,若内存中为0xFFFF,则x5=0xFFFFFFFF(-1)
LHU I-type 从内存加载无符号半字(16位)到寄存器:RD = ZeroExt(MEM[RS1+offset]) lhu x5, 40(x6):从地址x6+40读取两个字节,进行零扩展后存入x5
例如,若内存中为0xFFFF,则x5=0x0000FFFF(65535)
LW I-type 从内存加载字(32位)到寄存器:RD = MEM[RS1+offset] lw x5, 40(x6):从地址x6+40读取四个字节(一个字)存入x5
例如,若地址x6+40处存储值为0x12345678,则x5=0x12345678

内存写(Store)指令

指令 格式 功能 示例与解释
SB S-type 将寄存器中的字节存储到内存:MEM[RS1+offset] = RS2[7:0] sb x5, 40(x6):将x5的最低字节存储到地址x6+40
例如,若x5=0x12345678,则地址x6+40处将存储0x78
SH S-type 将寄存器中的半字存储到内存:MEM[RS1+offset] = RS2[15:0] sh x5, 40(x6):将x5的最低两个字节存储到地址x6+40
例如,若x5=0x12345678,则地址x6+40处将存储0x5678
SW S-type 将寄存器中的字存储到内存:MEM[RS1+offset] = RS2 sw x5, 40(x6):将x5的四个字节(一个字)存储到地址x6+40
例如,若x5=0x12345678,则地址x6+40处将存储0x12345678

注意

  1. Load 指令分为有符号和无符号两种,有符号指令会进行符号扩展,无符号指令进行零扩展
  2. Store 指令不区分有符号和无符号,因为只是简单地复制低位数据
  3. 内存地址 = 基址寄存器 + 偏移量,偏移量范围是 [-2048, 2047]
  4. 所有内存访问必须对齐,除非处理器支持非对齐访问(LW和SW地址应为4的倍数)

条件分支指令

指令 格式 功能 示例与解释
BEQ B-type 相等则分支:if (RS1 == RS2) PC += offset beq x5, x6, label:如果x5等于x6,则跳转到label处
例如,若x5=10,x6=10,则跳转到label指定的地址
BNE B-type 不相等则分支:if (RS1 != RS2) PC += offset bne x5, x6, label:如果x5不等于x6,则跳转到label处
例如,若x5=10,x6=20,则跳转到label指定的地址
BLT B-type 小于则分支(有符号):if (RS1 < RS2) PC += offset blt x5, x6, label:如果x5小于x6(有符号比较),则跳转到label处
例如,若x5=-5,x6=10,则跳转到label指定的地址
BLTU B-type 小于则分支(无符号):if (RS1 < RS2) PC += offset bltu x5, x6, label:如果x5小于x6(无符号比较),则跳转到label处
例如,若x5=0xFFFFFFFF(视为无符号数4294967295),x6=10,则不跳转
BGE B-type 大于等于则分支(有符号):if (RS1 >= RS2) PC += offset bge x5, x6, label:如果x5大于等于x6(有符号比较),则跳转到label处
例如,若x5=20,x6=10,则跳转到label指定的地址
BGEU B-type 大于等于则分支(无符号):if (RS1 >= RS2) PC += offset bgeu x5, x6, label:如果x5大于等于x6(无符号比较),则跳转到label处
例如,若x5=10,x6=0xFFFFFFFF(视为无符号数4294967295),则不跳转

条件分支指令的伪指令:

伪指令 等价指令 功能 示例与解释
BEQZ BEQ RS, x0, offset 等于零则分支:if (RS == 0) PC += offset beqz x5, label:如果x5等于0,则跳转到label处
BNEZ BNE RS, x0, offset 不等于零则分支:if (RS != 0) PC += offset bnez x5, label:如果x5不等于0,则跳转到label处
BLTZ BLT RS, x0, offset 小于零则分支:if (RS < 0) PC += offset bltz x5, label:如果x5小于0,则跳转到label处
BLEZ BGE x0, RS, offset 小于等于零则分支:if (RS <= 0) PC += offset blez x5, label:如果x5小于等于0,则跳转到label处
BGTZ BLT x0, RS, offset 大于零则分支:if (RS > 0) PC += offset bgtz x5, label:如果x5大于0,则跳转到label处
BGEZ BGE RS, x0, offset 大于等于零则分支:if (RS >= 0) PC += offset bgez x5, label:如果x5大于等于0,则跳转到label处

注意:分支指令的目标地址计算方法是将偏移量乘以 2,然后与 PC 相加,跳转范围是 [-4096, 4094] 字节。这是因为B型指令的立即数字段实际只有12位,且指令总是对齐到2字节,所以最低位总是0,不需要存储。

无条件跳转指令

1
JAL RD, label
  • 格式:J-type
  • 功能:将当前 PC+4 存入 RD(通常是 x1/ra),然后跳转到目标地址 PC + offset
  • 示例jal x1, function 调用函数,将返回地址存入x1,跳转到function标签处
    • 假设当前PC=0x1000,function位于0x1500,则执行后x1=0x1004,PC=0x1500
  • 跳转范围:以 PC 为基准,±1MB(因为J型指令的立即数字段有20位,左移1位后可表示±2^20字节)
1
JALR RD, offset(RS1)
  • 格式:I-type
  • 详细操作:将当前 PC+4 存入 RD,然后跳转到 RS1+offset 的地址,即 RD = PC+4; PC = (RS1 + offset) & ~1
  • 示例jalr x1, 0(x5) 通过寄存器间接跳转,将返回地址存入x1,跳转到x5的值指向的地址
    • 假设当前PC=0x1000,x5=0x1500,则执行后x1=0x1004,PC=0x1500
  • 跳转范围:以 RS1 为基准,±2KB(因为I型指令的立即数字段有12位,可表示±2^11字节)
  • 用途:实现函数返回、间接跳转和计算地址的跳转

无条件跳转指令的伪指令:

伪指令 等价指令 功能 示例与解释
J JAL x0, offset 无返回跳转:PC += offset j label:无条件跳转到label处,不保存返回地址
相当于jal x0, label,因为x0寄存器忽略写入
JR JALR x0, 0(rs) 无返回寄存器跳转:PC = rs jr x5:无条件跳转到x5寄存器存储的地址,不保存返回地址
相当于jalr x0, 0(x5)
RET JALR x0, 0(x1) 函数返回:PC = x1 ret:从函数返回,跳转到x1(返回地址)寄存器存储的地址
相当于jalr x0, 0(x1)
CALL AUIPC+JALR组合 长距离调用:保存返回地址并跳转到目标 call function:调用远距离函数
被汇编为AUIPC+JALR序列,可调用±2GB范围内的函数
TAIL AUIPC+JALR组合 长距离尾调用:不保存返回地址,直接跳转到目标 tail function:尾调用远距离函数
类似call,但不保存返回地址,实现尾递归优化

注意

  1. 长距离跳转可以使用 AUIPC+JALR 组合实现,克服了单条指令跳转范围的限制
  2. 尾调用是一种优化技术,用于减少函数调用的栈开销。当一个函数的最后操作是调用另一个函数时,可以直接跳转而不创建新的栈帧
  3. JAL与JALR的主要区别:JAL使用PC相对寻址,JALR使用寄存器值作为基址

RISC-V 指令寻址模式总结

寻址模式 解释 典型指令示例 具体解释
立即数寻址 操作数是指令本身的一部分 addi x5, x6, 20 操作数20直接包含在指令中
寄存器寻址 操作数存放在寄存器中 add x5, x6, x7 操作数来自寄存器x6和x7
基址寻址 地址 = 基址寄存器 + 偏移量 lw x5, 40(x6) 内存地址为x6+40,加载该地址的值到x5
PC 相对寻址 地址 = PC + 偏移量 beq x5, x6, label 如果条件满足,跳转到PC+偏移量处

RISC-V 汇编函数调用约定

函数调用过程概述

函数调用涉及栈操作,包括:

  1. 栈的操作
    • 压栈(Push):将数据保存到栈上,栈指针减小(RISC-V栈是向下生长的)
    • 出栈(Pop):从栈上取回数据,栈指针增大
    • RISC-V中通常使用addi sp, sp, -16来为栈分配16字节空间,然后用sw存储数据
  2. 栈帧
    • 每个函数调用都有自己的栈帧(Stack Frame)
    • 栈帧包含返回地址、保存的寄存器、局部变量等
    • 栈帧通常由栈指针(sp/x2)和帧指针(fp/s0/x8)界定

函数调用过程示例

# 调用者函数
save_registers:    # 保存调用者保存的寄存器
    addi sp, sp, -16
    sw ra, 12(sp)  # 保存返回地址
    sw t0, 8(sp)   # 保存临时寄存器
    sw t1, 4(sp)   # 保存临时寄存器
    
call_function:
    jal ra, function  # 调用函数
    
restore_registers:  # 恢复调用者保存的寄存器
    lw t1, 4(sp)
    lw t0, 8(sp)
    lw ra, 12(sp)
    addi sp, sp, 16
    ret

汇编编程时为何需要制定函数调用约定

函数调用约定(Calling Conventions)规定了函数间如何传递以下信息:

  • 调用参数:规定参数如何传递(通过寄存器或栈)
    • RISC-V使用a0-a7(x10-x17)寄存器传递前8个参数
    • 更多参数则通过栈传递
  • 返回地址:规定如何保存和恢复返回地址
    • RISC-V使用ra(x1)寄存器保存返回地址
  • 返回值:规定如何传递函数返回值
    • RISC-V使用a0-a1(x10-x11)寄存器传递返回值
  • 寄存器使用:规定寄存器的保存责任
    • 一些寄存器由调用者保存,另一些由被调用者保存

制定函数调用约定的原因

  • 确保互操作性:不同编译器或汇编程序员编写的函数可以互相调用
  • 优化执行效率:明确责任,避免不必要的寄存器保存
  • 简化调试:可预测的行为使调试更容易

函数调用过程中有关寄存器的编程约定

寄存器名 ABI 名 用途约定 保存责任 保存说明
x0 zero 读取时总为 0,写入时不起任何效果 N/A 不需要保存
x1 ra 存放函数返回地址 调用者 在调用前保存,如果被调用函数会调用其他函数
x2 sp 栈指针 被调用者 被调用者必须恢复栈指针到调用前的状态
x3 gp 全局指针 N/A 通常不变
x4 tp 线程指针 N/A 通常不变
x5-x7, x28-x31 t0-t6 临时寄存器,可能被被调用函数修改 调用者 如需保持值,调用者必须在调用前保存
x8-x9, x18-x27 s0-s11 保存寄存器,被调用函数必须保证不变 被调用者 被调用函数必须在返回前恢复原值
x10-x11 a0-a1 参数/返回值寄存器 调用者 用于传递前两个参数和返回值
x12-x17 a2-a7 参数寄存器 调用者 用于传递第3-8个参数

注意

  • “调用者保存”意味着如果调用者需要这些寄存器的值在调用后保持不变,必须自己负责保存(通常压入栈中)
  • “被调用者保存”意味着被调用函数必须保证这些寄存器的值在返回时与调用前相同(如修改了,必须先保存再恢复)
  • s0(x8)通常也用作帧指针(fp),指向当前栈帧的底部

函数调用过程中函数跳转和返回指令的编程约定

伪指令 等价指令 描述 实际操作
jal jal x1, offset 跳转到 offset,返回地址保存在 x1 将PC+4存入x1,然后PC += offset
jalr jalr x1, 0(rs) 跳转到 rs 寄存器值,返回地址保存在 x1 将PC+4存入x1,然后PC = rs + 0
call auipc + jalr 组合 长距离调用函数 保存返回地址并跳转到任意32位地址
ret jalr x0, 0(x1) 从函数返回 跳转到x1寄存器中存储的返回地址

函数调用示例

# 调用近距离函数
jal ra, function  # ra寄存器(x1)保存PC+4作为返回地址

# 调用远距离函数
call far_function

# 函数返回
ret  # 跳转到ra寄存器(x1)中的地址

函数调用过程中实现被调用函数的编程约定

函数实现通常包含以下部分:

  1. 函数起始部分(Prologue)

    • 减少 sp 的值,为函数的栈帧分配空间
    • 将需要保存的寄存器(如 s0-s11,ra)存入栈中
    • 示例:

        function:    addi sp, sp, -16     # 分配16字节栈空间    sw ra, 12(sp)        # 保存返回地址    sw s0, 8(sp)         # 保存s0(帧指针)    addi s0, sp, 16      # 设置帧指针
      
  2. 函数执行体

    • 函数的主要计算逻辑
    • 使用传入的参数(a0-a7寄存器)
    • 准备返回值(a0-a1寄存器)
  3. 函数退出部分(Epilogue)

    • 从栈中恢复保存的寄存器
    • 如果保存了 ra,需要恢复
    • 增加 sp 的值,释放栈帧
    • 使用 ret 指令返回
    • 示例:

        function_end:    lw s0, 8(sp)         # 恢复s0    lw ra, 12(sp)        # 恢复返回地址    addi sp, sp, 16      # 释放栈空间    ret                  # 返回到调用点
      

完整函数示例

# 计算两数之和的函数
add_numbers:
    # Prologue
    addi sp, sp, -8        # 分配栈空间
    sw s0, 4(sp)           # 保存s0
    sw ra, 0(sp)           # 保存返回地址
    
    # 函数主体(计算a0和a1的和)
    add a0, a0, a1         # 结果放在a0中作为返回值
    
    # Epilogue
    lw ra, 0(sp)           # 恢复返回地址
    lw s0, 4(sp)           # 恢复s0
    addi sp, sp, 8         # 释放栈空间
    ret                    # 返回

RISC-V 汇编与 C 混合编程

RISC-V 汇编调用 C 函数

要从汇编代码调用 C 函数,需要遵循 ABI(Application Binary Interface)规定:

  • 参数传递:使用 a0-a7 寄存器传递前 8 个参数,更多参数使用栈
  • 返回值:使用 a0-a1 寄存器传递返回值
  • 其他:遵循调用者/被调用者保存的寄存器约定
  • 栈对齐:确保调用前栈指针16字节对齐

汇编调用C函数示例

# 从汇编调用C函数 int add(int a, int b)
.global main
main:
    # 函数prologue
    addi sp, sp, -16
    sw ra, 12(sp)
    sw s0, 8(sp)
    addi s0, sp, 16
    
    # 准备参数并调用函数
    li a0, 10          # 第一个参数:10
    li a1, 20          # 第二个参数:20
    call add           # 调用add函数
    
    # 现在a0包含返回值
    mv s1, a0          # 保存返回值到s1
    
    # 函数epilogue
    lw s0, 8(sp)
    lw ra, 12(sp)
    addi sp, sp, 16
    ret

C 函数中嵌入 RISC-V 汇编

GCC 允许在 C 代码中内嵌汇编代码,使用 asm 关键字:

1
2
3
4
5
6
asm [volatile] (
    "汇编指令"
    : 输出操作数列表(可选)
    : 输入操作数列表(可选)
    : 可能影响的寄存器或者存储器(可选)
);

内联汇编参数说明

  • volatile:防止编译器优化,确保汇编代码按指定顺序执行
  • 输出操作数:用”=r”表示输出到寄存器,”=m”表示输出到内存
  • 输入操作数:用”r”表示寄存器输入,”m”表示内存输入,”i”表示立即数
  • 可能影响的寄存器:告知编译器哪些寄存器或内存可能被修改,用于优化

例如:

1
2
3
4
5
6
7
8
9
10
int foo(int a, int b)
{
    int c;
    asm volatile (
        "add %0, %1, %2" 
        : "=r" (c)       // 输出操作数
        : "r" (a), "r" (b) // 输入操作数
    );
    return c;
}

或者使用命名操作数:

1
2
3
4
5
6
7
8
9
10
int foo(int a, int b)
{
    int c;
    asm volatile (
        "add %[sum], %[add1], %[add2]" 
        : [sum] "=r" (c)
        : [add1] "r" (a), [add2] "r" (b)
    );
    return c;
}

更复杂的内联汇编示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 计算两数乘积
long multiply(long a, long b)
{
    long result;
    asm volatile (
        "mul %[res], %[a_val], %[b_val]"
        : [res] "=r" (result)
        : [a_val] "r" (a), [b_val] "r" (b)
        : // 无其他寄存器被修改
    );
    return result;
}

// 带有多条指令的内联汇编
int complex_calc(int a, int b, int c)
{
    int result;
    asm volatile (
        "add t0, %[a_val], %[b_val]\n\t"  // 将a与b相加,存入临时寄存器t0
        "sub %[res], t0, %[c_val]"        // 将t0减去c,存入结果
        : [res] "=r" (result)
        : [a_val] "r" (a), [b_val] "r" (b), [c_val] "r" (c)
        : "t0"  // 告知编译器t0寄存器被修改
    );
    return result;
}

注意

  • 汇编指令用双引号括起来,多条指令用分号或换行符分隔
  • “=r” 表示输出寄存器,”r” 表示输入寄存器
  • 汇编代码中的 %0, %1, %2… 或者 %[name] 代表操作数
  • volatile 关键字告诉编译器不要优化此汇编语句
  • 在多条指令之间,应使用 “\n\t” 作为分隔符,以确保汇编代码格式正确
This post is licensed under CC BY 4.0 by the author.