二、對象管理與命名空間(Namespace)
內核空間中不同類型的對象都通過對象管理器統一管理,並通過命名空間這一邏輯上的概念來組織各個對象,類似於資源管理器。Device目錄存放著通過IoCreateDevice創建的各種設備對象,包括文件系統驅動下創建的卷對象。FileSystem目錄存放著文件系統驅動對象和文件系統識別器設備對象(這些內容將在進階篇敘述)。更具體的描述請參看資料2。
到目前為止,我們還未討論過用戶模式下的應用程序如何與驅動程序發生交互,請暫時忘記“中斷門”、“陷阱門”這類“高深莫測”的術語(大肆宣揚這些術語反而有引入歧途的動機),這些包含在CPU硬件理論中的基礎知識不會對我們學習驅動編程有直接的影響,相反,值得一提的卻是CreateFile函數。文件是一個高度抽象的概念,既然內核中的對象可以被統一管理,外部的各種設備自然也不例外,它們都可以用文件來加以描述。從圖中我們看到計算機中的串口COM1,它對應著設備對象Serial0,而C:盤,對應著是卷設備對象HarddiskVolume4,這是一種稱為“符號鏈接”的映射,通過這個映射,用戶模式下的程序才能看到內核中的設備對象,也才可以通過CreateFile打開它們。形象的說,符號鏈接類似於小名,如大狗一般就稱為“旺財”,小狗就叫做“小白”。在內核中建立符號連接可使用IoCreateSymbolicLink,用戶模式下可用DefineDosDevice。
CreateFile的使用示例,注意“.”對應著命名空間裡的“GLOBAL??”:
if ((hDevice = CreateFile( "\\\\.\\IoctlTest",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL)) == INVALID_HANDLE_VALUE) {
另一種途徑就是Ioctl控制操作。
三、Ioctl控制碼
Ioctl控制碼的結構類似於消息(如WM_XXX)或NTSTATUS的定義方式,它是一個驅動程序預定義的4字節整數,定義它的宏為:
#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)
16-31 2-13 0-1 14-15
通過提供設備類型、功能碼(可看作函數的序號)、緩沖方式和存取權限,該宏就創建了一個Ioctl碼。設備驅動可以定義多個Ioctl碼(通過不同的功能碼來區分不同的功能函數)以提供不同的控制功能。
四、Ioctl的同異步與緩沖區操作
使用DeviceIoControl函數來實現用戶模式下的Ioctl操作,它的定義如下:
BOOL DeviceIoControl( HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped,
);
根據Ioctl碼的不同,DeviceIoControl函數可以發出兩種IRP:IRP_MJ_FILE_SYSTEM_CONTROL和IRP_MJ_DEVICE_CONTROL,前者代表了file system I/O control (FSCTL)請求,後者代表了設備的IOCTL請求。還有一種僅僅在內核模式下使用的IRP_MJ_INTERNAL_DEVICE_CONTROL,它用於內核不同組件間的通信。
DeviceIoControl需要提供輸入和輸出緩沖區:
該函數的使用請參看資料1的第九章三小節,最後一個參數的存在,使Ioctl可以以同步或異步(前提是hDevice以FILE_FLAG_OVERLAPPED方式打開)的方式來完成。同步方式下,函數必須等待內核中的操作完成才返回,否則將立即返回。
Ioctl碼的緩沖區類型可分為三類:METHOD_BUFFERED、METHOD_IN_DIRECT和METHOD_OUT_DIRECT、METHOD_NEITHER。不同類型的緩沖區體現了操作的效率上的不同:
我們暫不理會驅動程序如何找到用戶模式下的輸入、輸出緩沖區,以及如何定義拷貝緩沖區,先來關注一下這三種方式的不同之處。上兩圖清晰的表達了自身的特點:Buffered方式在輸入、輸出數據時都要產生用戶緩沖區與內核中的拷貝緩沖區間的復制操作,效率上較低,而Direct方式從字面上理解即為“直接”,從上圖也可看出,它的輸出操作是通過MDL方式完成的,這是一種用戶模式內存映射到系統(內核)內存的方法,避免了復制操作,效率上就提高了。
在Neither方式下,I/O管理器直接將用戶模式下的緩沖區地址傳遞給內核驅動程序,不做任何映射變換。驅動如果想直接使用這個地址,必須處於這個進程的上下文中,因為只有在同一個上下文中,用戶進程和驅動例程使用的同一個地址值,才能被系統映射到同一個內存頁面。
也許大家會疑惑驅動例程究竟是由哪個線程執行的?事實上,不同例程的代碼很可能由不同類型的線程來執行,有的屬於用戶模式下的線程,有的屬於操作系統的線程,有的甚至根本不是線程對象。我們以“上下文”來描述驅動例程運行的線程環境,某時刻的它運行在三種上下文的一種中:
- 系統進程上下文System process context
- 特定用戶線程(和進程)上下文A specific user thread (and process) context
- 任意用戶線程(和進程)上下文Arbitrary user thread (and process) context
我們不妨簡單的理解為,在某種上下文中,CPU正執行著我們的驅動例程指令。“上下文”做為一個概念上的抽象,在具體實現上有著明確的數據結構,為了更好的理解上下文,請閱讀資料6。隨著實踐的增加,讀者對此將有更深入的理解。
五、Ioctl上的實踐
Ioctl有著廣泛的應用,它使用簡單,功能卻很強大。筆者選擇了一些示例,讀者可以根據需要選讀。
(一)WINDDK\3790\src\general\ioctl
這個示例堪稱Ioctl應用的標准樣本。SioctlDeviceControl是實現自定義Ioctl碼的函數,需重點研究。IO_STACK_LOCATION子域Parameters的操作規范,讀者可以查閱Msdn上IRP_MJ_DEVICE_CONTROL節的說明。Io堆棧的重要性不亞於IRP,需要熟悉它的結構與和相關的函數。
這個示例還演示了如何手動加載服務,這是一個三板斧的過程:
安裝驅動程序流程:
1、調用OpenSCManager()打開服務控制管理器
2、調用CreateService()創建一個服務,服務類型為內核驅動
3、調用OpenService()取得服務句柄
啟動服務:
4、調用StartService()啟動服務
停止服務:
4、調用ControlService()停止服務
刪除服務:
4、調用DeleteService()刪除服務
5、調用CloseServiceHandle()關閉服務句柄
操作驅動程序流程:
1、調用CreateFile()取得設備句柄
2、調用DeviceIoControl()傳遞I/O控制代碼
3、調用CloseHandle()關閉設備句柄
(二)《Rootkits——Windows內核的安全防護》第4章2小節的內核鉤子
(三)《城裡城外看SSDT》
(四)《被占用文件操作三法》
示例henum需要在c文件首部添加#pragma comment(lib, "ntdll.lib"),並將DDK中的ntdll.lib復制到目錄下,關鍵函數為NtQuerySystemInformation與NtQueryInformationFile。後兩個示例和文件自刪除技術一樣,思路來自對內核對象的理解。
(五)實現了讀取磁盤序列號等操作的diskid32源碼
六、結語
本篇內容難度不大,我們在理解Ioctl設計思路的同時也進一步熟悉了各種數據結構。除了示例一,其他示例僅要求以最大能力去理解,本文的參考完成時間為不超過兩星期。