虚拟设备驱动程序结构

现在大家对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文件中,代码和数据被存放在几类运行属性不同的中。以下是一些可用的段类

这并不意味着你的VxD程序必须包含以上所有的段,你可以选择你的VxD程序需要的段。例如,如果你的VxD程序不进行实模式初始化,那么就不必包含RCODE段。
大多数时候,你要用到LCODE, PCODEPDATA段。作为一个VxD程序编写者,为你的代码和数据选择合适的段取决于你自己的判断。总的来说,你应该尽可能多的使用PCODEPDATA因为这样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里的每一个参数。

接下来是 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





由 Ryo 翻译,发表于 http://asm.yeah.net,英文版本来自 [Iczelion's Win32 Assembly Homepage]