[内核编程] 内核环境及其特殊性,驱动编程基础篇
[內核編程] 內核環(huán)境及其特殊性,驅動編程基礎篇
?在學習漢江獨釣一書后,打算總結一下內核編程應該注意的事項,以及有關的一些基礎知識。第一次接觸內核編程,還真是很生疏,很多東西不能一下馬上消化。這里做個回顧總結,好加深自己的印象。
## 1、內核編程環(huán)境
?這里涉及到兩個模式:內核模式和用戶模式。這個可以和CPU的等級聯(lián)系到一塊:ring0,ring1,ring2,ring3,特權等級依次降低,最底層ring0層擁有最高的特權等級。而windows簡化了這樣的特權等級層次次關系,分成了內核層和用戶層,對應的就是內核模式和用戶模式。平時我們所編寫的應用程序,運行后,在windows下就會生成一個對應的進程,針對的是單個進程進行程序的編寫,受到了隔離保護的,運行環(huán)境受到操作系統(tǒng)的保護,很多底層的問題無需考慮,這使得編寫應用程序變得相當容易。
【圖】CPU特權等級和windows層次對應關系
【圖】windows結構
所謂的內核模式,實際上就是不受操作系統(tǒng)管制的最底層結構,為操作系統(tǒng)提供各項服務的核心部件。在這里,理論上可以實現(xiàn)任何可以想到的功能,而不受到操作系統(tǒng)的制約,也正因為如此,問題也就產生了,面對共享的內存空間、共享資源,如何進行同步操作就成了一個關鍵的問題。再者,內核結構很多都是不公開的,并且又沒有操作系統(tǒng)的保護,所以一點系統(tǒng)出錯,就是直接藍屏或死機,這給內核調試帶來了很大的麻煩。
windows一般都是用系統(tǒng)進程來加載內核模塊的,但這并不是說內核代碼始終運行在System進程里,也就是說當DriverEntry被調用時,一般是位于System進程中的,其他時候則不一定。內核模塊位于內核空間,而內核空間又被所有的進程共享。因此,內核模塊實際上位于任何一個進程空間中。但是任意一段代碼的任意一次執(zhí)行,一定是位于某個進程空間中的。而至于這是哪一個進程,取決于請求的來源、處理的過程等。
## 2、常用數(shù)據(jù)類型
一般來說,在進行內核編程時,應當遵守WDK的編碼習慣,雖然這并不是必須的,但是如果不這么做,有可能還是會導致一些不穩(wěn)定性的問題。比如unsigned long 在64bit 環(huán)境下為8字節(jié)、而在32bit 環(huán)境下是4字節(jié),這個時候要把數(shù)據(jù)寫到磁盤上時,到底寫4個字節(jié)還是8個字節(jié)呢?問題也就出現(xiàn)了,所以WDK重新定義了這個類型,為ULONG。
以下是一些常用的數(shù)據(jù)類型的轉換關系:
- unsigned long ?--- ULONG
- unsigned char --- UCHAR
- unsigned int ?--- UINT
- void --- VOID
- unsigned long * ---- PULONG
- unsigned char * --- PUCHAR
- unsigned int * ?--- PUCHAR
- void* ---PVOID
- char* --- PCHAR
- ........
當然也不僅僅是這些,一般來說,指針前面加上P,unsigned 對應 U,基本類型名不便只是換成了大寫(例如char --- CHAR),當然這只是我個人見到的,也不排除有特例,我這么對應一般都沒有什么問題。
函數(shù)一般都會返回操作的狀態(tài),以說明操作的情況,內核中這個類型是NTSTATUS。調用函數(shù)時一般這樣做:
NTSTATUS status; ?status = function(..); if(NT_SUCCESS(status)){... 操作成功后要做的事情 ?...}
NT_SUCCESS() 可以判斷操作時候成功,當談status還有其他的狀態(tài),可以查看WDK。
驅動力字符串一般用一個結構來保存,是一個寬字符串:
| 1 2 3 4 5 | typedef?struct?_UNICODE_STRING{ ?????USHORT? Length; ?// Buffer的字節(jié)長度,實際存儲的長度 ?????USHORT? MaximumLength;?//Buffer的最大長度,即開辟的空間大小 ?????PWSTR??? Buffer;//存儲字符的緩沖區(qū) }UNICODE_STRING *PUNICODE_STRING; |
## 3、一些重要的數(shù)據(jù)結構
驅動編程中,比較重要的幾個數(shù)據(jù)結構是:驅動對象(DRIVER_OBJECT)、設備對象(DEVICE_OBJECT)和請求(IRP)。
a. 一個驅動對象代表一個驅動程序,或者說一個內核模塊。
b. 設備對象是由驅動創(chuàng)建的,一個驅動可以對應多個設備對象,設備對象是唯一能接收請求的實體。
c.windows內核中各部件的通信,是通過請求來完成的,來自用戶層的相關操作,會被IO管理器翻譯成請求(IRP或者與其等效的其它形式),處理這一個個的請求也就完成相應的操作。
?關于這三個對象的結構可以在wdm.h中找到。這里給出重要數(shù)據(jù)結構的關系圖:
【圖】設備棧結構,驅動垂直結構
【圖】驅動的水平結構,一個驅動可以闖將對個設備對象,這些設備構成一個鏈表結構
## 4、常用函數(shù),以及內核API的學習
編寫內核程序的時候,盡量使用內核API,雖然類似于wcscpy、memcpy等函數(shù)也可以用,但這些函數(shù)沒有長度的檢查,很容易發(fā)生溢出錯誤,應該避免使用。
主要的函數(shù)前綴有:Rtl-, Io-, Ke-, Zw-, Nt-, Ps-等。
有以下幾類:
Ex系列--分配內存,獲取互斥體等:
??ExAllocatePool,分配內存
??ExFreePool,釋放內存
??ExAcquireFastMutex獲取一個快速互斥體
??ExReleaseFastMutex釋放一個快速互斥體
??ExRaiseStatus拋出異常
????Zw--Nt系列文件操作函數(shù):
??ZwCreateFile--創(chuàng)建文件
??ZwWriteFile--寫文件
??ZwReadFile--讀取文件
??ZwQueryDirectoryFile--查詢目錄
??ZwDeviceIoControlFile--發(fā)出設備控制請求
??ZwCreateKey--打開一個注冊表鍵
??ZwQueryValueKey--讀取一個注冊表鍵
????Rtl系列字符串操作函數(shù):
??RtlInitUnicodeString--初始化一個Unicode字符串
??RtlCopyUnicode--拷貝字符串
??RtlAppendUnicodeToString--追加字符串到另一個字符串
??RtlStringCbPrintf--將字符打印到字符串中,相當于格式化字符串
??RtlCopyMemory--拷貝內存
??RtlMoveMemory--移動內存數(shù)據(jù)塊
??RtlZeroMemory--內存數(shù)據(jù)塊清零
??RtlCompareMemory--比較內存
??RtlGetVersion--得到當前windows版本
????Io開頭的IO管理函數(shù):
??IoCreateFile--打開文件,比ZwCreateFile函數(shù)更加底層
??IoCreateDevice--生成一個設備對象
??IoCallDriver發(fā)送請求
??IoCompleteRequest--完成IRP請求
??IoCopyCurrentIrpStackLocationToNext--講當前IRP??臻g拷貝到下一個棧空間
??IoSkipCurrentIrpStackLocationToNext--跳過當前IRP??臻g
??IoGetCurrentIrpStackLocation--得到當前IRP棧空間。
對于詳細的說明可以查看WDK的幫助文檔,API很多,我們不肯能全都記住,常使用,常動手,常查幫助,是很好的學習方法。見一個學一個,不記得就立即查幫助。相關的結構也可以直接查看WDK的頭文件,這里可以查到幫助文檔中沒有的一些信息哦!!
## 5、Womdows的驅動開發(fā)模型
?NT(KDM)、WDM、WDF(WDM的升級版)
## 6、WDK編程中的特殊性
調用源:沿著該函數(shù)往上走,再也不能找到其調用者,則這個函數(shù)就是調用源,一般的個單線程函數(shù)的調用源只有一個,也就是主函數(shù),比如我們常見的main函數(shù)。而在內核編程中,一個函數(shù)往往有多個調用源,主要可以追溯到的調用源有以下幾個:入口函數(shù)DriverEntry、卸載函數(shù)DriverUnload;各種分發(fā)函數(shù);處理請求時的完成函數(shù);其它回調函數(shù)。
在內核中,一個函數(shù)有可能同時被多個線程調用,這個是有就好保證多線程的安全性,也就是在輸入相同的情況下要保證輸出是相同的。多線程安全性可以依據(jù)以下幾個規(guī)則進行判斷:
- 可能運行于多線程環(huán)境下的函數(shù),必須是多線程安全的。
- 如果函數(shù)A的所有調用源都是運行于單線程的,那么函數(shù)A也是運行于單線程的。
- 如果函數(shù)A的調用源中,其中有一個可能運行于多線程環(huán)境下,并且在調用路徑上沒有將多線程序列化成單線程,那么函數(shù)A也是可能運行于多線程環(huán)境下的。
- 如果函數(shù)A的所有可能出現(xiàn)多線程的調用路徑上都被單線程化了,那么函數(shù)A就是單線程環(huán)境下的。
- 只是用函數(shù)內部資源,則函數(shù)是多線程安全的
- 如果函數(shù)在訪問全局變量或者靜態(tài)變量的操作采用強制的同步手段進行限制,可以等同于使用內部變量(上一條規(guī)則成立)。
內核代碼主要調用源運行環(huán)境:
DriverEntry、DriverUnload 單線程 —— 這兩個函數(shù)由系統(tǒng)進程的單一線程調用,不會出現(xiàn)多線程同步調用的情況
各種分發(fā)函數(shù) 多線程 ? —— 沒有文檔能保證分發(fā)函數(shù)不會被多線程同步調用,分發(fā)函數(shù)不會和DriverEntry同步,但是可能和DriverUnload同步
完成函數(shù) 多線程 —— 完成函數(shù)隨時可能被未知的線程調用
各種NDIS回調函數(shù) 多線程 —— 隨時可能被位置的線程調用
代碼的中斷級:
Windows為CPU的運行狀態(tài)定義了許多的級別,即IRQL,任一時間中,CPU總是運行在其中的某一級別,各個級別規(guī)定了CPU能做哪些事情,哪些不可以做。高中斷級可以搶占低中斷級。
級別定義如下:
#define?PASSIVE_LEVEL??0??;級別最低,CPU在用戶層,或剛進內核運行于管理層時就運行在此級別上
#define?LOW_LEVEL??0??;
#define?APC_LEVEL??1??;比PASSIVE_LEVEL略高,運行APC函數(shù)(進程與線程)時需要的級別
#define?DISPATCH_LEVEL??2??;相當于cpu運行在內核層,線程切換時級別從此下降
#define?PROFILE_LEVEL??27??;級別3以上用于硬件中斷。
#define?CLOCK1_LEVEL??28
#define?CLOCK2_LEVEL??28
#define?IPI_LEVEL??29
#define?POWER_LEVEL??30
#define?HIGH_LEVEL??31
兩個規(guī)則:
- 如果在調用路徑上沒有特殊情況(導致中斷級的提高或者降低),則一個函數(shù)執(zhí)行時的中斷級和它的調用源的中斷級相同。
- 如果在調用路徑上又獲取自旋鎖,則中斷級隨之升高,釋放自旋鎖,中斷級隨之下降。
處于高中斷級中的函數(shù)不能調用處于低中斷級中的API,若dispach級想調用passive級上的AIP,可以另創(chuàng)建一個線程專門執(zhí)行。
其他:
函數(shù)定義中,變量的類型前常常會有:IN、OUT這樣的字符,這些字符被定義成空,起到說明性的作用,IN表示輸入參數(shù),OUT表示輸出參數(shù)。
PAGED_CODE(),進行分頁測試,只要級別不高于APC_LEVEL,其代碼都允許換出,若發(fā)現(xiàn)更高級的中斷發(fā)出缺頁中斷,則發(fā)出異常,讓編程者知道。
指定函數(shù)位置的預編譯指令:
#pragma alloc_text(INIT, DriverEntry) #pragma alloc_text(INIT, DriverUnload) #pragma alloc_text(PAGE,NdisProtUnload) .... ....這個宏僅僅用來指定某個函數(shù)的可執(zhí)行代碼在編譯出來后在sys文件中的位置。內核模塊編譯出來后是一個PE文件的sys文件,這個文件的代碼段(text段)中有不同的節(jié)(Section),不同的節(jié)被加載到內存中之后處理情況不同。主要有3個節(jié):
- INIT節(jié):在初始化完畢之后就被釋放,不再占用內存空間。
- PAGE節(jié):位于可分頁交換的內存空間,這些空間在內存緊張的時候可以被交換到磁盤上以節(jié)省空間。
- PAGELK節(jié):不適用預編譯指定的時候,默然的狀態(tài)。加載后位于不可分頁交換的內存空間中。
VOID ASSERT( Expression ); —— 這個宏用于測試一個表達式,如果這個表達式的值是false,它就終止,并跳出到內核調試器?! ?/p>
## 7、課后習題
(1)內核編程環(huán)境和用戶應用程序編程環(huán)境有哪些不同?
編程模式可分為兩種:用戶模式和內核模式。
其中用戶應用程序的編程采用的是用戶模式,這里都是在操作系統(tǒng)的隔離環(huán)境中完成的,也就是說對于這個模式來說不用考慮通用寄存器,內存是共享的,可通過操作系統(tǒng)實現(xiàn)進程間的資源共享,這屬于單進程編程,利用的都是進程內的資源,不用擔心會產生什么沖突。
內核編程使用的是內核模式編程,其內核屬于操作系統(tǒng)的一個模塊供各個進程調用,在內核空間中資源都是共享的并且不受操作系統(tǒng)的限制,很容易發(fā)生沖突。
(2)Windows有哪幾種驅動開發(fā)模型?它們的發(fā)展現(xiàn)狀如何?
model是根據(jù)操作系統(tǒng)的類型不同而取名的,有KDM(windows NT),WDM(windows 98 —— windows 2000),WDF(WDM的升級版),一脈相承的,不用擔心過時。
(3)什么是用戶空間?什么事內核空間?
進程的空間實際上被分成兩個部分,一部分是進程獨立使用的用戶空間,一部分是容納操作系統(tǒng)內核的內核空間。具體到4G內存控件的32位windows系統(tǒng)上,低2G是用戶空間,高2G是內核空間。
(4)內核模塊運行在什么進程環(huán)境下??
內核模塊無處不在,內核模塊屬于操作系統(tǒng)的一個部分,為各進程提供服務,也就是說每個進程中都有可能運行有內核模塊,但是內核模塊一般是通過系統(tǒng)system進程進行加載的。
(5)請簡述驅動對象、設備對象、請求之間的關系。
驅動對象、設備對象、請求
內核編程采用的是面向對象的編程思想來進行編程的,把每個事物都看成一個對象。而驅動對象可以說是一個驅動程序也可以說是一個內核模塊。設備對象是唯一能接受請求的對象,而設備對象是在內核模塊中建立的,也就是說設備對象屬于驅動對象,設備對象在接收到請求以后交由驅動對象的分發(fā)函數(shù)進行處理。請求,內核模塊之間的交互都是通過一個個的請求實現(xiàn)的,比如說申請資源、讀取信息等,一般使用IRP請求,一個請求有可能要歷經多個設備,所以需要暫時存儲中間變量的??臻g。
(6)請簡述如何判斷對一個全局變量的訪問是否要加自旋鎖或者互斥體使之序列化。
當一個程序有可能會運行在多線程環(huán)境下的時候就需要保證其是多線程安全的,也就是說不能出現(xiàn)線程沖突。對一個全局變量的訪問時候要加自旋鎖或互斥體使之序列化,取決于被使用的變量是否影響到保證多線程的安全。
(7)請簡述如何估計當前代碼的中斷級。
1、如果調用途徑沒有特殊情況,中斷級和調用源一樣
2、獲自旋鎖中斷級升高,失自旋鎖中斷級降低
總結
以上是生活随笔為你收集整理的[内核编程] 内核环境及其特殊性,驱动编程基础篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 雉离爱吃的东西是什么(鸡形目雉科动物)
- 下一篇: 微信小程序业务-字符串生成二维码(wea