程序员的自我修养--链接、装载与库笔记:系统调用与API
系統調用(System Call)是應用程序(運行庫也是應用程序的一部分)與操作系統內核之間的接口,它決定了應用程序是如何與內核打交道的。無論程序是直接進行系統調用,還是通過運行庫,最終還是會到達系統調用這個層面上。
1. 系統調用介紹
什么是系統調用:在現代的操作系統里,程序運行的時候,本身是沒有權利訪問多少系統資源的。由于系統有限的資源有可能被多個不同的應用程序同時訪問,因此,如果不加以保護,那么各個應用程序難免產生沖突。所以現代操作系統都將可能產生沖突的系統資源給保護起來,阻止應用程序直接訪問。這些系統資源包括文件、網絡、IO、各種設備等。舉個例子,無論在Windows下還是Linux下,程序員都沒有機會擅自去訪問硬盤的某扇區上面的數據,而必須通過文件系統;也不能擅自修改任意文件,所有的這些操作都必須經由操作系統所規定的方式來進行,比如我們使用fopen去打開一個沒有權限的文件就會發生失敗。此外,有一些行為,應用程序不借助操作系統是無法辦到或不能有效地辦到的。為了讓應用程序有能力訪問系統資源,也為了讓程序借助操作系統做一些必須由操作系統支持的行為,每個操作系統都會提供一套接口,以供應用程序使用。這些接口往往通過中斷來實現,比如Linux使用0x80號中斷作為系統調用的入口,Windows采用0x2E號中斷作為系統調用入口。
系統調用覆蓋的功能很廣,有程序運行所必需的支持,例如創建/退出進程和線程、進程內存管理,也有對系統資源的訪問,例如文件、網絡、進程間通信、硬件設備的訪問,也可能有對圖形界面的操作支持,例如Windows下的GUI機制。
系統調用既然作為一個接口,而且是非常重要的接口,它的定義將十分重要。因為所有的應用程序都依賴于系統調用,那么,首先系統調用必須有明確的定義,即每個調用的含義、參數、行為都需要有嚴格而清晰的定義,這樣應用程序(運行庫)才可以正確地使用它;其次它必須保持穩定和向后兼容,如果某次系統更新導致系統調用接口發生改變,新的系統調用接口與之前版本完全不同,這是無法想象的,因為所有之前能正常運行的程序都將無法使用。所以操作系統的系統調用往往從一開始定義后就基本不做改變,而僅僅是增加新的調用接口,以保持向后兼容。
Linux系統調用:在x86下,系統調用由0x80中斷完成,各個通用寄存器用于傳遞參數,EAX寄存器用于表示系統調用的接口號,每個系統調用都對應于內核源代碼中的一個函數,它們都是以”sys_”開頭的,比如exit調用對應內核中的sys_exit函數。Linux內核版本提供了很多個系統調用,這些系統調用都可以在程序里面直接使用,它的C語言形式被定義在/usr/include/unistd.h中,比如我們完全可以繞過glibc的fopen、fread、fclose打開讀取和關閉文件,而直接使用open()、read()和close()來實現文件的讀取,使用write向屏幕輸出字符串(標準輸出的文件句柄為0),使用read系統調用來實現讀取用戶輸入(標準輸入的文件句柄為1)。不過由于繞過了glibc的文件讀取機制,所以所有位于glibc中的緩沖、按行讀取文本文件等這些機制都沒有了,讀取的就是文件的原始數據。我們也可以使用Linux的man命令查看每個系統調用的詳細說明,如: man 2 read。
系統調用的弊端:系統調用完成了應用程序和內核交流的工作,事實上,包括Linux,大部分操作系統的系統調用都有兩個特點:(1). 使用不便:操作系統提供的系統調用接口往往過于原始,程序員需要了解很多與操作系統相關的細節。如果沒有進行很好的包裝,使用起來不方便。(2). 各個操作系統之間系統調用不兼容:首先Windows系統和Linux系統之間的系統調用就基本上完全不同,雖然它們的內容很多都一樣,但是定義和實現大不一樣。即使是同系列的操作系統的系統調用都不一樣,比如Linux和UNIX就不相同。
“解決計算機的問題可以通過增加層來實現”,于是運行庫挺身而出,它作為系統調用與程序之間的一個抽象層可以保持著這樣的特點:(1). 使用簡便:因為運行庫本身就是語言級別的,它一般都設計相對比較友好。(2). 形式統一:運行庫有它的標準,叫做標準庫,凡是所有遵循這個標準的運行庫理論上都是相互兼容的,不會隨著操作系統或編譯器的變化而變化。
運行時庫將不同的操作系統的系統調用包裝為統一固定的接口,使得同樣的代碼,在不同的操作系統下都可以直接編譯,并產生一致的效果。這就是源代碼級上的可移植性。但是運行庫也有運行庫的缺陷,比如C語言的運行庫為了保證多個平臺之間能夠相互通用,于是它只能取各個平臺之間功能的交集。
2. 系統調用原理
特權級與中斷:現代的CPU常常可以在多種截然不同的特權級別下執行指令,在現代操作系統中,通常也據此有兩種特權級別,分別為用戶模式(User Mode)和內核模式(Kernel Mode),也被稱為用戶態和內核態。由于有多種特權模式的存在,操作系統就可以讓不同的代碼運行在不同的模式上,以限制它們的權力,提供穩定性和安全性。系統調用是運行在內核態的,而應用程序基本都是運行在用戶態的。用戶態的程序如何運行內核態的代碼呢?操作系統一般是通過中斷(Interrupt)來從用戶態切換到內核態。什么是中斷呢?中斷是一個硬件或軟件發出的請求,要求CPU暫停當前的工作轉手去處理更加重要的事情。
中斷一般具有兩個屬性,一個稱為中斷號(從0開始),一個稱為中斷處理程序(Interrupt Service Routine, ISR)。不同的中斷具有不同的中斷號而同時一個中斷處理程序一一對應一個中斷號。在內核中,有一個數組稱為中斷向量表(Interrupt Vector Table),這個數組的第n項包含了指向第n號中斷的中斷處理程序的指針。當中斷到來時,CPU會暫停當前執行的代碼,根據中斷的中斷號,在中斷向量表中找到對應的中斷處理程序,并調用它。中斷處理程序執行完成之后,CPU會繼續執行之前的代碼。
通常意義上,中斷有兩種類型,一種稱為硬件中斷,這種中斷來自于硬件的異常或其它事件的發生,如電源掉電、磁盤被按下等。另一種稱為軟件中斷,軟件中斷通常是一條指令(i386下是int),帶有一個參數記錄中斷號,使用這條指定用戶可以手動觸發某個中斷并執行其中斷處理程序。由于中斷號是很有限的,操作系統不會舍得用一個中斷號來對應一個系統調用,而更傾向于用一個或少數幾個中斷號來對應所有的系統調用。
基于int的Linux的經典系統調用實現:
(1). 觸發中斷:首先當程序在代碼里調用一個系統調用時,是以一個函數的形式調用的。
(2). 切換堆棧:在實際執行中斷向量表的第0x80號元素所對應的函數之前,CPU首先還要進行棧的切換。在Linux中,用戶態和內核態使用的是不同的棧,兩者各自負責各自的函數調用,互不干擾。但在應用程序調用0x80號中斷時,程序的執行流程從用戶態切換到內核態,這時程序的當前棧必須也相應地從用戶棧切換到內核棧。從中斷處理函數中返回時,程序的當前棧還要從內核棧切換回用戶棧。
(3). 中斷處理程序:在int指令合理地切換了棧之后,程序的流程就切換到了中斷向量表中記錄的0x80號中斷處理程序。內核里的系統調用函數往往以sys_加上系統調用函數名來命名,例如sys_fork、sys_open等。
Linux的新型系統調用機制:使用ldd來獲取一個可執行文件ls的共享庫的依賴情況,如下表所示,可以看到linux-vdso.so.1沒有與任何實際的文件相對應,這個共享庫是Linux用于支持新型系統調用的”虛擬”共享庫。linux-vdso.so.1并不存在實際的文件,它只是操作系統生成的一個虛擬動態共享庫(Virtual Dynamic Shared Library, VDSO)。可以通過Linux的proc文件系統來查看一個可執行程序的內存映像。命令cat ?/proc/self/maps可以查看cat命令自己的內存布局。我們可以看見地址0x7ffdc0d43000到0x7ffdc0d45000的地方被映射了vdso,也就是linux-vdso.so.1。
3. Windows API
API的全稱為Application Programming Interface,即應用程序編程接口。Windows API是指Windows操作系統提供給應用程序開發者的最底層的、最直接與Windows打交道的接口。在Windows操作系統下,CRT是建立在Windows API之上的。另外還有很多對Windows API的各種包裝庫,MFC就是很著名的一種以C++形式封裝的庫。很多操作系統是以系統調用作為應用程序最底層的,而Windows的最底層接口是Windows API。Windows API是Windows編程的基礎,盡管Windows的內核提供了數百個系統調用(Windows又把系統調用稱作系統服務(System Service)),但是出于種種原因,微軟并沒有將這些系統調用公開,而在這些系統調用之上,建立了這樣一個API層,讓程序員只能調用API層的函數,而不是如Linux一般直接使用系統調用。
Windows API概覽:Windows API是以DLL導出函數的形式暴露給應用程序開發者的。它被包含在諸多的系統DLL內,規模上非常龐大。微軟把這些Windows API DLL導出函數的聲明的頭文件、導出庫、相關文件和工具一起提供給開發者,并讓它們成為Software Development Kit(SDK)。SDK可以單獨地在微軟的官方網站下載,也可以被集成到Visual Studio這樣的開發工具中。當我們安裝了Visual Studio后,可以在SDK的安裝目錄下找到所有的Windows API函數聲明。其中有一個頭文件”Windows.h”包含了Windows API的核心部分,只要我們在程序里面包含了它,就可以使用Windows API的核心部分了。
Windows API隨著Windows版本的升級也經歷了好幾個版本,每次Windows進行大升級的時候,也會引入新版本的API。Windows API現在的數量已經十分龐大,它們按照功能被劃分成了幾大類別,如下圖所示:
由于Windows API所提供的接口還是相對比較原始的,所以直接使用API進行程序開發往往效率較低。Windows系統在API之上建立了很多應用模塊,這些應用模塊是對Windows API的功能的擴展。
為什么要使用Windows API:系統調用實際上是非常依賴于硬件結構的一種接口,它受到硬件的嚴格控制,比如寄存器的數量、調用時的參數傳遞、中斷號、堆棧切換等,都與硬件密切相關。如果硬件結構稍微發生改變,大量的應用程序可能就會出現問題(特別是那些與CRT靜態鏈接在一起的)。為了盡量隔離硬件結構的不同而導致的程序兼容性問題,Windows系統把系統調用包裝了起來,使用DLL導出函數作為應用程序的唯一可用的接口暴露給用戶。除了隔離硬件結構不同之外,Windows本身也有可能使用不同版本的內核,所以系統調用的接口自然也是不一樣的。
GitHub:https://github.com/fengbingchun/Messy_Test
總結
以上是生活随笔為你收集整理的程序员的自我修养--链接、装载与库笔记:系统调用与API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员的自我修养--链接、装载与库笔记:
- 下一篇: OpenCV代码提取:Windows上通