XV6系统调用实现
X86的保護機制
x86 有四個特權級,從 0(特權最高)編號到 3(特權最低)。在實際使用中,大多數的操作系統都使用兩個特權級,0 和 3,他們被稱為內核模式和用戶模式。當前執行指令的特權級存在于 %cs 寄存器中的 CPL 域中。
在 x86 中,中斷處理程序的入口在中斷描述符表(IDT)中被定義。這個表有256個表項,每一個都提供了相應的 %cs 和 %eip。
int指令的硬件動作
一個程序要在 x86 上進行一個系統調用,它需要調用 int n 指令,這里 n 就是 IDT 的索引。int 指令進行下面一些步驟(硬件自動完成):
- 從 IDT 中獲得第 n 個描述符,n 就是 int 的參數。
- 檢查 %cs 的域 CPL <= DPL,DPL 是描述符中記錄的特權級。
- 如果DPL < CPL,就在 CPU 內部的寄存器中保存 %esp 和 %ss 的值(即不會發生特權級切換,所以不用將程序低特權級別下的%esp 和 %ss保存到高特權級棧中)。
- 從一個任務段(TSS)描述符中加載 %ss 和 %esp(高特權級棧對應的%ss 和 %esp)。
- 將 %ss 壓棧(低特權級的%ss壓入高特權級棧 )。
- 將 %esp 壓棧(低特權級的%esp壓入高特權級棧)。
- 將 %eflags 壓棧。
- 將 %cs 壓棧。
- 將 %eip 壓棧。
- 清除 %eflags 的一些位。
- 設置 %cs 和 %eip 為描述符中的值。
int 指令是一個非常復雜的指令,可能有人會問是不是所有的這些操作都是必要的。檢查 CPL <= DPL 使得內核可以禁止一些特權級系統調用。例如,如果用戶成功執行了 int 指令,那么 DPL 必須是 3。如果用戶程序沒有合適的特權級,那么 int 指令就會觸發 int 13,這是一個通用保護錯誤。再舉一個例子,int 指令不能使用用戶棧來保存值,因為用戶可能還沒有建立一個合適的棧,因此硬件會使用任務段(TSS)中指定的棧(這個棧在內核模式中建立)。
當內陷發生時,處理器會做下面一些事。如果處理器在用戶模式下運行,它會從TSS中加載 %esp 和 %ss,把老的 %ss 和 %esp 壓入新的棧中。如果處理器在內核模式下運行,上面的事件就不會發生。處理器接下來會把 %eflags,%cs,%eip 壓棧。對于某些內陷來說,處理器會壓入一個錯誤字。而后,處理器從相應 IDT 表項中加載新的 %eip 和 %cs。
圖 3-1 展示了一個 int 指令之后的棧的情況,注意這是發生了特權級轉換(即描述符中的特權級DPL比 CPL中的特權級低的時候)棧的情況。如果這條指令沒有導致特權級轉換,x86 就不會保存 %ss 和 %esp。在任何一種情況下,%eip 都指向中斷描述符表中指定的地址,這個地址的第一條指令就是將要執行的下一條指令,也是 int n 的中斷處理程序的第一條指令。操作系統應該實現這些中斷處理程序。
操作系統可以使用 iret 指令來從一個 int 指令中返回。它從棧中彈出 int 指令保存的值,然后通過恢復保存的 %eip 的值來繼續用戶程序的執行。
特權級切換與棧切換
當特權級從用戶模式向內核模式轉換時,內核不能使用用戶的棧,因為它可能不是有效的。用戶進程可能是惡意的或者包含了一些錯誤,使得用戶的 %esp 指向一個不是用戶內存的地方。棧切換的方法是讓硬件從一個TSS中讀出新的%ss和新的 %esp 的值(內核態棧的%ss個%esp,函數 switchuvm會把用戶進程的內核棧頂地址存入TSS中)。
XV6系統調用之特權級切換與棧切換
xv6 使用一個 perl 腳本來產生IDT表項指向的中斷處理函數入口點。每一個入口都會壓入一個錯誤碼(如果 CPU 沒有壓入的話),壓入中斷號,然后跳轉到 alltraps。
Alltraps繼續保存處理器的寄存器:它壓入 %ds, %es, %fs, %gs, 以及通用寄存器EAX/EBX/ECX/EDX/ESP/EBP/ESI/EDI。這么做使得內核棧上壓入一個 trapframe(中斷幀)結構體,這個結構體包含了中斷發生時處理器的寄存器狀態(參見圖3-2)。處理器負責壓入 %ss,%esp,%eflags,%cs 和 %eip。處理器或者中斷入口會壓入一個錯誤碼(如果某些中斷,處理器不壓入錯誤碼,那么軟件中斷入口會壓入一個錯誤碼,實際上目的就是為了保持所有中斷的trapframe一致),而alltraps負責壓入剩余的。中斷幀包含了所有處理器從當前進程的內核態恢復到用戶態需要的信息,所以處理器可以恰如中斷開始時那樣繼續執行。特別的,userinit通過手動建立中斷幀來達到這個目標。
?Firgure 3-2的布局剛好和struct trapframe定義對應起來。
被保存的 %eip 是 int 指令下一條指令的地址。%cs 是用戶代碼段選擇符。%eflags 是執行 int 指令時的 eflags 寄存器,alltraps 同時也保存 %eax,它存有系統調用號,內核在之后會使用到它。
現在用戶態的寄存器都保存了,alltraps 可以完成對處理器的設置并開始執行內核的 C 代碼。處理器在進入中斷處理程序之前設置選擇符 %cs 和 %ss(硬件自動完成);alltraps 設置 %ds 和 %es。
一旦段設置好了,alltraps 就可以調用 C 中斷處理程序 trap 了。它壓入 %esp 作為 trap 的參數,%esp 指向剛在棧上建立好的中斷幀。然后它調用 trap。trap 返回后,alltraps 彈出棧上的參數然后執行標號為 trapret 處的代碼。我們在第二章闡述第一個用戶進程的時候跟蹤分析了這段代碼,在那里第一個用戶進程通過執行 trapret 處的代碼來退出到用戶空間。同樣地事情在這里也發生:彈出中斷幀會恢復用戶模式下的寄存器,然后執行 iret 會跳回到用戶空間。
現在我們討論的是發生在用戶模式下的中斷,但是中斷也可能發生在內核模式下。在那種情況下硬件不需要進行棧轉換,也不需要保存棧指針或棧的段選擇符;除此之外的別的步驟都和發生在用戶模式下的中斷一樣,執行的 xv6 中斷處理程序的代碼也是一樣的。而 iret 會恢復了一個內核模式下的 %cs,處理器也會繼續在內核模式下執行。
總結
- 上一篇: 电脑上免费的录屏软件有哪些?分享六款录屏
- 下一篇: MATLAB代码:基于分布式ADMM算法