子例程

子例程

  • 子程序(program, procedure)
    • 将一个组件的功能与其实现的细节分隔开来
    • 程序员只需理解组件的结构,不需要考虑其实现的细节,就能够把该组件作为一个程序块使用
    • 子程序使程序员能够以模块化的方式写程序,提高了构建复杂计算系统的能力
  • 子例程(subroutine, function)
    • 在一个程序中,多次执行某个程序片段
      • 在程序内,不必每次给出这个程序片段
      • 通过多次调用该程序片段实现

调用-返回机制

通常设置 jal/jalr 目的寄存器为 x1,保存返回地址/链接;基址寄存器一般为 x5。

  • jal
    • 计算出的地址被限制于内存一定范围中
    • 偏移范围为 20202202-20^{20} \backsim 2^{20} - 2
  • jalr
    • 无限制

伪指令 call 用于调用子例程

call LABEL

等价于

1
2
auipc RD, offsetHi       # offsetHi 为 offset 高 20 位
jalr  x1, offsetLo(RD)   # offsetLo 为 offset 低 12 位

伪指令 ret 用于从子例程返回

ret

等价于

jalr x0, 0(x1)

被调用者保存寄存器(callee-saved register)

由被调用者(callee)完成寄存器保存/恢复工作。

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
01              .data
02              .align  2
03 SaveReg1:    .word   0               # 保存寄存器的空间
04 #
05              .text
06              .align  2
07 # 省略代码,x8 <- x
08              addi    x10, x0, 3      # x10 <- 3,被乘数
09              addi    x11, x0, -2     # x11 <- -2,乘数
0A              call    Multiply
0B              ......                  # 使用 x8 做计算
0C              ......                  # 下一个任务
0D #
0E Multiply:    la      x5, SaveReg1
0F              sw      x8, 0(x5)       # callee save
10              andi    x9, x9, 0       # x9,积
11 Mloop:       beqz    x11, Mexit      # x11,乘数
12              andi    x8, x11, 1
13              beqz    x8, Mnext
14              add     x9, x9, x10     # x10,被乘数
15 Mnext:       srli    x11, x11, 1
16              slli    x10, x10, 1
17              j       Mloop
18 Mexit:       lw      x8, 0(x5)       # 寄存器恢复
19              ret

0E 和 0F 行将 x8 的值 x 保存到预留的空间中;18 行将 x8 的值恢复。

参数和返回值

1
2
3
4
5
6
7
8
......          ......
08              addi    x10, x0, 3      # x10 <- 3,被乘数
09              addi    x11, x0, -2     # x11 <- -2,乘数
0A              call    Multiply        # 调用子例程
......          ......
10              andi    x9, x9, 0       # x9,积
14              add     x9, x9, x10     # x10,被乘数
19              ret                     # 从子例程返回调用者
  • 对于 Multiply 子例程:
    • x10 和 x11 是参数,x9 是返回值
  • 对于调用 Multiply 子例程的程序
    • 先传递参数值,即为 x10 和 x11 赋值
    • 调用返回后,通过返回值 x9 得到乘法计算结果

调用者保存寄存器(caller-saved register)

由调用者(caller)完成的寄存器保存/恢复工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
......
09 SaveReg2:    .word   0, 0
......
1F              la  x6, SaveReg2
20              sw  x11, 0(x6)          # caller save
......
22              addi    x11, x0, 10     # x11,乘数
23              call    Multiply        # x9 = x10 * 10
......
25              la  x6, SaveReg2
26              lw  x11, 0(x6)          # 寄存器恢复
......
29              addi    x11, x11, 1     # x11 指向下一个字符
......
2C DoneS2I:     ret

09 行使用数据区的存储单元作为保存寄存器的空间;1F 和 20 行将 x11 的值保存到预留的空间中;25 和 26 行:将 x11 的值恢复。

在 23 行执行 call 后,x1 被改变(为 24 行 PC 值),待该子例程在 2C 行 ret 返回时,会返回到 24 行,发生了错误!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
......
09 SaveReg2:    .word 0, 0
......
1F              la  x6, SaveReg2
......
21              sw  x1, 4(x6)        # caller save
......
23              call  Multiply       # x9 = x10 * 10
......
25              la  x6, SaveReg2
......
27              lw  x1, 4(x6)        # 寄存器恢复
......
2C DoneS2I:     ret

09 行使用数据区的存储单元作为保存寄存器的空间;21 行将 x1 的值保存到预留的空间中;27 行将 x1 的值恢复。

寄存器的保存/恢复

基本原则:如果一个寄存器内的值将在该寄存器的值被改变之后再次被用到,必须在其值被改变之前将其保存,在再次使用它之前将其恢复。

  • 通过将寄存器的值存进内存,来保存它的值
  • 通过将内存保留的值加载回寄存器,来恢复它的值

如果在子例程中又调用了子例程,必须采用 caller-saved(调用者保存)的策略,保存/恢复返回地址 x1

RISC-V 各寄存器的 Saved by 可见 5-指令集体系

  • caller-saved 寄存器
    • 维护责任:调用者
    • 调用者要在子例程的调用前/后,保存/恢复寄存器
    • 被调用者不用关心这些寄存器旧值,可以直接覆写
    • 必须在每次发生子例程调用前后都进行保存恢复
  • callee-saved 寄存器
    • 维护责任:被调用者
    • 这些寄存器在使用前/后必须由被调用者保存/恢复
    • 调用者可以认为子例程返回后这些寄存器是没有被改写的
    • 在子例程调用时不需要调用者做任何动作

递归过程

即在一个子例程中调用这个子例程本身,称为递归子例程。

计算前 nn 个正整数的和(本例取 n=3n = 3):

Sn=Sn1+nS_n = S_{n - 1} + n

初始条件 S1=1S_1 = 1

nn 的值赋给 x10,调用 Sn 子例程,返回结果在 x11 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
01              .data
02              .align  2
03 SaveReg:     .word   0
04 #
05              .text
06              .align  2
07              .globl  main
08 main:        addi    x10, x0, 3       # n = 3
09              jal     x1, Sn           # call  Sn
0A              j       end
0B #
0C Sn:          la      x5, SaveReg
0D              sw      x1, 0(x5)        # caller save
0E              addi    x6, x0, 1
0F              beq     x10, x6, exit1   # n == 1 ?
10              addi    x10, x10, -1     # n - 1
11              jal     x1, Sn           # 调用 S(n-1)
12              addi    x10, x10, 1      # n
13              add     x11, x11, x10    # S(n) = S(n-1) + n
14              j       exit2
15 exit1:       addi    x11, x0, 1       # S(1) = 1
16 exit2:       la      x5, SaveReg
17              lw      x1, 0(x5)
18              ret

但是这样每一层递归都将 x1 保存在了 SaveReg 中,导致了浅层次保存的结果被覆盖,因此会产生无限循环。

即递归调用子例程时,保存返回地址 x1 的指令将前一次保存的寄存器值覆盖了.

采用栈机制解决。

栈是一种后进先出(LIFO, Last In, First Out)的数据结构,栈顶指针指向栈顶元素。

栈操作:

  • 压栈(push):将数据压入栈顶,栈顶指针加 1
  • 出栈(pop):将栈顶数据弹出,栈顶指针减 1

栈指针:

  • x2 寄存器
  • 永远指向栈顶

内存中的栈实现:

初始地址 x2 指向 0xBFFF FFF0。栈从高地址向低地址,即栈顶地址最小,栈顶在下(底)。

将保存在 x9 中的数值压栈:

1
2
push:   addi    x2, x2, -4
        sw      x9, 0(x2)

将栈顶的数值弹出到 x9:

1
2
pop:    lw      x9, 0(x2)
        addi    x2, x2, 4

栈协议:

  • 是一组对内存的访问控制规则
  • 满足后进先出(LIFO)原则
  • 包含方法:PUSH, POP

改进后的递归调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
01              lui    x2, 0xc0000
02              addi   x2, x2, -16       # x2 = 0xBFFF FFF0
03              addi   x10, x0, 3        # n = 3
04              jal    x1, Sn            # call Sn
05              j      end
06 #
07 Sn:          addi    x2, x2, -4       # push x1
08              sw      x1, 0(x2)
09              addi    x6, x0, 1
0A              beq     x10, x6, exit1   # n == 1 ?
0B              addi    x10, x10, -1     # n - 1
0C              jal     x1, Sn           # S(n-1)
0D              addi    x10, x10, 1      # n
0E              add     x11, x11, x10    # S(n) = S(n-1) + n
0F              j       exit2
10 exit1:       addi    x11, x0, 1       # x11,S(1) = 1
11 exit2:       lw      x1, 0(x2)        # pop x1
12              addi    x2, x2, 4
13              ret