使用 C++ 編寫內核模式驅動程序的優(yōu)點與缺點
轉 C++ 及其對象特性似乎與 Microsoft Windows Driver Model (WDM) 和 Windows Driver Foundation (WDF) 驅動程序的語義非常吻合。但是,對于內核模式驅動程序,C++ 語言的一些特性可能導致難以發(fā)現(xiàn)和解決的問題。為了幫助您進行合理選擇,本文將與您分享來自 Microsoft 關于使用 C++ 為 Windows 家族操作系統(tǒng)編寫內核模式驅動程序的調查的見解和建議。 此信息適用于以下操作系統(tǒng): 借助其對象特性,C++ 似乎與 Microsoft Windows Driver Model (WDM) 和 Windows Driver Foundation (WDF) 驅動程序的語義非常吻合,而且它為開發(fā)人員帶來的便利性和極富表現(xiàn)性的功能確實很有吸引力。但是,使用目前可用的 Microsoft 編譯器在 C++ 中編寫內核模式代碼涉及到一些技術問題,這些問題可能引起驅動程序代碼中的其他問題。 許多開發(fā)人員將 C++ 編譯器當作“超級 C”來使用,而沒有完全使用 C++ 的功能,因為 C++ 編譯器執(zhí)行的某些規(guī)則比標準 C 編譯器更加嚴格,而且提供一些能夠在驅動程序上下文中安全使用的附加特性。通常認為 C++ 編譯器的這種使用方式適合于內核模式代碼。正是一些“高級的”C++ 特性引起了內核模式代碼中的問題,例如非 POD("plain old data",如 C++ 標準所定義)類和繼承、模板和異常。這些問題主要是由 C++ 實現(xiàn)和內核環(huán)境引起,而不是 C++ 語言的內在屬性。 Microsoft 正在調查與使用 C++ 為 Microsoft Windows 家族操作系統(tǒng)編寫內核模式驅動程序相關的問題。本文將與您分享 Micorsoft 開發(fā)人員關于如何權衡使用 C++ 編寫驅動程程序的利弊的最新見解。 本文內容適用于創(chuàng)建內核模式驅動程序的標準 Windows Driver Development Kit (DDK) 構建環(huán)境(從 Windows Server 2003 Service Pack 1 (SP1) DDK 開始)。如果您使用的構建環(huán)境或編譯器不是由 DDK 或 Windows Driver Kit (WDK) 提供的,那么您應該確定本文討論的各個問題是否適用于您的開發(fā)環(huán)境,以及是否存在其他問題。確定該問題的信息可以通過文檔的形式從編譯器提供者獲得,但是正如下面所描述的,您可能更有必要檢查生成的代碼和鏈接圖。 本文不打算討論如何使用 C++ 編寫內核模式驅動程序,而是假設您了解編寫內核模式驅動程序的基本原理。有關編寫內核模式驅動程序的一般信息,請參閱內核模式體系結構指南和 Windows DDK 文檔中的設備特定信息。 內核模式代碼注意事項內核模式代碼必須考慮以下因素,以避免損壞數(shù)據(jù)、系統(tǒng)不穩(wěn)定和操作系統(tǒng)沖突。 內核管理其自己的內存頁:
并不是隨時都可以使用所有的處理器資源。
資源(尤其是堆棧)具有嚴格的限制。用戶空間中“廉價”的資源在內核模式中可能非常昂貴,或者要求采取不同的方法來獲取。具體來講,內核堆棧的大小是 3 頁。 內核模式中沒有提供所有的標準庫(C 或 C++)。
將 C++ 編譯器用于內核模式代碼請務必牢記,編譯器生成的正確的目標代碼未必是您期望的代碼,其組織方式也未必是您所期望的。事實總是如此,但是 C++ 比 C 更可能發(fā)生這種問題。您必須檢查目標代碼,以確保與您的期望一致,或者至少能在內核環(huán)境中正確工作。 目前可用的 C++ 編譯器的輸出不能保證在所有平臺和版本的內核模式都能工作。代碼使用的 C++“高級”特性越多,就越可能出現(xiàn)互操作性問題。 內核模式代碼的關鍵區(qū)域
安全和不安全的 C++ 構造 一個出色的經(jīng)驗法則是,如果有一種明顯的方式可以將 C++ 構造重新整理為合法的 C 代碼,那么它可能是安全的。一個示例就是聲明的松散排序,包括在 for 語句中聲明變量。 C++ 中更嚴格的類型檢查可能不允許技術上合法但是語義上錯誤的構造。這種更嚴格的類型檢查是一種提高驅動程序可靠性的有用方式。 涉及類層次結構或模板、異常,或各種形式的動態(tài)類型的任何內容都可能不安全。使用這些構造需要對生成的目標代碼進行非常仔細的分析。將類的使用限制到 POD 類能夠顯著降低風險。 檢查生成的代碼 要使用 C++ 編寫驅動程序,必須理解編譯器生成的代碼,確保目標代碼滿足內核模式要求,并確保其不會出現(xiàn)本文討論的問題。開發(fā)人員應該做好閱讀目標代碼、瀏覽鏈接圖的準備,以確保數(shù)據(jù)和代碼都位于合適的位置并且僅使用了內核安全的庫。檢查代碼的可分頁性、內聯(lián)函數(shù)和正確的程序順序。 我們強烈建議您立即閱讀和測試這方面的代碼,而不是等到編寫完源代碼再進行閱讀和測試。檢查早期的原型并測試潛在的疑難用法,這樣如果遇到了難以克服的 C++ 問題,您還有機會找到和實現(xiàn)替代解決方案。 內核模式驅動程序的 C++ 問題Microsoft 開發(fā)人員已經(jīng)發(fā)現(xiàn) C++ 中容易出現(xiàn)特定的內核模式驅動程序問題的一些區(qū)域。 內存中的代碼 C++ 編譯器為非 POD 類和模板生成代碼的方式使得很難確定執(zhí)行一個函數(shù)所需的所有代碼的去向,因此很難將代碼安全地分頁。編譯器能夠為至少下列對象自動生成代碼。如果這些對象不一致,開發(fā)人員無法直接控制插入這些代碼的節(jié),這意味著當需要這些代碼時,它們卻可能已經(jīng)被分頁出去。
C++ 編譯器沒有提供機制來直接控制這些實體在內存中的位置。C++ 的設計并沒有考慮控制內存位置的必要性。#pragma alloc_text 不能用于控制成員函數(shù)的位置,因為無法命名該成員函數(shù)(有多種原因)。編譯器生成的函數(shù)、擴展模板正文和編譯器生成 thunk 的 #pragma code_seg 的作用域比較模糊。沒有控制虛函數(shù)表的位置的機制,因為從編譯器的角度看,這種表既不是代碼也不是數(shù)據(jù)(虛函數(shù)表獨占了一節(jié))。 如果頭文件中的函數(shù)聲明為內聯(lián),但是編譯器沒有生成該函數(shù)的內聯(lián)代碼,那么根據(jù)使用該函數(shù)的位置,它可能被插入多個代碼段中。實例化一個類模板時,它會在首次使用它的節(jié)中生成,并且通常不會立即發(fā)現(xiàn)是哪一節(jié)生成了它。這兩個問題會造成不應該分頁的代碼變得可以分頁,或者應該分頁的代碼卻無法分頁。 如果使用了一種類層次結構,那么是否需要在訪問派生類時將基類代碼放入內存中完全取決于從派生類調用的基類函數(shù)(和編譯器是否能夠內聯(lián)這些函數(shù)),以及在哪些節(jié)插入這些函數(shù)。例如,如果派生類提供了一種不需要使用基類方法的方法,那么基類代碼就無需駐留在內存中。但是,難以確定何時屬于這種情形。另外,該層次結構及其類使用的任何 thunk 也可能需要駐留在內存中。 堆棧 異常也會影響到堆棧。請參閱本文稍后的“異常與 RTTI”。 動態(tài)內存 在用戶空間中,operator new 和 operator delete 非常方便,但是如果驅動程序使用了多個內存池或帶標記的內存,那么這兩個運算符會變得很麻煩。因為 "placement new" 帶有額外的操作數(shù),所以將選擇內存池或生成標記所需的信息傳入到重載的 operator new 中,但是這并不比直接使用內存函數(shù)容易多少。因為沒有帶有額外的參數(shù)的 "placement delete" 可以傳入標記或池類型,所以使用 operatordelete 時無法傳入標記(或內存控制,如果需要),也就不可能檢查位于釋放位置的標記是否是預期的標記,這極大地影響了使用標記內存的好處。不用提供標記就可以對內存進行 delete 操作,但是您需要確定不在驅動程序代碼中使用標記的風險和缺點是否大于其便利性。 內存跟蹤工具通常記錄進行分配的函數(shù)的返回地址。一些 C++ 編譯器將 operator new 實現(xiàn)為函數(shù),這使得所有內存分配似乎都來自同一個位置,從而影響了內存跟蹤工具在這方面的功能。雖然這個問題可以解決,但是您必須確定這樣做的好處是否大于直接使用內存分配的好處。 庫
C++ 函數(shù)的導出基于它們的完整簽名,而不是(像 C 函數(shù)那樣)只基于其名稱。C++ 函數(shù)的名稱被改編為包含類型信息,該信息是其簽名的一部分。盡管名稱改編的規(guī)則相當穩(wěn)定,但是無法保證改編的名稱不隨編譯器版本的變化而改變。因此,無法將 C++ 函數(shù)可靠地導出到不同版本的庫中,但是可以表示為 extern "C" 的 C++ 函數(shù)能夠做到。另外,使用 .def 文件能夠幫助減輕這個問題的風險。注意:extern "C" 函數(shù)的獨特性僅基于函數(shù)名稱,而不是像 C++ 中那樣基于整個簽名。 不是所有的庫函數(shù)都可以在內核模式下使用,尤其是與“高級” C++ 語言特性相關的函數(shù)。標準模板庫是實現(xiàn)許多 C++ 概念(例如大小可變的數(shù)組)的“常用”方法。但是,簡單地假定標準模板庫存在且可用是不安全的。盡管標準模板庫的大部分內容都實現(xiàn)為頭文件中的源代碼,但是這個庫也會偶爾使用內核環(huán)境中不可用或沒有用處的庫函數(shù)或其他特性。 標準模板庫還假設其使用的每個數(shù)據(jù)對象都存在于單個 DLL 中。盡管在大多數(shù)情況下,可以跨越 DLL 邊界傳遞 POD 對象的引用,但是傳遞比較復雜的結構(比如列表)的引用可能導致運行時錯誤并且難以診斷。已知問題包括:如果沒有為一個 DDL 分配內存,那么釋放它的內存就會導致失敗(至少在進行調試模式編譯時是這樣);各個 DDL 的 "end of list" 標記各不相同,這會導致意外的超越列表搜索。您必須清楚這些問題并采取步驟來預防它們。 我們不建議在內核模式驅動程序中使用標準模板庫函數(shù),因為無法假定標準模板庫已經(jīng)存在并且能正常工作。對于內核模式代碼,準確理解特定數(shù)據(jù)結構的實現(xiàn)方法有助于確保該數(shù)據(jù)結構不會違背內核空間的要求。專門的實現(xiàn)也可能比更常用的標準模板庫函數(shù)更小,但是庫通常能夠更好地滿足內核空間的要求。 異常與 RTTI 運行時類型信息 (RTTI) 還需要一個庫,內核模式的C++ 中目前還沒有這個庫。迄今為止,內核模式代碼中就這個庫的請求(如果有)非常少。現(xiàn)在還無法確定這種需求的缺乏是因為其他問題的掩蓋,還是因為它對內核模式無用。 編譯器版本 您應該謹慎控制兩個驅動程序之間或一個驅動程序和操作系統(tǒng)之間的任何接口,通常使用 C 而不用 C++ 編寫這些結構。否則,C++ 實現(xiàn)的版本間不兼容性可能導致互操作失敗。 靜態(tài)變量與全局范圍變量和初始化 C++ 標準允許在局部范圍內聲明 static 變量,以在首次使用時(首次進入該范圍時)對其初始化。這種實現(xiàn)方式不但會造成初始化期間的競爭條件,還會帶來與意外的線程間數(shù)據(jù)共享相關的高風險,因為聲明為 static 的變量是全局靜態(tài),而不是基于每個線程。最好在全局范圍內顯式地處理(在線程間共享的)全局靜態(tài)數(shù)據(jù),以確保訪問保護適合于所應用的條件。 如果 C++ 全局對象要求聲明初始化(全局構造函數(shù)),則無法調用這個構造函數(shù)。不應該使用需要構造函數(shù)的全局對象,或者必須開發(fā)一種機制來確保可以調用該構造函數(shù)。網(wǎng)絡上有一些消息來源聲稱已經(jīng)解決這個問題,其中可能有適合您的解決方案。 C++ 標準沒有指定全局對象的初始化順序,所以即使存在一種調用全局對象構造函數(shù)的機制,初始化順序也必須由驅動程序明確地控制,或者該順序無關緊要。 結束語Microsoft 既不認可也不反對使用 C++ 編寫內核模式驅動程序。這種保守態(tài)度一部分源于本文所述問題,也有一部分源于支持所有平臺的需要。在嘗試使用 C++ 進行任何內核模式開發(fā)之前,您必須清楚本文講述的已知問題和風險,也應該警惕其他的未知問題。 Microsoft 一直在調查研究在內核中更有效地使用 C++ 的方法。目前還不知道適用于用戶模式代碼的所有 C++ 特性是否都可用于內核模式代碼。
細心編碼和仔細檢查生成的代碼可以避免許多問題。也有一些問題非常難以克服。所有這些問題都需要開發(fā)人員格外小心和仔細分析。 |
posted on 2009-11-06 09:13 大熊的口袋 閱讀(900) 評論(0) 編輯 收藏 引用 所屬分類: cpp 、win32