一個作業系統的設計與實現——第23章 快速系統呼叫

樱雨楼發表於2024-09-01

23.1 什麼是快速系統呼叫

系統呼叫是作業系統為3特權級任務提供服務的一種手段。在32位作業系統中,我們透過中斷實現了系統呼叫。由於系統呼叫是一個使用非常頻繁的機制,且中斷也不是專門為系統呼叫設計的,因此,64位CPU提供了系統呼叫的專用機制:快速系統呼叫。

快速系統呼叫由專用的syscall指令發起,並由專用的sysret指令返回。syscall必須從3特權級轉移到0特權級,sysret必須從0特權級返回到3特權級。快速系統呼叫全程使用暫存器傳參,並且系統呼叫函式的cs:rip是預設好的,因此,syscall/sysret均不需要引數。

綜上,快速系統呼叫的整套機制都是非常固定的,這就帶來了高效率。

23.2 快速系統呼叫的安裝

在使用快速系統呼叫之前,需要先安裝好快速系統呼叫所需的元件,這涉及到4個MSR。

23.2.1 IA32_EFER

快速系統呼叫這個功能在初始狀態下是關閉的,其開關位於IA32_EFER的第0位。這個MSR我們已經見過了,它的編號為0xc0000080

23.2.2 IA32_STAR

這個MSR的低32位是保留位;第32~47位用於設定syscall使用的0特權級段選擇子;第48~63位用於設定sysret使用的3特權級段選擇子。

注意,這裡沒有說設定的是"程式碼段選擇子",而僅僅是"段選擇子",這是因為選擇子的設定有一套比較奇怪的定義:

  • 對於第32~47位,其數值本身會被視為0特權級程式碼段選擇子;這個數值加8得到的數值會被視為0特權級資料段選擇子
  • 對於第48~63位,其數值本身會被視為3特權級相容模式程式碼段選擇子;這個數值加8得到的數值會被視為3特權級資料段選擇子;這個數值加16得到的數值會被視為3特權級IA32-e模式程式碼段選擇子。那麼,當執行sysret時,其到底選擇哪個程式碼段呢?這個問題將在下文中討論

段選擇子是描述符索引值左移3位得到的,因此加8即為GDT中的下一個描述符。也就是說,第32~47位設定的是兩個連續的段描述符中的第一個;第48~63位設定的是三個連續的段描述符中的第一個。不過,由於我們的作業系統從不使用相容模式程式碼段,因此在GDT中並沒有定義這個描述符。

這個MSR的編號為0xc0000081

23.2.3 IA32_LSTAR

這個MSR用於設定系統呼叫函式的地址,其編號為0xc0000082

23.2.4 IA32_FMASK

這個MSR用於設定RFLAGS遮蔽掩碼。具體來說,當執行syscall時,rflags會變成這樣:rflags &= ~IA32_FMASK。在我們的作業系統中,這個MSR用於遮蔽IF位,遮蔽掩碼為0x200

這個MSR的編號為0xc0000084

23.3 syscall的執行細節

當執行syscall時,CPU會執行以下操作:

  • rcx = rip
  • r11 = rflags
  • cs = IA32_STAR[32:47]
  • rip = IA32_LSTAR
  • rflags &= ~IA32_FMASK

也就是說,rcxr11會被syscall使用,它們不能用於傳參。此外,syscall不會對rsp做任何處理,這是一個很重要的問題,我們將在下文中討論。

23.4 sysret的執行細節

當執行sysret時,CPU會執行以下操作:

  • rip = rcx
  • rflags = r11
  • 如果sysret沒有64位字首,則:cs = IA32_STAR[48:63];否則:cs = IA32_STAR[48:63] + 16

也就是說:

  1. 作業系統需要保護rcxr11
  2. sysret需要具有64位字首

上述第1點將在下文中討論;第2點在nasm中可使用o64 sysret實現。

23.5 系統呼叫的實現

請看本章程式碼23/Syscall.h

第3行,宣告瞭syscallInit函式。這個函式是用匯編語言實現的。

接下來,請看本章程式碼23/Syscall.s

第15~18行,將IA32_EFER的第0位置1,開啟快速系統呼叫功能。

第20~23行,設定IA32_STAR。在GDT中,3號描述符是0特權級程式碼段,4號描述符是0特權級資料段,這兩個段描述符對應於IA32_STAR的第32~47位;5號描述符是3特權級資料段,6號描述符是3特權級程式碼段,沒有相容模式程式碼段,因此,這裡應強行將4號描述符安裝到IA32_STAR的第48~63位,使得5號和6號描述符處於正確的位置。

第25~29行,將系統呼叫函式syscallHandle的地址安裝到IA32_LSTAR

第31~34行,將遮蔽掩碼0x200安裝到IA32_FMASK

至此,快速系統呼叫準備完畢。

syscallHandle函式為系統呼叫函式。在32位作業系統中,系統呼叫由中斷實現,中斷髮生時,CPU會自動切換到0特權級棧,由於0特權級棧是作業系統提供的,所以能夠保證它的安全。那麼,什麼叫"安全的棧"?如果不切換棧,到底有什麼問題?請看下例:

void test()
{
    char s[] = "666";
    __asm__ __volatile__("syscall");
}

將這段程式碼翻譯成組合語言,可以是:

test:
	mov dword [rsp - 4], '666'
	syscall
	ret

可以發現:這個函式的rsp是沒有也不需要實際減去4的,但如果將這樣的rsp提供給系統呼叫函式使用,就是錯誤的,因為系統呼叫函式不知道棧到底應該怎麼用。這就是不安全棧帶來的問題,因此,在系統呼叫時,切換到一個安全的棧是有必要的。

然而,syscall不會自動切換棧,我們需要手動完成這個操作。0特權級棧在TSS中,TSS的地址是0xffff800000092000,但想要使用這個地址,就必須先用一個暫存器週轉64位立即數。用哪個暫存器呢?無關乎ABI,似乎用哪個都不完美。此時,我們之前設定的IA32_GS_BASE派上了用場,使用gs就可以直接操作TSS了。不僅如此,我們的作業系統的TSS是延長到128位元組的,104位元組以後的一小段記憶體可用於在換棧前備份當前的rsp。至此,換棧問題就完美解決了。

第44行,將rsp備份到[TSS + 104]

第45行,切換到0特權級棧。

第47~48行,保護rcxr11。現在的棧是安全的,可以放心使用。

第50~51行,呼叫rax指定的函式。

第53~54行,恢復rcxr11

第56行,恢復3特權級棧。

第58行,從快速系統呼叫返回。

第60~63行,定義了系統呼叫表。1號系統呼叫保留給後續章節使用。

接下來,請看本章程式碼23/Start.s

_start函式是3特權級任務的真正入口,其用於使任務在結束後自動退出。

23.6 編譯與測試

本章程式碼23/Makefile增加了Syscall.sStart.s的編譯與連結命令。

本章程式碼23/Kernel.c23/Test.c測試了0與2號系統呼叫。

相關文章