RISCV操作系统-CH05-RISC-V-汇编
第5章 RISC-V 汇编语言编程
目录
参考文献
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA, Document Version 20191213
- Using as: https://sourceware.org/binutils/docs/as/
- 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(指令): 直接对应二进制机器指令的字符串,如
add
、lw
等 - pseudo-instruction(伪指令): 为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instructions),如
li
(load immediate)实际会被转换为lui
和addi
的组合 - directive(指示/伪操作): 通过类似指令的形式(以”.”开头),通知汇编器如何控制代码的产生等,不对应具体的指令,如
.text
、.data
等 - macro: 采用 .macro/.endm 自定义的宏,类似于C语言中的宏定义
- instruction(指令): 直接对应二进制机器指令的字符串,如
- 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 指令可以分为以下几类:
- 算术运算指令(如 ADD, SUB, ADDI 等):执行加减运算
- 逻辑运算指令(如 XOR, OR, AND 等):执行位操作
- 移位运算指令(如 SLL, SRL, SRA 等):执行位的左移和右移操作
- 内存读写指令(如 LB, LW, SB, SW 等):在内存和寄存器之间传输数据
- 分支与跳转指令(如 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) |
注意:
- 逻辑移位时补充的位为 0,无论原操作数是正数还是负数
- 算术右移时按照符号位值补足,保持数值的符号
- 没有算术左移指令,因为逻辑左移就可以实现等效功能(左移总是在低位补0)
- 移位常用于乘除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 |
注意:
- Load 指令分为有符号和无符号两种,有符号指令会进行符号扩展,无符号指令进行零扩展
- Store 指令不区分有符号和无符号,因为只是简单地复制低位数据
- 内存地址 = 基址寄存器 + 偏移量,偏移量范围是 [-2048, 2047]
- 所有内存访问必须对齐,除非处理器支持非对齐访问(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,不需要存储。
无条件跳转指令
JAL(Jump And Link)
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字节)
JALR(Jump And Link Register)
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,但不保存返回地址,实现尾递归优化 |
注意:
- 长距离跳转可以使用 AUIPC+JALR 组合实现,克服了单条指令跳转范围的限制
- 尾调用是一种优化技术,用于减少函数调用的栈开销。当一个函数的最后操作是调用另一个函数时,可以直接跳转而不创建新的栈帧
- 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 汇编函数调用约定
函数调用过程概述
函数调用涉及栈操作,包括:
- 栈的操作:
- 压栈(Push):将数据保存到栈上,栈指针减小(RISC-V栈是向下生长的)
- 出栈(Pop):从栈上取回数据,栈指针增大
- RISC-V中通常使用
addi sp, sp, -16
来为栈分配16字节空间,然后用sw
存储数据
- 栈帧:
- 每个函数调用都有自己的栈帧(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)中的地址
函数调用过程中实现被调用函数的编程约定
函数实现通常包含以下部分:
-
函数起始部分(Prologue):
- 减少 sp 的值,为函数的栈帧分配空间
- 将需要保存的寄存器(如 s0-s11,ra)存入栈中
-
示例:
function: addi sp, sp, -16 # 分配16字节栈空间 sw ra, 12(sp) # 保存返回地址 sw s0, 8(sp) # 保存s0(帧指针) addi s0, sp, 16 # 设置帧指针
-
函数执行体:
- 函数的主要计算逻辑
- 使用传入的参数(a0-a7寄存器)
- 准备返回值(a0-a1寄存器)
-
函数退出部分(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” 作为分隔符,以确保汇编代码格式正确