作者:靈劍
鏈接:https://www.zhihu.com/question/306127044/answer/555327651
對操作系統(tǒng)有過了解的童鞋都知道內(nèi)核態(tài),而且大家或多或少都聽過進入內(nèi)核態(tài),這到底是是啥意思呢?這篇文章就詳細給大家科普下。
建議首先先集中力量在計算機組成原理上,不過的確單看計算機組成原理也比較枯燥,可以結(jié)合起來稍微講一下。
太長不看的提前總結(jié):
- 內(nèi)核態(tài),或者說 CPU 的特權(quán)模式,是 CPU 的一種工作狀態(tài),它影響 CPU 對不同指令的執(zhí)行結(jié)果。操作系統(tǒng)通過跟 CPU 配合,設(shè)置特權(quán)模式和用戶模式,來防止應用程序進行越權(quán)的操作。
- 防止應用程序越權(quán)訪問內(nèi)存時使用了虛擬地址空間映射的技術(shù),這是操作系統(tǒng)軟件配合硬件的結(jié)果 MMU 共同實現(xiàn)的。在用戶模式下,應用程序訪問的內(nèi)存地址是虛擬內(nèi)存地址,會映射到操作系統(tǒng)指定的物理地址上。這個虛擬內(nèi)存地址空間就是你說的用戶空間。
- 內(nèi)核態(tài)是個操作系統(tǒng)概念,雖然對應到 CPU 的特權(quán)模式,但一般如果沒有操作系統(tǒng),就不說內(nèi)核態(tài)了,直接說運行在 CPU 特權(quán)模式應該沒毛病。
- 應用程序無法自由進入內(nèi)核狀態(tài),只能通過操作系統(tǒng)提供的接口調(diào)用進入,或者在硬件中斷到來時被動進入。
- 應用程序通過操作系統(tǒng)的功能來使用硬件。
首先從問題最關(guān)鍵的地方開始:歸根到底為什么需要保護模式?
從計算機組成原理的最基礎(chǔ)的理論開始講起。說到計算機,從馮諾依曼體系講起,最重要的就是五部分:運算器、控制器、存儲器、輸入設(shè)備、輸出設(shè)備。
其中,運算器是無狀態(tài)的;控制器配合一部分寄存器,但是寄存器數(shù)量很少,而且通常都很容易被修改;輸入設(shè)備、輸出設(shè)備只有接受指令的時候才能動作。歸根結(jié)底來說,整個計算機的運行狀態(tài)幾乎完全由存儲器和少數(shù)幾個寄存器控制。
也就是說,如果一段程序能夠完全控制物理內(nèi)存,那么它就能做到任意改變計算機的狀態(tài),包括干掉整個操作系統(tǒng)然后把自己變成操作系統(tǒng);把自己變成操作系統(tǒng)的一部分等等。通常來說操作系統(tǒng)肯定是不樂意的了。
早期的 DOS 這樣的操作系統(tǒng),運行在實際模式上,就遇到的是這樣的情況:它其實將要執(zhí)行的應用程序加載變成了操作系統(tǒng)的一部分,然后混合起來運行,哪一段是用戶程序、哪一段是操作系統(tǒng)并沒有很明確的界限:用戶程序退出就回到操作系統(tǒng);用戶程序觸發(fā)軟件中斷就到操作系統(tǒng),返回又回到用戶程序;用戶程序自己可以訪問大部分的硬件設(shè)備;用戶程序甚至可以隨意修改屬于操作系統(tǒng)的數(shù)據(jù)。于是,當時的許多病毒也毫不客氣地把自己直接連接到了操作系統(tǒng)的程序里面,一旦執(zhí)行就永遠駐留成為操作系統(tǒng)的一部分。當時在 DOS 上流行的病毒可謂多種多樣、五花八門。
單任務的情況下已經(jīng)有不少問題了,到了多任務模式下,問題就更嚴重了:
- 因為多個應用程序要獨立加載,如果兩個應用程序執(zhí)意要使用同一個內(nèi)存地址,那就會發(fā)生嚴重的問題,操作系統(tǒng)必須防止這種事情發(fā)生
- 外部設(shè)備一般來說都是很傻的,它并不知道多任務的存在,不管誰操作外部設(shè)備它都是一樣響應。這樣如果多個應用程序自己直接去操縱硬件設(shè)備,就會出現(xiàn)相互沖突,有可能一個程序的數(shù)據(jù)被發(fā)送到了另一個程序等等
- 操作系統(tǒng)必須自己響應硬件中斷,通過硬件中斷來切換任務上下文,讓合適的任務在合適的時機繼續(xù)執(zhí)行。如果應用程序自己把中斷響應的程序改掉了,整個操作系統(tǒng)都會崩潰
- 操作系統(tǒng)必須有能力在單個應用程序崩潰的情況下清理這個應用程序使用的資源,保證不影響其他應用程序;這就要求它必須清楚知道每個應用程序使用了哪些資源
這還只是考慮到應用程序都是善良的情況下,要對付惡意程序就需要更強的手段。
可我們前面說了,物理內(nèi)存就是整個計算機狀態(tài)的全部,如果程序有辦法讀寫所有的物理內(nèi)存和寄存器,那任何保護手段都無濟于事。所以要限制應用程序的行為,必須在應用程序和操作系統(tǒng)執(zhí)行時有不同的狀態(tài),核心問題在于保護關(guān)鍵寄存器和重要的物理內(nèi)存。
這個目標顯然是必須要硬件配合的,否則 CPU 如何區(qū)分當前究竟是執(zhí)行操作系統(tǒng)(開放所有能力)還是應用程序(限制危險功能)呢?那么我們?nèi)绻豢紤]實際結(jié)果,只從需求上面分析如何解決這個問題,應該可以得到以下結(jié)論:
- CPU 必須至少有兩種不同的狀態(tài):操作系統(tǒng)狀態(tài)和應用程序狀態(tài)。不同狀態(tài)下,相同指令會產(chǎn)生不同的結(jié)果,也就保證某些任務只有操作系統(tǒng)能執(zhí)行,某些任務只有應用程序能執(zhí)行。
- 操作系統(tǒng)必須有辦法配合 CPU,設(shè)置哪些內(nèi)存可以訪問,哪些內(nèi)存不能訪問(或者說只有操作系統(tǒng)狀態(tài)下能訪問),不能訪問的包括操作系統(tǒng)自己的代碼區(qū)和數(shù)據(jù)區(qū)、中斷向量表等。
- 應用程序狀態(tài)下不能直接訪問硬件設(shè)備
- CPU 在觸發(fā)中斷時需要自動切換到操作系統(tǒng)狀態(tài)(否則無法進行多任務切換)
- 操作系統(tǒng)狀態(tài)可以自由切換到應用程序狀態(tài);應用程序狀態(tài)不能任意切換到操作系統(tǒng)狀態(tài),但也需要有觸發(fā)進入操作系統(tǒng)代碼并切換到操作系統(tǒng)狀態(tài)的能力(否則無法調(diào)用操作系統(tǒng)功能)
現(xiàn)在我們回到實際 CPU 的設(shè)計上,顯然實際 CPU 的設(shè)計者的思路跟我們是差不多的。這里我們叫做操作系統(tǒng)狀態(tài)的,在實際操作系統(tǒng)概念中就叫做內(nèi)核狀態(tài),在 CPU 設(shè)計上則叫做特權(quán)模式;我們叫做應用程序狀態(tài)的,在實際操作系統(tǒng)概念中叫做用戶態(tài),CPU 設(shè)計上叫做用戶模式。
注意到,內(nèi)核態(tài)并不是一個東西,沒有處于什么地方一說,它是 CPU 的兩種狀態(tài)之一。如果不是說進入內(nèi)核態(tài),而是說切換到內(nèi)核態(tài),可能你就沒有這種誤解了。都怪intel將系統(tǒng)調(diào)用的指令起名字叫sysenter,所以大家都比較習慣說“進入”內(nèi)核態(tài)。
實際上 CPU 可能被細分為更多的運行模式,而不僅僅是特權(quán)和用戶兩種模式,不過操作系統(tǒng)至少需要這兩種。有的時候特權(quán)和用戶模式也指的并不是一種真正的模式,而是一類模式,比如好幾種類似的但略有區(qū)別的運行模式都合成特權(quán)模式之類。
這種特權(quán) + 用戶的多模式切換的運行方式,就叫做(x86)CPU 的保護模式功能。保護模式之所以是一個模式,有一定的歷史原因,因為 intel CPU 每一代產(chǎn)品都會盡量兼容之前的產(chǎn)品,早期的 CPU 啟動時是實時模式,沒有這種模式切換的功能,后來的 CPU 為了兼容早期的 CPU,啟動時也處于實模式,需要引導程序主動進入保護模式,然后才擁有多模式切換的能力。這些是歷史原因和一些細節(jié)問題。
對于 CPU 本身來說,CPU 是不知道究竟哪一段代碼屬于應用程序、哪一段代碼屬于操作系統(tǒng)的,它沒有能力識別當前執(zhí)行的代碼究竟應不應該有權(quán)限,因此它只負責按照程序邏輯來執(zhí)行:如果指令自己要求自己進入用戶模式,CPU 就進入用戶模式,但進去之后,就只有特定的方法才能再回到特權(quán)模式。所以并不是說進入特權(quán)模式就一定是操作系統(tǒng)代碼了,CPU 并沒有這個保證。但是,我們說了,保護模式設(shè)計的目標就是為了讓應用程序代碼受到限制,如果應用程序的代碼進入了特權(quán)模式,這個限制就完全失效了,所以操作系統(tǒng)設(shè)計上會使用各種各樣的巧妙手段,配合CPU的功能,保障應用程序只能通過跳轉(zhuǎn)到操作系統(tǒng)代碼的方式來切換到內(nèi)核態(tài)上,這樣也就間接保障了內(nèi)核態(tài)下執(zhí)行的都是操作系統(tǒng)(包括驅(qū)動)的代碼。
接下來我們討論如何限制內(nèi)存訪問的問題,這也是這個設(shè)計中最困難的一部分。相比來說,在用戶模式下禁用一部分指令功能比較簡單,無非是控制器里加入相應的組合邏輯,判斷當前狀態(tài),如果狀態(tài)為用戶模式則拒絕執(zhí)行特權(quán)指令而已。而內(nèi)存讀寫則不一樣,指令是相同的,只是訪問的內(nèi)存地址不同,這時候有些地址是可以訪問的,有些地址則不能訪問,能不能訪問的區(qū)別僅僅在內(nèi)存地址上。要知道,CPU 是支持利用寄存器間接尋址的,因此這個非法的指令不可能在譯碼的階段就發(fā)現(xiàn),而是必須在執(zhí)行期間發(fā)現(xiàn);同時,哪些地址可以訪問,哪些地址不能訪問,必須完全是可配置的,操作系統(tǒng)有極大的自由。最后,這個系統(tǒng)還必須對應用程序有最基礎(chǔ)的友好性,不能讓應用程序太難寫。
既然內(nèi)存里每一個單元是否允許訪問都需要能夠設(shè)置,而內(nèi)存的大小是不確定的,那這個設(shè)置的數(shù)量也不確定,而且會較為龐大,在寸土寸金(?)的 CPU 里放這么多、這么復雜的設(shè)置是很不合適的,唯一可行的方案就是通過內(nèi)存自己來管理內(nèi)存——使用一部分內(nèi)存用來存儲其他內(nèi)存應該如何使用的配置。這樣,實際訪問內(nèi)存時,就需要——
先訪問內(nèi)存中的內(nèi)存配置,根據(jù)內(nèi)存配置判斷要訪問的內(nèi)存是否允許訪問,如果不允許訪問需要觸發(fā)非法操作的中斷,而如果允許訪問則正常訪問;同時,內(nèi)存中的內(nèi)存配置也是內(nèi)存的一部分,所以內(nèi)存中的內(nèi)存配置也會受到內(nèi)存中的內(nèi)存配置的管理。
僅僅從這個拗口程度上也能知道這是一件多么復雜的事情,使用內(nèi)存自己來管理內(nèi)存,這就好比左腳踩著右腳上天梯,一個不小心玩脫了就出大事了。而且為了讓帶配置的內(nèi)存使用起來有效率,還需要大量使用緩存技術(shù)。
CPU 中引入了一種稱為 MMU 的單元,它可能是現(xiàn)代 CPU 最復雜的組件之一了。它能從內(nèi)存中以指定格式加載配置,從而影響用戶模式下訪問內(nèi)存的特性。為了方便進程切換,這個格式往往有復雜的數(shù)據(jù)結(jié)構(gòu),還要支持多種多樣的配置功能。在用戶模式下,所有內(nèi)存訪問經(jīng)過 MMU,從而對內(nèi)存的訪問受到了保護;在特權(quán)模式下,內(nèi)存訪問繞過 MMU,直接訪問物理內(nèi)存,從而獲得完整的權(quán)限。
從具體設(shè)計上來說,最直接的想法就是用戶模式和特權(quán)模式都使用相同的內(nèi)存地址,只是在用戶模式下設(shè)置哪些內(nèi)存可訪問,哪些不可訪問。這種方法是否可行呢?實際上是可行的,不過略有一些缺陷:
- 在保護模式出現(xiàn)之前,編譯器都是針對實模式設(shè)計的,在編譯過程中,使用哪些內(nèi)存地址范圍、內(nèi)存的什么位置放什么數(shù)據(jù),都完全是編譯器可以自己決定的。即使是保護模式出現(xiàn)之后,操作系統(tǒng)的部分也需要相同的編譯方式。如果應用程序的編譯需要放棄這一套邏輯,改成所有地址都由操作系統(tǒng)分配,那現(xiàn)有的匯編程序和編譯器都需要重寫,這個代價難以接受。
- 應用程序經(jīng)常會需要使用一大片連續(xù)的內(nèi)存空間,比如說涉及數(shù)組的一系列算法。如果內(nèi)存空間全部都是動態(tài)分配的,那有些程序可能會不斷地申請小塊小塊的空間,從而讓內(nèi)存空間碎片化,沒有連續(xù)成片的內(nèi)存。等這些程序退出之后,釋放出來的內(nèi)存都是小塊、不連續(xù)的,操作系統(tǒng)就沒法讓其他應用程序使用連續(xù)成片的內(nèi)存了。
- 安全上有隱患,雖然應用程序沒法讀取其他內(nèi)存,但是應用程序可以知道哪些內(nèi)存已經(jīng)被其他應用程序用了,于是可以從內(nèi)存地址的分配上分析出一些信息,例如當前操作系統(tǒng)可能執(zhí)行了哪些其他應用程序,這些應用程序可能處于什么狀態(tài)等等。還有可能因為 CPU 實現(xiàn)的 bug 導致應用程序能以意想不到的方式讀取到不應當能讀取的數(shù)據(jù)。
- 現(xiàn)代操作系統(tǒng)希望支持一些高級的內(nèi)存管理方式,例如虛擬內(nèi)存——將一部分不使用的內(nèi)存暫時放在磁盤上,這樣可以用較少的內(nèi)存支撐更多的應用程序;寫時復制——兩個應用程序使用相同的內(nèi)存塊,希望能暫時使用同一個物理內(nèi)存地址,但是其中一個需要修改的時候再將它復制成兩份獨立的內(nèi)存塊,從而節(jié)約內(nèi)存。
現(xiàn)代 MMU 通常使用虛擬地址空間的技術(shù)來解決這個問題,也就是你說的“用戶空間”。在用戶模式下,所有訪問內(nèi)存的地址實際上都是虛擬地址,它與實際的物理地址是對應不上的。這樣,即便兩個應用程序使用了相同的地址,它們也可以做到互不干擾,只需要通過技術(shù)手段讓它們實際映射到不同的物理地址就行了。MMU和操作系統(tǒng)通過稱作頁表的數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)虛擬地址到物理地址的映射,一般來說在x86-64系統(tǒng)中,內(nèi)存按照4KB的大小分成頁,每個地址對齊的頁可以獨立從任意一個虛擬地址段,映射到任意一個物理內(nèi)存地址段,兩個起始地址的低12位都是0(也就是所謂地址對齊,這樣任意一個虛擬地址映射到物理地址時,最低12位不需要動)。頁表的結(jié)構(gòu)在每次進入用戶模式之前都可以重新設(shè)置,這樣切換進程之后,頁表發(fā)生了變化,同一個虛擬地址就會映射到不同的物理地址上,這就同時實現(xiàn)了多個目標:
- 應用程序有獨立的虛擬地址空間
- 應用程序只能訪問已經(jīng)映射了的虛擬地址空間,未映射的物理地址無法訪問(實現(xiàn)了保護內(nèi)存)
- 頁表和中斷向量表,理所當然不會被映射出來
- 部分RISC(x86是 CISC)的架構(gòu)上,內(nèi)存和外部設(shè)備有統(tǒng)一的地址空間,不映射外設(shè)的地址,也就阻止了對外設(shè)的訪問
- 應用程序看來連續(xù)的內(nèi)存,在物理內(nèi)存上不需要是連續(xù)的,內(nèi)存使用的效率很高
- 以某些方式訪問某些頁面時可以觸發(fā)操作系統(tǒng)的中斷,操作系統(tǒng)可以趁這個機會修改頁表,這就給操作系統(tǒng)實現(xiàn)高級內(nèi)存管理功能打下了基礎(chǔ)
最后我們來說一下應用程序怎么訪問外部設(shè)備的問題。我們說了,用戶模式下應用程序無法直接訪問硬件設(shè)備,但如果完全沒法利用硬件設(shè)備,那就太不方便了。這兩者的權(quán)衡是,應用程序通過操作系統(tǒng)使用硬件,也就是說應用程序給操作系統(tǒng)發(fā)起請求,操作系統(tǒng)處理請求時將請求轉(zhuǎn)發(fā)到硬件,硬件響應后,再將請求轉(zhuǎn)發(fā)回應用程序。
許多硬件使用中斷和 DMA 來傳輸信號或數(shù)據(jù)。這種情況下,操作系統(tǒng)開始操作后,到硬件操作完成前會有一段空閑時間,這時候操作系統(tǒng)可以將當前應用程序掛起,先去執(zhí)行其他的應用程序。當硬件操作完成時,會觸發(fā)中斷,中斷向量表在內(nèi)存中,是操作系統(tǒng)提前設(shè)置好的,指向了操作系統(tǒng)自己的代碼;同時,這個中斷也會立即強迫 CPU 進入特權(quán)模式。這時候操作系統(tǒng)就有機會來處理硬件返回的數(shù)據(jù)了,同時根據(jù)進程優(yōu)先級,可以將之前掛起的進程重新切換回來重新開始繼續(xù)執(zhí)行。
不同硬件往往有不同的接口,但操作系統(tǒng)會希望提供給應用程序統(tǒng)一的接口,這中間就涉及到驅(qū)動適配的問題,廠家的驅(qū)動程序可以將通用的請求轉(zhuǎn)化為自己家硬件能識別的請求格式。
保護模式不意味著應用程序訪問硬件的能力變?nèi)趿耍瑢嶋H上,應用程序訪問硬件的能力完全取決于操作系統(tǒng)是否允許。別說是 windows PE,實際上任意版本的 Windows 都是可以允許一個最高權(quán)限的用戶程序直接讀寫物理硬盤的(通過 CreateFileEx 的 Windows API 就可以,就跟打開一個普通文件一樣),唯一的問題在于 Windows 依賴很多磁盤文件,如果在普通 Windows 執(zhí)行過程中格式化系統(tǒng)崩潰,操作系統(tǒng)會崩潰,而 Windows PE 比較小,可以將重要的東西都整個加載到內(nèi)存里,就可以在保持操作系統(tǒng)正常工作的情況下格式化硬盤了。