# 如何 使用 gdb 追蹤 xv6 的 system call 過程
本文目標:照著以下步驟就可以看到整個 system call 的過程
整個過程大致上都是照著[這個影片](https://www.youtube.com/watch?v=T26UuauaxWA)做的,但其中有幾個步驟稍稍的不同。
## 1. 架設 xv6 的環境
https://pdos.csail.mit.edu/6.S081/2022/tools.html
## 2. 用 gdb-multiarch debug xv6 的方式
這裡會需要開啟 2 個終端機,
先在其中一個終端機輸入
```shell=
make qemu-gdb # 這是被 debug 的對象
```
在另一個終端機輸入
```shell=
gdb-multiarch # 開啟 debuger 會開始針對上面的那個終端機中的程式進行除錯
```
不知道也沒關係的點:```gdb-multiarch``` 是透過 ```.gdbinit``` 這個檔案找到 debug 的對象的
```shell=
# .gdbinit
set confirm off
set architecture riscv:rv64
target remote 127.0.0.1:26000 # 透過這個來找到 qemu
symbol-file kernel/kernel
set disassemble-next-line auto
set riscv use-compressed-breakpoints yes
```
## 3. 追蹤 ```write``` 這個 system call
目標:先觀察執行 ```make qemu``` 後 xv6 開機時,會顯示的畫面
```
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$
```
xv6 第一個執行的程式是 ```sh```,它會先 print 出字元 ```$``` print 的過程需要使用 system call ```write``` 我們的目標就在於追蹤這個 ```write``` 的過程。
### 找到對應的 C 語言程式碼
在 ```user/sh.c``` 中
```C=
// user/sh.c
int
getcmd(char *buf, int nbuf)
{
write(2, "$ ", 2); // here
memset(buf, 0, nbuf);
gets(buf, nbuf);
if(buf[0] == 0)
return -1;
return 0;
}
```
### 找到對應的組合語言
```write(2, "$ ", 2);``` 這行式碼編譯出來的組合語言被放在 ```user/sh.asm``` 當中,對應到的組合語言片段如下:
```asm=
0000000000000e16 <write>:
.global write
write:
li a7, SYS_write
e16: 48c1 li a7,16
ecall
e18: 00000073 ecall
ret
e1c: 8082 ret
```
組合語言解釋
* ```wirte``` 這個 procedure 位於 0xe16 中(virtual address)
* ```li a7, SYS_write```
* ```li``` 指的是 load immediate 整個指令也就是把 ```SYS_write```(16, 定義在 ```kernel/syscall.h```) 存放到 register ```a7``` 當中
* ```ecall``` 這個指令會觸發 trap
### 觸發 trap 之後,會發生什麼事情?
觸發 trap 之後可以進入到 kernel mode,觸發 trap 有三種可能的情況
* system call
* interrupt
* exception
在這個例子中,是 ```ecall``` 用 system call 的方式觸發 trap,在說明 trap 之後會怎麼樣前,要先來介紹一些相關的 register:
* ```stvec```: 當 trap 發生時,RISC-V 會把 program counter 存在 ```stvec``` 中
* ```scause```: RISC-V 會把觸發 trap 的原因放到 ```scause``` 中
* ```sscratch```: 進入到 kernel space 前 ```sscratch``` 會儲存 trapframe 的位置
* ```sstatus```: Supervisor status rigister
* `satp`: Supervisor Address Translation and Protection Register
* `sepc`: Supervisor Exception Program Counter, 進入 supervisor mode 後,用來紀錄回到 user mode 時,要回到什麼 address 開始執行
當 trap 發生時,會做的事情:
1. 把 ```pc``` 複製到 ```secp```(Supervisor Exception Program Counter) 中
2. 把現在的狀態 (user or supervisor) 紀錄在 ```sstatus``` 的 SPP bit
3. 把造成 trap 的原因紀錄在 ```scause```
4. 把 ```stvec``` 放到 ```pc``` 中
5. 開始根據 ```pc``` 往下執行
### 開始 debug
1. 把中斷點設在 ```ecall```
```gdb=
(gdb) break *0xe18 # 根據 user/sh.asm, ecall 的 address (virtual)
(gdb) continue # continue 往下執行直到 ecall (正準備執行,但還沒)
(gdb) layout asm # 可以比較方便的看到目前執行的指令
```
到了這裡,我們準備要執行 ```ecall``` 以觸發 trap 了,執行 trap 就是想要從 ```stvec``` 的那裡執行
> 這裡我用**影片中的方法無法正常執行**,影片中是用 ```(gdb) si``` 就可以往下跑到 trap 的流程,可是我在自己執行的結果卻是會直接把 trap 的流程跑完,直到 ```ecall``` 的下一個指令 ```ret``` 那裡(可能是 gdb 的版本不同),想要追蹤 trap 的流程就必須要自行設定中斷點。
2. 使用 ```si``` 往下執行之前,先 print 出 ```stvec``` 的值,並且把中斷點設到那個 address (virtual)
```gdb=
(gdb) print/x $stvec # 用 /x (hex) 的格式 print 出 stvec 的值
$1 = 0x3ffffff000
(gdb) break *0x3ffffff000 # break *stvec
(gdb) si # 執行 ecall, 真正進入 supervisor mode 並且處理由 system call 觸發的 trap
```
### ```stvec``` 中的 0x3ffffff000 指向哪一段程式碼
執行 ```ecall``` 之後
* 會跑到 `stvec` 指向的位置 0x3ffffff000 開始執行
* 也就是會跑到 `kernel/trampoline.S` 的 `<uservec>`
* `<uservec>` 會一直執行到 `jr t0` 為止
* 之後會跑到到 `kernel/trap.c: usertrap()` 執行
### `kernel/trap.c: usertrap()`
接下來就是 C 語言了
* 可以用 `(gdb) layout src` 來切換到 C 語言的檢視模式
```clike=
// trap.c
void
usertrap(void)
{
/* ... */
if(r_scause() == 8){
syscall();
}
/* ... */
usertrapret();
}
```
### `kernel/syscall.c: syscall()`
想要直接跑到 `kernel/syscall.c: syscall()` 執行了話,可以使用
```gdb=
(gdb) break syscall
(gdb) continue
```
跳到 `syscall()` 執行
在 lab syscall 中有提到這段程式碼
### ```usertrapret()```
想要直接跑到 `kernel/syscall.c: syscall()` 執行了話,可以使用
```gdb=
(gdb) break usertrapret
(gdb) continue
```
跳到 `usertrapret()` 執行
`next` 到最後一行的 ```trampoline_userret``` 時,再 `next` 一次,之後又會進入到 assembly 的部份 (`kernel/trampoline.S: <userret>`)
```gdb=
(gdb) layout asm # 以 assembly 檢視
```
### ```kernel/trapmpoline.s``` 中的 ```userret```
在 ```sret``` 那裡,也要設 break point 才能夠回到 ```user/sh.asm``` 的 ```ecall``` 下面的 ```ret```
### ```user/sh.asm``` 的 ```ret```
```gdb=
(gdb) file user/_sh
(gdb) si
(gdb) layout src # 已經執行完 write(2, "$ ", 2);
```
整個流程到此結束
## 有用的指令
### QEMU
ctrl + a c 可以 (進入/退出)到 QEMU 的 console
* `info reg`: 可以印出所有的 register
* `info mem`: 可以印出所有的 page table
### gdb
* `p/x $satp` 印出目前 pagetable 的 **physicall** address
* `x/3i 0xe06` print 出 3 個 instructions
```
(gdb) x/3i 0xe06
=> 0xe06: li a7,16
0xe08: ecall
0xe0c: ret
```
* `stepi`: execute one instruction
* `print $pc` print program counter
* `print/x $stvec`: 得知 trap 發生時,會跑到什麼地方開始執行
* `print/x $sepc`: `ecall` 之後,紀錄 user mode 的下一個 instruction 的位置 (紀錄 `ecall` 的位置)
* 想要在 `user/sh.c` 的 main 中設定 break point
```gdb=
(gdb) symbol-file user/_sh
(gdb) b main
```
* 遇到 `fork()` 時,gdb 預設是繼續追 parent,如果要追 child, 可以用:
```gdb=
(gdb) info inferior
(gdb) inferior 2
```
```gdb=
(gdb) info threads
(gdb) thread $pid
```
```gdb=
set follow-fork-mode child
```
## 疑問
* 從 user mode 進入到 kernel mode 之後,用來指向 page table 的 register `satp` 有什麼變化?kernel 的 page table 有固定的位置碼?
* `ecall` 之後,扔然使用了是同一個 page table
* kernel 可不可以同時有許多 process?
* ```kernel/vm.c``` 這個檔案在整個 system call 的過程中,起到了什麼樣的作用?
* kernel mode 使用的是 physical memory 嗎?
## 參考資料
* https://pdos.csail.mit.edu/6.S081/2022/schedule.html
* https://blog.csdn.net/dai_xiangjun/article/details/123967946
* https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec06-isolation-and-system-call-entry-exit-robert/6.4-ecall-zhi-ling-zhi-hou-de-zhuang-tai
* https://blog.csdn.net/dai_xiangjun/article/details/124098461