解析Linux系統呼叫的實現機制

系統呼叫的概述:

所謂系統呼叫就是使用者在程式中呼叫作業系統所提供的一些子功能,系統呼叫可以被看做特殊的公共子程式。系統中的各種共享資源都由作業系統統一掌管,因此在使用者程式中,凡是與資源有關的操作(如儲存分配、進行I/0傳輸以及管理檔案等),都必須透過系統呼叫方式向作業系統提出服務請求,並由作業系統代為完成。通常,一個作業系統提供的系統呼叫命令有幾十乃至上百條之多。

解析Linux系統呼叫的實現機制

為什麼需要系統呼叫:

linux核心中設定了一組用於實現系統功能的子程式,稱為系統呼叫。系統呼叫和普通庫函式呼叫非常相似,只是系統呼叫由作業系統核心提供,運行於核心態,而普通的函式呼叫由函式庫或使用者自己提供,運行於使用者態。

一般的,程序是不能訪問核心的。它不能訪問核心所佔記憶體空間也不能呼叫核心函式。CPU硬體決定了這些

為了和使用者空間上執行的程序進行互動,核心提供了一組介面。透過該介面,應用程式可以訪問硬體裝置和其他作業系統資源。這組介面在應用程式和核心之間扮演了使者的角色,應用程式傳送各種請求,而核心負責滿足這些請求(或者讓應用程式暫時擱置)。實際上提供這組介面主要是為了保證系統穩定可靠,避免應用程式肆意妄行,惹出大麻煩。

解析Linux系統呼叫的實現機制

系統呼叫在使用者空間程序和硬體裝置之間添加了一箇中間層。該層主要作用有三個:

1、它為使用者空間提供了一種統一的硬體的抽象介面。比如當需要讀些檔案的時候,應用程式就可以不去管磁碟型別和介質,甚至不用去管檔案所在的檔案系統到底是哪種型別。

2、系統呼叫保證了系統的穩定和安全。作為硬體裝置和應用程式之間的中間人,核心可以基於許可權和其他一些規則對需要進行的訪問進行裁決。舉例來說,這樣可以避免應用程式不正確地使用硬體裝置,竊取其他程序的資源,或做出其他什麼危害系統的事情。

3、每個程序都執行在虛擬系統中,而在使用者空間和系統的其餘部分提供這樣一層公共介面,也是出於這種考慮。如果應用程式可以隨意訪問硬體而核心又對此一無所知的話,幾乎就沒法實現多工和虛擬記憶體,當然也不可能實現良好的穩定性和安全性。在Linux中,系統呼叫是使用者空間訪問核心的

惟一

手段;除異常和中斷外,它們是核心惟一的合法入口。

解析Linux系統呼叫的實現機制

Linux系統呼叫的執行路徑

為了將核心程式與使用者程式隔離開,在硬體層面上提供了一次機制,將程式執行的狀態分為了不同的級別,從0到3,數字越小,訪問級別越高。0代表核心態,在該特權級別下,所有記憶體上的資料都是可見的,可訪問的。3代表使用者態,在這個特權級下,程式只能訪問一部分的記憶體區域,只能執行一些限定的指令。

作業系統在建立GTD表的時候,將GTD的每個表項中的2位(4種特權級別)設定為特權位(DPL),然後作業系統將整個記憶體分為不同的段,不同的段,在GDT對應的表項中的DPL位是不同的。比如核心記憶體段的所有特權位都為00。而使用者程式訪存時,在保護模式下都是透過段暫存器+IP暫存器來訪問的,而段暫存器裡則用兩位表示當前程序的級別(CPL),是位於核心態還是使用者態。

既然如此,那我們還有什麼辦法可以呼叫作業系統的核心程式碼呢?作業系統為了實現系統呼叫,提供了一個主動進入核心的

惟一

方式:中斷指令int。int指令會將GDT表中的DPL改為3,讓我們可以訪問核心中的函式。所以所有的系統呼叫都必須透過呼叫int指令來實現,大致的過程如下:

使用者程式中包含一段包含int指令的程式碼

作業系統寫中斷處理,獲取相調程式的編號

作業系統根據編號執行相應的程式碼

下面我們以printf函式的呼叫為例,說明該函式是如何一步一步最終落在核心函式上去的。

解析Linux系統呼叫的實現機制

圖1:應用程式、庫函式和核心系統呼叫之間的關係

printf函式是C語言的一個庫函式,它並不是真正的系統呼叫,在Unix下,它是透過呼叫write函式來完成功能的。

write函式內部就是呼叫了int中斷。一般的系統呼叫都是呼叫0x80號中斷。而作業系統中一般不會的顯式的寫出write的實現程式碼,而是透過_syscall3宏展開的實現。_syscall3是專門用來處理有3個引數的系統呼叫的函式的實現。同理還有_syscall0、_syscall1和_syscall2等,目前最大支援的引數個數為3個,這三個引數是透過ebx, ecx,edx傳遞的。如果有系統呼叫的引數超過了3個,那麼可以透過一個引數結構體來進行傳遞。

解析Linux系統呼叫的實現機制

解析Linux系統呼叫的實現機制

所以宏展開後,write函式的實現實現為:

解析Linux系統呼叫的實現機制

我們看到實際函式內部並沒有做太多的事情,主要就是呼叫int 0x80,將把相關的引數傳遞給一些通用暫存器,呼叫的結果透過eax返回。其中一個很重要的呼叫引數是__NR_write這個也是一個宏,就是wirte的系統呼叫號,在linux/include/unistd。h中被定義為4,同樣還有很多其他系統呼叫號。因為所有的系統呼叫都是透過int 0x80,那怎麼知道具體需要什麼功能呢,只能透過系統呼叫號來識別。

下面我們來看看int 0x80是如何執行的。這是一個系統中斷,作業系統對於中斷處理流程一般為:

解析Linux系統呼叫的實現機制

前3項通常由處理中斷的硬體電路完成,後3項通常由軟體(中斷服務程式)完成。

解析Linux系統呼叫的實現機制

圖2:系統呼叫中斷處理流程

那0x80號中斷的處理程式是什麼呢,我們可以看一下作業系統是如何設定這個中斷向量表的。在作業系統初始化時shecd_init函數里,呼叫了

解析Linux系統呼叫的實現機制

我們深入看一下set_system_gate函式做了什麼

解析Linux系統呼叫的實現機制

透過上面的程式碼,我們可以看出,set_system_gate把第0x80中斷表的表項中中斷處理程式入口地址設定為&system_call。並且把那一項IDT表中的DPL設定了為3, 方便使用者程式可以去訪問這個地址。

所以init 0x80最終會被system_call這個函式地址處的程式碼來實際處理。讓我們看下system_call做了什麼事情。

解析Linux系統呼叫的實現機制

我們可以發現,上面程式碼中大部分程式碼是暫存器狀態儲存與恢復,堆疊段的切換。核心程式碼為call sys_call_table(,%eax,4),它是一個函式呼叫,函式的地址為sys_call_table(,%eax,4) = sys_call_table + 4*%eax說明sys_call_table為一個數組入口,陣列中的元素長度都為4個位元組,我們要訪問陣列中的第%eax個元素。而%eax即為系統呼叫號。sys_call_table就是所有系統呼叫的函式指標陣列。

解析Linux系統呼叫的實現機制

到這裡,我們找到了最終真正的執行核心函式地址sys_write,這個是操作實現的核心程式碼,所有的螢幕列印就是由該函式最終實現。它裡面涉及IO的一些硬體驅動函式,我們在這裡就不再繼續深入了。

到此,我們已經透過printf這樣一個上層的函式介面,清楚作業系統是如何一步步為了我們提供了一個核心呼叫的方法。如此的精細控制,讓人感嘆。

下面簡單說明一下,如何在作業系統原始碼中新增兩個我們自己的系統呼叫whoami和iam

iam系統呼叫把我們指定的一個字串儲存在核心中。

whoami把核心中的透過iam設定的那個字串讀取出來。

下面是具體的操作步驟。

解析Linux系統呼叫的實現機制

要注意的是:在系統呼叫的過程中,段暫存器ds和es指向核心資料空間,而fs被設定指向使用者資料空間。因此在實際資料塊資訊傳遞過程中Linux核心就可以利用fs暫存器來執行核心資料空間與使用者資料空間之間的資料複製工作,並且在複製過程中核心程式不需要對資料邊界範圍作任何檢查操作。邊界檢查操作由CPU自動完成。核心程式的實際資料傳送工作可以使用get_fs_byte()和puts_fs_bypte()等函式進行。

解析Linux系統呼叫的實現機制

對程式設計感興趣,想了解更多的程式設計知識,關注頭條號一起玩轉程式設計

更多程式設計資訊、乾貨持續更新中~