MIT6828 学习笔记 010 (lec 5)

MIT6828 学习笔记 010 (lec 5)

RayAlto OP

RISC-V 调用约定、栈帧和 GDB

有时(比如 lab syscall )需要一些 C 语言无法表达的代码

RISC-V 抽象计算机:没有像 C 语言一样的流程控制、变量的概念、类型等等;基本的 ISA :程序计数器、 32 个通用寄存器 (x0-x31)

寄存器名字保存者描述
x0zerohardwired zero
x1ra调用方返回地址
x2sp被调用方栈指针
x3gp全局指针
x4tp线程指针
x5-x7t0-t2调用方临时寄存器
x8s0/fp被调用方保存寄存器/栈指针
x9s1被调用方保存寄存器
x10-x11a0-a1调用方函数参数/返回值
x12-x17a2-a7调用方函数参数
x18-x27s2-s11被调用方保存寄存器
x28-x31t3-t6调用方临时寄存器
pc程序计数器

比如下面的代码:

1
2
3
4
5
6
7
int sum_to(int n) {
int acc = 0;
for (int i = 0; i <= n; i++) {
acc += i;
}
return acc;
}

用汇编实现:

1
2
3
4
5
6
7
8
9
10
# sum_to(n)
# 从 a0 读参数,往 a0 返回结果
sum_to:
mv t0, a0 # t0 <- a0
li a0, 0 # a0 <- 0
loop:
add a0, a0, t0 # a0 <- a0 + t0
addi t0, t0, -1 # t0 <- t0 - 1
bnez t0, loop # if t0 != 0: pc <- loop
ret

ret 的语义:

1
2
ret :=
pc <- ra

其他函数如何调用这个 sum_to

1
2
3
main:
li a0, 10 # a0 <- 10
call sum_to

call 的语义:

1
2
3
call label :=
ra <- pc + 4 ; 把下一个指令的地址存放在 ra
pc <- label ; 跳转到 label

嵌套调用:

1
2
3
4
5
6
7
8
9
sum_then_double:
call sum_to
li t0, 2 # t0 <- 2
mul a0, a0, t0 # a0 <- a0 * t0
ret

main:
li a0, 10
call sum_then_double

会导致死循环: maincall 使 ra 指向自己的下一行(第 9 行下面),然后进入 sum_then_doublesum_then_doublecall 又使 ra 指向自己的下一行(第 3 行),然后进入 sum_tosum_to 进行 ret 进入 ra 指向的语句(第 3 行),第 5 行的 ret 又会进入 ra 指向的语句第 3 行,导致死循环。

尝试解决:把 ra 放进其他寄存器——无论如何都要用 call&ret 所以只是把问题延后了。答案:使用栈

1
2
3
4
5
6
7
8
9
sum_then_double:
addi sp, sp, 16 # 使栈指针指向 64 bit 之后
sd ra, 0(sp) # ra -> sp[0]
call sum_to
li t0, 2 # t0 <- 2
mul a0, a0, t0 # a0 <- a0 * t0
ld ra, 0(sp) # ra <- sp[0]
addi sp, sp, -16 # 使栈指针指向 64 bit 之前
ret

问:上面用 a0 接收参数,把返回值放进了 a0 ,为啥不用别的,比如从 t2 接收参数,把返回值放进 t3 ?答:这就是调用约定, a0-a7 用于传递参数,放不下的话把剩下的放进栈里; a0, a1 保存返回值。

手写出来的汇编代码应该遵循调用约定,就像 GCC 编译器编译出的代码也是遵循调用约定的,这样才能使每个人的代码都能互相操作。

汇编和 C 语言互相调用:遵守调用约定,在 C 代码写出函数原型,编译器就能知道怎么调用汇编代码。

目录
MIT6828 学习笔记 010 (lec 5)