程式扎記: [ Windows DDP ] Windows 驅動程式的基本概念

標籤

2010年12月14日 星期二

[ Windows DDP ] Windows 驅動程式的基本概念

前言 : 
驅動程式被作業系統載入在內核模式下, 它與 Windows 作業系統內核的其他元件進行密切互動. 對 Windows 作業系統內核的各個元件深入了解, 將有助於編寫出效能優良的驅動程式. 在這個 Windows Device Driver Programming 系列教學中, 提到的內核結構與特性, 都是基於 Windows 2000 為對像說明, 同時對其它較高版本的 Windows 也有著同樣的適用性. 

Windows 特性 : 
當 Windows 發展到 Windows 2000 時, 已經比先前版本的 Windows 有了質的飛躍. Windows 2000 在設計上是十分先進的. 接著微軟出的 Windows 2003 與 XP 其基本上沒有太大變化. 而在 Windows 2000 設計之初, 微軟公司架構師制定了如下的設計目標 : 
- 可攜性 
可攜性是指只需要少量修改, 作業系統即可在盡可能多的硬體平台上執行. Windows 必須能執行在多種硬體體系結構上, 根據市場需求而能相對容易的移植到新的硬體上. 
為了實作移值性, Windows 被設計成軟體分層的體系結構. 和硬體緊密關聯的只有硬體抽象層 (HAL). 而作業系統其它重要元件幾乎和硬體沒有關連性, 這使作業系統的大部分不依賴於特定硬體. 當需要移植時, 只需要些改相關硬體抽象層即可. 
- 相容性 
相容性是指讓應用程式盡可能能在多個版本上執行. Windows 家族體系龐大, 應該讓應用程式可以執行於各個版本的 Windows. 這需要 Windows 在各個版本有非常好的相容性. 而這體現在 Windows 有著一致的 Win32 API 介面. 儘管各個版本 Windows 的 API 實作方式不盡相同, 但是 API 保持著一貫的名稱與呼叫介面. 這保證了大多數的應用程式, 不用重新編譯即可在不同版本的 Windows 上執行. 
另外為了支援以前部分 DOS 程式和 16位元 的 Windows 程式, Windows 最大限度保留了對原有 DOS 函式的支援和對舊的 16 位元 API 的支援. 同時 Windows 引入了環境子系統概念. 不同的環境子系統, 向各自的應用程式提供對應的 API 支援. 
- 健壯和可靠性 
Windows 的健壯性和可靠性主要源自於使用者模式和內核模式的劃分. 應用程式執行在權限層級別較低的使用者模式. 在使用者模式下, 所有的錯誤操作都會被作業系統偵測到, 並給予提示. 而作業系統的大部分核心程式碼, 執行在權限級別較高的內核模式下. 作業系統是經過嚴格測試過的, 可以保證正確性. 對於任何涉及硬體的操作, 應用程式都無法在使用者模式下完成, 必須透過對內核模式中的系統呼叫來完成. 關於內核模式和使用者模式的講述, 等下會有更深入介紹. 
Windows 的健壯性同樣來自於自身的分層設計, 每層的特權不同, 處在最上層的應用程式對於作業系統的操作僅僅限於對 API 的操作. API 式作業系統提供給應用程式的唯一介面. 當應用程式想存取硬體裝置時, 必須向作業系統提出申請. 作業系統會檢驗應用程式透過 API 提供的請求, 並檢驗請求參數. 當認為參數非法時, 會返回一個錯誤, 這大幅提高了系統的健壯性和可靠性. 
- 可擴充性 
可擴充性是指作業系統應該易於增加新的功能與支援新的硬體, 並且對已存在程式碼影響達到最小. 在 Windows 2000 中一個重要的設計就是內核從執行體元件分離了出來. 作業系統內核只負責執行序的調度工作. 執行序在自己的執行序 context 中. 執行序 context 指的是 CPU 暫存器的狀態, 比如堆疊暫存器, 指令暫存器, 還包括執行序 ID, 執行序的優先權等執行序相關資訊. 
內核的主要功能是調度執行序活動, 而其他作業系統元件, 如記憶體管理元件, 處理程序管理元件等作為獨立於內核的元件, 統稱執行程序元件. 執行程序元件按照模組化的方法設計, 在需要改進時後可以個別修正或者增加執行程序元件. 而這樣設計確保了 Windows 2000 的可擴充性. 
- 效能 
Windows 在整體設計上是基於分層的, 各個分層之間的呼叫會從某種程度上帶來一些效能的損失. 然而這點效能的損失可以在其他地方彌補過來. 例如在硬體抽象層, 功能呼叫是透過巨集來呼叫的, 而不是透過函式呼叫. 
另外 Windows 的 I/O 操作是基於非同步設計的. 也就是執行緒在發起一個 I/O 操作的時機可以不等待這個 I/O 操作完成. 就發起另外的 I/O 操作請求. 這樣 CPU 不會將時間浪費在等待 I/O 操作完成上. 同時 Windows 是基於多處理程序和多執行緒的. 並盡可能使多個工作 (Task) 並存執行. 在內核調度執行緒時候, 應該盡量讓各個執行緒看起來同時執行, 而不是處於等待狀態, 這樣會最小化處理器等待時間. 

使用者模式和內核模式 : 
Windows 從總體上分為內核模式 (Kernel Mode) 和使用者模式 (User Mode). 談到作業系統的內核模式和使用者模式, 一般會和 CPU 的特權聯繫起來. CPU 一般會有多個特權層, 例如 Intel 的 386 CPU 就有 4 個特權層, 分別是 第0環 (Ring0), 第1環 (Ring1) 一直到第3環 (Ring3). 其中 Ring0 的特權最高, 也就是可以執行任意程式碼, 而 Ring3 特權最低, 只能執行有限的程式碼. 其它的 CPU 也有類似的權限層級別. 
Windows 將內核模式執行在 CPU 的 Ring0 層, 而將使用者模式執行在 CPU 的 Ring3 層, Ring0/Ring3 層是 CPU 上的概念, 而使用者模式與內核模式是作業系統上的概念. 
Windows 的核心程式碼執行在內核模式下, 而非核心程式碼執行在使用者模式下. 執行在內核模式下的 Windows 的核心元件是安全的, 而且不會受到惡意攻擊, 所以這些核心元件可以進行所有權限的操作. 而執行在使用者模式下的應用程式, 是不安全而且容易受到攻擊的. 所以使用者模式下的應用程式的權限是受到限制的. 如果應用程式想要進行一些敏感操作, 如直接存取記憶體, 實體Port, 應用程式需要向內核模式下的元件提出請求. 
這個主題介紹的 Windows 驅動程式都是執行在內核模式下. 編寫驅動程式主要是為了操作硬體裝置. 這些硬體裝置的操作包括存取實體映射記憶體, 裝置連接埠. 
在早期的作業系統 (如 DOS) 中, 沒有使用者模式和內核模式之分, 所有程式都執行在 Ring0 層. 應用程式可以直接不用驅動程式而直接操作硬體裝置. 而在 Windows 作業系統中, 對硬體的操作必須透過驅動程式來完成. 驅動程式相當於 Windows 內核的 "增補程式". 針對不同的硬體裝置會有不同的 "增補程式". 
驅動程式執行在內核模式下, 擁有作業系統最高權限. 因此編寫驅動程式需要格外小心. 在使用者模式下的各種保護措施, 在內核模式下都沒有. 例如在應用程式中對空指標操作時, 作業系統會彈出對話方塊. 提示這個非法操作並終止程序. 然而在驅動程式中對空指標進行操作時, 作業系統不會檢測這個操作是否非法, 而不小心就會造成系統崩潰. 
接著我們來查看 Windows 執行內核模式與使用者模式的執行狀況. 以 Windows 為例. 在控制台點選 "系統管理工具" > "效能" 後打開效能監視器如下圖, 紅色線代表使用者模式占用 CPU time, 而綠色則是內核模式占用的 CPU time : 
 

作業系統分層 : 
現代的作業系統都是基於分層設計思路設計的. Windows 總體上分為使用者模式與內核模式. 內核模式的介面對使用者模式的應用程式提供服務. 在內核模式下, 各個模組各司其職, 並且有良好機制保證各個模組間進行正確的通訊. 
- Windows 作業系統總體架構 
Windows 的設計思想是將內核設計的盡量可能小, 並且採用 "客戶-伺服" (主從) 的結構, 作業系統的各個元件或者模組是透過訊息進行通訊. 而應用程式和作業系統是互相隔離的. 作業系統的核心程式碼執行在 特權模式下(內核模式), 而應用程式執行在非特權模式下, 即使用者模式 : 


下圖是 Windows 作業系統的簡化結構. 其中有一條線將 Windows 作業系統劃分成兩個部分, 即使用者模式和內核模式, 在使用者模式下, 應用程式呼叫各自子系統的 API 介面. 其中子系統包括 Win32 子系統, OS/2 子系統, POSIX 子系統等. 這些子系統是為了 Windows 可以更好地相容舊的 16 位元程式和移植其它子系統的程式而設計的. 而 Win32 子系統是 Windows 最主要的子系統, 其他子系統都是透過 Win32 子系統的介面實現的 :  

Win32 子系統將 API 函式轉化為 Native API 函式. 在 Native API 介面中, 已經沒有了子系統的概念, 它將呼叫轉化為系統服務函式呼叫. 其中 Native API 穿過使用者模式和內核模式的介面, 達到了內核模式. 系統服務函式透過 I/O 管理器將訊息傳遞給驅動程式. 在內核模式下, 執行體元件提供了大量的內核函式提供驅動程式呼叫. 內核主要負責處理程序與執行緒的調度情況. 驅動程式透過硬體抽象層與具體硬體進行操作. 

- 應用程式與 Win32 子系統 
在 Windows 的設計之初, Windows 設計者為了將其它作業系統的程式方便的移直到 Windows 上, 因此設計了子系統. 
每個應用程式在編譯與鏈結的時候, 都會指明該應用成是屬於哪個子系統. 例如 "/subsystem:windows" 是指明這個應用程式是 Win32 子系統的應用程式. 該應用程式可以呼叫 Win32 子系統的 API. 
Win32 子系統是最純正的 Windows 子系統, 提供了大量的 API 函式. 程式師只要熟練使用這些 Win32 的 API 就可以編寫出 Windows 應用程式. Windows API 分為三類, 分別是 USER 函式, GDI 函式和 KERNEL 函式 : 

* USER 函式 : 這類函式管理視窗, 功能表與對話方塊與控制項.
* GDI 函式 : 這類函式在實體裝置上執行繪圖操作.
* KERNEL 函式 : 這類函式管理非 GUI 資源, 例如 處理程序, 執行緒, 檔案與同步服務等.

在 Windows 系統目錄中有對應的三個系統檔案, 分別是 USER32.dll, GDI32.dll 和 KERNEL32.dll. 這三個檔案提供了以上三類 API 的介面. 當應用程式載入時候, 作業系統除了把應用程式載入到記憶體中, 同時也將以上三個 DLL 檔載入到記憶體中. 而這三個 DLL 檔負擔了 Win32 子系統的全部 "重擔". 所有的 Win32 API 的實作都是在這三個 dll 中. 然而在 Windows 2000 後, 這三個 dll 檔的實作全部被移到了內核模式. 而原本的三個 dll 只剩下了"空殼". 如果你想知道某個程式載入了哪些 DLL, 你可以下載 Dependency Walker(下載), 並將應用程式的執行檔拉到 Dependency Walker 視窗中可以得到下圖 : 


USER32 模組和 GDI 模組的實作紛紛進入內核模式, 這從某種程度上背離了 Windows 開始設計的初衷. 因為內核模式下模組越多, 系統不穩定機率越大. 但是將這兩個 DLL 的實作放到內核模式會大大提高圖形介面的執行效率. 另外 KERNEL32 模組也是基於內核的執行體元件實作的, 其中對於執行緒的調度是基於執行體下面的內核實作的. 

- Native API 
大部分 Win32 子系統的 API 都透過 Native API 實作的. Native API 函式一般都是在 Win32 API 上加上 Nt 兩個字母. 例如 CreateFile 函式對應著 NtCreateFile 函式. 所有Native API 都是在 Ntdll.dll 中實作的. 而之前提到的三個 Win32 子系統的核心 dll 檔 都是依賴於 Ntdll.dll. 
在 Win32 的底下設置一層 Native API 的呼叫是基於版本相容的考量. Win32 API 從 Windows NT 到 Windows 2000 再到 Windows XP 基本保持一致. 變化的只是 Native API. 作為應用程式的開發者. 只需了解 Win32 API. 而不用關心 Native API 的變化. 這種機制可以讓 Windows NT 上的程式直接在更高版本的 Windows 上執行, 而不用重新編譯. 

- 系統服務 
Native API 從使用者模式穿越進入內核模式, 呼叫系統服務. 從使用者模式到內核模式是透過軟插斷(Interrupt) 的方式進入的. 在不同版本 Windows 下其實作方法略有不同. 在 Windows 2000 是透過 "int 2eh" 進入內核模式. 在 Windows XP 下是透過 "sysenter" 指令完成的. 
插斷會將 Native API 中的參數和系統服務號的參數一同傳進內核模式, 不同的 Native API 會對應不同的系統服務號. 在系統服務元件中. 有一個系統服務描述表 (System Service Descriptor Table). 根據這個系統服務號為索引, 從表中可以查出對應系統函式的函式位址. 
在系統呼叫中, 會檢驗參數的合法性, 這也式作業系統最後一道屏障. 以後任何操作, 作業系統都不會作出任何檢查, 任何的錯誤都將導致系統的崩潰. 

- 執行程式元件 
Windows 執行體元件位於 Ntoskrnl.exe 的上層. 而內核位於其下層. 在 Windows 2000 以後, 執行體元件和內核從邏輯上分開. 這是從程式碼模組化和可攜性的角度考慮. 執行程式元件是內核模式下的一組服務函式. 它們都位於 ntoskrnl.exe 中. 執行服務元件又可以細分為若干個元件. 如 物件管理程式, 虛擬記憶體管理程式, I/O 管理器等. 

- 驅動程式 
驅動程式是這個主題的重點, I/O 管理器接收應用程式的請求後, 新建對應的 IRP, 並傳送至驅動程式進行處理, 有如下幾種處理方法 : 

* 根據 IRP 的請求, 直接操作具體硬體, 然後完成此 IRP 並返回.
* 將此 IRP 的請求, 轉法到更底層的驅動中, 並等待底層驅動的返回.
* 接收到 IRP 請求後, 分配新的 IRP 到其他驅動程式中, 並等待返回.

驅動程式處理 IRP 的過程往往不是單獨操作, 而是將以上這幾種操作結合在一起. 關於驅動程式處理 IRP 的問題, 後續章節會接著介紹. 

從應用程式到驅動程式 : 
打開 Windows 的裝置管理員, 可以發現這裡羅列著電腦裡所安裝的所有裝置. 這些裝置有些是實體裝置如網卡, 顯示卡等. 有些則是虛擬裝置, 如虛擬光碟機. 它沒有對應PC的硬體裝置. 還有一些是介於實體與虛擬裝置間. 比如磁碟的磁區管理裝置. 磁碟對應磁碟裝置, 磁碟上的分割區又會產生磁區裝置, 這完全是邏輯上的概念. 
PC 上的裝置千奇百萬, 所實作的功能完全不同, 如何用統一的介面操作不同的裝置, 是一個很麻煩的問題. Windows 的設計者為了簡化對不同裝置的操作, 實作對不同裝置統一介面. 將所有裝置以普通檔案看待. 也就是在 Windows 中, 無論何種裝置, 都用操作檔案的辦法去操作裝置. 
對所有裝置的操作統一成和檔案操作一樣的動作, 這一方法可以整理成 打開, 關閉, 讀寫與取消等操作. 下表列舉整理了檔案操作和裝置所使用的 Win32 API 函式 : 


下面更深入介紹 Win32 API 是如何一步步對裝置驅動程式進行讀寫操作. 下圖是整個命令傳遞過程 : 


這裡以 CreateFile API 為例, 其它操作裝置的 API 類似. 首先應用程式呼叫 CreateFile API. 這個 API 是由 Win32 子系統的三大模組中的 Kernel32.dll 實作的. CreateFile 函式會呼叫 Ntdll.dll 中的 NtCreateFile 函式, 其中 NtCreateFile 是未文檔化的函式. 程式師最好不要直接使用這個函式. 
NtCreateFile 的作用是穿越使用者模式的邊界, 進入到內核模式. 這個步驟是透過軟插斷實作的. 進入到內核模式後, 會呼叫系統的服務函式. 這裡會呼叫同名的系統服務 NtCreateFile. (這裡的 NtCreateFile 雖然和 Native API 同名, 但是屬於內核模式的系統服務呼叫) 
NtCreateFile 系統函式呼叫透過 I/O 管理器, 新建 IRP 並傳輸到裝置的驅動程式中. IRP (I/O Request Package) 即輸入輸出請求包, 是驅動程式中重要的資料結構. 驅動程式的執行全是靠 IRP 驅動的. 
驅動程式根據 IRP 進行對應操作. 這些操作一般是對裝置的直接操作, 例如對埠的讀寫操作. 對埠的讀取操作根據不同的硬體平台, 實作方式會有所不同. Windows 根據不同的硬體平台, 會有不同的硬體抽像層 (HAL). 硬體抽象層提供一組巨集, 如 READ_PORT_BUFFER_UCHAR. 

2 則留言:

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!