本文目標:照著以下步驟就可以看到整個 system call 的過程 整個過程大致上都是照著這個影片做的,但其中有幾個步驟稍稍的不同。
2. 用 gdb-multiarch debug xv6 的方式
這裡會需要開啟 2 個終端機,
先在其中一個終端機輸入
make qemu-gdb # 這是被 debug 的對象
在另一個終端機輸入
gdb-multiarch # 開啟 debuger 會開始針對上面的那個終端機中的程式進行除錯
不知道也沒關係的點:gdb-multiarch
是透過 .gdbinit
這個檔案找到 debug 的對象的
# .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
動機:$
的出現
make qemu
後 xv6 開機時,會顯示的畫面
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$
xv6 第一個執行的程式是 sh
,它最一開始會 print 出字元 $
print 的過程需要使用 system call write
。
我們的目標就在於追蹤這個
$
如何經由 system callwrite
所產生出來的過程。
這篇文章分為以下兩個部份
- 以程式碼的角度解釋
- 用
gdb
觀察實際運行的步驟
以程式碼的角度解釋
直接說結論有以下的重點:
user/sh.c
:write(2, "$ ", 2)
user/sh.asm
:write
找到對應的 C 語言程式碼
在 user/sh.c
中
// user/sh.c
int
getcmd(char *buf, int nbuf)
{
write(2, "$ ", 2); // <------ where `sh` print `$` !
memset(buf, 0, nbuf);
gets(buf, nbuf);
if(buf[0] == 0)
return -1;
return 0;
}
找到對應的組合語言
write(2, "$ ", 2);
這行式碼編譯出來的組合語言被放在 user/sh.asm
當中,對應到的組合語言片段如下:
write(2, "$ ", 2);
10: 4609 li a2,2
12: 00001597 auipc a1,0x1
16: 2ee58593 addi a1,a1,750 # 1300 <malloc+0xea>
1a: 4509 li a0,2
1c: 00001097 auipc ra,0x1
20: de8080e7 jalr -536(ra) # e04 <write>
0000000000000e04 <write>:
.global write
write:
li a7, SYS_write
e04: 48c1 li a7,16
ecall
e06: 00000073 ecall
ret
e0a: 8082 ret
組合語言解釋
wirte
這個 procedure 位於0xe04
中(virtual address)li a7, SYS_write
li
指的是 load immediate,把SYS_write
(16, 定義在kernel/syscall.h
) 存放到 registera7
當中
ecall
這個指令會觸發 trap
觸發 trap 之後,會發生什麼事情?
觸發 trap 之後可以進入到 kernel mode,觸發 trap 有三種可能的情況
- system call <—
ecall
觸發的 trap - 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 rigistersatp
: Supervisor Address Translation and Protection Registersepc
: Supervisor Exception Program Counter, 進入 supervisor mode 後,用來紀錄回到 user mode 時,要回到什麼 address 開始執行
當 trap 發生時,會做的事情:
- 把
pc
複製到secp
(Supervisor Exception Program Counter) 中 - 把現在的狀態 (user or supervisor) 紀錄在
sstatus
的 SPP bit - 把造成 trap 的原因紀錄在
scause
- 把
stvec
放到pc
中 - 開始根據
pc
往下執行
4. 實際使用 gdb
追蹤這個過程
- 把中斷點設在
ecall
(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 的流程就必須要自行設定中斷點。
- 使用
si
往下執行之前,先 print 出stvec
的值,並且把中斷點設到那個 address (virtual)
(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 語言的檢視模式
// trap.c
void
usertrap(void)
{
/* ... */
if(r_scause() == 8){
syscall();
}
/* ... */
usertrapret();
}
kernel/syscall.c: syscall()
想要直接跑到 kernel/syscall.c: syscall()
執行了話,可以使用
(gdb) break syscall
(gdb) continue
跳到 syscall()
執行
在 lab syscall 中有提到這段程式碼
usertrapret()
想要直接跑到 kernel/syscall.c: syscall()
執行了話,可以使用
(gdb) break usertrapret
(gdb) continue
跳到 usertrapret()
執行
next
到最後一行的 trampoline_userret
時,再 next
一次,之後又會進入到 assembly 的部份 (kernel/trampoline.S: <userret>
)
(gdb) layout asm # 以 assembly 檢視
kernel/trapmpoline.s
中的 userret
在 sret
那裡,也要設 break point 才能夠回到 user/sh.asm
的 ecall
下面的 ret
user/sh.asm
的 ret
(gdb) file user/_sh
(gdb) si
(gdb) layout src # 已經執行完 write(2, "$ ", 2);
整個流程到此結束
有用的指令
QEMU
ctrl + a c 可以 (進入/退出)到 QEMU 的 console
info reg
: 可以印出所有的 registerinfo mem
: 可以印出所有的 page table
gdb
p/x $satp
印出目前 pagetable 的 physicall addressx/3i 0xe06
print 出 3 個 instructions
(gdb) x/3i 0xe06
=> 0xe06: li a7,16
0xe08: ecall
0xe0c: ret
stepi
: execute one instructionprint $pc
print program counterprint/x $stvec
: 得知 trap 發生時,會跑到什麼地方開始執行print/x $sepc
:ecall
之後,紀錄 user mode 的下一個 instruction 的位置 (紀錄ecall
的位置)- 想要在
user/sh.c
的 main 中設定 break point
(gdb) symbol-file user/_sh
(gdb) b main
- 遇到
fork()
時,gdb 預設是繼續追 parent,如果要追 child, 可以用:
(gdb) info inferior
(gdb) inferior 2
(gdb) info threads
(gdb) thread $pid
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