1. 程式人生 > 其它 >6.s081 : trap

6.s081 : trap

Traps

Calling Convention

C資料型別和對齊

在RV32編譯器中, int是32bits, longpointerint相同, 都是32bits.

在RV64編譯器中, int是32bits, 但longpointer是64bits.

在RV32和RV64, long long是64bits的整數, float是32bits的浮點數, double是64bits的浮點數, long double是128bits的浮點數.

charunsigned 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停止運行當前指令, 將控制轉移到處理該事件的特殊程式碼:

  1. 系統呼叫: user程式執行ecall指令來要求核心完成一些任務.
  2. 異常: user或kernel指令做了一些非法操作.
  3. 裝置中斷: 裝置發出訊號說明自己需要得到注意(裝置硬體結束讀或寫).

將這三種情況稱為trap. trap是透明的, 也就是說, 程式碼執行時發生trap, 之後恢復, 程式碼本身不需要知道發生了什麼. 通常的發生順序是:

  1. trap要求將控制轉移到核心.
  2. 核心儲存暫存器和其他的狀態, 以便trap結束後恢復原始碼.
  3. 核心執行正確的處理程式碼(系統呼叫的實現程式碼或裝置驅動).
  4. 核心恢復儲存的狀態並從trap返回.
  5. 起初程式碼從它被打斷的地方恢復.

用作恢復執行的程式碼的暫存器和狀態非常重要:

  • 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, 都做以下處理:

  1. 如果trap是裝置中斷, 並sstatus的SIE bit被清除, 下面幾步都不做.
  2. 清除SIE bit.
  3. pc複製到sepc.
  4. sstatus的SPP bit儲存目前模式(user/supervisor).
  5. 設定scause來反應trap的原因.
  6. stvec複製到pc.
  7. 在新pc開始執行.

cpu不切換到核心頁表, 不切換到核心棧, 不儲存任何出了pc的暫存器. 這些都是由核心軟體完成的. cpu完成最少的工作為了給軟體提供靈活性從而提高效率.

user空間下trap的執行流程

shell中呼叫write系統呼叫為例子:

  1. uservec(kernel/trampoline.s)
  2. usertrap(kernel/trap.c)
  3. syscall(kernel/syscall.c)
  4. sys_write(kernel/sysproc.c)
  5. usertrapret(kernel/trap.c)
  6. userret(kernel/trampoline.s)

ecall指令前的狀態

wirte函式的實現, 首先將SYS_write載入到a7, 即執行第16個系統呼叫. 之後執行ecall指令, 從這開始, 程式碼跳轉到核心, 在核心完成任務後, 會繼續執行ecall之後的指令ret.

ecall加上斷點, 繼續執行. 此時pc

此時暫存器內容為

由於pcsp的值都很小, 當前程式碼執行在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實際改變三件事:

  1. ecall將程式碼從user改到supervisor mode.
  2. ecallpc值存放在sepc暫存器中.
  1. 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中.

    這是返回到使用者空間之前執行的最後兩條指令, 會將a0sscratch的值交換, a0又是通過傳遞引數獲取trapframe頁的地址.

    這是核心返回到使用者空間最後的c函式, c函式的最後一件事是呼叫fn函式, 傳遞TRAPFRAME(trapframe的地址, 儲存在a0)和user page table(儲存在a1). fn就是trampoline中的程式碼.

  • xv6在每個user page table都映射了trapframe頁(0x3fffffe000), 每個程序都有自己的trapframe頁.

由於a0sscratch交換, 此時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的地址.

下一條指令是交換satpt1, 這條指令完成後, 程式會從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存放系統呼叫號, 每個系統呼叫號對應一個系統呼叫. syscalla7獲取系統呼叫號, 並用其索引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暫存器.

由於在trampolinesret會將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的值找出trapframea0的值, 找到這個值後, 將其儲存在t0暫存器中, 再將t0sscratch交換.

之後就恢復除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.
  • 重新開啟中斷.