RISC-V 基础知识扫盲

文章目录

  • [RISC-V 基础知识扫盲](#RISC-V 基础知识扫盲)
    • [1. RISC-V 基础概念](#1. RISC-V 基础概念)
      • [1.1 寄存器](#1.1 寄存器)
      • [1.2 指令格式](#1.2 指令格式)
    • [2. 算术指令](#2. 算术指令)
      • [2.1 ADD - 寄存器加法](#2.1 ADD - 寄存器加法)
      • [2.2 ADDI - 立即数加法](#2.2 ADDI - 立即数加法)
      • [2.3 ADD2 - 处理负数的加法](#2.3 ADD2 - 处理负数的加法)
      • [2.4 SUB - 寄存器减法](#2.4 SUB - 寄存器减法)
      • [2.5 SUBI - 立即数减法(伪指令)](#2.5 SUBI - 立即数减法(伪指令))
      • [2.6 NEG - 取负(伪指令)](#2.6 NEG - 取负(伪指令))
      • [2.7 LUI - 加载高位立即数](#2.7 LUI - 加载高位立即数)
      • [2.8 AUIPC - PC相对加立即数](#2.8 AUIPC - PC相对加立即数)
    • [3. 逻辑指令](#3. 逻辑指令)
      • [3.1 AND - 逻辑与](#3.1 AND - 逻辑与)
      • [3.2 ANDI - 立即数与](#3.2 ANDI - 立即数与)
      • [3.3 NOT - 位反(伪指令)](#3.3 NOT - 位反(伪指令))
    • [4. 移位指令](#4. 移位指令)
      • [4.1 SLLI - 逻辑左移](#4.1 SLLI - 逻辑左移)
      • [4.2 SRLI - 逻辑右移](#4.2 SRLI - 逻辑右移)
      • [4.3 SRAI - 算术右移](#4.3 SRAI - 算术右移)
    • [5. 跳转指令](#5. 跳转指令)
      • [5.1 BNE - 不相等分支](#5.1 BNE - 不相等分支)
      • [5.2 JALR - 跳转并链接寄存器](#5.2 JALR - 跳转并链接寄存器)
    • [6. 内存访问指令](#6. 内存访问指令)
      • [6.1 LA - 加载地址(伪指令)](#6.1 LA - 加载地址(伪指令))
      • [6.2 LB - 加载字节(符号扩展)](#6.2 LB - 加载字节(符号扩展))
      • [6.3 LBU - 加载字节(零扩展)](#6.3 LBU - 加载字节(零扩展))
      • [6.4 SB - 存储字节](#6.4 SB - 存储字节)
    • [7. 常用伪指令](#7. 常用伪指令)
      • [7.1 LI - 加载立即数](#7.1 LI - 加载立即数)
      • [7.2 MV - 移动寄存器](#7.2 MV - 移动寄存器)
      • [7.3 NOP - 空操作](#7.3 NOP - 空操作)
    • [8. 函数调用约定](#8. 函数调用约定)
      • [8.1 叶子函数调用](#8.1 叶子函数调用)
        • [8.1.1 完整栈流程示意图](#8.1.1 完整栈流程示意图)
        • [8.1.2 调试](#8.1.2 调试)
      • [8.2 嵌套函数调用](#8.2 嵌套函数调用)
        • [8.2.1 逐段解析](#8.2.1 逐段解析)
        • [8.2.2 完整调用流程](#8.2.2 完整调用流程)
        • [8.2.3 叶子函数 vs 嵌套函数](#8.2.3 叶子函数 vs 嵌套函数)
      • [8.3 调用约定总结](#8.3 调用约定总结)
    • [9. 汇编与C混合编程](#9. 汇编与C混合编程)
      • [9.1 汇编调用C函数](#9.1 汇编调用C函数)
      • [9.2 C调用汇编函数(内联汇编)](#9.2 C调用汇编函数(内联汇编))
    • [10. 构建和调试](#10. 构建和调试)
      • [10.1 构建系统文件](#10.1 构建系统文件)
      • [10.2 常用命令](#10.2 常用命令)
      • [10.3 GDB调试配置](#10.3 GDB调试配置)
    • 附录:指令速查表

RISC-V 基础知识扫盲

1. RISC-V 基础概念

1.1 寄存器

RISC-V有32个通用寄存器,每个32位(RV32架构):

寄存器 别名 用途
x0 zero 恒为0,写入被忽略
x1 ra 返回地址
x2 sp 栈指针
x3 gp 全局指针
x4 tp 线程指针
x5-x7 t0-t2 临时寄存器
x8 s0/fp 保存寄存器/帧指针
x9 s1 保存寄存器
x10-x17 a0-a7 函数参数/返回值
x18-x27 s2-s11 保存寄存器
x28-x31 t3-t6 临时寄存器

1.2 指令格式

RISC-V指令有多种格式:

  • R型 :寄存器-寄存器操作(如 add x5, x6, x7
  • I型 :寄存器-立即数操作(如 addi x5, x6, 1
  • S型 :存储操作(如 sb x6, 0(x5)
  • B型 :分支操作(如 bne x5, x6, loop
  • U型 :长立即数操作(如 lui x5, 0x12345
  • J型 :跳转操作(如 jal x5, target

2. 算术指令

2.1 ADD - 寄存器加法

文件add/test.s

asm 复制代码
li x6, 1          # x6 = 1
li x7, 2          # x7 = 2
add x5, x6, x7    # x5 = x6 + x7 = 3

说明

  • 格式:ADD RD, RS1, RS2
  • 功能:RS1 和 RS2 相加,结果存入 RD
  • 这是R型指令

2.2 ADDI - 立即数加法

文件addi/test.s

asm 复制代码
li x6, 2           # x6 = 2
addi x5, x6, 1     # x5 = x6 + 1 = 3

说明

  • 格式:ADDI RD, RS1, IMM
  • 功能:将符号扩展的12位立即数(-2048~+2047)加到RS1
  • 这是I型指令

2.3 ADD2 - 处理负数的加法

文件add2/test.s

asm 复制代码
li x6, 1           # x6 = 1
li x7, -2          # x7 = -2 (补码表示)
add x5, x6, x7     # x5 = 1 + (-2) = -1

说明

  • RISC-V使用补码表示负数
  • add 指令可以处理正数加负数

2.4 SUB - 寄存器减法

文件sub/test.s

asm 复制代码
li x6, -1          # x6 = -1
li x7, -2          # x7 = -2
sub x5, x6, x7     # x5 = -1 - (-2) = 1

说明

  • 格式:SUB RD, RS1, RS2
  • 功能:用RS1减去RS2

2.5 SUBI - 立即数减法(伪指令)

文件subi/test.s

asm 复制代码
li x6, 30           # x6 = 30
addi x5, x6, -20    # x5 = 30 + (-20) = 10

说明

  • RISC-V没有 SUBI 指令!
  • 减法通过加负数实现:addi 支持负立即数
  • x5 = x6 - 20 等价于 addi x5, x6, -20

2.6 NEG - 取负(伪指令)

文件neg/test.s

asm 复制代码
li x6, 1           # x6 = 1
neg x5, x6         # x5 = -x6
# 实际汇编为: sub x5, x0, x6

说明

  • NEG 是伪指令
  • 实际汇编为 SUB RD, x0, RS
  • 因为 x0=0,所以 0 - x6 = -x6

2.7 LUI - 加载高位立即数

文件lui/test.s

asm 复制代码
lui x5, 0x12345      # x5 = 0x12345 << 12 = 0x12345000
addi x5, x5, 0x678   # x5 = x5 + 0x678 = 0x12345678

说明

  • 这条指令的作用是:把立即数放到寄存器的高20位,低12位全部填0
  • 格式:LUI RD, IMM
  • 将20位立即数放到寄存器的高20位,低12位清零
  • 配合 ADDI 可构造任意32位值

为什么这样设计?

因为 addi 只能表示 -2048 ~ +2047 的立即数范围(12位有符号)

指令 立即数位数 能表示的范围
ADDI 12位 -2048 ~ +2047
LUI 20位 0 ~ 0xFFFFF_000

两者配合就能构造任意32位整数

  • LUI 负责高20位
  • ADDI 负责低12位(分多次加也行)

2.8 AUIPC - PC相对加立即数

文件auipc/test.s

asm 复制代码
auipc x5, 0x12345    # x5 = PC + (0x12345 << 12)
auipc x6, 0          # x6 = PC (获取当前PC值)

说明

  • 格式:AUIPC RD, IMM
  • 用于构建PC相对地址
  • 设置立即数为0可获取当前PC值

假设你有一段数据在距离当前PC 0x1000 字节的地方:

复制代码
auipc x5, 0           # x5 = PC(当前地址)
addi  x5, x5, 0x1000  # x5 = PC + 0x1000(数据地址)

为什么要用PC相对地址?

因为程序可以被加载到内存的任意位置

加载方式 问题
绝对地址 如果程序被加载到不同位置,地址就错了
PC相对 不管加载到哪里,PC + offset 都能正确找到

这就是所谓的 位置无关代码 (PIC - Position Independent Code)


3. 逻辑指令

3.1 AND - 逻辑与

文件and/test.s

asm 复制代码
li x6, 0x10      # x6 = 0b00010000
li x7, 0x11      # x7 = 0b00010001
and x5, x6, x7   # x5 = 0b00010000 = 0x10

说明

  • 格式:AND RD, RS1, RS2
  • 功能:RS1 与 RS2 按位与
  • 常用于位掩码操作

3.2 ANDI - 立即数与

文件andi/test.s

asm 复制代码
li x6, 0x10      # x6 = 0b10000000
andi x5, x6, 0x01  # x5 = 0b00000000 = 0

说明

  • 格式:ANDI RD, RS1, IMM
  • 立即数同样符号扩展

3.3 NOT - 位反(伪指令)

文件not/test.s

asm 复制代码
li x6, 0xffff0000
not x5, x6          # x5 = ~x6 = 0x0000ffff
# 实际汇编为: xori x5, x6, -1

说明

  • NOT 是伪指令
  • 实际为 XORI RD, RS, -1

4. 移位指令

4.1 SLLI - 逻辑左移

文件slli/test.s

asm 复制代码
li x6, 1         # x6 = 1
slli x5, x6, 3    # x5 = x6 << 3 = 8

说明

  • 格式:SLLI RD, RS1, IMM
  • 低位补零
  • 等价于乘以 2^n

4.2 SRLI - 逻辑右移

文件srli/test.s

asm 复制代码
li x6, 0x80000000  # x6 = 0b10000000... (无符号最大)
srli x5, x6, 3     # x5 = x6 >> 3 = 0x10000000

说明

  • 格式:SRLI RD, RS1, IMM
  • 高位补零(无符号数右移)
  • 等价于无符号除以 2^n

对应C代码(srli/test.c):

c 复制代码
unsigned int i = 0x80000000;
i = i >> 4;  // 编译生成 srli

4.3 SRAI - 算术右移

文件srai/test.s

asm 复制代码
li x6, 0x80000000  # x6 = 负数 (符号位为1)
srai x5, x6, 4      # x5 = 算术右移4位,符号位扩展

说明

  • 格式:SRAI RD, RS1, IMM
  • 高位用符号位填充(处理有符号数)
  • 等价于有符号除以 2^n(向下取整)

对应C代码(srai/test.c):

c 复制代码
int i = 0x80000000;
i = i >> 4;  // 编译生成 srai

5. 跳转指令

5.1 BNE - 不相等分支

文件bne/test.s

asm 复制代码
li x5, 0      # i = 0
li x6, 5      # 循环上限
loop:
    addi x5, x5, 1    # i++
    bne x5, x6, loop  # if (i != 5) goto loop

说明

  • 格式:BNE RS1, RS2, IMM
  • 不相等时跳转到PC相对地址
  • 用于实现循环和条件分支

5.2 JALR - 跳转并链接寄存器

文件jalr/test.s

asm 复制代码
_start:
    li x6, 1
    li x7, 2
    jal x5, sum       # 调用 sum,返回地址存入 x5

sum:
    add x6, x6, x7   # x6 = x6 + x7
    jalr x0, 0(x5)   # 返回 (x0=0 表示丢弃返回值)

说明

  • 格式:JALR RD, RS1, IMM
  • 基址寄存器 + 偏移寻址
  • ±1 KiB 寻址范围
  • 常用于函数调用和返回

一句话总结

  • jal x5, sum:跳去 sum,把返回地址存 x5
  • jalr x0, 0(x5):跳回 x5 存的地址,返回地址不要了(存到 x0 等于丢弃)

6. 内存访问指令

6.1 LA - 加载地址(伪指令)

文件la/test.s

asm 复制代码
la x5, _start    # x5 = &_start
jr x5            # 跳转到 x5 中的地址

说明

  • LA 是伪指令
  • 实际汇编为 AUIPC + ADDI 两条指令
  • 用于访问任意32位地址

6.2 LB - 加载字节(符号扩展)

文件lb/test.s

asm 复制代码
la x5, _array       # x5 = array 的地址
lb x6, 0(x5)        # x6 = *(char*)x5 (符号扩展)
lb x7, 1(x5)        # x7 = *(char*)(x5+1) (符号扩展)

说明

  • 格式:LB RD, IMM(RS1)
  • 加载8位,符号扩展到32位
  • 偏移范围:-2048 ~ 2047

la伪指令,实际会变成两条指令:

  • _array 标签的地址 装进 x5(不是数据本身!)
asm 复制代码
auipc x5, 0          # x5 = PC
addi  x5, x5, offset # x5 = PC + offset

什么是符号扩展?

假设内存中存的是 0xff(-1的补码表示):

指令 扩展方式 结果
lb 符号扩展 0xff → 0xffffffff(-1)
lbu 零扩展 0xff → 0x000000ff(+255)

8位只能表示256个值,但需要表示正负数怎么办?

计算机用 补码 来同时表示正数和负数:

8位二进制 作为无符号 作为有符号
0xFF 11111111 255 -1
0xFE 11111110 254 -2
... ... ... ...
0x02 00000010 2 2
0x01 00000001 1 1
0x00 00000000 0 0

最高位是符号位

  • 0 → 正数(0 ~ 127)
  • 1 → 负数(补码计算:-256 ~ -1)

怎么算负数的补码?

复制代码
负数 = 256 - 正数

例如 -1: 256 - 1 = 255 = 0xFF
例如 -5: 256 - 5 = 251 = 0xFB

符号扩展 vs 零扩展

  • 零扩展(LBU)- 你是正数

    0xFF (8位) → 000000FF (32位) = 255

    前面全补0

  • 符号扩展(LB)- 你是负数

    0xFF (8位) → FFFFFFFF (32位) = -1

    前面全补1(因为最高位是1,表示负数)

例如:

c 复制代码
char c1 = 255;   // 在C里 char 是有符号的,255 = -1
unsigned char c2 = 255;  // 无符号,就是255

// 汇编:假设之前的代码让x5和x6指向了某个存了0xFF的地址
lb x5, 0(x5)   // 取出来是 -1
lbu x6, 0(x6)  // 取出来是 255

6.3 LBU - 加载字节(零扩展)

文件lbu/test.s

asm 复制代码
la x5, _array       # x5 = array 的地址
lbu x6, 0(x5)       # x6 = *(unsigned char*)x5 (零扩展)
lbu x7, 1(x5)       # x7 = *(unsigned char*)(x5+1) (零扩展)

说明

  • 格式:LBU RD, IMM(RS1)
  • 加载8位,零扩展到32位
  • 用于无符号字节数据

6.4 SB - 存储字节

文件sb/test.s

asm 复制代码
li x6, 0xffffffab    # x6 = 0xffffffab
la x5, _array        # x5 = array 的地址
sb x6, 0(x5)         # *x5 = (char)x6 = 0xab

说明

  • 格式:SB RS2, IMM(RS1)
  • 存储RS2的低8位到内存

7. 常用伪指令

7.1 LI - 加载立即数

文件li/test.s

asm 复制代码
# 小范围立即数 [-2048, +2047]
li x5, 0x80           
# 汇编为:
# 	addi x5, x0, 0x80

# 大范围立即数
li x6, 0x12345001
# 汇编为:
#   lui x6, 0x12345
#   addi x6, x6, 0x001

# lower-12最高位为1(需要补偿)
li x7, 0x12345FFF
# 汇编为:
#   lui x7, 0x12346      # 0x12346 = 0x12345 + 1
#   addi x7, x7, -1

说明

  • LI 是伪指令
  • 根据立即数范围汇编为不同序列

可以通过 make debug 看具体的汇编指令:

shell 复制代码
zgl@zgl-virtual-machine:~/code/risc_v/riscv-operating-system-mooc/code/asm/li$ make debug
Press Ctrl-C and then input 'quit' to exit GDB and QEMU
-------------------------------------------------------
Reading symbols from test.elf...
Breakpoint 1 at 0x80000000: file test.s, line 32.
0x00001000 in ?? ()
=> 0x00001000:  97 02 00 00     auipc   t0,0x0
1: /z $x5 = 0x00000000
2: /z $x6 = 0x00000000
3: /z $x7 = 0x00000000

Breakpoint 1, _start () at test.s:32
32              li x5, 0x80
=> 0x80000000 <_start+0>:       93 02 00 08     li      t0,128
1: /z $x5 = 0x80000000
2: /z $x6 = 0x00000000
3: /z $x7 = 0x00000000
(gdb) s
34              addi x5, x0, 0x80
=> 0x80000004 <_start+4>:       93 02 00 08     li      t0,128
1: /z $x5 = 0x00000080
2: /z $x6 = 0x00000000
3: /z $x7 = 0x00000000
(gdb) 
38              li x6, 0x12345001
=> 0x80000008 <_start+8>:       37 53 34 12     lui     t1,0x12345
   0x8000000c <_start+12>:      13 03 13 00     addi    t1,t1,1 # 0x12345001
1: /z $x5 = 0x00000080
2: /z $x6 = 0x00000000
3: /z $x7 = 0x00000000
(gdb) 
40              lui x6, 0x12345
=> 0x80000010 <_start+16>:      37 53 34 12     lui     t1,0x12345
1: /z $x5 = 0x00000080
2: /z $x6 = 0x12345001
3: /z $x7 = 0x00000000
(gdb) 
41              addi x6, x6, 0x001
=> 0x80000014 <_start+20>:      13 03 13 00     addi    t1,t1,1
1: /z $x5 = 0x00000080
2: /z $x6 = 0x12345000
3: /z $x7 = 0x00000000
(gdb) 
45              li x7, 0x12345FFF
=> 0x80000018 <_start+24>:      b7 63 34 12     lui     t2,0x12346
   0x8000001c <_start+28>:      93 83 f3 ff     addi    t2,t2,-1 # 0x12345fff
1: /z $x5 = 0x00000080
2: /z $x6 = 0x12345001
3: /z $x7 = 0x00000000
(gdb) 
47              lui x7, 0x12346
=> 0x80000020 <_start+32>:      b7 63 34 12     lui     t2,0x12346
1: /z $x5 = 0x00000080
2: /z $x6 = 0x12345001
3: /z $x7 = 0x12345fff
(gdb) 
48              addi x7, x7, -1
=> 0x80000024 <_start+36>:      93 83 f3 ff     addi    t2,t2,-1
1: /z $x5 = 0x00000080
2: /z $x6 = 0x12345001
3: /z $x7 = 0x12346000
(gdb) 
stop () at test.s:51
51              j stop                  # Infinite loop to stop execution
=> 0x80000028 <stop+0>: 6f 00 00 00     j       0x80000028 <stop>
1: /z $x5 = 0x00000080
2: /z $x6 = 0x12345001
3: /z $x7 = 0x12345fff

7.2 MV - 移动寄存器

文件mv/test.s

asm 复制代码
li x6, 30
mv x5, x6            # x5 = x6
# 等价于: addi x5, x6, 0

7.3 NOP - 空操作

文件nop/test.s

asm 复制代码
nop                  # do nothing
# 等价于: addi x0, x0, 0

8. 函数调用约定

8.1 叶子函数调用

文件cc_leaf/test.s

asm 复制代码
# 调用 square(3)
li a0, 3
call square

square:
    addi sp, sp, -8    # prologue: 分配栈空间
    sw s0, 0(sp)        # 保存 s0
    sw s1, 4(sp)        # 保存 s1
    mv s0, a0           # s0 = 参数
    mul s1, s0, s0      # s1 = s0 * s0
    mv a0, s1           # 返回值放入 a0
    lw s0, 0(sp)        # epilogue: 恢复 s0
    lw s1, 4(sp)        # 恢复 s1
    addi sp, sp, 8      # 恢复栈指针
    ret

说明

  • 叶子函数:不调用其他函数的函数
  • 不需要保存 ra
  • 需要保存 s0-s11(被调用者保存寄存器)

叶子函数 square(n) 完整解析,这是一个计算 的函数。

8.1.1 完整栈流程示意图

注意:

  • 寄存器在 CPU 内部,不在内存中
  • 栈在 内存
  • 两者是完全独立的存储位置

所以:

  • 寄存器变化确实从栈的示意图中看到
  • 需要用调试器(如GDB)才能观察

假设:

  • 初始 sp = 0x1000(栈底,高地址)
  • 栈向下增长(向低地址)
  • 每个格子 = 4字节(1 word)

第0步:调用前 (caller 准备)

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │  (未使用)
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤  ← sp (初始指向0x1000)
        │     ?      │
0xFFC   ├────────────┤
        │     ?      │
0xFF8   └────────────┘
低地址

状态:
- a0 = 3
- ra = 返回地址 (假设 0x1234)
- sp = 0x1000

第1步:进入 square,执行 addi sp, sp, -8

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤  ← 旧的栈顶 (即将变)
        │     ?      │
0xFFC   ├────────────┤
        │  空闲1     │
0xFF8   ├────────────┤  ← sp (新位置: 0xFF8)
        │  空闲2     │
0xFF4   └────────────┘
低地址

变化: sp = 0x1000 - 8 = 0xFF8
栈已分配8字节 (0xFF8 ~ 0xFFF)

第2步:执行 sw s0, 0(sp)

保存 s0 的旧值(假设调用前 s0 = 0xABCD

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤
        │     ?      │
0xFFC   ├────────────┤
        │  空闲1     │
0xFF8   ├────────────┤  ← sp (0xFF8)
        │  s0=0xABCD │  ← 已保存
0xFF4   └────────────┘
低地址

现在栈内容:
- 0xFF8: 0xABCD (原 s0 值)
- 0xFFC: 未使用
- 寄存器: s0 暂未改变,仍是 0xABCD

第3步:执行 sw s1, 4(sp)

保存 s1 的旧值(假设调用前 s1 = 0x1234

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤
        │     ?      │
0xFFC   ├────────────┤
        │  s1=0x1234 │  ← 已保存
0xFF8   ├────────────┤  ← sp (0xFF8)
        │  s0=0xABCD │
0xFF4   └────────────┘
低地址

现在栈内容:
- 0xFF8: 0xABCD (原 s0)
- 0xFFC: 0x1234 (原 s1)
- 寄存器: s0, s1 仍是原值

第4步:执行 mv s0, a0mul s1, s0, s0

计算过程中,栈不变

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤
        │     ?      │
0xFFC   ├────────────┤
        │  s1=0x1234 │  (栈中未变)
0xFF8   ├────────────┤  ← sp
        │  s0=0xABCD │
0xFF4   └────────────┘
低地址

寄存器变化:
- s0 = 3 (从 a0 来)
- s1 = 9 (3×3)
- a0 = 9 (准备返回值)

注意: 栈中保存的还是旧值,没有被修改

第5步:执行 mv a0, s1

返回值设置,栈仍然不变

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤
        │     ?      │
0xFFC   ├────────────┤
        │  s1=0x1234 │
0xFF8   ├────────────┤  ← sp
        │  s0=0xABCD │
0xFF4   └────────────┘
低地址

寄存器:
- a0 = 9 (返回值)
- s0 = 3, s1 = 9 (被修改了,但栈中还有旧值)

第6步:执行 lw s0, 0(sp)

从栈恢复 s0 的旧值

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤
        │     ?      │
0xFFC   ├────────────┤
        │  s1=0x1234 │
0xFF8   ├────────────┤  ← sp
        │  s0=0xABCD │  → 读回 s0 寄存器
0xFF4   └────────────┘
低地址

效果:
- s0 从 3 恢复为 0xABCD
- 栈内容不变(只是读取)

第7步:执行 lw s1, 4(sp)

从栈恢复 s1 的旧值

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤
        │     ?      │
0xFFC   ├────────────┤
        │  s1=0x1234 │  → 读回 s1 寄存器
0xFF8   ├────────────┤  ← sp
        │  s0=0xABCD │
0xFF4   └────────────┘
低地址

效果:
- s1 从 9 恢复为 0x1234
- 所有寄存器恢复到调用前的状态

第8步:执行 addi sp, sp, 8

释放栈空间

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤  ← sp (恢复为 0x1000)
        │     ?      │
0xFFC   ├────────────┤
        │  废弃数据  │  (逻辑上已释放)
0xFF8   ├────────────┤
        │  废弃数据  │
0xFF4   └────────────┘
低地址

变化: sp = 0xFF8 + 8 = 0x1000
栈内容还在,但下次函数调用会覆盖

第9步:执行 ret

返回 caller

复制代码
高地址
0x1008  ┌────────────┐
        │     ?      │
0x1004  ├────────────┤
        │     ?      │
0x1000  ├────────────┤  ← sp (回到初始位置)
        │     ?      │
0xFFC   ├────────────┤
        │   (空闲)   │
0xFF8   ├────────────┤
        │   (空闲)   │
0xFF4   └────────────┘
低地址

最终状态:
- sp = 0x1000 (恢复)
- a0 = 9 (返回值)
- s0, s1 恢复原值
- 栈干净,返回 caller 继续执行

栈变化总结表

步骤 sp 值 栈中 说明
初始 0x1000 -
addi sp,sp,-8 0xFF8 8字节空闲 分配空间
sw s0,0(sp) 0xFF8 0xFF8: s0旧值 保存s0
sw s1,4(sp) 0xFF8 0xFF8: s0旧值 0xFFC: s1旧值 保存s1
计算 0xFF8 不变 修改寄存器
lw s0,0(sp) 0xFF8 不变 恢复s0
lw s1,4(sp) 0xFF8 不变 恢复s1
addi sp,sp,8 0x1000 废弃 释放空间
ret 0x1000 空闲 返回
8.1.2 调试

实际debug查看:我们会发现s0和s1为0,这是因为旧值就是0,以实际为主

shell 复制代码
zgl@zgl-virtual-machine:~/code/risc_v/riscv-operating-system-mooc/code/asm/cc_leaf$ make debug
Press Ctrl-C and then input 'quit' to exit GDB and QEMU
-------------------------------------------------------
Reading symbols from test.elf...
Breakpoint 1 at 0x80000000: file test.s, line 19.
0x00001000 in ?? ()
=> 0x00001000:  97 02 00 00     auipc   t0,0x0
1: /z $sp = 0x00000000
2: /z $ra = 0x00000000
3: /z $a0 = 0x00000000
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000

Breakpoint 1, _start () at test.s:19
19              la sp, stack_end        # prepare stack for calling functions
=> 0x80000000 <_start+0>:       17 01 00 00     auipc   sp,0x0
   0x80000004 <_start+4>:       13 01 01 07     addi    sp,sp,112 # 0x80000070
1: /z $sp = 0x00000000
2: /z $ra = 0x00000000
3: /z $a0 = 0x00000000
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) s
_start () at test.s:21
21              li a0, 3
=> 0x80000008 <_start+8>:       13 05 30 00     li      a0,3
1: /z $sp = 0x80000070
2: /z $ra = 0x00000000
3: /z $a0 = 0x00000000
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
22              call square
=> 0x8000000c <_start+12>:      ef 00 80 00     jal     ra,0x80000014 <square>
1: /z $sp = 0x80000070
2: /z $ra = 0x00000000
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
square () at test.s:31
31              addi sp, sp, -8
=> 0x80000014 <square+0>:       13 01 81 ff     addi    sp,sp,-8
1: /z $sp = 0x80000070
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
32              sw s0, 0(sp)
=> 0x80000018 <square+4>:       23 20 81 00     sw      s0,0(sp)
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
33              sw s1, 4(sp)
=> 0x8000001c <square+8>:       23 22 91 00     sw      s1,4(sp)
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
37              mv s0, a0
=> 0x80000020 <square+12>:      13 04 05 00     mv      s0,a0
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
38              mul s1, s0, s0
=> 0x80000024 <square+16>:      b3 04 84 02     mul     s1,s0,s0
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000003
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
39              mv a0, s1
=> 0x80000028 <square+20>:      13 85 04 00     mv      a0,s1
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000003
4: /z $s0 = 0x00000003
5: /z $s1 = 0x00000009
6: /z $s2 = 0x00000000
(gdb) 
42              lw s0, 0(sp)
=> 0x8000002c <square+24>:      03 24 01 00     lw      s0,0(sp)
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000009
4: /z $s0 = 0x00000003
5: /z $s1 = 0x00000009
6: /z $s2 = 0x00000000
(gdb) 
43              lw s1, 4(sp)
=> 0x80000030 <square+28>:      83 24 41 00     lw      s1,4(sp)
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000009
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000009
6: /z $s2 = 0x00000000
(gdb) 
44              addi sp, sp, 8
=> 0x80000034 <square+32>:      13 01 81 00     addi    sp,sp,8
1: /z $sp = 0x80000068
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000009
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
square () at test.s:46
46              ret
=> 0x80000038 <square+36>:      67 80 00 00     ret
1: /z $sp = 0x80000070
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000009
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000
(gdb) 
stop () at test.s:26
26              j stop                  # Infinite loop to stop execution
=> 0x80000010 <stop+0>: 6f 00 00 00     j       0x80000010 <stop>
1: /z $sp = 0x80000070
2: /z $ra = 0x80000010
3: /z $a0 = 0x00000009
4: /z $s0 = 0x00000000
5: /z $s1 = 0x00000000
6: /z $s2 = 0x00000000

8.2 嵌套函数调用

文件cc_nested/test.s

asm 复制代码
_start:
	la sp, stack_end	# prepare stack for calling functions

	# aa_bb(3, 4);
	li a0, 3
	li a1, 4
	call aa_bb

stop:
	j stop			# Infinite loop to stop execution

# int aa_bb(int a, int b)
# return a^2 + b^2
aa_bb:
	# prologue
	addi sp, sp, -16
	sw s0, 0(sp)
	sw s1, 4(sp)
	sw s2, 8(sp)
	sw ra, 12(sp)

	# cp and store the input params
	mv s0, a0
	mv s1, a1

	# sum will be stored in s2 and is initialized as zero
	li s2, 0

	mv a0, s0
	jal square
	add s2, s2, a0

	mv a0, s1
	jal square
	add s2, s2, a0

	mv a0, s2

	# epilogue
	lw s0, 0(sp)
	lw s1, 4(sp)
	lw s2, 8(sp)
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

# int square(int num)
square:
	# prologue
	addi sp, sp, -8
	sw s0, 0(sp)
	sw s1, 4(sp)

	# `mul a0, a0, a0` should be fine,
	# programing as below just to demo we can contine use the stack
	mv s0, a0
	mul s1, s0, s0
	mv a0, s1

	# epilogue
	lw s0, 0(sp)
	lw s1, 4(sp)
	addi sp, sp, 8

	ret

	# add nop here just for demo in gdb
	nop

	# allocate stack space
stack_start:
	.rept 12
	.word 0
	.endr
stack_end:

	.end			# End of file

说明

  • 嵌套函数:调用其他函数的函数
  • 必须保存 ra ,因为 jal 会覆盖它
  • 调用子函数前保存所有被调用者保存寄存器

这是一个嵌套函数调用 的例子:aa_bb 函数内部调用了 square 函数。

C语言对应的逻辑

c 复制代码
int square(int num) { return num * num; }

int aa_bb(int a, int b) {
    return square(a) + square(b);  // 嵌套调用!
}
aa_bb(3, 4) = square(3) + square(4) = 9 + 16 = 25
8.2.1 逐段解析

第1段:调用者准备

asm 复制代码
_start:
    la sp, stack_end    # 设置栈指针
    li a0, 3            # 参数 a = 3
    li a1, 4            # 参数 b = 4
    call aa_bb          # 调用 aa_bb(a, b)
指令 作用
la sp, stack_end 初始化栈(为函数调用准备)
li a0, 3 参数1 = 3
li a1, 4 参数2 = 4
call aa_bb 跳转到 aa_bb,并把返回地址存入 ra

第2段:aa_bb 的 prologue

asm 复制代码
aa_bb:
    addi sp, sp, -16      # 分配16字节栈空间
    sw s0, 0(sp)          # 保存 s0(被调用者保存寄存器)
    sw s1, 4(sp)          # 保存 s1
    sw s2, 8(sp)          # 保存 s2
    sw ra, 12(sp)         # 保存返回地址!← 关键!

为什么要保存 ra?

  • call aa_bb 时,返回地址已经存到了 ra
  • 但接下来 aa_bb 要调用 square,会用 jal 指令
  • jal覆盖 ra
  • 所以必须先把原来的 ra 保存到栈上

栈布局(分配16字节后):

复制代码
sp + 0   → s0(保存)
sp + 4   → s1(保存)
sp + 8   → s2(保存)
sp + 12  → ra(保存返回地址)← 关键!
sp + 16  → ← 新的 sp(往上走)

第3段:保存参数

asm 复制代码
    mv s0, a0      # s0 = a = 3
    mv s1, a1      # s1 = b = 4
指令 作用
mv s0, a0 参数 a 暂存到 s0(因为后面 a0 要用来传参给 square)
mv s1, a1 参数 b 暂存到 s1

第4段:第一次调用 square(a)

asm 复制代码
    li s2, 0           # s2 = 0(累加和初始化)

    mv a0, s0          # a0 = 3(准备传参)
    jal square         # 调用 square(3),返回地址存入 ra
    add s2, s2, a0     # s2 = s2 + 返回值 = 0 + 9 = 9

jal square 做了什么?

  1. 跳转到 square 函数
  2. 把返回地址(call 下一条指令的地址)存入 ra

此时栈状态

复制代码
aa_bb 的栈                  square 的栈(在上面)
sp+16 ──────────────────
sp+12  │ 旧 ra(aa_bb的返回值) │  ← 被覆盖前保存过
sp+8   │  s2 = 0             │
sp+4   │  s1 = 4             │  ← s1 被覆盖前保存过
sp+0   │  s0 = 3             │  ← s0 被覆盖前保存过
                           ├───────────────────
                           │  square 的栈帧    │
                           └───────────────────

第5段:第二次调用 square(b)

asm 复制代码
    mv a0, s1          # a0 = 4(准备传参)
    jal square         # 调用 square(4),返回地址存入 ra
    add s2, s2, a0     # s2 = 9 + 16 = 25

jal square 再次覆盖 ra,但没关系,aa_bb 已经不需要它了(因为已经保存了)。

第6段:设置返回值

asm 复制代码
    mv a0, s2          # a0 = 25(返回值)

第7段:aa_bb 的 epilogue

asm 复制代码
    lw s0, 0(sp)        # 恢复 s0
    lw s1, 4(sp)        # 恢复 s1
    lw s2, 8(sp)        # 恢复 s2
    lw ra, 12(sp)       # 恢复返回地址!← 关键!
    addi sp, sp, 16     # 释放栈空间
    ret                 # 返回到 _start(跳转到 ra 存的地址)

为什么恢复 ra?

  • _start 调用 aa_bb 时,返回地址存在 ra
  • call aa_bb 覆盖了 ra,但之前保存过了
  • 现在从栈里读回来,ret 才能正确返回到 stop 标签

第8段:square 函数(叶子函数)

asm 复制代码
square:
    addi sp, sp, -8     # 分配8字节
    sw s0, 0(sp)        # 保存 s0
    sw s1, 4(sp)        # 保存 s1

    mv s0, a0           # s0 = 参数(3或4)
    mul s1, s0, s0      # s1 = s0 * s0
    mv a0, s1           # 返回值 a0 = s1

    lw s0, 0(sp)        # 恢复 s0
    lw s1, 4(sp)        # 恢复 s1
    addi sp, sp, 8      # 释放栈
    ret                 # 返回到 aa_bb(跳转到 ra)

square 不需要保存 ra?

  • 对!因为 square叶子函数(不再调用其他函数)
  • ret 用的是 ra,而 ra 不会被再次覆盖(因为 square 内部没有 call)
8.2.2 完整调用流程
复制代码
_start:
    li a0, 3
    li a1, 4
    call aa_bb      ──────────┐
                              │
aa_bb:                        │
    addi sp, sp, -16           │
    sw ra, 12(sp)    保存 ra  │
    ...                       │
    jal square    ─────────────┼────┐
                              │    │
square:                       │    │
    ...                       │    │
    mul s1, s0, s0  计算 3²   │    │
    mv a0, s1       返回值=9  │    │
    ret      ──────────────────┘    │
                              │
aa_bb:                        │                  
    add s2, s2, a0    第一次结果    │
    jal square    ─────────────┼────┐
                              │    │    │
square:                       │    │    │
    ...                       │    │    │
    mul s1, s0, s0  计算 4²   │    │    │
    mv a0, s1       返回值=16 │    │    │
    ret      ──────────────────┘    │
                              │
aa_bb:                        │                     
    add s2, s2, a0    第二次结果    │
	mv a0, s2				│    │
	lw s0, 0(sp)			│    │
	lw s1, 4(sp)			│    │
	lw s2, 8(sp)			│    │
	lw ra, 12(sp)    恢复 ra  │    │
	addi sp, sp, 16  		│    │
    ret      ──────────────────┘
    
stop:
    j stop
8.2.3 叶子函数 vs 嵌套函数
叶子函数 (square) 嵌套函数 (aa_bb)
调用其他函数? 是(调用了 square)
需要保存 ra? 不需要 需要
保存哪些寄存器? s0, s1 s0, s1, s2, ra

关键区别

  • 嵌套函数 :调用者可能覆盖 ra,必须保存
  • 叶子函数 :不再调用别人,ra 不会被覆盖,不用保存

8.3 调用约定总结

寄存器 别名 说明
a0-a7 参数/返回值 函数参数(前8个)和返回值
ra 返回地址 保存返回调用者的地址
sp 栈指针 指向栈顶
s0-s11 保存寄存器 被调用者负责保存
t0-t6 临时寄存器 调用者负责保存

9. 汇编与C混合编程

9.1 汇编调用C函数

文件asm2c/

汇编代码 (asm2c/test.s):

asm 复制代码
la sp, stack_end     # 设置栈指针
li a0, 1             # 参数1 = 1
li a1, 2             # 参数2 = 2
call foo              # 调用 C 函数 foo(int a, int b)

C代码 (asm2c/test.c):

c 复制代码
int foo(int a, int b)
{
    int c = a + b;    // 纯C函数,实现加法
    return c;
}

说明

  • 汇编通过 a0-a7 传递参数
  • C函数是普通的C函数
  • 返回值通过 a0 传出

9.2 C调用汇编函数(内联汇编)

文件c2asm/

汇编代码 (c2asm/test.s):

asm 复制代码
_start:
    la sp, stack_end
    li a0, 1
    li a1, 2
    call foo          # 调用 foo

C代码 (c2asm/test.c):

c 复制代码
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;
}

说明

  • C函数内部使用 asm volatile 内联汇编
  • : [sum]"=r"(c) 表示输出操作数,结果存到 c
  • : [add1]"r"(a), [add2]"r"(b) 表示输入操作数,从 a、b 取值
  • 返回值通过 a0 传出

10. 构建和调试

10.1 构建系统文件

build.mk - 章节配置:

makefile 复制代码
SECTIONS_Arithmetic = add add2 sub addi subi neg nop mv lui li
SECTIONS_Logical = and andi not
SECTIONS_Shifting = slli srli srai
SECTIONS_Load_Store = lb lbu sb auipc la
SECTIONS_Branch = bne
SECTIONS_Jump = jalr
SECTIONS_CallingConventions = cc_leaf cc_nested
SECTIONS_others = asm2c c2asm

rule.mk - 构建规则:

makefile 复制代码
CROSS_COMPILE = riscv64-unknown-elf-
CFLAGS = -nostdlib -fno-builtin -march=rv32g -mabi=ilp32 -g -Wall

10.2 常用命令

bash 复制代码
# 编译
make

# 运行(QEMU)
make run

# 调试(QEMU + GDB)
make debug

10.3 GDB调试配置

gdbinit

gdb 复制代码
display/z $x5    # 有符号十进制显示 x5
display/z $x6
display/z $x7
set disassemble-next-line on
b _start
target remote : 1234
c

附录:指令速查表

算术指令

指令 格式 功能
add ADD RD, RS1, RS2 RD = RS1 + RS2
addi ADDI RD, RS1, IMM RD = RS1 + IMM
sub SUB RD, RS1, RS2 RD = RS1 - RS2
neg NEG RD, RS RD = -RS (伪指令)
lui LUI RD, IMM RD = IMM << 12
auipc AUIPC RD, IMM RD = PC + (IMM << 12)

逻辑指令

指令 格式 功能
and AND RD, RS1, RS2 RD = RS1 & RS2
andi ANDI RD, RS1, IMM RD = RS1 & IMM
not NOT RD, RS RD = ~RS (伪指令)

移位指令

指令 格式 功能
slli SLLI RD, RS1, IMM RD = RS1 << IMM (逻辑左移)
srli SRLI RD, RS1, IMM RD = RS1 >> IMM (逻辑右移)
srai SRAI RD, RS1, IMM RD = RS1 >> IMM (算术右移)

跳转指令

指令 格式 功能
bne BNE RS1, RS2, IMM if (RS1 != RS2) PC += IMM
jal JAL RD, IMM RD = PC+4; PC += IMM
jalr JALR RD, RS1, IMM RD = PC+4; PC = RS1 + IMM

内存指令

指令 格式 功能
lb LB RD, IMM(RS1) RD = (int8_t)(RS1+IMM)
lbu LBU RD, IMM(RS1) RD = (uint8_t)(RS1+IMM)
sb SB RS2, IMM(RS1) (uint8_t)(RS1+IMM) = RS27:0
la LA RD, label RD = &label (伪指令)

伪指令

伪指令 等价指令 功能
li lui/addi 加载立即数
mv addi 移动寄存器
nop addi x0, x0, 0 空操作
not xori rd, rs, -1 位反
neg sub rd, x0, rs 取负

相关推荐
W_LuYi1852 天前
手撸极简zkEVM验证器:RISC-V电路实践
java·risc-v
大唐游子11 天前
MIT 6.1810 开发环境搭建(Xv6)
risc-v
nvd1111 天前
绝地求生:如何在 2026 年把 OpenAI Codex 强行交叉编译到 RISC-V 架构
架构·risc-v
Eloudy12 天前
可在开源 RISC-V 上的裸机操作系统
开源·risc-v
hai31524754313 天前
RISC-V核E203核前向旁路的架构性顽疾
驱动开发·架构·硬件架构·硬件工程·risc-v
时光飞逝的日子13 天前
基于 RISC-V 架构的边缘 AI 推理引擎优化设计
risc-v·模型量化·推理引擎·边缘 ai·向量扩展·低功耗优化
国科安芯14 天前
基于RISC-V架构的商业航天级MCU国产化技术路径与产业生态研究
网络·分布式·单片机·嵌入式硬件·架构·risc-v·安全性测试
国科安芯14 天前
AS32S601商业航天级抗辐照MCU芯片:架构设计与技术特性研究
单片机·嵌入式硬件·算法·安全·架构·risc-v
国科安芯14 天前
AS32S601芯片抗辐照性能试验验证与空间环境适应性分析
前端·分布式·单片机·嵌入式硬件·架构·risc-v·安全性测试