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
也就是說,rcx
和r11
會被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
也就是說:
- 作業系統需要保護
rcx
與r11
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行,保護rcx
與r11
。現在的棧是安全的,可以放心使用。
第50~51行,呼叫rax
指定的函式。
第53~54行,恢復rcx
與r11
。
第56行,恢復3特權級棧。
第58行,從快速系統呼叫返回。
第60~63行,定義了系統呼叫表。1號系統呼叫保留給後續章節使用。
接下來,請看本章程式碼23/Start.s
。
_start
函式是3特權級任務的真正入口,其用於使任務在結束後自動退出。
23.6 編譯與測試
本章程式碼23/Makefile
增加了Syscall.s
與Start.s
的編譯與連結命令。
本章程式碼23/Kernel.c
與23/Test.c
測試了0與2號系統呼叫。