內核態與用戶態的區別,這個寫的比較好

有一些問題相當基礎嘛……應該是初學計算機組成原理和操作系統吧,建議首先先集中力量在計算機組成原理上,不過的確單看計算機組成原理也比較枯燥,可以結合起來稍微講一下。

 

太長不看的提前總結:

  1. 內核態,或者說CPU的特權模式,是CPU的一種工作狀態,它影響CPU對不同指令的執行結果。操作系統通過跟CPU配合,設置特權模式和用戶模式,來防止應用程序進行越權的操作
  2. 防止應用程序越權訪問內存時使用了虛擬地址空間映射的技術,這是操作系統軟件配合硬件的MMU共同實現的。在用戶模式下,應用程序訪問的內存地址是虛擬內存地址,會映射到操作系統指定的物理地址上。這個虛擬內存地址空間就是你說的用戶空間。
  3. 內核態是個操作系統概念,雖然對應到CPU的特權模式,但一般如果沒有操作系統,就不說內核態了,直接說運行在CPU的特權模式應該沒毛病。
  4. 應用程序無法自由進入內核態,只能通過操作系統提供的接口調用進入,或者在硬件中斷到來時被動進入
  5. 應用程序通過操作系統功能來使用硬件

 

首先從問題最關鍵的地方開始:歸根到底爲什麼需要保護模式?

從計算機組成原理的最基礎的理論開始講起。說到計算機,從馮諾依曼體系講起,最重要的就是五部分:運算器、控制器、存儲器、輸入設備、輸出設備。

其中,運算器是無狀態的;控制器配合一部分寄存器,但是寄存器數量很少,而且通常都很容易被修改;輸入設備、輸出設備只有接受指令的時候才動作。歸根結底來說,整個計算機的運行狀態幾乎完全由存儲器和少數幾個寄存器控制。

也就是說,如果一段程序能夠完全控制物理內存,那麼它就能做到任意改變計算機的狀態,包括幹掉整個操作系統然後把自己變成操作系統;把自己變成操作系統的一部分等等。通常來說操作系統肯定是不樂意的了。

早期的DOS這樣的操作系統,運行在實模式上,就遇到的是這樣的情況:它其實將要執行的應用程序加載變成了操作系統的一部分,然後混合起來運行,哪一段是用戶程序、哪一段是操作系統並沒有很明確的界限:用戶程序退出就回到操作系統;用戶程序觸發軟中斷就到操作系統,返回又回到用戶程序;用戶程序自己可以訪問大部分的硬件設備;用戶程序甚至可以隨意修改屬於操作系統的數據。於是,當時的許多病毒也毫不客氣地把自己直接連接到了操作系統的程序裏面,一旦執行就永遠駐留成爲操作系統的一部分。當時在DOS上流行的病毒可謂多種多樣、五花八門。

單任務的情況下已經有不少問題了,到了多任務模式下,問題就更嚴重了:

  1. 因爲多個應用程序要獨立加載,如果兩個應用程序執意要使用同一個內存地址,那就會發生嚴重的問題,操作系統必須防止這種事情發生
  2. 外部設備一般來說都是很傻的,它並不知道多任務的存在,不管誰操作外部設備它都是一樣響應。這樣如果多個應用程序自己直接去操縱硬件設備,就會出現相互衝突,有可能一個程序的數據被髮送到了另一個程序等等
  3. 操作系統必須自己響應硬件中斷,通過硬件中斷來切換任務上下文,讓合適的任務在合適的時機繼續執行。如果應用程序自己把中斷響應程序改掉了,整個操作系統都會崩潰
  4. 操作系統必須有能力在單個應用程序崩潰的情況下清理這個應用程序使用的資源,保證不影響其他應用程序;這就要求它必須清楚知道每個應用程序使用了哪些資源

這還只是考慮到應用程序都是善良的情況下,要對付惡意程序就需要更強的手段。

可我們前面說了,物理內存就是整個計算機狀態的全部,如果程序有辦法讀寫所有的物理內存和寄存器,那任何保護手段都無濟於事。所以要限制應用程序的行爲,必須在應用程序和操作系統執行時有不同的狀態,核心問題在於保護關鍵寄存器和重要的物理內存

這個目標顯然是必須要硬件配合的,否則CPU如何區分當前究竟是執行操作系統(開放所有能力)還是應用程序(限制危險功能)呢?那麼我們如果不考慮實際結果,只從需求上面分析如何解決這個問題,應該可以得到以下結論:

  1. CPU必須至少有兩種不同的狀態:操作系統狀態和應用程序狀態。不同狀態下,相同指令會產生不同的結果,也就保證某些任務只有操作系統能執行,某些只有應用程序能執行。
  2. 操作系統必須有辦法配合CPU,設置哪些內存可以訪問,哪些內存不能訪問(或者說只有操作系統狀態下能訪問),不能訪問的包括操作系統自己的代碼區和數據區、中斷向量表等。
  3. 應用程序狀態下不能直接訪問硬件設備
  4. CPU在觸發中斷時需要自動切換到操作系統狀態(否則無法進行多任務切換)
  5. 操作系統狀態可以自由切換到應用程序狀態;應用程序狀態不能任意切換到操作系統狀態,但也需要有觸發進入操作系統代碼並切換到操作系統狀態的能力(否則無法調用操作系統功能)

 

現在我們回到實際CPU的設計上,顯然實際CPU的設計者的思路跟我們是差不多的。這裏我們叫做操作系統狀態的,在實際操作系統概念中就叫做內核態,在CPU設計上則叫做特權模式;我們叫做應用程序狀態的,在實際操作系統概念中叫做用戶態,CPU設計上叫做用戶模式。

注意到,內核態並不是一個東西,沒有處於什麼地方一說,它是CPU的兩種狀態之一。如果不是說進入內核態,而是說切換到內核態,可能你就沒有這種誤解了。都怪intel將系統調用的指令起名字叫sysenter,所以大家都比較習慣說“進入”內核態。

實際上CPU可能被細分爲更多的運行模式,而不僅僅是特權和用戶兩種模式,不過操作系統至少需要這兩種。有的時候特權和用戶模式也指的並不是一種真正的模式,而是一類模式,比如好幾種類似的但略有區別的運行模式都合成特權模式之類。

這種特權 + 用戶的多模式切換的運行方式,就叫做(x86)CPU的保護模式功能。保護模式之所以也是一個模式,有一定的歷史原因,因爲intel CPU每一代產品都會盡量兼容之前的產品,早期的CPU啓動時是實模式,沒有這種模式切換的功能,後來的CPU爲了兼容早期的CPU,啓動時也處於實模式,需要引導程序主動進入保護模式,然後才擁有多模式切換的能力。這些是歷史原因和一些細節問題。

 

對於CPU本身來說,CPU是不知道究竟哪一段代碼屬於應用程序、哪一段代碼屬於操作系統的,它沒有能力識別當前執行的代碼究竟應不應該有權限,因此它只負責按照程序邏輯來執行:如果指令自己要求自己進入用戶模式,CPU就進入用戶模式,但進去之後,就只有特定的方法才能再回到特權模式。所以並不是說進入特權模式就一定是操作系統代碼了,CPU並沒有這個保證。但是,我們說了,保護模式設計的目標就是爲了讓應用程序代碼受到限制,如果應用程序的代碼進入了特權模式,這個限制就完全失效了,所以操作系統設計上會使用各種各樣的巧妙手段,配合CPU的功能,保障應用程序只能通過跳轉到操作系統代碼的方式來切換到內核態上,這樣也就間接保障了內核態下執行的都是操作系統(包括驅動)的代碼。

 

接下來我們討論如何限制內存訪問的問題,這也是這個設計中最困難的一部分。相比來說,在用戶模式下禁用一部分指令功能比較簡單,無非是控制器里加入相應的組合邏輯,判斷當前狀態,如果狀態爲用戶模式則拒絕執行特權指令而已。而內存讀寫則不一樣,指令是相同的,只是訪問的內存地址不同,這時候有些地址是可以訪問的,有些地址則不能訪問,能不能訪問的區別僅僅在內存地址上。要知道,CPU是支持利用寄存器間接尋址的,因此這個非法的指令不可能在譯碼的階段就發現,而是必須在執行期間發現;同時,哪些地址可以訪問,哪些地址不能訪問,必須完全是可配置的,操作系統有極大的自由。最後,這個系統還必須對應用程序有最基礎的友好性,不能讓應用程序太難寫。

既然內存裏每一個單元是否允許訪問都需要能夠設置,而內存的大小是不確定的,那這個設置的數量也不確定,而且會較爲龐大,在寸土寸金(?)的CPU裏放這麼多、這麼複雜的設置是很不合適的,唯一可行的方案就是通過內存自己來管理內存——使用一部分內存用來存儲其他內存應該如何使用的配置。這樣,實際訪問內存時,就需要——

先訪問內存中的內存配置,根據內存配置判斷要訪問的內存是否允許訪問,如果不允許訪問需要觸發非法操作的中斷,而如果允許訪問則正常訪問;同時,內存中的內存配置也是內存的一部分,所以內存中的內存配置也會受到內存中的內存配置的管理。

僅僅從這個拗口程度上也能知道這是一件多麼複雜的事情,使用內存自己來管理內存,這就好比左腳踩着右腳上天梯,一個不小心玩脫了就出大事了。而且爲了讓帶配置的內存使用起來有效率,還需要大量使用緩存技術。

CPU中引入了一種稱爲MMU的單元,它可能是現代CPU最複雜的組件之一了。它能從內存中以指定格式加載配置,從而影響用戶模式下訪問內存的特性。爲了方便進程切換,這個格式往往有複雜的數據結構,還要支持多種多樣的配置功能。在用戶模式下,所有內存訪問經過MMU,從而對內存的訪問受到了保護;在特權模式下,內存訪問繞過MMU,直接訪問物理內存,從而獲得完整的權限。

從具體設計上來說,最直接的想法就是用戶模式和特權模式都使用相同的內存地址,只是在用戶模式下設置哪些內存可訪問,哪些不可訪問。這種方法是否可行呢?實際上是可行的,不過略有一些缺陷:

  1. 在保護模式出現之前,編譯器都是針對實模式設計的,在編譯過程中,使用哪些內存地址範圍、內存的什麼位置放什麼數據,都完全是編譯器可以自己決定的。即使是保護模式出現之後,操作系統的部分也需要相同的編譯方式。如果應用程序的編譯需要放棄這一套邏輯,改成所有地址都由操作系統分配,那現有的彙編程序和編譯器都需要重寫,這個代價難以接受。
  2. 應用程序經常會需要使用一大片連續的內存空間,比如說涉及數組的一系列算法。如果內存空間全部都是動態分配的,那有些程序可能會不斷地申請小塊小塊的空間,從而讓內存空間碎片化,沒有連續成片的內存。等這些程序退出之後,釋放出來的內存都是小塊、不連續的,操作系統就沒法讓其他應用程序使用連續成片的內存了。
  3. 安全上有隱患,雖然應用程序沒法讀取其他內存,但是應用程序可以知道哪些內存已經被其他應用程序用了,於是可以從內存地址的分配上分析出一些信息,例如當前操作系統可能執行了哪些其他應用程序,這些應用程序可能處於什麼狀態等等。還有可能因爲CPU實現的bug導致應用程序能以意想不到的方式讀取到不應當能讀取的數據。
  4. 現代操作系統希望支持一些高級的內存管理方式,例如虛擬內存——將一部分不使用的內存暫時放在磁盤上,這樣可以用較少的內存支撐更多的應用程序;寫時複製——兩個應用程序使用相同的內存塊,希望能暫時使用同一個物理內存地址,但是其中一個需要修改的時候再將它複製成兩份獨立的內存塊,從而節約內存。

現代MMU通常使用虛擬地址空間的技術來解決這個問題,也就是你說的“用戶空間”。在用戶模式下,所有訪問內存的地址實際上都是虛擬地址,它與實際的物理地址是對應不上的。這樣,即便兩個應用程序使用了相同的地址,它們也可以做到互不干擾,只需要通過技術手段讓它們實際映射到不同的物理地址就行了。MMU和操作系統通過稱作頁表的數據結構來實現虛擬地址到物理地址的映射,一般來說在x86-64系統中,內存按照4KB的大小分成頁,每個地址對齊的頁可以獨立從任意一個虛擬地址段,映射到任意一個物理內存地址段,兩個起始地址的低12位都是0(也就是所謂地址對齊,這樣任意一個虛擬地址映射到物理地址時,最低12位不需要動)。頁表的結構在每次進入用戶模式之前都可以重新設置,這樣切換進程之後,頁表發生了變化,同一個虛擬地址就會映射到不同的物理地址上,這就同時實現了多個目標:

  1. 應用程序有獨立的虛擬地址空間
  2. 應用程序只能訪問已經映射了的虛擬地址空間,未映射的物理地址無法訪問(實現了保護內存)
  3. 頁表和中斷向量表,理所當然不會被映射出來
  4. 部分RISC(x86是CISC)的架構上,內存和外部設備有統一的地址空間,不映射外設的地址,也就阻止了對外設的訪問
  5. 應用程序看來連續的內存,在物理內存上不需要是連續的,內存使用的效率很高
  6. 以某些方式訪問某些頁面時可以觸發操作系統的中斷,操作系統可以趁這個機會修改頁表,這就給操作系統實現高級內存管理功能打下了基礎

 

最後我們來說一下應用程序怎麼訪問外部設備的問題。我們說了,用戶模式下應用程序無法直接訪問硬件設備,但如果完全沒法利用硬件設備,那就太不方便了。這兩者的權衡是,應用程序通過操作系統使用硬件,也就是說應用程序給操作系統發起請求,操作系統處理請求時將請求轉發到硬件,硬件響應後,再將請求轉發迴應用程序。

許多硬件使用中斷和DMA來傳輸信號或數據。這種情況下,操作系統開始操作後,到硬件操作完成前會有一段空閒時間,這時候操作系統可以將當前應用程序掛起,先去執行其他的應用程序。當硬件操作完成時,會觸發中斷,中斷向量表在內存中,是操作系統提前設置好的,指向了操作系統自己的代碼;同時,這個中斷也會立即強迫CPU進入特權模式。這時候操作系統就有機會來處理硬件返回的數據了,同時根據進程優先級,可以將之前掛起的進程重新切換回來重新開始繼續執行。

不同硬件往往有不同的接口,但操作系統會希望提供給應用程序統一的接口,這中間就涉及到驅動適配的問題,廠家的驅動程序可以將通用的請求轉化爲自己家硬件能識別的請求格式。

保護模式不意味着應用程序訪問硬件的能力變弱了,實際上,應用程序訪問硬件的能力完全取決於操作系統是否允許。別說是Windows PE,實際上任意版本的Windows都是可以允許一個最高權限的用戶程序直接讀寫物理硬盤的(通過CreateFileEx的Windows API就可以,就跟打開一個普通文件一樣),唯一的問題在於Windows依賴很多磁盤文件,如果在普通Windows執行過程中格式化系統盤,操作系統會崩潰,而Windows PE比較小,可以將重要的東西都整個加載到內存裏,就可以在保持操作系統正常工作的情況下格式化硬盤了。

轉自:https://www.zhihu.com/question/306127044

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章