6.s081 : trap
Traps
Calling Convention
C資料型別和對齊
在RV32編譯器中, int
是32bits, long
和pointer
和int
相同, 都是32bits.
在RV64編譯器中, int
是32bits, 但long
和pointer
是64bits.
在RV32和RV64, long long
是64bits的整數, float
是32bits的浮點數, double
是64bits的浮點數, long double
是128bits的浮點數.
char
和unsigned char
是8-bit, unsigned short
是16-bit, 當儲存在RISC-V整數暫存器時, 是用0填充的.
signed char
是8-bit有符號整數, short
是16-bit有符號整數, 當儲存在暫存器中時, 是符號填充的.
RVG呼叫傳統
a0-a7: 8個整數暫存器.
fa0-fa7: 8個浮點暫存器.
-
如果傳遞的引數是
struct
, 那麼struct
指標對齊. 如果傳遞的引數是浮點型別, 並且i<8, 那麼傳遞進浮點暫存器fai
(如果浮點引數是union
的部分, 或者是結構的陣列, 那麼傳遞進整數暫存器), 如果是整數, 那麼傳遞進整數暫存器ai
. -
小於指標位元組大小的引數傳遞進暫存器的最低有效位.
-
當兩倍於指標位元組大小的原始引數被傳遞到棧, 它們本身是對齊的. 當傳遞進整數暫存器, 它們儲存在一個對齊的奇偶暫存器對(奇儲存最低有效位). 例如, 在RV32中,
void foo(int, long long)
int
傳遞到a0暫存器,long long
傳遞到a2和a3暫存器中. -
如果引數大於兩倍指標位元組大小, 是通過引用傳遞的(
struct
的有的部分沒有通過暫存器傳遞, 那麼就通過棧傳遞, 棧指標sp
指向還沒被傳遞的第一個引數). -
函式返回值儲存在整數暫存器a0和a1和浮點暫存器fa0和fa1. 只有當要返回的浮點是隻包含一個或兩個浮點值的結構時, 在浮點暫存器中返回. 其他返回值如果符合兩個指標位元組大小就在a0和a1中返回, 如果大於兩個, 那麼返回值是通過記憶體傳遞的, 呼叫函式會在呼叫時分配記憶體並用指標指向這塊記憶體)
-
棧是向下生長的, 棧指標16 byte對齊.
七個整數暫存器t0-t6和12個浮點暫存器ft0-ft11是隻在函式呼叫有效, 並且, 是caller儲存的.
12個整數暫存器s0-s11和12個浮點暫存器fs0-fs11也只在函式呼叫有效, 並由callee儲存.
Lec5
c語言如何轉換成彙編.
- 處理器不能理解c語言, 處理器能夠理解二進位制編碼後的彙編.
- 每個處理器都有一個關聯的ISA(Instruction Sets Architecture).
- 要讓c語言執行到處理器上, 首先要寫出c程式, 之後c程式需要被編譯成彙編, 之後彙編會被翻譯成二進位制檔案(.obj或.o).
暫存器是用來進行任何運算和資料讀取的最快方式.
- callee saved: 暫存器在函式呼叫時不會儲存.
- caller saved: 暫存器在函式呼叫時會儲存(可以被其他被呼叫函式重寫).
所有暫存器都是64bit, 如果有一個32bit整數, 會通過前面補32個0(無符號)或1(有符號)來使這個整數變成64bit並存入暫存器.
棧的每個區域都是一個stack frame, 每次執行一次函式呼叫就會產生一個stack frame. 函式通過移動stack pointer來完成stack frame的空間分配.
棧是向下生長的, 建立一個新的stack frame時, 回對當前stack pointer減. stack frame包含著儲存的暫存器, 區域性變數, 如果函式引數多於8個, 那麼多餘的就會儲存在棧中.
-
返回值儲存在stack frame的第一位
-
指向前一個stack frame的指標儲存在棧的固定位置(返回值之後)
-
sp(stack pointer)
指向棧的底部並表示當前stack frame的位置 -
fp(frame pointer)
指向當前stack frame的頂部
CH4 Traps and system calls
三種事件導致cpu停止運行當前指令, 將控制轉移到處理該事件的特殊程式碼:
- 系統呼叫: user程式執行
ecall
指令來要求核心完成一些任務. - 異常: user或kernel指令做了一些非法操作.
- 裝置中斷: 裝置發出訊號說明自己需要得到注意(裝置硬體結束讀或寫).
將這三種情況稱為trap
. trap
是透明的, 也就是說, 程式碼執行時發生trap
, 之後恢復, 程式碼本身不需要知道發生了什麼. 通常的發生順序是:
trap
要求將控制轉移到核心.- 核心儲存暫存器和其他的狀態, 以便
trap
結束後恢復原始碼. - 核心執行正確的處理程式碼(系統呼叫的實現程式碼或裝置驅動).
- 核心恢復儲存的狀態並從
trap
返回. - 起初程式碼從它被打斷的地方恢復.
用作恢復執行的程式碼的暫存器和狀態非常重要:
sp
(stack pointer): 指向棧的指標.pc
(program counter): 程式計數器.- 表明當前mode的標誌位, 這個標誌位表明當前是supervisor mode還是user mode.
satp
(supervisor address traslation and protection): 包含了指向pagetable的實體記憶體地址.stvec
(supervisor trap vector base address register): 指向核心中處理trap的指令的起始地址.sepc
(supervisor exception program counter): 在trap中儲存程式計數器的值(pc之後會被stvec重寫). sret指令將sepc複製到pc來從trap返回.sscratch
(supervisor scratch register): 核心會在其存放一個值, 以便於trap開始執行.sstatus
: sstatus中的SIE bit控制硬體中斷是否開啟, SPP bit表明trap是發生在user mode還是supervisor mode, 並控制sret返回到什麼模式.scause
: trap發生的原因.
以上的暫存器只能在supervisor mode處理, 在user mode下不能被讀寫. 同時, supervisor mode下, 可以使用PTE_U
為0的PTE, supervisor mode中的程式碼不能讀寫任意實體地址, 也需要通過page table訪問記憶體.
每個cpu都有這些暫存器, 一個trap
可以被多個cpu同時處理.
RISC-V硬體對所有trap
, 都做以下處理:
- 如果
trap
是裝置中斷, 並sstatus
的SIE bit被清除, 下面幾步都不做. - 清除SIE bit.
- 將
pc
複製到sepc
. - 在
sstatus
的SPP bit儲存目前模式(user/supervisor). - 設定
scause
來反應trap
的原因. - 將
stvec
複製到pc
. - 在新
pc
開始執行.
cpu不切換到核心頁表, 不切換到核心棧, 不儲存任何出了pc
的暫存器. 這些都是由核心軟體完成的. cpu完成最少的工作為了給軟體提供靈活性從而提高效率.
user空間下trap
的執行流程
以shell
中呼叫write
系統呼叫為例子:
uservec
(kernel/trampoline.s)usertrap
(kernel/trap.c)syscall
(kernel/syscall.c)sys_write
(kernel/sysproc.c)usertrapret
(kernel/trap.c)userret
(kernel/trampoline.s)
ecall
指令前的狀態
wirte
函式的實現, 首先將SYS_write
載入到a7
, 即執行第16個系統呼叫. 之後執行ecall
指令, 從這開始, 程式碼跳轉到核心, 在核心完成任務後, 會繼續執行ecall
之後的指令ret
.
給ecall
加上斷點, 繼續執行. 此時pc
為
此時暫存器內容為
由於pc
和sp
的值都很小, 當前程式碼執行在user mode下
此時的使用者頁表, 只包含6條pte, 第三行是無效頁來作為guard page. 最後兩條pte非常大, 對映在虛擬地址的頂部, 分別是trampoline
page(0x3ffffff000)和trapframe
page(0x3fffffe000). 這兩條pte都沒有設定PTE_U
, 所以只能在supervisor mode下訪問.
目前還是在user mode下, 接下來要執行ecall
, 要進入supervisor mode.
ecall指令之後的狀態
執行ecall
之後, 目前的pc
在0x3ffffff004, 也就是trampoline
頁處.
此時頁表仍然是shell
的使用者頁表, 同時暫存器的值也沒變. 在將這些暫存器的值儲存之前, 不能使用任何暫存器
核心事先設定好了stvec
的內容為0x3ffffff000.
目前已經在supervisor mode下, 由於可以讀取trampoline
頁的內容, 通過ecall
走到trampoline
頁的, ecall
實際改變三件事:
ecall
將程式碼從user改到supervisor mode.ecall
將pc
值存放在sepc
暫存器中.
ecall
跳轉到stvec
指向的指令(trampoline
).
接下來我們需要:
- 儲存32個使用者暫存器的內容.
- 切換到kernel page table.
- 建立一個kernel stack, 並將
sp
(stack pointer)指向那個kernel stack. - 跳轉到核心c程式碼中的某些位置(trap.c).
ecall
可以完成以上一些任務, 但為了提供最大靈活性, ecall
沒有完成.
usrevec
函式
由於在RISC-V中, supervisor mode下, 程式碼不允許直接訪問實體記憶體, 只能使用page table的內容. 由於是在supervisor mode下, 是可以修改satp
中的值的, 但當前暫存器都儲存著使用者暫存器, 所以要騰出暫存器來完成操作.
-
第一個任務是騰出一個暫存器來完成一些操作. 通過
trampoline
開頭的csrrw
指令, 將a0
暫存器中的內容和sscratch
暫存器中的內容交換, 現在,a0
中的內容已經儲存下來了,uservec
可以通過操作a0
來完成一些任務. 同時,sscratch
中的值儲存在a0
,sscratch
的值在核心進入user space之前, 會將trapframe
中的地址(0x3fffffe000)儲存在sscratch
中.這是返回到使用者空間之前執行的最後兩條指令, 會將
a0
和sscratch
的值交換,a0
又是通過傳遞引數獲取trapframe
頁的地址.這是核心返回到使用者空間最後的c函式, c函式的最後一件事是呼叫
fn
函式, 傳遞TRAPFRAME
(trapframe
的地址, 儲存在a0
)和user page table(儲存在a1
).fn
就是trampoline
中的程式碼. -
xv6在每個user page table都映射了
trapframe
頁(0x3fffffe000), 每個程序都有自己的trapframe
頁.
由於a0
和sscratch
交換, 此時a0
儲存著trapframe
的地址
接下來通過對a0
中儲存著的地址操作來將user
暫存器儲存到trapframe
中.
儲存完暫存器, 仍然uservec
中, 接下來需要設定sp
.
將a0指向的記憶體地址+8也就是kernel_sp
載入到sp
. trapframe
中的kernel_sp
是由kernel進入使用者空間之前設定好的, 它的值是這個程序的kernel stack, 也就是虛擬地址的頂端.
下一條指令是向tp
暫存器寫入資料. 通過將cpu編號也就是hartid
儲存在tp
暫存器中, 可以來確定當前執行在哪個cpu上.
下一條指令是向t0
暫存器寫入資料, 這裡寫入的是我們要執行的第一個c函式的指標, 也就是usertrap
.
下一條指令是向t1
暫存器寫入資料, 這裡寫入的是kernel page table的地址.
下一條指令是交換satp
和t1
, 這條指令完成後, 程式會從user page table切換到kernel page table.
最後一條指令是從trampoline
跳躍到c程式碼中(t0
儲存的是usertrap
的地址).
所以, 我們以kernel stack, kernel pagetable的狀態跳轉到usertrap
函式.
usertrap
函式
usertrap
的任務是決定trap
的原因, 處理它並返回.
usertrap
做的第一件事是更改stvec
暫存器. 將stvec
指向kernelvec
, 這是核心空間trap
處理程式碼的位置.
並且, 需要知道當前的程序, 通過myproc()
函式來查詢, myproc()
會根據當前cpu核的編號hartid
(uservec
時儲存在tp
暫存器)找出當前執行的程序.
找到了當前程序, 接下來要儲存使用者pc
, 仍然在sepc
中, 但可能會發生這種情況: 當程式還在trap
中被處理時, 會切換到另一個程序, 並進入那個程序的使用者空間, 那個程序再呼叫一個系統呼叫而導致sepc
被覆蓋. 所以要用trapframe
來儲存這個pc
.
接下來需要找出出發trap
的原因. 由於是系統呼叫, 所以scause=8
所以可以進到if語句中. 接下來會檢視是否程序被killed, 如果是, 就直接返回.
由於此時sepc
中的值是使用者觸發trap
時的pc
, 當我們恢復使用者程式時, 希望在下一條指令恢復, 所以對儲存的pc
加4.
中斷會被trap
硬體關閉, 所以顯示開啟中斷.
接著就呼叫syscall
函式.
系統呼叫的引數會存放在a0
, a1
, ..., 並且會在a7
存放系統呼叫號, 每個系統呼叫號對應一個系統呼叫. syscall
從a7
獲取系統呼叫號, 並用其索引syscalls
. 執行系統呼叫並返回.
從syscall
返回後, 回到usertrap
函式, 會再次檢查程序是否被killed, 因為不能恢復一個killed程序.
最後usertrap
呼叫usertrapret
函式.
usertrapret
函式
usertrapret
首先會關閉中斷. 因為要更新stvec
暫存器來指向user space的trap
處理程式碼. 之前在usertrap
中, 將stvec
指向kernel space的trap
處理程式碼, 而此時我們仍然在核心中, 如果這是發生一箇中斷, 那麼程式指向會走向user space的trap
處理程式碼.
接著, 為了下一次從使用者空間轉換到核心空間可以用到這些資料, 將其儲存到暫存器中.
- 儲存了kernel page table的指標.
- 儲存了當前使用者程序的kernel stack.
- 儲存了
usertrap
函式指標, 這樣trampoline
會跳轉到這個函式(通過寫入t0
暫存器). - 從
tp
暫存器讀取當前cpu編號(hartid
), 並存儲到trapframe
中.
接下來要修改sstatus
暫存器, 其SPP bit控制了sret
指令的行為. 如果該位為0, 指向sret
時返回到user mode. 其SPIE bit控制了中斷是否開啟, 由於在usertrapret
中關閉了中斷, 所以需要開啟, 最後將修改的資料寫入sstatus
暫存器.
由於在trampoline
的sret
會將pc
設為sepc
暫存器中的值, 所以要把儲存在trapframe
中的sepc
值寫入sepc
.
接下來由於要進入user space, 所以要根據user page table的地址生成相應的satp
值.並將這個指標作為第二個引數傳遞給彙編程式碼(trampoline
). 而第一個引數就是TRAPFRAME
(trapframe
的地址). 之後計算出要跳轉的彙編程式碼的地址(trampoline
中的userret
函式).
userret
函式
userret
先切換page table, 通過usertrapret
傳入的第二個引數(儲存在a1
暫存器中), 將user page table儲存在satp
暫存器中. 由於user page table也映射了trampoline
, 所以程式不會崩潰.
此時, a0
的值為trapframe
的地址. 112(a0
)是trapframe
中儲存的a0
的值. 也就是通過a0
的值找出trapframe
中a0
的值, 找到這個值後, 將其儲存在t0
暫存器中, 再將t0
和sscratch
交換.
之後就恢復除a0
外的所有暫存器.(trapframe
中的a0
是執行系統呼叫的返回值).
除了a0
, 使用者暫存器都恢復了, 此時a0
還儲存著trapframe
的地址, 接下來要交換sscratch
(之前從trapframe
中取出a0
放入sscratch
, 所以sscratch
儲存的是系統呼叫的返回值).
交換後, a0
儲存著的是返回值, sscratch
儲存著的是trapframe
的地址值, 以供下一次trap
使用.
sret
是最後一條指令, 執行完後會:
- 程式切換回user mode(之前
sstatus
中設定了SPP bit為0, 說明返回user mode). SEPC
複製到pc
.- 重新開啟中斷.