全部組件概覽
圖片來自《Windows Internal》
橫線以上的部分是使用者態的進程,下面的組件是核心態的服務。
使用者態的線程在一個保護的進程空間之中運行,儘管在核心執行模式之下他們還是有權利訪問系統空間的。因此,系統支援的進程,服務進程,使用者應用程式和環境子系統都用自己的私人進程地址空間。
注意圖中的Subsystem dll的部分。在Windows 2000中,使用者應用程式不會直接調用系統原生服務,他們會通過環境子系統動態連結程式庫來完成調用。子系統dll扮演的角色就是把一個Windows對外公開函數調用轉換成合適的內部的(非公開的)作業系統服務調用。
使用者態進程包括以下幾個。
*系統支援的進程- 比如登陸進程,或者是session manager等等不是由Service control manager啟動的進程。
*服務進程- Win32服務的宿主,比如計劃任務和背景列印程式。許多伺服器端應用程式擁有像服務一樣啟動並執行組件。比如SharePoint的Timer Service,Search Service等等。
*使用者應用程式- Win32,MS-Dos之類的
*環境子系統- 通過一系列可調用的函數向user application暴露作業系統原生服務的組件。Win32就是一個環境子系統。
核心態組件包括以下幾個。
*可動作項目- 包括一些基本的作業系統服務,比如說記憶體管理,進程線程管理,安全,IO,處理序間通訊。
*核心- 由底層作業系統函數組成,比如線程計劃,中斷和異常分配,多處理器同步。還提供一系列常規對象和基礎對象,其他的可動作項目用這些對象來實現其他進階架構。
*裝置驅動- 包括硬體驅動程式,比方說把使用者的IO函數調用轉換為某硬體裝置的IO請求,和檔案系統,網路系統一樣。
*硬體抽象層(HAL)- 相當於為核心、裝置驅動程式還有其他的可執行部分建立起來的一層隔離層,為他們屏蔽掉真實硬體的區別。也就是說,核心,裝置驅動程式什麼的看到的不是硬體,而是硬體的抽象。HAL去跟真正的硬體打交道。
*視窗系統和圖形系統- 實現GUI函數,也就是Win32 User functions, GDI functions。處理視窗,使用者介面控制項和繪圖。
該表列出了一些windows作業系統重要的組件。
Filename |
Components |
|
Ntoskrnl.exe |
Executive and kernel |
核心和執行者 |
Ntkrnlpa.exe |
Executive and kernel with support for Physical Address Extension (PAE), which allows addressing of up to 64 GB of physical memory |
支援實體位址延伸的核心和執行者,允許64GB的實體記憶體定址 |
Hal.dll |
Hardware abstraction layer |
硬體抽象層 |
Win32k.sys |
Kernel-mode part of the Win32 subsystem |
Win32子系統的核心態部分 |
Ntdll.dll |
Internal support functions and system service dispatch stubs to executive functions |
內部支援函數和系統服務 |
Kernel32.dll, Advapi32.dll, User32.dll, Gdi32.dll |
Core Win32 subsystem DLLs |
核心Win32子系統dll |
中斷和異常
圖片來自《Windows Internal》
中斷和異常都是作業系統用來把處理器從正常控制流程中轉移出來的功能。術語“陷阱”指的是處理器的一種機制,這種機制可以在異常或者中斷髮生時捕獲正在執行的線程,並且將控制流程轉換到一個作業系統指定好的地方去。這個指定的地方就是trap handler,該函數專門處理中斷和異常。
中斷和異常有什麼區別呢?中斷是非同步事件,跟當前處理器正在執行什麼沒關係。中斷主要是由IO裝置,處理器時鐘引起的,他們可以被屏蔽和開啟。對比之下,異常是一個同步事件,由某一個特定的指令引起。比如說在同樣的條件下,用同樣的資料執行同樣的代碼兩次,就會引發異常。舉例呢,異常包括非法訪問記憶體,特定的debugger指令,除零操作等。
注意,核心對待系統服務調用的方式,是把它看做異常來處理的。
硬體和軟體都可以產生異常和中斷。舉例說明,匯流排錯誤造成的異常由硬體產生的,而除0異常是一個軟體的bug。同樣,IO裝置可以產生中斷,核心自身可以產生一個軟體中斷(APC,DPC)。
當一個硬體異常或中斷產生之後,處理器記錄下足夠的機器狀態,以便於它可以回到這個控制流程的點上,好像什麼都沒有發生過一樣的繼續執行下去。要達到這種效果,處理器在被中斷線程的核心棧中建立起一個陷阱架構(trap frame),用來儲存該線程的運行狀態。這個陷阱架構通常是一個線程完整內容相關的一部分。核心處理軟體中斷有兩種方式,要不就像對待部分硬體中斷一樣,要不就是線上程發起跟軟體中斷相關的核心功能調用時,同步處理。
中斷處理
硬體中斷的典型是通過IO裝置產生的,這些IO裝置在他們需要服務的時候必須通知處理器。中斷驅動的裝置允許作業系統最大化的將IO相關的操作與主要操作重疊起來。也就是說,啟動一個線程,線上程中完成與裝置相關的IO轉換的任務,主操作在另外的線程中仍並行的運行著。裝置完成之後,該裝置中斷處理器,請求服務。定點裝置,印表機,鍵盤,磁碟驅動和網卡基本上都是中斷驅動的。
軟體也可以產生中斷。比如說核心啟動一個軟體中斷來初始化線程分配,並且非同步進入一個線程的執行之中。核心還可以禁用中斷,從而處理器可以不受影響的執行下去,這種做法並不常見,一般用在關鍵的時刻,比如說當前正在處理一個中斷或者正在處理一個異常。
核心安裝中斷陷阱處理器來響應裝置的插斷要求。中斷陷阱處理器將控制, 要不轉移給外部處理中斷的部分(routine)ISR,要不轉移給內部的核心響應中斷的函數。裝置驅動為ISR們提供裝置中斷服務,核心為其他種類的中斷提供中斷處理部分。
接下來的部分,你會發現硬體是如何通知處理器它要進行裝置中斷的,以及核心支援的中斷種類,還有裝置驅動與核心的互動方式,恩,還有核心識別的軟體中斷(還會介紹一些用於實現這些的核心對象)。
異常處理
相對於可以在任何時間發生的中斷,異常是由於運行中的程式的執行直接導致的。Win32引入了一個名為structured exception handling的設施,它允許應用程式在異常產生的時候獲得控制權。應用程式可以修複錯誤狀態然後回去繼續執行;展開棧(unwind stack)終止報錯的子程式;或者像系統聲明該異常沒有被識別,請系統繼續尋找合適的異常處理部分。
在X86上,所有的異常都有預定義好的中斷號碼,這些中斷號碼直接和IDT中用於處理異常的,指向陷阱處理器的入口相關。
除去那些足夠簡單以至於trap handler可以解決掉的異常之外,所有的異常都接受來自一個叫做exception dispatcher的系統模組的服務。exception dispatcher的任務是尋找到一個能夠解決(dispose of)這個異常的異常處理器(exception handler)。系統定義好的這些與架構無關(architecture independent)的異常包括非法訪問記憶體,整數除零,整數溢出,浮點異常和debugger斷點等。這種異常的完整列表可以在Win32 API手冊中找到。
核心捕捉這些異常對於使用者程式來說是透明的。比如說,正在debug程式,遇到了一個斷點。這時,核心會產生一個異常,通過調用debugger來處理這個異常,這樣程式就暫停在了我們指定斷點的地方,並且VS2005這樣的debugger處於啟用狀態。核心處理某些其他異常的時候,僅僅是對調用者返回不成功的狀態代碼。
無論是顯式的由軟體引發的異常還是隱式的由硬體產生的異常,當它發生時,核心中會有一連串的事件發生。
1. CPU硬體轉移控制流程到trap handler中,trap handler建立trap frame(如同中斷髮生時一樣)。trap frame的作用是能夠在異常處理結束之後讓系統成功的恢複執行。
2. Trap handler還建立一個異常記錄,包括異常的原因和一些相關的資訊。
debugger的斷點是非常常見的異常來源。所以說,異常分配器(exception dispatcher)的第一個行動就是看遭受異常的進程是否有一個debugger進程跟它相關聯。如果是,那麼它就把包含異常的進程的第一手的debug資訊(通過LPC連接埠)傳遞給debugger連接埠。(資訊發送給session manager進程,由它來指派合適的debugger進程)。
如果沒有debugger相關聯,或者說debugger不處理這個異常,那麼異常處理器就會切換開關到使用者態模式,並調用一個routine來尋找一個基於Frame(frame-based)的異常處理器。如果沒找到,或者沒有任何部分能處理該異常,異常分配器切換回核心態,然後再次調用debugger來允許使用者做進一步的debugging。(這叫做second-chance notification).
所有的win32線程,在未處理異常的棧的頂部,都被聲明了一個異常處理器(exception handler)。這個異常處理器在Win32內建函式start-of-process或者start-of-thread中被聲明。start-of-process函數在進程中的第一線程啟動並執行時候開始運行,它調用主入口。start-of-thread函數在使用者建立另一個額外線程的時候被調用。它調用CreadThread中特定的,使用者提供的線程開啟routine(thread Start routine)。
這些內部啟動函數的一般代碼顯示如下:
void Win32StartOfProcess(LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm){ __try { DWORD dwThreadExitCode = lpStartAddr(lpvThreadParm); ExitThread(dwThreadExitCode); } __except(UnhandledExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode()); }}
注意,當線程中有異常發生時,Win32 unhandled exception filter被調用。它在註冊表中尋找索引值HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug,來決定是直接運行debugger還是先問一下使用者。
windows2000上預設的debugger是Dr.Watson. 它並不是一個真正的debugger, 而更像是一個驗屍工具,採集應用程式崩潰時的各種狀態並記錄到一個記錄檔之中(Drwtsn32.log), 並且產生一個dump檔案(user.dmp), 預設情況下這兩個都可以在目錄\Documents And Settings\All Users\Documents\DrWatson 中找到。
記錄檔包含基本的資訊,例如exception code, name of the image that failed, 已經載入了的dll的列表, 調用棧和引起異常的指令資訊。
crash dump檔案中包含異常發生時進程的私人分頁(private page)。dump檔案不包含code pages中的exe和dll檔案。這個檔案可以被WinDbg開啟。 因為user.dmp檔案可以在任意一次進程崩潰時被覆蓋,你只能擁有系統最近的一次crash的dump檔案,除非你重新命名了這個檔案或者在每一次崩潰之後都將該檔案拷貝走。
如果debugger沒有在運行,並且沒有frame-based的handler被找到,核心就向跟線程的進程相關聯的異常連接埠發送資訊。這個異常連接埠(exception port)如果存在,就是由控制這個線程的環境子系統註冊的。這個異常連接埠給環境子系統一個機會來轉化這個異常為一個特定環境的訊號或者異常。最後,如果核心走到這一步了,子系統還沒處理異常的話,核心就會執行一個預設的exception handler,該handler就簡簡單單的終止引發異常的線程。
系統服務分配(System Service Dispatching)
核心的trap handler分配中斷,異常和系統服務調用。前面,我們已經看到了中斷和異常的處理工作。這裡我們來看看系統服務吧。
系統服務分配是由一個在x86處理器上執行int 0x2e指令所觸發的。因為執行int指令的結果是觸發trap,Windows在IDT的46entry處填寫資訊來指向系統服務分配器(system service dispatcher). trap引起執行的線程轉移到核心模式下,然後進入service dispatcher的系統中。一些參數被傳入EAX寄存器中,用來指示多少系統服務被請求。還有一些被傳入EBX寄存器中來指出傳遞給系統服務的參數列表的地址。
下面的代碼展現了系統服務調用的代碼
NtWriteFile:
mov eax, 0x0E ; system service number for NtWriteFile
mov ebx, esp ; point to parameters
int 0x2E ; execute system service trap
ret 0x2C ; pop parameters off stack and return to caller
系統服務分配器- KiSystemService,確認正確的最小參數數目,從使用者態棧下拷貝調用者參數到核心態棧中(這樣使用者才不能在核心態訪問這些參數的時候修改他們了)。然後執行系統服務。
軟終端請求的等級 Software Interrupt Request Levels (IRQLs)
儘管中斷控制器會進行某種程度上的中斷優先順序排序,Windows還是弄了一個自己的中斷優先順序表,也就是interrupt request levels(IRQL). 核心在內部使用數字0到31來在內部表示IRQLs,數字越大,表示的優先順序就越高。相對於核心定義了軟體的IRQLs的標準集合,HAL(硬體抽象層)將硬體中斷映射到IRQL上。
中斷是按照優先順序來順序來被響應的,高優先順序的中斷優先佔有服務。當高等級的中斷髮生時,處理器儲存中斷線程的狀態,啟用相關的trap dispatchers。Trap Dispatcher載入IRQL,然後調用終端服務routine。在service routine執行結束之後,interrupt dispatcher降低處理器的IRQL到未曾中斷時的樣子,然後載入儲存的機器狀態。中斷的線程回到原來的地方繼續執行。當核心降低IRQL,低等級的中斷被實現。如果這個發生了的話,核心重複剛才的過程來處理新的中斷。
IRQL優先順序 VS thread-scheduling優先順序
thread-scheduling優先順序是線程的一個屬性,而IRQL是一個中斷源(滑鼠,鍵盤)的屬性。另外,任意一個處理器都有一個選項,該選項可以修改作業系統代碼執行的功能。
任意一個處理器的IRQL選項都可以決定處理器可以接受到那些中斷。IRQL還可以被用來同步訪問核心態的資料結構。當核心態的線程運行時,他通過直接方式,或者調用KeRaiseIrql 和 KeLowerIrql,或者更常見的,間接的通過一些需要核心同步對象的函數調用,來提高或者降低處理器的IRQL。
核心態的線程依靠它想做什麼來提高或者降低處理器的IRQL。比如說,當一個中斷髮生的時候,trap handler(或者是處理器)提高處理器的IRQL為與中斷源相同的IRQL。這樣做會屏蔽掉所有低於那個IRQL的中斷(僅對那個處理器而言),保證了服務中斷的處理器不會被相同等級的或者更低等級的中斷所搶走。屏蔽掉的中斷要不被另一個處理器處理,或者被掛起等待IRQL降下來。所以,系統中所有的組件,包括核心和裝置驅動,都儘可能的保持一個比較低水平的IRQL。他們這樣做因為當IRQL沒有長時間被保持在一個不必要的高水平上的時候,驅動程式可以及時的響應硬體中斷。
因為修改處理器的IRQL對於系統的操作有如此重大的影響,這種修改只能在核心態中完成。使用者態模式下的線程不能修改處理器的IRQL。這意味著當使用者態程式執行的時候處理器的IRQL總是保持在一個低水平上。只有處理器執行核心態代碼的時候,IRQL才可能高一些。
每一個中斷等級都有自己的目的。比如說,核心用inter-processor interrupt (IPI) 來請求另一個處理器來做點什麼(比如分配某個線程來執行或者更新它的旁側模式緩衝儲存區)。系統時鐘在某個時間間隔之後產生一個終端,核心通過更新時鐘,衡量線程執行時間來響應中斷。如果硬體平台支援雙時鐘,核心會增添另一個時鐘中斷等級來衡量效能。HAL提供一些列的中斷等級供靠中斷方式驅動的裝置使用;準確的IRQL數值隨處理器和系統設定的不同而不同。核心使用軟體中斷來初始化thread scheduling還有來非同步打斷線程的執行。
預定義的IRQLs
讓我們從高往低看吧。
- HIGH LEVEL---僅在KeBugCheckEx當住了系統時,核心使用HIGH LEVEL,從而屏蔽掉所有的中斷。
- Power fail---指示了系統電源崩潰時的code,不過這個IRQL從來沒被使用過。
- Inter-processor interrupt---在請求另一個處理器執行什麼動作的時候才使用。
- Clock--- 該等級被系統時鐘使用,系統用它來跟蹤一天中的時間,還有衡量CPU和線程時間。
- Profile--- 在核心收集資料(profiling)時,或者開啟了效能衡量機制時,系統系統時鐘使用該等級。當核心profiling時,核心的profiling trap handler記錄下被中斷的代碼地址。隨著時間的過去,一張地址表被建立起來,有工具可以分析它。Win2k resources kit包含一個叫做KernelProfiler(Kernprof.exe)的工具,你可以使用它來查看profiling-generated資料。
- device--- 等級被用來區分優先順序次序的裝置使用
- DPC/dispatch 和APC----是核心和裝置驅動產生出來的軟體中斷使用的。
- Passive---是最低的等級,根本就不是中斷等級,它是通暢線程執行發生時的一種配置,任何終端都允許產生的。
有一個重要的限制。在DPC/dispatch等級或更高等級的代碼運行時,它不能等待一個對象,如果它等了,會迫使scheduler選擇另一個線程來運行。
另一個限制就是,IRQL DPC/dispatch或更高等級的代碼只能訪問未分頁記憶體。這個限制實際上是上一個限制的副作用。當一個分頁錯誤發生的時候,記憶體管理器會初始化磁碟IO然後等待檔案系統驅動程式來從磁碟上讀取記憶體分頁。這樣的等待會輪流一群毆球scheduler來perform內容相關的切換(可能切換到空閑進程,如果沒有使用者線程在等待運行)。所以違反這個規則,scheduler不會被啟用(因為讀盤的時候IRQL還是DPC/dispatch水平或更高水平上)。如果任意一個規則被違反了,系統會崩潰併產生一個IRQL_NOT_LESS_OR_EQUAL的崩潰代碼。違反這些規則是裝置驅動程式的一個常見bug。