現在大家對vmm和vxd有了一定的了解,接下來我們來看一看如何編寫vxd代碼。首先,你必須具備Windows 95/98 Device Driver Development Kit。Window95 ddk只有MSDN 訂戶才能拿到,但Windows98 ddk卻可以免費從Microsoft公司取得。盡管Windows 98 ddk是面向WDM的,但你還是可以用它來開發VxD程序。你可以從 http://www.microsoft.com/hwdev/ddk/install98ddk.htm?下載Window98 ddk。
你可以下載整個軟件包(大約30M),也可以只下載你感興趣的部分。如果你沒有下載整個軟件包,那麼別忘了下載other.exe
裡面的Window95 ddk documentation。Windows98 ddk 包含了6.11d版的MASM。你需要把它升級為最新版。如果你不知道到哪裡去下載最新的版本,可以去我的主頁上查一查。
Window9x DDK包含了一些Masm32包所不具有的重要庫文件。
LE文件格式
VxD采用線性可執行文件格式(LE)。這種文件格式是為OS/2 2.0版設計的。它同時包含16位和32位代碼,這點也是VxD程序的需要。回想VxD在Windows3.x的時代,在那時,從Dos啟動Windows,Windows在把機器轉到保護模式之前需要在實模式下做一些初始化。實模式的16位代碼必須和32位代碼一起放在可執行文件中。所以LE文件格式理所當然的選擇。幸運的,Windows NT驅動程序不必在實模式下初始化,所以它們不必使用LE文件格式。它們用的是PE文件格式。
在LE文件中,代碼和數據被存放在幾類運行屬性不同的段中。以下是一些可用的段類。
LCODE 頁面鎖定的代碼和數據段 這種段被鎖定在內存裡。換句話說,這段永遠不會被放到硬盤上去,所以你一定要謹慎的使用這種段類以免浪費寶貴的內存。那些每時每刻都必須放在內存中的代碼和數據應該放在這個段裡。尤其是那些硬件中斷處理程序。
PCODE 可調頁代碼段 VMM可以對這種段實行調頁處理,在這種段裡的代碼不必時刻放在內存裡,當VMM需要物理內存的時候,它就會把這段放到硬盤上去。
PDATA 可調頁數據段
ICODE 僅用於的初始化段 這種段裡的代碼僅僅用來進行VxD的初始化。當初始化完成後,VMM就把這段從內存中釋放。
DBOCODE 僅用於調試的代碼數據段 當你要調試VxD程序時,就要用到這種段裡的代碼和數據,例如,它包含要調試的消息的處理代碼。
SCODE 靜態代碼和數據段 這種段時刻存在於內存中,即使VxD已經卸載,這種段對某些動態的VxD程序很有用,這些VxD程序需要在某一Windows進程裡不停的加載/卸載而又要紀錄上次的環境和狀態。
RCODE 實模式初始化代碼數據段 這種段包含實模式初始化需要的16位代碼和數據。
16ICODE 16ICODE USE16保護模式初始化數據段 這是一個16位的段,它包含VxD要從保護模式拷貝到V86模式的代碼。例如,如果你要把一些V86的代碼拷貝到一個虛擬機上時,你想拷貝的代碼就要放在這裡。如果你把它放在其他的段裡,編譯程序就會產生錯誤的代碼,例如,它會產生32位代碼而不是16位代碼。
MCODE 鎖定的消息字串 這種段包含了由VMM消息宏幫助編譯的消息字串,這有助於你構造你的驅程的國際版本。
這並不意味著你的VxD程序必須包含以上所有的段,你可以選擇你的VxD程序需要的段。例如,如果你的VxD程序不進行實模式初始化,那麼就不必包含RCODE段。
大多數時候,你要用到LCODE, PCODE和PDATA段。作為一個VxD程序編寫者,為你的代碼和數據選擇合適的段取決於你自己的判斷。總的來說,你應該盡可能多的使用PCODE和PDATA因為這樣VMM就可以在需要的時候把段調入調出內存。另外,硬件中斷程序及其所用到的服務必須放在 LCODE段裡。
你不能直接地使用這些段類,你要用這些段類來定義段,這些段的定義被存放在模塊定義文件(.def)中。下面是一個標准的模塊定義文件:
VXD FIRSTVXD
SEGMENTS
_LPTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LDATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_DATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
CONST CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TLS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_BSS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LMGTABLE CLASS 'MCODE' PRELOAD NONDISCARDABLE IOPL
_LMSGDATA CLASS 'MCODE' PRELOAD NONDISCARDABLE IOPL
_IMSGTABLE CLASS 'MCODE' PRELOAD DISCARDABLE IOPL
_IMSGDATA CLASS 'MCODE' PRELOAD DISCARDABLE IOPL
_ITEXT CLASS 'ICODE' DISCARDABLE
_IDATA CLASS 'ICODE' DISCARDABLE
_PTEXT CLASS 'PCODE' NONDISCARDABLE
_PMSGTABLE CLASS 'MCODE' NONDISCARDABLE IOPL
_PMSGDATA CLASS 'MCODE' NONDISCARDABLE IOPL
_PDATA CLASS 'PDATA' NONDISCARDABLE SHARED
_STEXT CLASS 'SCODE' RESIDENT
_SDATA CLASS 'SCODE' RESIDENT
_DBOSTART CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBOCODE CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBODATA CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_16ICODE CLASS '16ICODE' PRELOAD DISCARDABLE
_RCODE CLASS 'RCODE'
EXPORTS
FIRSTVXD_DDB @1
第一個聲明定義了VxD的名稱,一個VxD的名稱必須是全部大寫的,我曾經試過用小寫,結果VxD除了把自己載入內存外什麼也不干。
接下來是段的定義,段的定義包括三個部分:段的名稱,段類和要求的段的運行屬性。你可以看到很多段都基於相同的段類,例如,_LPTEXT, _LTEXT, _LDATA都是基於LCODE段類而且屬性也完全一樣。這樣定義段有利於使代碼更容易理解。如:LCODE可以包含代碼和數據,對於一個程序員來說,如果他能把數據放到_LDATA段裡,把代碼放到_LTEXT 段裡,就會顯得很容易理解。最後,這兩個段都會被編譯到最後的可執行程序的同一個段內。
一個VxD程序導出且僅導出一個標記:它的設備描述塊(DDB)。DDB實際上是一個結構,它包含了VMM需要知道的所有的VxD信息。你必須在模塊定義文件中導出DDB。
在大多數時候,你可以把上面的.DEF文件用到你的新建的VxD項目中去。你只要把.DEF文件裡第一行和最後一行的VxD名字改掉就可以了。在一個匯編的VxD項目中,段的定義是不必要的,段的定義主要用於C的VxD項目編寫,但用在匯編裡也是可以的。你會得到一大堆警告的信息但是它能匯編成功。你也可以刪掉你在你的項目裡沒有用到的段定義從而去掉這些討厭的警告信息。
vmm.inc包含了許多用於定義你的源文件中的段的宏:
_LTEXT VxD_LOCKED_CODE_SEG
_PTEXT VxD_PAGEABLE_CODE_SEG
_DBOCODE VxD_DEBUG_ONLY_CODE_SEG
_ITEXT VxD_INIT_CODE_SEG
_LDATA VxD_LOCKED_DATA_SEG
_IDATA VxD_IDATA_SEG
_PDATA VxD_PAGEABLE_DATA_SEG
_STEXT VxD_STATIC_CODE_SEG
_SDATA VxD_STATIC_DATA_SEG
_DBODATA VxD_DEBUG_ONLY_DATA_SEG
_16ICODE VxD_16BIT_INIT_SEG
_RCODE VxD_REAL_INIT_SEG
每個宏都有它相對應的結束宏,例如,如果你要在你的源文件中定義一個_LTEXT段,你應該這樣寫:
VxD_LOCKED_CODE_SEG
(把你的代碼寫在這裡)
VxD_LOCKED_CODE_ENDS
VxD結構
現在你了解了LE文件裡的段,我們可以繼續來看一下源文件。你會發現VxD程序有一個特點,那就是它用了很多的宏。你可以看到在VxD中宏幾乎無處不在,這都成為一個習慣了。這些宏用來隱藏一些底層的細節,也增加了源程序的可移植性。如果你有興趣,你可以看一看像vmm.inc這一類的庫文件中的這些宏的定義。
下面是VxD源文件結構:
.386p
include vmm.inc
DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD
end
這段源程序給人的第一印象就是:它並不像一個匯編源程序。那是因為它用了很多宏。讓我們來分析一下源程序以便你能很快理解它。
.386p
告訴編譯器我們要使用包括CPU特權指令的80386指令系統。你也可以使用.486p或者.586p.
include vmm.inc
你的每個VxD源程序都必須包含imm.inc,因為它包含了你在源程序裡所要用到的宏的定義。你還可以根據需要包含其他的庫文件。
DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
正如我們剛才說的,VMM通過VxD程序的設備描述塊(DDB)來獲取它所需要知道的關於VxD的所有信息。一個設備描述塊是一個結構,它包含了許多關於VxD的重要信息,比如VxD的名字,它的設備ID,它的VxD服務函數入口(如果有的話),等等。你可以在imm.inc裡查一查這個結構,它被定義為VxD_Desc_Block。你必須在.DEF 文件裡導出這個結構。這個結構有22個數據,但是你只用填寫其中的幾個。然後vmm.inc包含的一個宏會為你初始化並填寫這些數據。這個宏叫做DECLARE_VIRTUAL_DEVICE。它的格式如下:
Declare_Virtual_Device Name, MajorVer, MinorVer, CtrlProc, DeviceID, InitOrder, V86Proc, PMProc, RefData
你可以看到:VxD源程序中的標號是不區分大小寫的,你可以用大寫,小寫或者混合起來用都可以。讓我們來看一下Declare_virtual_device裡的每一個參數。
Name VxD的名字 最多8個字符。它必須是大寫!在系統中的所有VxD程序裡,它們的名字不能重復,每個VxD的名字應該是唯一的。這個宏同時也會根據這個名字產生DDB的名字,產生的辦法就是:在這個名字的後面加上_DDB。所以如果你的VxD的名字是FIRSTVXD, Declare_Virtual_Device這個宏就會把DDB的名字定為FIRSTVXD_DDB。記住,你還要在.DEF文件裡導出DDB。所以你必須使DDB的名字和.DEF文件定義中的相同。
MajorVer 和 MinorVer 你的VxD的主要的和次要的版本。
CtrlProc 你的VxD程序的設備控制函數的名字。設備控制函數是一個接受和處理VxD程序的控制消息的函數。你可以把設備控制函數看作Window函數的等價物。既然我們要用Begin_Control_Dispatch這個宏來生成我們的設備控制函數,那麼我們應該使用一個標准格式的名字,那就是VxD的名字_Control。 Begin_Control_Dispatch這個宏把_Control 加到它後面的那個名字上(而我們又通常把VxD的名字寫在它後面)作為設備控制函數的名字,所以我們就應該把VxD的名字加上_Control作為CtrlProc 參數的值。
DeviceID 你的VxD程序的16位唯一標識符 當且僅當你的VxD程序需要處理以下情況時你需要用到這個ID:
你的VxD程序導出一些供其他VxD程序使用的VxD服務。因為20H中斷接口用設備ID來定位/區分VxD程序,所以一個唯一的ID對你的VxD程序是必要的。
Your VxD 你的VxD程序要在初始化中斷2FH,1607H時通知實模式程序它的存在。
Some 一些實模式軟件(TSR)要用中斷2FH,1605H來加載你的VxD程序。
如果你的VxD程序不需要一個唯一的設備ID,你可以把這一項設為UNDEFINED_DEVICE_ID ,如果你需要它,你可以去Microsoft要一個。
InitOrder 初始化的順序,簡單的說,就是加載的順序。VMM就按照這個次序來加載VxD程序。每個VxD程序都有一個加載次序號,例如:
VMM_INIT_ORDER EQU 000000000H
DEBUG_INIT_ORDER EQU 000000000H
DEBUGCMD_INIT_ORDER EQU 000000000H
PERF_INIT_ORDER EQU 000900000H
APM_INIT_ORDER EQU 001000000H
你可以看到:VMM, DEBUG和 DEBUGCMD是首先加載的VxD程序,然後是PERF和APM。初始化順序值越低的VxD程序越先被加載。如果你的VxD程序在初始化時需要用到其他VxD程序提供的服務,那麼你必須把初始化順序的值設得比你所要調用的那個VxD程序的大,這樣,當你的VxD程序加載時,你所要的VxD就已經在內存中為你准備好了。如果不想去管你的VxD的初始化順序,就把這個參數填寫為UNDEFINED_INIT_ORDER 。
V86Proc和PMProc 你的程序可以導出供V86和保護模式程序使用的API,這兩個參數就是用來填寫這些API的地址。記住,VxD程序除了監控系統虛擬機外,還要監控一個或多個運行在DOS或者保護模式下的虛擬機程序。理所當然的,VxD程序要為DOS和保護模式程序提供API支持。如果你不導出這些API,你可以不填這兩個參數。
RefData 輸入輸出監視器(IOS)要用到的參考數據。只有一種情況下你要用到這個參數:當你在為IOS編寫一個層驅動程序時。否則,你可以不填這個參數。
接下來是 Begin_Control_Dispatch宏。
Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD
這兩個宏定義了設備控制函數,當VxD的控制消息發生時,VMM就調用這個函數。你必須填寫設備控制函數名字的前半部分,在本例中,我們用的是 FIRSTVXD。這個宏會在你輸入的前半部分後加上_Control作為設備控制函數的名字。這個名字一定要和你在Declare_virtual_device 宏中給參數CtrlProc填的名字一致。設備控制函數總是放在鎖定段(VxD_LOCKED_CODE_SEG)內的。上面定義的設備控制函數什麼也不干。你需要說明你的VxD程序要響應什麼控制消息,以及處理這個消息的函數,你可以用Control_Dispatch宏來實現這一點。
Control_Dispatch message, function
例如,如果你的VxD程序只要處理Device_Init 消息,你的設備控制程序要這樣寫:
Begin_Control_Dispatch FIRSTVXD
Control_Dispatch Device_Init, OnDeviceInit
End_Control_DispatchFIRSTVXD
OnDeviceInit就是要處理Device_Init消息的函數的名字。你可以給你的函數取任何你想取的名字。
你可以用end 直接地結束你的VxD源程序。
綜上所述,一個VxD程序至少包含一個設備控制塊和一個設備控制函數。你要用Declare_Virtual_Device宏來定義一個設備控制塊,用Begin_Control_Dispatch宏來定義一個設備控制程序。你必須在.def文件中的EXPORTS下面填寫設備控制塊的名字,從而導出該設備控制塊。
編譯VxD
編譯的過程和編譯普通的win32程序一樣。先調用ml.exe編譯asm源文件,然後用link.exe來連接object文件。不同的地方是ml.exe和link.exe後所帶的命令行參數不同。
ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 firstvxd.asm
-coff 表明COFF數據格式
-c 只匯編,不調用連接程序來連接,這樣我們就可以在調用link.exe的時候使用跟多的參數。
-Cx 保存公共,外部標記。
-D<text> 定義一個文本宏,例如,-DBLD_COFF定義了一個文本宏BLD_COFF,這個宏用來作為編譯的條件。如果你有興趣,你可以在庫文件中查找BLD_COFF,自己親眼看看它對匯編過程起什麼作用。上面的命令行定義了三個文本宏:BLD_COFF,IS_32和MASM6。如果你對C編程熟悉的話,你會知道這些定義相當於完成以下功能:
#define BLD_COFF
#define IS_32
#define MASM6
link -vxd -def:firstvxd.def firstvxd.obj
-vxd 表明我們要根據obj文件來生成一個VxD文件。
-def:<.DEF file> 指定該VxD文件的模式定義文件。
我覺得用makefile很方便,如果你不喜歡用makefile,你也可以創建批處理文件來自動完成編譯過程。我的makefile如下:
NAME=firstvxd
$(NAME).vxd:$(NAME).obj
link -vxd -def:$(NAME).def $(NAME).obj
$(NAME).obj:$(NAME).asm
ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 $(NAME).asm