這一篇介紹一下系統調用,熟悉一下流程。很多做客戶端的同學根本不知道這些內容。建議花時間看看相關的知識。最好的方式還是去看源碼,反匯編,才能深刻的理解。
系統調用
程序運行的時候,本身是沒有權限訪問多少系統資源的。系統資源有限,如果操作系統不進行控制,那么各個程序難免會產生沖突。線程操作系統都將可能產生沖突的系統資源保護起來,阻止程序直接訪問。比如文件、網絡、IO、各種設備等。
比如無論在Windows還是Linux中,程序員都不能直接去訪問硬盤的某扇區上的數據,必須通過文件系統,也不能擅自修改任意文件。所有這些操作必須經過操作系統規定的方式進行。比如用fopen打開沒有權限的文件就會失敗。
比如:想要程序延遲執行一段時間,不借助操作系統就是使用循環,這樣會白白消耗CPU,造成資源浪費。如果使用操作系統提供的定時器就可以方便有效。
系統調用涵蓋的功能很廣,有程序運行鎖必須的支持,如創建和退出進程、線程,進程內存管理,對系統資源的訪問等。
Linux系統調用
在x86下,系統調用是通過0x80中斷完成,各個通用寄存器用于傳遞參數。EAX寄存器用于表示系統調用的接口號。
比如:EAX=1表示退出進程,EAX=2表示創建進程,EAX=3表示讀文件或IO,EAX=4表示寫文件或IO。每個系統調用對應到內核源碼中的一個函數,他們都是以sys_
開頭的,比如exit調用對應內核中的sys_exit函數
Linux內核提供了幾百個系統調用,下面列舉部分
所以完全可以不使用glibc封裝的fopen、fread、fclose操作文件,而直接使用系統函數open,read,close實現。
技巧:Linux中可以使用man查看系統調用詳情,使用參數2表示系統調用手冊(比如 man 2 read)
如果直接使系統調用會有非常多的問題:
- 使用不方便,操作系統提供的系統調用接口往往過于原始。程序員需要了解很多跟操作系統相關的細節
- 各個操作系統之間系統調用不兼容
于是增加一個層來解決,系統調用與程序之間增加一個抽象層。這個層就是前面所說的glibc,或者API.
系統調用原理
這里單單以Linux為例,至于Windows調用原理暫時省略。
用戶態、內核態及中斷
現代操作系統中有兩種特權級別,分為用戶模式和內核模式。
多個模式存在,那么操作系統就可以讓不同代碼運行在不同模式下,進而限制代碼的權限,提高穩定性、安全性。一般普通程序在用戶態,操作會受到限制。系統調用運行在內核態,應用程序基本都是運行在用戶態被限制。
用戶態的程序通過中斷來運行內核態的代碼,進而從用戶態切換到內核態。
中斷
中斷是操作系統的一個概念。中斷是一個硬件或者軟件發出的請求,要求CPU暫停當前的工作轉手去執行更加重要的事情。
比如在編輯文本的時候,敲擊鍵盤那CPU如何得知道呢?一種是輪詢,CPU每隔一小段時間就去詢問鍵盤是否有鍵按下,但是除非一直打字,否則大部分輪訓都是得到沒有鍵按下的結果。這樣就白白浪費掉了很多資源。
另一種方式就是當鍵盤按下的時候,鍵盤芯片發一個信號給CPU,然后CPU接收到信號之后就只到鍵盤按下了。然后再去詢鍵盤具體是哪個鍵被按下,這樣的信號就是一種中斷。——硬件中斷
中斷一般有兩個屬性,一個是中斷號(從0開始),一個稱為中斷處理程序(ISR),不同中斷具有不同的中斷號,一個中斷號對應一個中斷處理程序。在內核中保存了一個數組,叫做中斷向量表,這個數組第n項包含了指向第n個中斷號的中斷處理程序的指針。當中斷來的時候,CPU就會暫停當前執行的代碼,根據中斷的中斷號,在中斷向量表中找到對應的中斷處理程序,并且調用他,處理完之后,CPU繼續執行之前的代碼。
中斷有硬件中斷,這種來至于硬件的異常或者其他時間的發生比如斷電,鍵盤按下。另一種是軟件中斷,軟件中斷一般是一條指令(i386下是int),帶有一個參數記錄中斷號,這個指令用戶可以手動觸發某個中斷并執行中斷處理程序。比如int 0x80這條指令就會調用第0x80號的中斷處理程序。
中斷號是有限的,不可能每一個系統調用都對應一個中斷號,更加合理的是用一個或少數幾個中斷號來對應所有的系統滴啊用。比如Linux中int 0x80來觸發所有的系統調用。
系統調用會有一個系統調用號,表明是哪一個系統調用,這個系統調用通常就是系統調用在系統調用表中的位置。比如Linux中的fork函數調用號是2,這個系統調用號在執行int指令前就會被放到某個固定的寄存器中,對應的中斷代碼會取得這個系統調用號,并且調用正確的函數。
比如Linux中int 0x80為例,系統調用號使用eax來傳入,用戶將系統調用號保存在eax,然后使用int 0x80調用中斷,中斷服務傳給信號就可以從eax取得系統調用號,進行調用相應的函數。
基于int的Linux經典系統調用
下面以fork為例
觸發中斷
首先當程序在代碼里面調用一個系統調用時,是以一個函數的形式調用的,比如fork:
int main() {
fork();
}
fork函數是對系統調用fork的封裝,可以用下面的宏定義:
_syscall0(pid_t, fork)
_syscall0
是一個宏,用于定義一個沒有參數的系統調用封裝。他的第一個參數為這個系統調用的返回值類型,pid_t代表進程的id。第二個參數是系統調用的名字。_syscall0
展開之后 會形成一個與系統調用名稱同名的函數。下面是i386的syscall0
匯編解釋:
-
__asm__
是一個gcc關鍵字,表示接下來要嵌入匯編代碼,volatile關鍵字告訴GCC對這段代碼不進行任何優化 -
__asm__
第一個參數是一個字符串,代表匯編代碼的文本,這里匯編代煤制油int $0x80
,表示要調用0x80號中斷 -
=a(__res)
表示用eax(a表示eax)輸出返回數據并存儲到_res中 -
0(_NR ##name)
表示用_NR ##name
為輸入,0
指示由編譯器選擇和輸出相同的寄存器(eax)來傳遞參數。
__syscal_return
是另外一個宏
最終fork函數匯編之后
當系統調用如果有參數的話會用syscall1
相比syscall0多了個
b
,它表示把arg1強制轉換為long,然后保存在ebx最為輸入。
所以如果系統內調用如果有1個參數,則參數通過ebx來傳遞。x86下的linux系統調用參數最多有6個。分別使用6個寄存器來傳遞。分別是ebx,ecx,ed想,esi,edi和ebp。
當進行系統調用的時候,CPU執行到int $0x80時,會保存現場以便恢復。接著切換到內核態,然后CPU查找中斷向量表低0x80元素。
切換堆棧
在實際執行中斷向量表中的第0x80好中斷之前,CPU還要進行堆棧的奇幻,在Linx中,用戶態和內核態使用的是不同的棧,兩者各自負責各自的函數調用,互不干擾。
在執行0x80中斷的時候,程序從用戶態切換到內核態。這時相應的棧也必須切換到內核棧,從中斷處理函數中返回時,程序當前棧需要從內核棧切換到用戶棧。
當前棧是指ESP值所在的棧空間,如果ESP的值位于用戶棧范圍能,那么程序的當前棧就是用戶棧。內核棧同理。并且SS的值需要指向當前棧所在的頁。所以用戶棧切換到內核棧就是:
- 保存當前ESP、SS的值
- 將ESP、SS的值設置為內核棧的值
(反過來同理)
- 恢復原理ESP、SS的值
- 用戶態的ESP和SS的值保存在內核棧上,
當發生中斷的時候,CPU除了進入內核態之外還會做如下事情:
- 找到當前進程的內核棧(每一個進程都有一個內核棧)
- 在內核棧中一猜壓入用戶態的寄存器SS、ESP、EFLAGS、CS、EIP
當內核從系統調用中返回,則調動iret指令回到用戶態,iret指令會從內核棧里面彈出SS、ESP、EFLAGS、CS、EIP的值。使得棧恢復到用戶態的狀態。
中斷處理程序
在int指令切換了棧之后,程序就切換到中斷向量表轉給你記錄0x80號中斷處理陳旭。下面是linux i386中斷服務流程
內核的系統調用函數往往以sys_加上系統調用函數名了,比如sys_fork,sys_open等。
關于sys開頭的系統內調用函數如何從用戶那里獲得參數的。是通過寄存實現的。我們知道用戶調用系統調用時,根據系統電泳參數數量不同,一次將參數放入EBX,EXC,EDX,ESI,EBP這6個寄存器。如果一個參數的系統調用就是EBX,兩個參數的系統調用就是EBX和ECX
小結
通過閱讀,歸根結底還是要懂匯編并且去看源碼才能把整個過程分析正確。平時所使用的把所有的細節都已經屏蔽了。地下還深藏著許多玄機。——而這一點確實大都數開發人員的短板