# RISC-V assembly

首先是 call.c:

#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int g(int x) {
  return x+3;
}
int f(int x) {
  return g(x);
}
void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}

然后是 call 的汇编:

int g(int x) {
   0:	1141                	addi	sp,sp,-16
   2:	e422                	sd	s0,8(sp)
   4:	0800                	addi	s0,sp,16
  return x+3;
}
   6:	250d                	addiw	a0,a0,3
   8:	6422                	ld	s0,8(sp)
   a:	0141                	addi	sp,sp,16
   c:	8082                	ret

000000000000000e <f>:

int f(int x) {
   e:	1141                	addi	sp,sp,-16
  10:	e422                	sd	s0,8(sp)
  12:	0800                	addi	s0,sp,16
  return g(x);
}
  14:	250d                	addiw	a0,a0,3
  16:	6422                	ld	s0,8(sp)
  18:	0141                	addi	sp,sp,16
  1a:	8082                	ret

000000000000001c <main>:

void main(void) {
  1c:	1141                	addi	sp,sp,-16
  1e:	e406                	sd	ra,8(sp)
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7a050513          	addi	a0,a0,1952 # 7c8 <malloc+0xe8>
  30:	00000097          	auipc	ra,0x0
  34:	5f8080e7          	jalr	1528(ra) # 628 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	274080e7          	jalr	628(ra) # 2ae <exit>

我主要说一下各个指令的作用 auipc 将当前 pc 值 load 到特定的寄存器中。比如

30:	00000097          	auipc	ra,0x0
34:	5f8080e7          	jalr	1528(ra) # 628 <printf>

把当前 pc 的值加上 0x0 也就是 30 放到 ra 中,因为 ra 就是 return address, jalr 会将 pc+4 存储给指定的寄存器,反汇编语句里省略了指定寄存器,是因为默认给 ra,所以 ra= 0x38

1528(ra) 表示 ra 中的值加上 1528 生成一个地址,然后去这个地址寻找到数据作为指令进行解释并执行也就是跳转操作。printf 执行完毕后就会执行 ret 指令,ret 指令就去 ra 中把 return address 找到并且并且读取开始执行。

有意思的是这个:

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, (char*)&i);

这个会打印 He110 World 。这里官方给出的代码没有进行指针强转,会没法编译。riscv 存数据的方式就是小端存储也就是正常的人类顺序。 64 对应 o89 对应。。。跑题了,反正就是一一进行解释,最后显示 Hello world

In the following code, what is going to be printed after 'y=' ? (note: the answer is not a specific value.) Why does this happen?

printf("x=%d y=%d", 3);

因为 printf 需要三个参数,所以给 y 复制的寄存器就是 a2

# gdb 常用指令

# 开启调试

make qemu-gdb CPU=1 # 限制cpu个数
risriscv64-unknown-elf-gdb # 使用riscv gdb

# gdb 命令

Ctrl-c

Halt the machine and break in to GDB at the current instruction. If QEMU has multiple virtual CPUs, this halts all of them.

c (or continue)

Continue execution until the next breakpoint or Ctrl-c .

si (or stepi)

Execute one machine instruction.

b function or b file:line (or breakpoint)

Set a breakpoint at the given function or line.

b **addr* (or breakpoint)

Set a breakpoint at the EIP addr.

set print pretty

Enable pretty-printing of arrays and structs.

info registers

Print the general purpose registers, eip , eflags , and the segment selectors. For a much more thorough dump of the machine register state, see QEMU's own info registers command.

x/*N*x *addr*

Display a hex dump of N words starting at virtual address addr. If N is omitted, it defaults to 1. addr can be any expression.

x/*N*i *addr*

Display the N assembly instructions starting at addr. Using $eip as addr will display the instructions at the current instruction pointer.

symbol-file *file*

(Lab 3+) Switch to symbol file file. When GDB attaches to QEMU, it has no notion of the process boundaries within the virtual machine, so we have to tell it which symbols to use. By default, we configure GDB to use the kernel symbol file, obj/kern/kernel . If the machine is running user code, say hello.c , you can switch to the hello symbol file using symbol-file obj/user/hello .

关于 x 模式,这里有个简单列表:

n:是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,
一个内存单元的大小由第三个参数u定义。

f:表示addr指向的内存内容的输出格式,s对应输出字符串,此处需特别注意输出整型数据的格式:
  x 按十六进制格式显示变量.
  d 按十进制格式显示变量。
  u 按十进制格式显示无符号整型。
  o 按八进制格式显示变量。
  t 按二进制格式显示变量。
  a 按十六进制格式显示变量。
  c 按字符格式显示变量。
  f 按浮点数格式显示变量。
  i 按照指令方式进行打印。

u:就是指以多少个字节作为一个内存单元-unit,默认为4。u还可以用被一些字符表示:
  如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.

<addr>:表示内存地址。

# qemu 命令

Ctrl+a x 退出

Ctrl+a c 进入 consle 模式,可以用 info mem 进行页表打印。

# Trap

# Trap 代码执行流程

image-20220426095322352

# trap 进入

让我们再来回顾这张图

image-20220426095540263

我们的目的就是通过 ecall 跳转到内核态,而核心的就是 trampoline 和 trapframe 两个页。trampoline page 中存放了处理在用户态处理 trap 的代码,并且这个 map 是系统为所有进程完成的,包括内核页表。

打开 qemu 之后,可以用 info mem 查看页表:

image-20220426100039263

可能这里把三级页表拆开了,反正可以看到 vaddr 中最下面两个最大的地址就是 trampoline page 和 trapframe page。

# ecall 做的事情

第一,ecall 将代码从 user mode 改到 supervisor mode。因为无论是 trampoline 还是 trapframe 都不能在用户态访问,因为标志位没有 u

第二,ecall 将程序计数器的值保存在了 SEPC 寄存器中。因为这是 trap 返回地址。

第三,ecall 把 stvec 寄存器中地址 load 到 pc 中。(stvec 就是中断向量地址,也就是 trampoline page 地址,因为 trampoline 就是用于存放处理 trap 的指令)

# 保存寄存器状态

使用 csrrw a0 sscratch 命令把 a0sscratch 两个寄存器的值进行交换。sscratch 中保存的实际上就是 trapframe page 的地址,然后我们使用 save 指令把寄存器的值进行保存(这里截了一部分图,实际上上面还有很多保存寄存器的指令):

image-20220426101940437

# 加载处理内核数据

image-20220426103708995

这四条 load 指令,分别 load 了内核栈的栈顶指针、当前运行的 cpuid、处理终端的 usertrap () 函数的地址、kernel pagetable 的 id。

执行上面指令后,再次调用 info mem 可以看到:

image-20220426103942318

我们成功进入了内核!

# 处理系统调用

trap.c 会保存当前的 sepc ,检查状态判断是否 scause 也就是 trap 原因。如果 scause 为 8,那就执行 syscall() ,syscall 会调用根据 a7 来判断导致执行那种系统调用,并把执行结果放在 a0

# trap 返回

image-20220426110357638

内核发现这个进程并没有被杀死,于是它执行 trap 返回,也就是 usertrapret ()。

进入这个函数内:

image-20220426110914010

这部分其实做了很多事情,但是都是一些镜像的事情,也就是 trap 进入需要什么,这里就保存什么。

我们一路快进:

image-20220426111215836

boom!又回到了 trampoline!

最后把之前保存的数据重新 load 进去,ok,整个系统调用完成!

# 参考资料:

http://xyfjason.top/2021/11/30/xv6-mit-6-S081-2020-Lab4-traps/

https://www.ysblog.cc/archives/mit6s081lab4

lab tools

更新于

请我喝[茶]~( ̄▽ ̄)~*

Kalice 微信支付

微信支付

Kalice 支付宝

支付宝