# 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
对应 o
, 89
对应。。。跑题了,反正就是一一进行解释,最后显示 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 代码执行流程
# trap 进入
让我们再来回顾这张图
我们的目的就是通过 ecall 跳转到内核态,而核心的就是 trampoline 和 trapframe 两个页。trampoline page 中存放了处理在用户态处理 trap 的代码,并且这个 map 是系统为所有进程完成的,包括内核页表。
打开 qemu 之后,可以用 info mem
查看页表:
可能这里把三级页表拆开了,反正可以看到 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
命令把 a0
和 sscratch
两个寄存器的值进行交换。sscratch 中保存的实际上就是 trapframe page 的地址,然后我们使用 save 指令把寄存器的值进行保存(这里截了一部分图,实际上上面还有很多保存寄存器的指令):
# 加载处理内核数据
这四条 load 指令,分别 load 了内核栈的栈顶指针、当前运行的 cpuid、处理终端的 usertrap () 函数的地址、kernel pagetable 的 id。
执行上面指令后,再次调用 info mem
可以看到:
我们成功进入了内核!
# 处理系统调用
trap.c
会保存当前的 sepc
,检查状态判断是否 scause
也就是 trap 原因。如果 scause
为 8,那就执行 syscall()
,syscall 会调用根据 a7
来判断导致执行那种系统调用,并把执行结果放在 a0
。
# trap 返回
内核发现这个进程并没有被杀死,于是它执行 trap 返回,也就是 usertrapret ()。
进入这个函数内:
这部分其实做了很多事情,但是都是一些镜像的事情,也就是 trap 进入需要什么,这里就保存什么。
我们一路快进:
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