數據類型
基本數據類型
- unsigned long 重新定義爲 UlONG
- unsigned char 重新定義爲 UCHAR
- unsigned int 重新定義爲 UINT
- void 重新定義爲 VOID
- unsigned int * 重新定義爲 PUINT
- unsigned long * 重新定義爲 PULONG
- unsigned char * 重新定義爲 PUCHAR
返回狀態
絕大部分內核API的返回值是一個返回狀態。返回狀態的類型爲NTSTATUS
NTSTATUS MyFunction()
{
NTSTATUS status;
//打開一個文件
status = ZwCreateFile(...)'
if(!NT_SUCCESS(status)){
//如果出錯則直接返回錯誤
return status;
}
}
使用NT_SUCCESS() 判斷返回值是否成功。遇到錯誤時,在WDK的頭文件中找定義尋找答案。
字符串
字符串有特殊的數據結構,該結構定義爲:
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING *PUINCODE_STRING;
重要的數據結構
驅動對象
一個驅動對象代表一個驅動程序或一個內核模塊。驅動對象的結構如下:
typedef struct _DRIVER_OBJECT{
//結構的類型和大小 CSHORT 爲 short重定義
CSHORT Type;
CSHORT Size;
//設備對象,一條設備對象的單鏈表
PDEVICE_OBJECT DeviceObject;
//這個內核模塊在內核空間中的開始地址和大小
PVOID DriverStart;
ULONG DriverSize;
...
//驅動的名字
UNICODE_STRING DriverName;
...
//快速IO分發函數
PFAST_IP_DISPATCH FastIoDispatch;
...
///驅動的卸載函數
PDRIVER_UNLOAD DriverUnload;
//普通分發函數
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION+1];
}DRIVER_OBJECT;
內核模塊不生成一個進程,只填寫相應的回調函數供Windows調用。回調函數包括普通分發函數和快速IO分發函數。
設備對象
在內核世界裏,大部分“消息”都是以請求(IRP)方式傳遞,設備對象(DEVICE_OBJECT)是唯一可以接受請求的實體。
設備對象的數據結構爲:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_DEVICE_OBJECT{
//和驅動對象一樣
CSHORT Type;
USHORT Size;
//引用計數
ULONG ReferenceCount;
//這個設備所屬的驅動對象
struce _DRIVER_OBJECT *DriverObject;
//下一個設備對象。在一個驅動對象中有n個設備,這些設備用這個指針連接
//作爲一個單項的鏈表
struct _DEVICE_OBJECT *NextDevice;
//設備類型
DEVICE_TYPE DeviceType;
//IRP棧大小
HAR StackSize;
...
}DEVICE_OBJECT;
驅動對象生成多個設備對象。Windows向設備對象發送請求,這些請求被驅動對象的分發函數所捕獲。當內核向一個設備發送一個請求時,驅動對象的分發函數中的某一個會被調用。分發函數的原型如下:
//一個典型的分發函數,第一個參數是device是請求的目標設備,第二個參數irp 是請求的指針
NTSTATUS MyDispatch(PDEVICE_OBJECT device, PIRP irp);
具體如何處理,由分發函數內容決定。
請求
大部分請求以IRP的形式發送。IRP也是一個內核數據結構。因爲該結構要表示無數種實際請求,所以該結構非常複雜。我們沒有必要去了解所有的細節。只需要有一個初步印象,結構如下:
typedef struce DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP
{
//類型和大小
CSHORT Type;
USHORT Size;
//內存描述符鏈表指針。描述一個緩衝區。可以理解爲一個內核請求一般都需要一個緩衝區。(如讀硬盤需要有讀出緩衝區)
PMDL MdlAddress;
...
//下面這個共用體中也有一個SystemBuffer。這是比MdlAddress稍微簡單的表示緩衝區的一種方式。IPR請求用SystemBuffer還是MdlAddress緩存取決於這次請求的IO方式。
union {
struct _IRP *MasterIrp;
__volatile LONG Irpcount;
PVOID SystemBuffer;
}AssociatedIrp;
//IO狀態。一般請求完成之後的返回情況放在這裏。
IO_STATUS_BLOCK IoStatus;
//IRP棧空間大小
CHAR StackCount;
//IRP當前棧空間
CHAR CurrentLocation;
...
//用來取消一個未決請求函數
__volatile PDRIVER_CANCEL CancelRoutine;
//與SystemBuffer和MdlAddress一樣都可以表示緩衝區,但是緩衝區的特性稍有不同。
PVOID UserBuffer;
union{
...
//發出這個請求的線程
PETHREAD Thread;
...
struct{
LIST_ENTRY LsitEntry;
union{
//一個IRP棧空間元素
struct _IO_STACK_LOCATION *CurrentStackLocation;
...
};
};
...
} Overlay;
...
} Tail;
}IRP,*PIRP;
一個IRP往往要傳遞n個設備才能完成。在傳遞過程中,有可能會有一些“中間變換”,導致請求的參數變化。爲了保存變化的參數,給每次“中轉“都留一個”棧空間“。一個請求並非簡單的一個輸入並等待一個輸出,而是經過許多中轉才得以完成。而且在中轉的每一個步驟,輸入都可以改變,所有可變部分的輸入信息保存在一個棧似的結構中。即IRP棧空間的作用。
函數調用
WDK中出現的特殊代碼
如
define IN
define OUT
這樣以來,IN和OUT就被定義成了空。無論出現在代碼的任何地方,都不會對代碼產生實質性的影響。在WDK的代碼中,用來作爲函數的說明。IN表示這個參數用於輸入;OUT表示這個參數用來返回結果。如:
NTSATUS ZwQueryInformationFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG Length,
IN FILE_INFORMATION_CLASS FileInofrmationClass
);
IN和OUT是比較傳統的參數說明宏。在WDK紅到處可見更復雜的參數說明宏,比如:
__in_bcount(StausBufferSize) IN PVOID StatusBuffer,
...
__in_bount 不但說明參數StatusBuffer是一個輸入參數,而且也說明StatusBuffer作爲一個緩衝區,它的字節長度被另一個參數StausBufferSize所指定。再見到類似的說明宏,按字面意思理解即可。
然後對於函數指定位置的預編譯指令。比如下面的例子:
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE,NdisProtUnload)
#pragma alloc_text(PAGE,NdisProtOpen)
#pragma alloc_text(PAGE,NdisProtClose)
#pragma alloc_text這個宏僅僅用來指定某個函數的可執行代碼在編譯出來後在sys文件中的位置。內核模塊編譯出來之後是一個PE格式的sys文件,這個文件的代碼段(text段)中有不同的節(section),不同的節被加載到內存中之後處理情況不同。我們主要關心三種節,INIT節的特點是在初始化完畢之後就被釋放,不佔據內存;PAGE節的特點是位於可以進行分頁交換的內存空, 這些空間在內存緊缺的時候可以被交換到硬盤上以節省內存。如果沒有用預編譯指令,則代碼默認位於PAGELK節,加載後位於不可分頁交換的內存空間中。
如函數 DriverEntry 顯然只需要在初始化階段執行一次,因此這個函數一般都用#pragma alloc_text(INIT,DriverEntry) 使之位於初始化後立刻釋放的空間內。爲了節約內存,可以把很多函數放在PAGE節中。但是這種函數不可以在Dispatch級調用,因爲這種函數的調用可能誘發缺頁中斷,而缺頁中斷處理不能在Dispatch級完成。爲此,一般用一個宏PAGED_CODE() 進行測試,如果發現當前中斷級爲Dispatch級,則程序直接報異常,好讓程序員及早發現。
示例:
#pragma alloc_text(PAGE,SfAttchToMountedDevice)
...
NTSTATUS
SfAttachToMountedDevice(
IN PDEVICE_OBJECT DeviceObject,
IN PDEVICE_OBJECT SFilterDeviceObject
)
{
...
PAGED_CODE();
...
}
代碼的中斷級
代碼的中斷級主要有兩種:Passive 和 Dispatch,Dispatch級比Passive級高。在實際編程時,許多具有比較複雜功能的內核API都要求必須在Passive級執行。只有比較簡單的函數能在Dispatch級執行。
在調用任何一個內核API之前,必須查看WDK文檔,瞭解這個內核API的中斷級要求。
如何判斷我們正在編寫的代碼的中斷級。暫時可以簡單的根據蝦米那的規則來處理:
- 規則一: 如果在調用路徑上沒有特殊的情況,則一個函數執行時的中斷級和它的調用元的中斷級相同。
- 規則二: 如果在調用路徑上有獲取自旋鎖,則中斷級隨之升高;如果在調用路徑上有釋放自旋鎖,則中斷級隨之下降。
當前代碼的中斷級基本上取決於調用元的中斷級和調用路徑。給出內核代碼主要調用元的運行中斷級。
調用源 | 一般的運行中斷級 |
---|---|
DriverEntry, DrierUnload | Passive級 |
各種分發函數 | Passive級 |
完成函數 | Dispatch級 |
各種NDIS回調函數 | Dispatch級 |
今明日後續計劃:
繼續學習驅動編程。