您正在查看: c++builder 分类下的文章

C++ Builder 研究--罗云彬VxD教程--虚拟设备驱动程序结构

现在大家对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, PCODEPDATA段。作为一个VxD程序编写者,为你的代码和数据选择合适的段取决于你自己的判断。总的来说,你应该尽可能多的使用PCODEPDATA因为这样VMM就可以在需要的时候把段调入调出内存。另外,硬件中断程序及其所用到的服务必须放在 LCODE段里。
你不能直接地使用这些段类,你要用这些段类来定义段,这些段的定义被存放在模块定义文件(.def)中。下面是一个标准的模块定义文件:

VXD FIRSTVXD
SEGMENTS
_LPTEXTCLASS '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包含了许多用于定义你的源文件中的段的宏:_LTEXTVxD_LOCKED_CODE_SEG _PTEXTVxD_PAGEABLE_CODE_SEG _DBOCODEVxD_DEBUG_ONLY_CODE_SEG _ITEXTVxD_INIT_CODE_SEG _LDATAVxD_LOCKED_DATA_SEG _IDATAVxD_IDATA_SEG _PDATAVxD_PAGEABLE_DATA_SEG _STEXTVxD_STATIC_CODE_SEG _SDATAVxD_STATIC_DATA_SEG _DBODATAVxD_DEBUG_ONLY_DATA_SEG _16ICODEVxD_16BIT_INIT_SEG _RCODEVxD_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文件定义中的相同。
  • MajorVerMinorVer 你的VxD的主要的和次要的版本。
  • CtrlProc 你的VxD程序的设备控制函数的名字。设备控制函数是一个接受和处理VxD程序的控制消息的函数。你可以把设备控制函数看作Window函数的等价物。既然我们要用Begin_Control_Dispatch这个宏来生成我们的设备控制函数,那么我们应该使用一个标准格式的名字,那就是VxD的名字_ControlBegin_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, DEBUGDEBUGCMD是首先加载的VxD程序,然后是PERFAPM。初始化顺序值越低的VxD程序越先被加载。如果你的VxD程序在初始化时需要用到其他VxD程序提供的服务,那么你必须把初始化顺序的值设得比你所要调用的那个VxD程序的大,这样,当你的VxD程序加载时,你所要的VxD就已经在内存中为你准备好了。如果不想去管你的VxD的初始化顺序,就把这个参数填写为UNDEFINED_INIT_ORDER

  • V86ProcPMProc 你的程序可以导出供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_DispatchFIRSTVXD
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 定义一个文本宏,例如,-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: 指定该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

C++ Builder 研究--罗云彬VxD教程--虚拟机管理器

虚拟机管理器(VMM)是Windows 95的实际操作系统,它建立和维护一个管理虚拟机的框架,同时为其他vxd程序提供许多重要的服务。其中三种重要的服务是:

  • 内存管理
  • 中断处理
  • 线程调度

内存管理

VMM使用Intel 80386或更新的处理器的内存调页能力来为系统虚拟机创建一个32位的虚地址空间。它把这个地址空间分为四个不同的部分:
  • V86区 地址从0H到10FFEFH,这个区属于当前执行的虚拟机。
  • 应用程序私有区地址从4MB到2GB。这是win32应用程序运行的空间。每个win32的进程都有它自己的2GB(要减去4MB)。
  • 应用程序共享区地址从2GB到3GB。这个区域是在虚拟机内的所有 应用程序共享的。系统DLL(user32,kernel32,gid32)都驻存在这里。所有的Win16程序也放在这里,因为它们行为都是不规范的的:它们对内存中的其他Win16程序进行读写。只有在这个区域里,Win16程序才可以看到其他所有的Win16程序。内存映射文件和分配给DPMI的内存也被存放在这里。
  • 系统共享区地址从3GB到4GB。这里是VMM和VXM存放的地方。
VMM为VxD程序提供三种VxD服务:
  • 页面内存服务 这种服务分配/管理页面大小为4KB的内存。这是提供的最低级的服务,其他所有的服务都是建立在页面内存服务上的。
  • 堆内存服务 管理小的内存块。这种高级别的内存管理服务建立在页面内存服务的基础上。
  • 表服务管理可用来实行链结表的固定大小的内存块。

处理中断

在保护模式下,中断指向中断描述表(IDT)。VMM通过VxD的帮助监视虚拟机的IDT。通常VMM处理IDT内几乎所有的中断入口。它进行第一级的中断处理:保存被中断程序的状态,把控制传送到第二级的中断处理,第二级的中断处理通常由各种VxD程序来进行实际的处理。当第二级中断处理程序完成了它的工作之后,它把控制转交给重分派程序,由重分派程序来恢复被中断程序的状态并从先前被中断的地方继续执行。
上面的描述太过简单。由于被中断的虚拟机的时间片可能已过,重分派也许不会马上执行。VxD程序通过VMM服务如:Set_PM_Int或Hook_V86_Int_Chain来安装中断处理。VxD程序不应该直接改动IDT中的中断入口(除非你很确切的知道将发生的后果)。

线程调度

VMM使用两个调度器组件来在虚拟机之间实现有优先级的多线程处理:
  • 主调度器
  • 时间片管理器副调度器
主调度器的任务是选择有最高优先级的线程来执行。这种选择在VMM处理一个中断(如计时器中断)时进行。选择的结果决定了当VMM从中断服务返回时由哪一个线程/虚拟机获得控制权。主调度器工作的结果是确定的,一个线程要么获得控制权,要么没有,只有一个线程可以得到控制权。VMM和其他的VxD可以通过VMM服务来调整线程的执行优先级。例如,当一个硬中断发生时,VMM就会增加中断处理的执行优先级以便让它在尽可能短的时间内有更高的机会被调用。
副调度器通过主调度器提供的服务来给享有最高优先级的线程分配cpu时间。副调度器给每个线程一个时间片。当一个线程执行到它的时间片完结时,副调度器就增加下一个线程的优先级,这样它就会被主调度器选中并执行。

你可以从Walter Oney's Systems Programming for Windows 95Windows 95 DDK 文档里面得到关于这个问题的细节。

C++ Builder 研究--罗云彬VxD教程--虚拟设备驱动程序初步

在本教程里,我假定读者对诸如虚8086模式,调页,GDT,LDT,IDT之类的INTEL 80x86保护模式的操作比较熟悉。如果你不了解这些,那你要先在 http://developer.intel.com/design/pentium/manuals/阅读INTEL的文档。

内容:

Windows95是一个运行在最高级特权,第0层级别的多线程操作系统。所有的应用程序都运行在最低级特权,第3层级别上。这样就限制了

应用程序对系统的操作。它们不能使用cpu特权指令,不能直接访问I/O端口,等等。你对gdi32,kernal32和user32这三个大的系统组件一

定很熟悉。你肯定会认为这样重要的代码段一定是在第0层级别下运行的。但是实际上,它们和其他的应用程序一样,是在第三层级别下运行的。这就是说它们并不比Windows计算器,或者扫雷游戏有更多的权限。系统的控制实权掌握在虚拟级管理器(VMM)
虚拟设备驱动程序(VxD)手中。

这一切都是由dos引起的。在Window 3.x的时代,在市场上有很多成功的dos软件。Windows 3.x必须同时运行普通的Windows程序和dos程序,否则,它就会失去市场。

这个局面是很难处理的,因为dos程序和Windows程序有本质的不同。dos程序认为它们拥有系统的一切:键盘,cpu,内存,硬盘等等。dos程序不知道怎样和其他程序合作,而Windows程序(从那时候起)是可靠的多任务合作系统。也就是每个Windows程序都必须通过GetMessage或PeekMessage来和其他程序进行交流。

解决办法就是,在一个8086虚拟机上运行所有的dos程序,而在另一个叫做系统虚拟机的虚拟机上运行其他所有的Windows程序。Windows负责把cpu运算时间轮流的分给每个虚拟机。这样,在Windows 3.x里。Windows程序之间用的是合作多任务,而虚拟机之间用的是优先级多任务。

什么是一个虚拟机?一个虚拟机是被软件创建的一个假象。一个虚拟机和在它上面运行的程序交互,就像这个程序是在真正的机器上运行一样。这样,一个程序不知道也不关心自己是否是在虚拟机上运行。只要虚拟机准确的像一个真的机器一样响应程序,我们就可以把它当成一个真正的机器。

你可以把虚拟机这种实机器和软件之间的接口看作一种API。这种不寻常的API由中断,BIOS调用和I/O端口组成。如果Windows能够以某种方法完美的模拟这个API,那么在虚拟机上运行的程序就会表现的和它们在实际器上运行时完全一样。

这就是为什么会出现VMM和VxD的原因。为了协调和监视虚拟机(VMs),Windows需要一个程序来分配任务。这个程序就是虚拟机管理器(VMM)。

虚拟机管理器

VMM是一个32位的保护模式程序。它的主要任务是建立和维护一个支持虚拟机的框架。例如,它要创建,运行和结束一个虚拟机。VMM是众多的系统VxD程序之一,它被放在你的系统目录下的VMM32.VxD文件中。VMM本身是一个VxD程序,但它被当作一个监视其他VxD程序的监视器。让我们来看一下Windows95的启动次序:
  • 加载io.sys。
  • 执行config.sys和autoexec.bat。
  • 调用win.com。
  • win.com运行VMM32.VxD,VMM32.VxD实际上是个简单的dos的exe文件。
  • VMM32.VxD用xms驱动程序把VMM加载到扩展内存。
  • VMM初始化自身及其它的默认VxD。
  • VMM把机器转入到保护模式并创建系统虚拟机。
  • 最后被加载的虚拟外壳设备在系统虚拟机上通过运行krnl386.exe来启动Windows。
  • krnl386.exe加载所有的文件,最后是Windows95外壳。
  • 正如你所看到的,VMM是第一个被加载到内存的VxD程序。它创建系统虚拟机并初始化其他的VxD程序。它也为这些VxD程序提供许多服务。
    VMM和VxD的操作模式和真正的程序不同。在大多数时候,它们是潜伏的。当应用程序在系统中运行时,这些VxD程序没有被激活。当某些需要它们处理的中断/错误/事件发生时,它们才被唤醒。
    VMM是不可重入的。这意味着VxD程序必须使它们的访问和VMM服务同步。在有些情况下调用VMM服务是不安全的,比如VMM正在处理一个硬件中断。在这段时间内,VMM是不允许重进入的。作为一个VxD编写者,你必须对你的所作所为极度的小心。记住,你是在最高特权级别,第0层级别,如果你代码有错的话,谁也管不到。

    虚拟设备驱动程序

    虚拟设备驱动程序被简称为VxDx 代表各种设备的名字,如虚拟键盘驱动程序(vkd),虚拟鼠标驱动程序(vmd)等等。VxD程序是硬件成功初始化的途径。记得dos程序认为它们拥有系统的一切,当它们在虚拟机中运行时,Windows需要给它们一个实机器的替身。VxD程序就是这些替身。VxD程序通常虚拟一些硬件设备,所以,例如当一个dos程序认为它在同键盘通讯时,实际是虚拟键盘驱动程序在和dos程序通讯。一个VxD程序通常控制真正的硬件设备并对该设备在各个虚拟机之间的共享进行管理。
    尽管如此,并不是说每个VxD程序必须和一个硬件设备相连。虽然VxD程序是用来虚拟硬件设备的,但是我们也可以把VxD程序看作是在第0级别的dll。例如,如果你需要做一些只有在第0级别才能做的工作,你就可以编一个VxD程序来为你完成这个工作。这样,由于此VxD程序并没有虚拟任何设备,你就可以把它仅仅看作是你的程序的扩展。
    在我们更深入的讨论VxD和创建我们的VxD程序之前,让我先说一些有关于VxD的事情。
    • VxD程序是Windows 9x特有的,它在Windows NT下不能运行。所以如果你的程序是依靠VxD的,它就不能被移植到Windows NT平台上去。
    • VxD是系统中权力最大的实体。由于它们可以对系统作任何事情,所以它们是极度危险的。一个恶意的/错误的VxD程序可以毁掉整个系统。对于恶意的/错误的VxD程序没有任何的保护措施。
    • 通常的,不用VxD也有很多办法能达到你的目的。在采用VxD的解决办法之前一定要三思。如果用其他的可以在第三层级别实施的办法,就使用这个办法。
    Windows 95下有两种VxD:
    • 静态VxD
    • 动态VxD
    静态VxD是那些从系统启动就被加载,在系统关闭之前一直存在于内存中的VxD程序。这种VxD可以追溯至Windows 3.x的时代。动态VxD时只有Windows 9x下才有的。动态VxD程序可以在需要的时候被加载/卸载。这些程序大多数都是用来控制设置管理器和输入输出监视器加载的即插即用设备的。你可以在你的win32应用程序里加载或卸载动态VxD程序。

    VxD程序之间的通讯

    VxD程序,包括VMM,通过以下三种途径在相互之间进行通讯:
    • 控制消息
    • 服务API
    • 回调
    控制消息: 当有VMM感兴趣的事件发生时,它就向系统中所有载入的VxD程序发送控制消息。控制消息就像是第三层级别的Windows应用程序的消息。每个VxD程序都有一个接受和处理控制消息的函数,叫做设备控制函数。系统控制消息总共有50多个。控制消息不多的原因是系统中通常加载了很多VxD程序,而每个VxD程序在收到一个控制消息时都要进行处理。如果控制消息太多,就会导致系统停滞。所以控制消息只包括那些与虚拟机有关的重要消息,如:一个虚拟机被创建,被销毁等等。作为对系统控制消息的附加,一个VxD程序可以定义自己的控制消息,这些消息可以用来和那些能响应这些消息的VxD程序通讯。

    服务函数: 一个VxD程序,包括VMM在内,通常要导出一系列的被别的VxD程序调用的公共函数,这些函数被称为VxD服务。调用这些服务的机制和在第三层级别运行的的应用程序有很大的不同:每个导出VxD服务的VxD程序必须有一个唯一的ID,你可以从Microsoft得到一个这样的ID。这个ID是一个包含了一个VxD唯一的身份验证的16位的数字,例如:

      UNDEFINED_DEVICE_ID EQU 00000H
      VMM_DEVICE_ID EQU 00001H
      DEBUG_DEVICE_ID EQU 00002H
      VPICD_DEVICE_ID EQU 00003H
      VDMAD_DEVICE_ID EQU 00004H
      VTD_DEVICE_ID EQU 00005H
    你可以看到VMM的ID是1,VPICD的ID是3,等等。VMM用这些ID来找到导出所需VxD服务的VxD程序。当一个VxD程序导出VxD服务时,它把所有服务的地址存在一个表里面。所以,你还需要通过服务分支表里面服务的索引来找到你所要的服务。例如,如果你要调用第一个服务,GetVersion服务,你就要指定0(这个索引是从0开始的)。调用VxD服务的实机制包括中断20h,你的代码产生一个中断20h,并带有一个双字的值,这个值包含了设备ID和服务索引。例如,如果你要调用一个VxD程序导出的VxD服务,假设VxD程序设备ID是000DH,服务号码是1,那么代码应该是:
      int20h
      dd 000D0001h
    跟在中断20H后的双字的高字包含设备ID。低字是在服务列表中的索引。
    当20H中断执行时,VMM得到了控制权,并马上检测跟着的双字。然后它提出设备ID用来找到VxD程序,用服务索引来定位在那个VxD程序中的所要求的服务的地址。
    你可以看到这个操作时很费时的。VMM必须浪费很多时间来定位VxD程序和所要服务的地址,所以VMM作了个小小的弊 。当中断20H操作成功后,VMM抓取链接。这就是说,VMM用直接的服务调用来替代20H中断和它后面的双字。所以上面的20H中断代码片断就被改变成:
      call dword ptr [VxD_Service_Address]
    这个把戏是成功的,因为int 20h+dword加一个双字用6个字节,正好和call dword ptr结构相等。所以接下来的服务调用是快速而有效的。这个方法具有直接性,简洁性。在好的一方面,它减轻了VMM和VxD载入器的工作量,因为它们不用定位VxD中所有的服务,那些没有执行过的服务将会保持原样。再不那么好的一方面,一旦一个静态VxD程序导出的服务被调用,那么就不可能把这个静态的VxD程序卸载了。由于VMM把调用锁定到VxD服务的实际地址上,如果提供这个服务的VxD程序从内存中被卸载了,其他VxD程序调用这个服务时就会很快的因为调用无效的内存地址而导致系统崩溃。没有办法来消除抓取的链接。这个问题的结论是动态VxD不适合作为服务提供者。

    回调: 回调或者回调函数是在VxD程序中给其他的VxD程序调用的函数,不要把回调函数和VxD服务搞混淆了。回调函数不像服务那样是公共的,它们是私有函数,VxD在特定的情况下把它们的地址送给其他的VxD程序。例如,当一个VxD程序在处理一个硬件中断时,由于VMM是不可重入的,这个VxD程序不能使用VxD服务,否则会引起页面错误(重入VMM)。这个VxD程序可以把它自己的一个回调函数的地址给VMM,这样VMM就可以在能忍受页面错误时调用这个函数。回调函数的想法不是VxD独有的。许多Windows API都在用。最好的例子也许是窗口函数,你把窗口函数的地址填在WINDCLASS或WINDCLASSEX结构里并把它当作函数来调用RegisterClass或者RegisterClassEx。当有这个窗口的消息传来时,Windows就会调用你的窗口函数。另一个例子是窗口接管函数。你的程序把接管函数的地址送给Windows,这样当你感兴趣的事件发生时,Windows就会调用你的接管函数。
    上述三种方法是VxD之间通讯的,我们还要讲对V86,保护模式和win32应用程序的接口。在下一章里,我们要学习VxD对Win32应用程序的接口。

    C++ Builder 研究--罗云彬VxD教程--虚拟8086模式的内存管理

    下边我们用到的V86即指虚拟8086模式。 在以前的教程中,你学习了怎样模拟V86中断,但还有一个问题没有解决:在VxD和V86代码之间交换数据。我们将在此学习如何使用V86内存管理器来实现这个功能。在这里下载例子程序



    理论



    假如你的VxD和一些V86程序一起运行,如何传送大量数据到V86程序中或从V86程序中传送大量数据迟早是一个大问题。通过寄存器传送大量数据是不现实的。可能你的下一个想法是在ring0中分配一大块内存,并且通过一些寄存器传送其指针到V86程序,使其能访问这些数据。假如你这样做,可能会破坏你的系统,因为V86的地址定位方式需要segment:offset对,而不是线性定位方式。对这个问题,有很多解决的方法。然而,我选择了一个由V86内存管理器提供的一种简便的方法。



    如你能在你可使用的V86内存范围内找到一个空闲的内存块作为通讯缓冲区,这将解决其中的一个问题。然而,指针传送的问题依然存在。你可以通过V86内存管理器的服务来解决这两个问题。V86内存管理器是为V86应用管理内存的静态VxD。它还为V86应用提供EMS和XMS服务和为其他VxD提供API传送服务。API传送是一个从ring0拷贝数据到V86范围内的缓冲区并且传送V86缓冲区地址到V86代码的过程。V86内存管理器有一个在V86内存范围内的传送缓冲区,其含有VxD拷贝到V86内存范围内的数据,反之亦然。初始的缓冲区是4K。你以调用V86MMGR_Set_Mapping_Info来增加它的大小。



    现在你知道了传送缓冲区,我们如何拷入或拷出数据呢?这个问题通过调用两个服务来解决:V86MMGR_Allocate_Buffer和V86MMGR_Free_Buffer。



    V86MMGR_Allocate_Buffer从传送缓冲区分配一块内存并且从ring0拷贝一些数据到分配的V86缓冲区。V86MMGR_Free_Buffer正好相反:它从分配的V86内存块拷贝一些数据到ring0缓冲区并且释放由V86MMGR_Allocate_Buffer分配的内存块。



    记住,V86在内存管理器象堆栈一样管理被分配的缓冲区。这意味着分配/释放必须按先进后出的规则。所以如你调用了两次V86MMGR_Allocate_Buffer,第一个V86MMGR_Free_Buffer将释放由第二个V86MMGR_Allocate_Buffer调用而分配的缓冲区。



    我们来看一下V86MMGR_Allocate_Buffer的定义,它是一个基本寄存器传送参数的服务。



    EBX 当前VM的句柄

    EBP 指向当前VM的客户寄存器结构的指针

    ECX 从传送缓冲区分配的字节数 CARRY FLAG 进位标志位,如你不想从ring0缓冲区拷贝数据到分配的内存块就清零, 如你想从ring0缓冲区拷贝数据到分配的内存块就置1

    FS:ESI 指向ring0缓冲区的selector:offset指针,缓冲区中有要被拷贝到被分配的 缓冲区中的数据如果进位标志位被清零,则忽略它。

    假如调用成功,进位标志位被清零并且ECX包含在传送缓冲区中的字节数。这个数值应小于你要求的数值,所以你应保持这个数值,V86MMGR_Free_Buffer待会要用到它。EDI的高字包含被分配的内存块的V86段地址,偏移地址在在低字中。进位标志位当错误发生时被置位。



    V86MMGR_Free_Buffer和V86MMGR_Allocate_Buffer接受同样的参数。



    当你调用V86MMGR_Allocate_Buffer时,你在当前VM的V86内存范围内分配了一块内存,并且把其地址放到了EDI中。你可以使用这些服务传送数据到V86中断中或从V86中断中取得数据。



    在附加的API传送中,V86内存管理器也给其他VxDs提供了API映射服务。API映射服务是映射一些在扩展内存中的页到每个VM的V86内存范围。你可以使用V86MMGR_Map_Pages执行API映射。使用这个服务,页被映射到每个VM的同一线性地址空间上。如你仅仅工作在一个VM上,这将浪费地址空间。因为API映射比API传送要慢,所以你尽可能使用API传送方式。API映射仅仅使用在一些要访问同一线性地址空间并作用到所有VM的V86操作上。



    例子:



    这个例子演示了API传送方式,使用了int 21h的440Dh功能(从代码66h)。这个中断调用得到媒体ID,你的第一个固定磁盘的卷标号。



    ;---------------------------------------------------------------

    ; VxDLabel.asm

    ;---------------------------------------------------------------

    .386p

    include \masm\include\vmm.inc

    include \masm\include\vwin32.inc

    include \masm\include\v86mmgr.inc

    VxDName TEXTEQU

    ControlName TEXTEQU

    VxDMajorVersion TEXTEQU <1>

    VxDMinorVersion TEXTEQU <

    C++ Builder 研究--罗云彬VxD教程--客户寄存器结构

    我们将学习本教程中另外一个重要的结构,叫客户寄存器结构。在本文中,V86指虚拟8086模式。在这里下载例子程序



    理论



    VxDs与正常的win32/win16/DOS应用程序有很大不同。大多数情况下,当其他应用程序正常工作时,它们是休眠的。它们象一个监管者一样工作,其作用是监视ring-3应用程序并在其出错时改正它们。下面是其工作时的典型的情况:



    1、中断发生时

    2、VMM得到控制权时

    3、VMM存贮寄存器组的值时

    4、VMM服务于中断或调用其他VxDs完成此工作时

    5、VMM交还控制权给被中断的程序时



    在以上过程中令人感兴趣的是,VMM只有这一种方式能影响被中断的应用程序,即修改存储的寄存器映象。例如,VMM认为被中断的程序应该返回到另外一个地址,它就修改存储的寄存器映象中CS:IP的值,当这个程序被重新分派时,它将在新的CS:IP处开始执行。



    VMM在客户寄存器结构中存储中断点处的寄存器值。



    Client_Reg_Struc STRUC

    Client_EDI DD ?

    Client_ESI DD ?

    Client_EBP DD ?

    Client_res0 DD ?

    Client_EBX DD ?

    Client_EDX DD ?

    Client_ECX DD ?

    Client_EAX DD ?

    Client_Error DD ?

    Client_EIP DD ?

    Client_CS DW ?

    Client_res1 DW ?

    Client_EFlags DD ?

    Client_ESP DD ?

    Client_SS DW ?

    Client_res2 DW ?

    Client_ES DW ?

    Client_res3 DW ?

    Client_DS DW ?

    Client_res4 DW ?

    Client_FS DW ?

    Client_res5 DW ?

    Client_GS DW ?

    Client_res6 DW ?

    Client_Alt_EIP DD ?

    Client_Alt_CS DW ?

    Client_res7 DW ?

    Client_Alt_EFlags DD ?

    Client_Alt_ESP DD ?

    Client_Alt_SS DW ?

    Client_res8 DW ?

    Client_Alt_ES DW ?

    Client_res9 DW ?

    Client_Alt_DS DW ?

    Client_res10 DW ?

    Client_Alt_FS DW ?

    Client_res11 DW ?

    Client_Alt_GS DW ?

    Client_res12 DW ?

    Client_Reg_Struc ENDS



    你可以看到这个结构分为两个部分:Client_xxx和Client_Alt_xxx。在这稍作说明,在一个给定的VM中,可能有两个运行的线程:V86和保护模式。当V86程序运行时,假如一个中断产生,Client_xxx将包含V86程序的寄存器映象,Client_Alt_xxx将包含保护模式程序的寄存器映象。相应的,当保护模式程序运行时,假如一个中断产生,Client_xxx将包含保护模式程序的寄存器映象,Client_Alt_xxx将包含V86程序的寄存器映象。Client_resX被保留而没有使用。



    在查看过这个结构后,你可能有一问题:怎样改变寄存器中的一个字节,比如al?上面的结构仅仅描述了字和双字大小的寄存器组。不用担心,在vmm.inc找一找。那有两个为此附加的结构:Client_Word_Reg_Struc和Client_Byte_Reg_Struc。假如你想以字或字节大小来访问寄存器,根据你的需要转换Client_Reg_Struc到Client_Word_Reg_Struc或Client_Byte_Reg_Struc。



    下一个问题:我们如何得到一个指向客户寄存器结构的指针?



    这相当简单:一般地,当VMM调用我们的VxD时,把客户寄存器结构的地址放在ebp中。在这里的客户寄存器结构是当前VM的。你可以从VM的句柄中得到这个指针。记住,VM的句柄是VM控制块的线性地址。



    cb_s STRUC



    CB_VM_Status DD ?

    CB_High_Linear DD ?

    CB_Client_Pointer DD ?

    CB_VMID DD ?

    CB_Signature DD ?



    cb_s ENDS

    CB



    CB_Client_Pointer包含指向VM的客户寄存器结构的指针。例如:你可用下边的代码得到指向当前VM中的客户寄存器结构的指针:



    VMMCall Get_Cur_VM_Handle ; return the current VM handle in ebx

    assume ebx:ptr cb_s

    mov ebp,[ebx+CB_Client_Pointer] ; pointer to client reg struct



    现在我们了解了客户寄存器结构,我们可以用它来开始工作了。我们将使用客户寄存器结构去传送寄存器组的值到一个DOS中断中,也就是,int 21h,功能2h,显示一个字符。这个DOS服务把要显示的字符放在dl中。假如我们传送响铃字符(07h)到这个服务,将通过PC喇叭发出一声响。



    记住,int 21h是一个DOS服务,因而其在V86模式下是可用的,我们如何在VxD中调用一个V86中断?一个方法是使用Exec_Int服务。这个VMM服务把要调用的中断号放在eax中。它模拟指定的中断然后返回到调用的VM。然而,它必须在一个嵌套执行块中被调用。嵌套执行块被Begin_Nest_V86_Exec (或 Begin_Nest_Exec)和End_Nest_Exec包括起来。如果我们要调用int 21h功能2h,我们需要在嵌套执行块内转换Client_Byte_Reg_Struc结构的Client_ah和Client_Dl,然后把值21h放在eax中。当一切准备好了,就调用Exec_Int。



    例子:



    例子是一个动态VxD,它调用int 21h的功能2使PC喇叭发声。



    .386p

    include \masm\include\vmm.inc

    include \masm\include\vwin32.inc

    include \masm\include\v86mmgr.inc

    VxDName TEXTEQU

    ControlName TEXTEQU

    VxDMajorVersion TEXTEQU <1>

    VxDMinorVersion TEXTEQU <