當前位置:編程學習大全網 - 編程語言 - 查R3層函數對應的內核函數的方法

查R3層函數對應的內核函數的方法

3.2 獲得實際數據

這壹章我們壹直都在開發壹個可以捕獲串口上的數據的過濾程序。現在虛擬設備已經綁定了真正的串口設備,那麽,實際上如何從虛擬設備得到串口上流過的數據呢?答案是根據“請求”。操作系統將請求發送給串口設備,請求中就含有要發送的數據,請求的回答中則含有要接收的數據。下面分析這些“請求”,以便得到實際的串口數據流。

3.2.1 請求的區分

Windows的內核開發者們確定了很多的數據結構,在前面的內容中我們逐漸地和DEVICE_OBJECT(設備對象)、FILE_OBJECT(文件對象)和DRIVER_OBJECT(驅動對象)見了面。文件對象暫時沒有什麽應用(但是在本書後面的文件系統過濾中,文件對象是極為重要的)。讀者需要了解的是:

(1)每個驅動程序只有壹個驅動對象。

(2)每個驅動程序可以生成若幹個設備對象,這些設備對象從屬於壹個驅動對象。在壹個驅動中可否生成從屬於其他驅動的驅動對象的設備對象呢?從IoCreateDevice的參數來看,這樣做是可以的,但是筆者沒有嘗試過這樣的應用。

(3)若幹個設備(它們可以屬於不同的驅動)依次綁定形成壹個設備棧,總是最頂端的設備先接收到請求。

請註意:IRP是上層設備之間傳遞請求的常見數據結構,但是絕對不是唯壹的數據結構。傳遞請求還有很多其他的方法,不同的設備也可能使用不同的結構來傳遞請求。但在本書中,90%的情況下,請求與IRP是等價概念。

串口設備接收到的請求都是IRP,因此只要對所有IRP進行過濾,就可以得到串口上流過的所有數據。串口過濾時只需要關心有兩種請求:讀請求和寫請求。對串口而言,讀指接收數據,而寫指發出數據。串口也還有其他的請求,比如打開和關閉、設置波特率等。但是我們的目標只是獲得串口上流過的數據,而不關心打開關閉和波特率是多少這樣的問題,所以壹概無視。

請求可以通過IRP的主功能號進行區分。IRP的主功能號是保存在IRP棧空間中的壹個字節,用來標識這個IRP的功能大類。相應的,還有壹個次功能號來標識這個IRP的功能細分小類。

讀請求的主功能號為IRP_MJ_READ,而寫請求的主功能號為IRP_MJ_WRITE。下面的方法用於從壹個IRP指針得到主功能號(這裏的變量irp是壹個PIRP,也就是IRP的指針)。

// 這裏的irpsp稱為IRP的棧空間,IoGetCurrentIrpStackLocation獲得當前棧空間

// 棧空間是非常重要的數據結構

PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);

if(irpsp->MajorFunction == IRP_MJ_WRITE)

{

// 如果是寫…

}

else if(irpsp->MajorFunction == IRP_MJ_READ)

{

// 如果是讀…

}

3.2.2 請求的結局

對請求的過濾,最終的結局有3種:

(1)請求被允許通過了。過濾不做任何事情,或者簡單地獲取請求的壹些信息。但是請求本身不受幹擾,這樣系統行為不會有變化,皆大歡喜。

(2)請求直接被否決了。過濾禁止這個請求通過,這個請求被返回錯誤了,下層驅動程序根本收不到這個請求。這樣系統行為就變了,後果是常常看見上層應用程序彈出錯誤框提示權限錯誤或者讀取文件失敗之類信息。

(3)過濾完成了這個請求。有時有這樣的需求,比如壹個讀請求,我們想記錄讀到了什麽。如果讀請求還沒有完成,那麽如何知道到底會讀到什麽呢?只有讓這個請求先完成再去記錄。過濾完成這個請求時不壹定要原封不動地完成,這個請求的參數可以被修改(比如把數據都加密壹番)。

當過濾了壹個請求時,就必須把這個請求按照上面3種方法之壹進行處理。當然這些代碼會寫在壹個處理函數中。如何使用這個處理函數將在後面的小節中提及,這裏先介紹這些處理方法的代碼應該怎麽寫。

串口過濾要捕獲兩種數據:壹種是發送出的數據(也就是寫請求中的數據),另壹種是接收的數據(也就是讀請求中的數據)。為了簡單起見,我們只捕獲發送出的數據,這樣,只需要采取第1種處理方法即可。至於第2、3兩種處理方法,讀者會在後面的許多過濾程序中碰到。

這種處理最簡單。首先調用IoSkipCurrentIrpStackLocation跳過當前棧空間;然後調用IoCallDriver把這個請求發送給真實的設備。請註意:因為真實的設備已經被過濾設備綁定,所以首先接收到IRP的是過濾設備的對象。代碼如下(irp是過濾到的請求):

// 跳過當前棧空間

IoSkipCurrentIrpStackLocation(irp);

// 將請求發送到對應的真實設備。記得我們前面把真實設備都保存在s_nextobj

// 數組中。那麽這裏i應該是多少?這取決於現在的IRP發到了哪個

// 過濾設備上。後面講解分發函數時讀者將了解到這壹點

status = IoCallDriver(s_nextobj[i],irp);

3.2.3 寫請求的數據

那麽,壹個寫請求(也就是串口壹次發送出的數據)保存在哪裏呢?回憶前面關於IRP結構的描述(第2章的2.3.3節),裏面壹***有3個地方可以描述緩沖區:壹個是irp->MDLAddress,壹個是irp->UserBuffer,壹個是irp->AssociatedIrp.SystemBuffer。不同的IO類別,IRP的緩沖區不同。SystemBuffer是壹般用於比較簡單且不追求效率情況下的解決方案:把應用層(R3層)中內存空間中的緩沖數據拷貝到內核空間。

UserBuffer則是最追求效率的解決方案。應用層的緩沖區地址直接放在UserBuffer裏,在內核空間中訪問。在當前進程和發送請求進程壹致的情況下,內核訪問應用層的內存空間當然是沒錯的。但是壹旦內核進程已經切換,這個訪問就結束了,訪問UserBuffer當然是跳到其他進程空間去了。因為在Windows中,內核空間是所有進程***用的,而應用層空間則是各個進程隔離的。

當然壹個更簡單的解決方案是把應用層的地址空間映射到內核空間,這需要在頁表中增加壹個映射。當然這不需要編程者手工去修改頁表,通過構造MDL就能實現這個功能。MDL可以翻譯為“內存描述符鏈”,但是本書按業界傳統習慣壹律稱之為MDL。IRP中的MDLAddress域是壹個MDL的指針,從這個MDL中可以讀出壹個內核空間的虛擬地址。這就彌補了UserBuffer的不足,同時比SystemBuffer的完全拷貝方法要輕量,因為這個內存實際上還是在老地方,沒有拷貝。

回到串口的問題,那麽串口寫請求到底用的是哪種方式呢?老實點說,筆者並不清楚也沒有去調查到底是哪種方式。但是如果用下面的編碼方式,無論采用哪種方式,都可以把數據正確地讀出來。

PBYTE buffer = NULL;

if(irp->MdlAddress != NULL)

buffer = (PBYTE)MmGetSystemAddressForMdlSafe(irp->MdlAddress);

else

buffer = (PBYTE)irp->UserBuffer;

if(buffer == NULL)

buffer = (PBYTE)irp->AssociatedIrp.SystemBuffer;

這其中涉及壹個函數MmGetSystemAddressForMdlSafe,有興趣的讀者可以在WDK的幫助中查閱壹下這個函數的含義。同時也可以深入了解壹下MDL,但是對閱讀本書重要性不是很明顯。本書的後面涉及從MDL得到系統空間虛擬地址的情況下,都簡單地調用MmGetSystemAddressForMdlSafe。

此外是緩沖區有多長的問題。對壹個寫操作而言,長度可以如下獲得:

ULONG length = irpsp->Parameters.Write.Length;

3.3 完整的代碼

3.3.1 完整的分發函數

下面基於前面的描述,我們再嘗試編寫壹個分發函數。這個函數處理所有串口的寫請求,所有從串口輸出的數據都用DbgPrint打印出來。也就是說,讀者打開DbgView.exe,就可以看到串口的輸出數據了。這當然不如壹些比較專業的串口嗅探軟件好,但是讀者可以以這個例子為基礎開發更專業的工具。

NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp)

{

PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);

NTSTATUS status;

ULONG i,j;

// 首先要知道發送給了哪個設備。設備最多壹***有CCP_MAX_COM_ID

// 個,是前面的代碼保存好的,都在s_fltobj中

for(i=0;i<CCP_MAX_COM_ID;i++)

{

if(s_fltobj[i] == device)

{

// 所有電源操作,全部直接放過

if(irpsp->MajorFunction == IRP_MJ_POWER)

{

// 直接發送,然後返回說已經被處理了

PoStartNextPowerIrp(irp);

IoSkipCurrentIrpStackLocation(irp);

return PoCallDriver(s_nextobj[i],irp);

}

// 此外我們只過濾寫請求。寫請求,獲得緩沖區及其長度

// 然後打印

if(irpsp->MajorFunction == IRP_MJ_WRITE)

{

// 如果是寫,先獲得長度

ULONG len = irpsp->Parameters.Write.Length;

// 然後獲得緩沖區

PUCHAR buf = NULL;

if(irp->MdlAddress != NULL)

buf =

(PUCHAR)MmGetSystemAddressForMdlSafe(

irp->MdlAddress,NormalPagePriority);

else

buf = (PUCHAR)irp->UserBuffer;

if(buf == NULL)

buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer;

// 打印內容

for(j=0;j<len;++j)

{

DbgPrint("comcap: Send Data: %2x\r\n",

buf[j]);

}

}

// 這些請求直接下發執行即可,我們並不禁止或者改變它

IoSkipCurrentIrpStackLocation(irp);

return IoCallDriver(s_nextobj[i],irp);

}

}

// 如果根本就不在被綁定的設備中,那是有問題的,直接返回參數錯誤

irp->IoStatus.Information = 0;

irp->IoStatus.Status = STATUS_INVALID_PARAMETER;

IoCompleteRequest(irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;

}

3.3.2 如何動態卸載

前面只說了如何綁定,但是沒說如何解除綁定。如果要把這個模塊做成可以動態卸載的模塊,則必須提供壹個卸載函數。我們應該在卸載函數中完成解除綁定的功能;否則,壹旦卸載壹定會藍屏。

這裏涉及到3個內核API:壹個是IoDetachDevice,負責將綁定的設備解除綁定;另壹個是IoDeleteDevice,負責把我們前面用IoCreateDevice生成的設備刪除掉以回收內存;還有壹個是KeDelayExecutionThread,純粹負責延時。這三個函數的參數相對簡單,這裏就不詳細介紹了,需要的讀者請查閱WDK的幫助。

卸載過濾驅動有壹個關鍵的問題:我們要終止這個過濾程序,但是壹些IRP可能還在這個過濾程序的處理過程中。要取消這些請求非常的麻煩,而且不壹定能成功。所以解決方案是等待5秒來保證安全地卸載掉。只能確信這些請求會在5秒內完成,同時等待之前我們已經解除了綁定,所以這5秒內不會有新請求發送過來處理。這對於串口而言是沒問題的,但是並非所有的設備都如此。所以讀者在後面的章節會看到不同的處理方案。

#define DELAY_ONE_MICROSECOND (-10)

#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)

#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)

void ccpUnload(PDRIVER_OBJECT drv)

{

ULONG i;

LARGE_INTEGER interval;

// 首先解除綁定

for(i=0;i<CCP_MAX_COM_ID;i++)

{

if(s_nextobj[i] != NULL)

IoDetachDevice(s_nextobj[i]);

}

// 睡眠5秒。等待所有IRP處理結束

interval.QuadPart = (5*1000 * DELAY_ONE_MILLISECOND);

KeDelayExecutionThread(KernelMode,FALSE,&interval);

// 刪除這些設備

for(i=0;i<CCP_MAX_COM_ID;i++)

{

if(s_fltobj[i] != NULL)

IoDeleteDevice(s_fltobj[i]);

}

}

3.3.3 完整的代碼

這個驅動的完整代碼比較簡單。前面已經介紹了壹些函數,請把這些函數都拷貝下來集中到comcap.c文件裏。再建立壹個目錄,名為comcap來容納這個文件。這個文件的內容大致如下:

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)

{

size_t i;

// 所有的分發函數都設置成壹樣的

for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)

{

driver->MajorFunction[i] = ccpDispatch;

}

// 支持動態卸載

driver->DriverUnload = ccpUnload;

// 綁定所有的串口

ccpAttachAllComs(driver);

// 直接返回成功即可

return STATUS_SUCCESS;

}

然後編寫壹個SOURCE文件,內容如下:

TARGETNAME=comcap

TARGETPATH=obj

TARGETTYPE=DRIVER

SOURCES =comcap.c

將這個文件也放在comcap目錄下。參考1.1.3節中的方法編譯,然後加載執行這個驅動。設法通過串口傳輸數據,打開DbgView.exe就能看到輸出信息了。

這個例子的代碼在隨書附帶光盤源代碼的comcap目錄下。

本章的示例代碼

本章的例子在源代碼目錄comcap下,編譯結果為comcap,可以動態加載和卸載。編譯的方法請參考本書的附錄“如何使用本書的源碼光盤”。加載後,如果有數據從串口輸出,打開DbgView.exe就會看到輸出信息了。

壹般的讀者可能沒有使用串口的打印機,但是可以用如下的方法簡單地使用串口,以便讓這個程序起作用:打開“開始”菜單→“所有程序”→“附件”→“通訊”→“超級終端”,然後任意建立壹個連接,如圖3-1所示。

圖3-1 打開“超級終端”用串口撥號

註意連接時使用的COM1就是第壹個串口。這樣單擊“確定”按鈕之後,在上面的文本框中任意輸入字符串就會被發送到串口。此時如果加載了comapp.sys,那麽在DbgView.exe中就應該可以看到輸出信息如圖3-2所示。

圖3-2 用comcap捕獲的串口數據

練習題

1.紙上練習

(1)在這壹章中,所謂的過濾是什麽意思?有什麽意義?

(2)何為內核對象?我們已經接觸到了哪幾種內核對象?

(3)何為設備對象?妳能在Windows系統中指出已經存在的至少5個設備對象嗎?

(4)DO是什麽的簡稱?

(5)何為綁定?哪些內核API可以實現設備的綁定?

2.上機練習

(1)編譯comcap.c並執行,用DbgView看輸出結果。

(2)對comcap.c進行修改,使之對所有的串口輸出都禁止,然後測試。

(3)用WinObj找到並口設備的名字,並把comcap.c的代碼改為對並口的過濾。

(4)有條件的讀者請找壹臺並口打印機,嘗試打印壹個文本文件,然後用DbgView觀察從並口過濾攔截到的數據。

  • 上一篇:加拿大最強的專業有哪些
  • 下一篇:簡述VLAN中的DOTIQ和ISL的封裝方法
  • copyright 2024編程學習大全網