Windows核心開發-2-開始核心開發-2-
第一個驅動程式:
直接採用vs2019中的Empty WDM Driver 模組建立:
初始的專案資料夾中有一個Driver Files裡面會有一個.inf的檔案,沒用直接刪除就好,然後在原始檔裡面建立一個.cpp的原始檔。
DriverEntry和Unload Routines
DriverEntry:
每個驅動都有一個入口點,叫做DriverEntry,就好比平常寫的C/C++程式碼裡面的main函式。DriverEntry是由一個叫做IRQL_PASSIVE_LEVEL(0)的系統程序調用出來的。DriverEntry函式原型:
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING
RegistryPath);
程式碼原型裡的_In _是原始碼註釋語言(SAL)中的一部分,用來描述函式如何使用其引數,SAL對於編譯器來說可以直接忽略,但是對程式設計師很有幫助。
相關連結:Understanding SAL | Microsoft Docs
這裡的最小的DriverEntry示例可以只返回一個狀態,比如:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}
該DriverEntry函式包含在<ntddk.h>標頭檔案中,但是添加了標頭檔案後仍然是編譯失敗的,因為編譯器會把警告當場錯誤來報錯,但是不建議刪除該功能,因為有時候警告就是會導致錯誤誕生:
可以對應修改這些警告,比如這裡將形參刪除,但是這樣僅對於C++好用,因為C++有函式過載,所以這裡用不上。這裡有一個很經典的解決辦法,就是採用一個巨集函式:
UNREFERENCED_PARAMETER();
這樣就可以暫時解決掉前面的報錯說形參沒有使用了:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
但是這樣仍然不行:
可以很明顯得看出,編譯器沒問題,但是Linker連結器出了問題,DriverEntry是一個C函式,必須用C的linker來link,但是這裡我們採用的是C++的預設,所以必須給該函式設定為C的預設LInker才行
。
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
這樣最簡單的驅動程式的原始碼就寫好了,就相當於C語言中的:
#include<stdio.h>
int main()
{
return 0;
}
Unload Routines:
這是一個驅動的解除安裝函式,就相當於C++中類的解構函式一樣,當驅動被解除安裝的時候就會自動呼叫該函式。在DriverEntry函式中建立的東西需要由Unload Routines來釋放,這就非常像C++類中的建構函式和解構函式的關係了。如果沒有該函式來釋放驅動載入時所開闢的內容就會導致洩露,直到下次電腦重啟時核心產生的洩露才會清楚。
該函式的函式指標,必須在DriverEntry給DriverEntry的引數DriverObject中的DriverUnload欄位賦值才行。Unload函式和DriverEntry函式一樣都需要接受一個_In _ PDRIVER_OBJECT DriverObject 引數,但是Unload函式不需要返回值,直接用void 定義就好。
比如:
#include <ntddk.h>
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
}
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
DriverObject->DriverUnload = SampleUnload;
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
就是一個非常簡單但是可以用的驅動了。
部署驅動程式Deploying the Driver
前面已經寫好了一個驅動程式,但是我們還需要把它跑起來,驅動程式不像平時寫的普通程式一樣,採用IDE就可以正常使用了,需要將其載入到系統裡面,通常為了避免風險,採用虛擬機器來部署驅動程式。
安裝驅動程式就像是在User使用者態安裝服務.exe一樣,需要呼叫CreateService API或者採用現有的工具,這裡採用比較常用的Sc.exe來進行部署核心驅動。
註冊驅動
採用管理員許可權的命令列:
sc create sample type=kernel binPath=C:\DriverTest\MyDriver3.sys
其中 sample是建立的名字,然後type表示建立的許可權,binPath後面的是驅動的路徑。如果沒問天會彈出一個成功的識別符號。
並且可以在登錄檔裡面查到:
使用Win+R的彈出框裡面輸入regedit.exe,檢視路徑\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\sample,這就是我們剛剛寫好且用sc創造的驅動:
啟動驅動
前面是註冊了該驅動,還得使用它。這裡也和User使用者態的服務Service類似,需要採用StartService API來使用或者採用一些軟體,這裡也可以用Sc.exe來繼續使用。
sc start sample
這裡的sample就是前面註冊的驅動的名字。
但是這個通常會失效,因為對於64bit位的windows系統,載入驅動必須得要有驅動的簽名才行,這裡為了學習方便,避開簽名這個東西,可以直接把系統置為測試版本。
bcdedit /set testsigning on
如果你要生成除了Windows10以外的版本,可以在專案屬性裡面配置你的驅動要部署在的系統環境裡面:
最後再使用前面sc來載入驅動時會看到一個關於驅動的輸出:
有了這個輸出就表明我們的驅動已經成功載入了,可以使用Process Explorer工具來確認是否載入成功(下載地址:https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer)
這裡的驅動名稱是你自己的sys驅動名稱。
解除安裝驅動
不用了將驅動程式卸下,同樣的可以採用 ControlService API或者Sc.exe來處理。Sc指令:
sc stop sample
就OK了。
簡單跟蹤
為了確保函式有確切被呼叫,這裡提供一種基礎的跟蹤辦法來確保函式被使用,驅動採用KdPrint這個巨集來輸出類似於printf風格的文字,該巨集的內容可以被核心的偵錯程式,或者其它工具檢視到。
KdPrint這個巨集只在debug模式下采用,它的底層呼叫的其實是DbgPrint 核心Kernel API。
下面更新一下DriverEntry和Unload函式:
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("Sample driver Unload called! \n"));
}
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
DriverObject->DriverUnload = SampleUnload;
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("Sample driver initialized successfully\n !"));
return STATUS_SUCCESS;
}
需要注意的是該巨集函式在呼叫時採用了兩個括號,因為它是一個巨集函式,但是又顯然它是可以接受任意變數的,由於巨集函式不能接受可變的變數引數,所以編譯器實際上呼叫的是DbgPrint函式。這裡理解不了沒關係,先這樣用著就行。
重新生成驅動並載入來檢視這些Print資訊,這裡需要採用一個核心的偵錯程式才行,但是為了方便,先採用一個系統的內部工具:DebugView來檢視。在使用DebugView之前,需要先給它在登錄檔裡面配置內容不然用不上。
在\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\裡新建一個Key(項)Key名為DebugPrintFilter,並且新增一個DWORD型別的值名為DEFAULT,這個DEFAULT要和預設的一個值區別開來,後面那個預設的值是登錄檔中的每一個項都有的,那個值暫時先不用管,然後給該名為DEFAULT型別為DWORD的變數賦值為8,如下圖所示:
然後下載DebugView(DebugView - Windows Sysinternals | Microsoft Docs),並用管理員身份開啟它,然後再在Capture選項中去掉Capture Win32 和Capture Global Win32,選中Capture Kernel:
這樣,再使用Sc.exe來重新載入驅動就可以看到KdPrint列印的內容了。
總結Summary
這裡明白瞭如何寫一個驅動,以及如何再電腦上部署和檢視驅動的訊息,算是驅動入門了。