wdm驅(qū)動開發(fā)之路(5) |
|
WDM開發(fā)之路(五) 上一篇中我們學習了核心層編程環(huán)境和異常處理的一部分,這篇我們繼續(xù)學習異常處理的余下部分和內(nèi)存管理,。 在上一篇中,,我們知道在Try-Except塊中的過濾表達式值可以有以下三種: EXCEPTION_EXECUTE_HANDLE (1) EXCEPTION_CONTINUE_SEARCH (0) EXCEPTION_CONTINUE_EXECUTION (-1)
EXCEPTION_EXECUTE_HANDLE 指示錯誤處理程序執(zhí)行Except塊中的用戶定義異常處理語句。 EXCEPTION_CONTINUE_SEARCH 操作系統(tǒng)將繼續(xù)掃描堆棧以定位相應的錯誤處理程序,。此時用戶定義的錯誤處理程序?qū)⒌貌坏綀?zhí)行,,如果找不到合適的其它錯誤處理程序來執(zhí)行,此時系統(tǒng)將崩潰,。 EXCEPTION_CONTINUE_EXECUTION 指示操作系統(tǒng)返回到出現(xiàn)異常處重試,。在內(nèi)核模式中不能返回此種類型的異常,因為你沒辦法改變導致異常的情況,。 更多的有關異常的信息可以調(diào)用以下兩個函數(shù)來獲得:(它們是微軟編譯器內(nèi)部所實現(xiàn)的,,這也從一個側面說明了其它廠商的編譯器不能生成驅(qū)動的原因) 注:微軟的驅(qū)動程序使用一些專有的格式,比如,,vxd就是使用LE格式,,但普通Win32程序則使用PE格式文件,由于微軟不向其它廠商開放使用許可,,所以象borland等廠商的編譯器不能生成驅(qū)動程序,。 GetExceptionCode() 返回當前異常的數(shù)值代碼。它是一個NTSTATUS類型,。此函數(shù)只能用于__except表達式和它后面的處理代碼中,。 GetExceptionInformation() 返回EXCEPTION_POINTERS結構的內(nèi)存地址,此結構包含異常相關的詳細信息,,包括發(fā)生時的寄存器詳細內(nèi)容等,。 程序中的bug可以導致異常并使系統(tǒng)調(diào)用異常處理機制。應用程序開發(fā)者應該熟悉Win32 API中的RaiseException函數(shù),,它可以生成任意異常,。在WDM驅(qū)動程序中,你可以調(diào)用表中列出的例程,。由于下面規(guī)則,,我不能給你舉一個使用這些函數(shù)的例子: 僅當你知道存在一個異常處理代碼并知道你真正在做什么時,才可以在非任意線程上下文下生成一個異常,。 用于生成異常的服務函數(shù) 服務函數(shù) 描述 ExRaiseStatus 用指定狀態(tài)代碼觸發(fā)異常 ExRaiseAccessViolation 觸發(fā)STATUS_ACCESS_VIOLATION異常 ExRaiseDatatypeMisalignment 觸發(fā)STATUS_DATATYPE_MISALIGNMENT異常 特別地,,不要通過觸發(fā)異常來告訴你的調(diào)用者你一般執(zhí)行狀態(tài)中的信息,你完全可以返回狀態(tài)代碼,。應該盡量避免使用異常,,因為堆棧回卷機制非常消耗資源,。 系統(tǒng)錯誤 BugCheck(導致系統(tǒng)不能繼續(xù)運行的致命錯誤) Bug check是系統(tǒng)檢測到的錯誤,,一旦發(fā)現(xiàn)這種錯誤,系統(tǒng)立即以一種可控制的方式關閉,。許多內(nèi)核模式部件運行時都進行一致性檢測,,如果某個系統(tǒng)部件發(fā)現(xiàn)一個不可恢復的錯誤,將生成一個bug check,。如果可能,,所有內(nèi)核模式部件都先登記遇到的錯誤,然后繼續(xù)運行,,而不是調(diào)用KeBugCheckEx,,除非這種錯誤將使系統(tǒng)本身變得不可靠。程序可以在任何IRQL上調(diào)用KeBugCheckEx,。如果程序發(fā)現(xiàn)一個不可恢復的錯誤,,并且該程序繼續(xù)運行將會破壞系統(tǒng),那么該程序就調(diào)用KeBugCheckEx函數(shù),,這個函數(shù)將使系統(tǒng)以一種可控制的方式關閉,。 當內(nèi)核模式中出現(xiàn)不可恢復錯誤時,會出現(xiàn)一個稱為死亡藍屏(BSOD blue screen of death)的畫面,驅(qū)動程序開發(fā)者應該十分熟悉它,。在內(nèi)部,,這種錯誤被稱為bug check,它的主要特征是,,系統(tǒng)盡可能以正常的方式關閉并彈出一個死亡藍屏,。一旦死亡藍屏出現(xiàn),則表明系統(tǒng)已經(jīng)死掉必須重啟動,。
可以按下面方式調(diào)用KeBugCheckEx: KeBugCheckEx(bugcode, info1, info2, info3, info4); bugcode是一個數(shù)值,,指出出錯的原因,info1,、info2等是整型參數(shù),,將出現(xiàn)在死亡藍屏中以幫助程序員了解錯誤細節(jié)。該函數(shù)從不返回(!),。 我不將解釋死亡藍屏中的信息,。Microsoft自己的bugcheck代碼在DDK頭文件bugcodes.h中列出;對該代碼的更完整解釋以及各種參數(shù)的含義可以在KBase文章Q103059 “Descriptions of Bug Codes for Windows NT”中找到,。 如果需要,,你也可以創(chuàng)建自己的bugcheck代碼。Microsoft定義的值是從1(APC_INDEX_MISMATCH)到0xDE(POOL_CORRUPTION_IN_FILE_AREA)之間的整數(shù),。為了創(chuàng)建你自己的bugcheck代碼,,你需要定義一個整型常量(類似STATUS_SEVERITY_SUCCESS的狀態(tài)代碼),并指出customer標志或非0的facility代碼,。例如: #define MY_BUGCHECK_CODE 0x002A0001 ... KeBugCheckEx(MY_BUGCHECK_CODE, 0, 0, 0, 0); 使用非0的facility代碼(例子中為42)或customer標志(例子中為0)是為了與Microsoft使用的代碼區(qū)分開,。 現(xiàn)在,我已經(jīng)告訴你如何生成自己的BSOD,,那么我們什么時候使用它呢,?回答是決不,或者僅在驅(qū)動程序的內(nèi)部調(diào)試中使用,。我們不可能寫出這樣的驅(qū)動程序,,它發(fā)現(xiàn)了一個錯誤并且只有通過關閉系統(tǒng)才能解決。更好的做法是記錄這個錯誤(使用錯誤登記工具,如系統(tǒng)事件日志或WMI)并返回一個狀態(tài)碼,。 這是在不得已的情況下為了盡可能減少損失而采取的措施,。如果不這樣處理,系統(tǒng)將以一種不可預料的方式結束運行,,可能會造成不可挽回的損失,。我曾經(jīng)在編寫驅(qū)動時發(fā)生過源代碼無緣故地丟失的情況,這也是不健壯的驅(qū)動程序產(chǎn)生的副作用,。 下面我們將要一起學習核心層的內(nèi)存管理,。在windows下由于采用了保護模式,,內(nèi)存和實模式的dos下完全不同。記得在dos下我們可以訪問幾乎所有的內(nèi)存區(qū)域,,如訪問硬盤的Bios記取硬的序列號,,主板的B ios區(qū)以記取主板序列號,破除BIOS口令等,。但在Windows下,,系統(tǒng)對內(nèi)存進行分區(qū)訪問,分為系統(tǒng)區(qū)和用戶區(qū),。并且由于采用了硬盤交換技術,我們可以申請超過實際內(nèi)存大小很多的內(nèi)存,。但此時我們得到的內(nèi)址地址再也不是內(nèi)存的物理地址了,,取而代之是的虛擬內(nèi)存地址。 雖然Windows這樣的帖心保護給我們寫應用程序帶來了很大方便,,不用再費心考慮內(nèi)存是否夠用的問題,,但也給我們寫驅(qū)動程序帶來了很多麻煩。我們在驅(qū)動程序中使用內(nèi)存時要加倍小心,,否則一丁點小的錯誤都會要了系統(tǒng)的命(死亡藍屏或立即死機),。 首先我們來看幾個概念性的問題: 虛擬內(nèi)存: 把一定的空間劃分成固定大小的塊,這些塊在技術上叫“頁”,,X86系列的處理器的頁的大小為4k,alpha系統(tǒng)的為8K,,這些頁可以放在內(nèi)存中,也可以放到磁盤上,。對于程序來說,,它們所使用的內(nèi)存就是由一系列處理過的內(nèi)存地址表示的區(qū)域(虛擬內(nèi)存地址),可能在物理內(nèi)存中也可能在磁盤上的文件中,。它并不清楚所使用的內(nèi)存的真實形態(tài),。這一切由操作系統(tǒng)透明處理,。當然,,應用程序也可以顯式地申請操作系統(tǒng)告訴它這些內(nèi)存的物理地址。 如上圖所示,,假設一個程序使用了24K的內(nèi)存,我們把它分成6頁,。如果系統(tǒng)只有12K的物理內(nèi)存可用時,它會把頻繁使用或?qū)⒁褂玫膮^(qū)域放到物理內(nèi)存中,,以加快運行速度和滿足內(nèi)存分配需要。但如果所有的應用程序經(jīng)常所需的內(nèi)存總和大大超過物理內(nèi)存的數(shù)量,,那操作系統(tǒng)就會不斷地在物理內(nèi)存和磁盤文件之間換進換出這些分頁塊,,雖然還能運行程序,但這將導致系統(tǒng)變得很慢,。這是用時間換空間的方法了,。 分頁一(物理內(nèi)存中) 分頁二(磁盤上) 分頁三(物理內(nèi)存中) 分頁四(磁盤上) 分頁五(物理內(nèi)存中) 分頁六(磁盤上)
可分頁內(nèi)存和不可分頁內(nèi)存: 上面講了內(nèi)存可以分頁,但并不是每個分頁內(nèi)存區(qū)都可以換到磁盤文件中。我們把可以換出來的分頁叫作“可分頁內(nèi)存”,,只能永久駐留在物理內(nèi)存中而不能被換出來頁的叫作“非分頁內(nèi)存”,。 如果在DISPATCH_LEVEL或者更高的中斷級中訪問分頁內(nèi)存,就會引起缺頁故障,,內(nèi)核會崩潰,。如果在PASSIVE_LEVEL中斷級訪問沒有駐留在物理內(nèi)存中的分頁內(nèi)存時,內(nèi)核會阻塞我們的線程,,直到內(nèi)存管理器把此頁重新裝回物理內(nèi)存中,。 不要隨意使用非分頁內(nèi)存,因為系統(tǒng)的資源是有限的,,如果永久駐留在物理內(nèi)存中的頁太多,,將導致可分頁內(nèi)存更加頻繁地進行交換,降低系統(tǒng)性能,。 在開發(fā)驅(qū)動程序時我們必須遵守這樣幾個原則: 決不(或幾乎從不)直接引用用戶模式的內(nèi)存地址,。 因為我們不能確切知道用戶模式內(nèi)存地址所指向的真實物理地址。 執(zhí)行在高于或等于DISPATCH_LEVEL級的代碼不可以引發(fā)頁故障,。 在驅(qū)動程序的checked版中,,你可以使用PAGED_CODE預處理宏(在wdm.h中聲明)來幫助發(fā)現(xiàn)有違背這個規(guī)則的代碼。例如: NTSTATUS DispatchPower(PDEVICE_OBJECT fdo, PIRP Irp) { PAGED_CODE() ... } PAGED_CODE包含條件編譯語句,。在checked-build方式中,,如果當前IRQL太高,它就打印出一行信息并生成一個斷言失敗,。在free-build方式中,,它不做任何事。如果測試驅(qū)動程序時包含DispatchPower代碼的頁正好在內(nèi)存中,,那么你不會發(fā)現(xiàn)已經(jīng)在一個提升的IRQL上調(diào)用了DispatchPower函數(shù),。即使這樣,PAGED_CODE仍能查出問題,。如果該頁碰巧不在內(nèi)存中,,系統(tǒng)將產(chǎn)生一個bug check。 我們可以調(diào)用以下幾個函數(shù)來分配可分而和非可分頁內(nèi)存塊: ExAllocatePool(…) 調(diào)用方式如下: PVOID p = ExAllocatePool(type, nbytes); type參數(shù)是表2中列出的POOL_TYPE枚舉常量,,nbytes是要分配的字節(jié)數(shù),。返回值是一個內(nèi)核模式虛擬地址指針,指向已分配的內(nèi)存塊,。如果內(nèi)存不足,,則返回一個NULL指針。如果指定的內(nèi)存池類型為“must succeed”類型,,即NonPagedPoolMustSucceed或NonPagedPoolCacheAlignedMustS,,那么內(nèi)存不足將導致一個代碼為MUST_SUCCEED_POOL_EMPTY的bug check,。 表2 內(nèi)存池類型 描述 NonPagedPool 從非分頁內(nèi)存池中分配內(nèi)存 PagedPool 從分頁內(nèi)存池中分配內(nèi)存 NonPagedPoolMustSucceed 從非分頁內(nèi)存池中分配內(nèi)存,如果不能分配則產(chǎn)生bugcheck NonPagedPoolCacheAligned 從非分頁內(nèi)存池中分配內(nèi)存,,并確保內(nèi)存與CPU cache對齊 NonPagedPoolCacheAlignedMustS 與NonPagedPoolCacheAligned類似,,但如果不能分配則產(chǎn)生bugcheck PagedPoolCacheAligned 從分頁內(nèi)存池中分配內(nèi)存,并確保內(nèi)存與CPU cache對齊 調(diào)用ExAllocatePool時的最基本原則是被分配內(nèi)存塊是否可以交換出內(nèi)存,。這取決于驅(qū)動程序的哪一部分需要訪問這塊內(nèi)存,。如果在大于或等于DISPATCH_LEVEL級上使用該內(nèi)存塊,那么必須從非分頁池中分配內(nèi)存,。如果你總是在低于DISPATCH_LEVEL級上使用內(nèi)存塊,,那么既可以從非分頁池中分配內(nèi)存也可以從分頁池中分配內(nèi)存。 你獲得的內(nèi)存塊至少是按8字節(jié)邊界對齊的,。如果把某結構的實例放到分配的內(nèi)存中,,那么編譯器賦予結構成員的4或8字節(jié)偏移在新內(nèi)存中也將是4或8字節(jié)偏移。但在某些RISC平臺上,,結構成員可能以雙字和四字對齊。出于性能上的考慮,,希望內(nèi)存塊能適合處理器cache行的最少可能數(shù),,使用XxxCacheAligned類型代碼可以達到這個要求。如果請求的內(nèi)存多于一頁,,那么內(nèi)存塊將從頁的邊界開始,。 ExAllocatePoolWithTag 調(diào)用ExAllocatePool是從內(nèi)核模式堆中分配內(nèi)存的標準方式。另一個函數(shù)ExAllocatePoolWithTag,,與ExAllocatePool稍有不同,,它提供了一個有用的額外特征。當使用ExAllocatePoolWithTag時,,系統(tǒng)在你要求的內(nèi)存外又額外地多分配了4個字節(jié)的標簽,。這個標簽占用了開始的4個字節(jié),位于返回指針所指向地址的前面,。調(diào)試時,,如果你查看分配的內(nèi)存塊會看到這個標簽,它幫助你識別有問題的內(nèi)存塊,。例如: PVOID p = ExAllocatePoolWithTag(PagedPool, 42, 'KNUJ'); 在這里,,我使用了一個32位整數(shù)常量作為標簽值。在小結尾的計算機如x86上,,組成這個標簽的4個字節(jié)的順序與正常拼寫相反,。 WDM.H中聲明的內(nèi)存分配函數(shù)受一個預處理宏POOL_TAGGING控制。WDM.H(NTDDK.H中也是)中無條件地定義了POOL_TAGGING,,結果,,無標簽的函數(shù)實際上是宏,,它真正執(zhí)行的是有標簽函數(shù)并加入標簽‘ mdW’(指明為WDM的內(nèi)存塊)。如果在未來版本的DDK中沒有定義POOL_TAGGING,,那么帶標簽函數(shù)將成為無標簽函數(shù)的宏,。Microsoft現(xiàn)在還沒打算改變POOL_TAGGING的設置。 由于POOL_TAGGING宏的存在,,當你在程序中調(diào)用ExAllocatePool時,,最終被調(diào)用的將是ExAllocatePoolWithTag。如果你關閉了該宏,,自己去調(diào)用ExAllocatePool,,但ExAllocatePool內(nèi)部仍舊調(diào)用ExAllocatePoolWithTag并帶一個‘enoN’(即None)的標簽。因此你無法避免產(chǎn)生內(nèi)存標簽,。所以你應該明確地調(diào)用ExAllocatePoolWithTag并加上一個你認為有意義的標簽,。實際上,Microsoft強烈鼓勵你這樣做,。 ExAllocatePool的其它形式 盡管ExAllocatePoolWithTag函數(shù)是分配堆內(nèi)存時應該使用的函數(shù),,但在某些特殊場合你也可以使用該函數(shù)的另外兩種形式: • ExAllocatePoolWithQuota 分配一塊內(nèi)存并充入當前線程的調(diào)度配額中,該函數(shù)僅用于頂層驅(qū)動程序,,如文件系統(tǒng)驅(qū)動程序或其它運行在非任意線程上下文中的驅(qū)動程序,。 • ExAllocatePoolWithQuotaTag 同上,但加入一個標簽,。 釋放內(nèi)存塊 調(diào)用ExFreePool可以釋放由ExAllocatePool分配的內(nèi)存塊: ExFreePool((PVOID) p); 你確實需要記錄分配的內(nèi)存以便在該內(nèi)存不再需要時釋放它,,因為沒有人為你做這些事。例如,,在AddDevice函數(shù)中,,有一個IoRegisterDeviceInterface調(diào)用,該函數(shù)存在副作用:它分配了一塊內(nèi)存以保存接口名,。你有責任在以后釋放該內(nèi)存,。 不用說,訪問從內(nèi)核模式內(nèi)存池中分配來的內(nèi)存必須格外小心,。因為驅(qū)動程序代碼可能執(zhí)行在處理器的最高特權模式下,,在這里,系統(tǒng)對內(nèi)存數(shù)據(jù)沒有任何保護,。 運行時控制分頁能力 表3列出了一些服務函數(shù),,你可以在運行時使用它們調(diào)整驅(qū)動程序的分頁布局。這些函數(shù)的功能是釋放被不再需要的代碼和數(shù)據(jù)所占用的物理內(nèi)存,。在第八章中,,我將講述如何向電源管理器寄存你的設備,這樣,,在一段不活動時期后設備可以自動掉電,。掉電期間是釋放鎖定內(nèi)存頁的最佳時期,。 表3. 動態(tài)鎖定和解鎖驅(qū)動程序占用內(nèi)存頁的例程 服務函數(shù) 描述 MmLockPagableCodeSection 鎖定含有給定地址的代碼段 MmLockPagableDataSection 鎖定含有給定地址的數(shù)據(jù)段 MmLockPagableSectionByHandle 用MmLockPagableCodeSection返回的句柄鎖定代碼段(僅用于Windows 2000) MmPageEntireDriver 解鎖所有屬于某驅(qū)動程序的頁 MmResetDriverPaging 恢復整個驅(qū)動程序的編譯時分頁屬性 MmUnlockPagableImageSection 為一個鎖定代碼段或數(shù)據(jù)段解鎖 限于篇幅,這類函數(shù)詳細的用法可以參見DDK文檔說明,。這部分我們主要學習了內(nèi)存管理部分,。如我們前面所說,內(nèi)存管理在內(nèi)核編程中是非常重要的,。通常我們在應用程序編寫中的不良習慣都不應該帶到驅(qū)動程序開發(fā)中,。你必須清楚一點:在用戶態(tài)最多只是彈出一個提示框的故障,在內(nèi)核模式中等待你的將是“死機”,。
|
|