VxD入门教程
1.背景知识为了看懂本篇所给的例子,需要C, 汇编及Windows 设备驱动程序的相关知识。
2.开发工具
需要 Micros以下采
用 VC++ 6.0 为例)
3.目的
利用以上所列的开发工具,编写一个动态装入的 VxD, 利用该VxD可以读取内存
地址0
处的中断向量表。
4.开始
第一步:
用 VC++ 新建一个工程,取名为 MiniVxd, 类型为 Win32 App 或 Console App
因为V
C++不提供直接生成
VxD的App Wizard,所以只好自己在以上两种App中选择一个,然后再进行手工修
改一下
,需要修改的内容
只有一处,即在 Link 的命令行上加上一个选项
/VXD
第二步:
新建一个空文件,在其中输入以下代码:
TITLE MINIVXD - By Ding Kai
.386p
include vmm.inc
MINIVXD_DYNAMIC EQU 1
; 定义设备驱动程序块 DDB, 由于是动态装入的,所以设备ID和装入顺序可以取
未定
义值.
DECLARE_VIRTUAL_DEVICE MINIVXD, 1, 0, MsgDispatch,UNDEFINED_DEVICE_ID,
Undefined_Init_Order, 0, 0
VXD_LOCKED_CODE_SEG
; 定义消息派遣表,本例中只需要一条消息,即W32_DEVICEIOCONTROL,用于从
Win32Ap
p中利用
; DeviceIoControl API函数同本VxD进行通讯.
BeginProc MsgDispatch
Control_Dispatch W32_DEVICEIOCONTROL, W32DeviceIoControl, sCall,
clc
ret
EndProc MsgDispatch
VXD_LOCKED_CODE_EN
END
将文件存盘,文件名取为 VxdStub.asm, 然后用下列命令编译该文件:
Aml -coff -W2 -DBLD_COFF -DIS_32 -c -Cx -DMASM6 -I\95DDK\INC32
VxdStub.asm
这将会生成 vxdstub.obj,将此OBJ文件加入到工程文件中。
第三步:
创建一个 C 语言文件,文件名取为 MiniVxd.c,在文件中输入以下代码:
#define WIN40SERVICES
#pragma warning (disable:4229)
#include
#define WANTVXDWRAPS
#pragma intrinsic(memcpy)
#pragma VxD_LOCKED_DATA_SEG
// 此处可以定义任何在VxD中需要的全局变量.
char AnyData[200];
#pragma VxD_LOCKED_CODE_SEG
///////////////////////////////////////////
DWORD __stdcall GetMemory( DWORD dwDDB, PDIOCPARAMETERS pD )
{
BYTE *p;
// 此处忽略了参数检查,实际应用中切不可如此 !!
p = (PBYTE)pD->lpvOutBuffer;
memcpy(p, 0, 1024); // 读取中断向量表,共 1024 字节
return 1024;
}
///////////////////////////////////////////////////////
/* Win32 interface */
///////////////////////////////////////////////////////
int __stdcall
W32DeviceIoControl( DWORD dwIoCtrlCode, /*ecx*/
DWORD dwDDB, /*ebx*/
DWORD pDIOCParams ) /*esi*/
{
PDIOCPARAMETERS pD;
pD = (PDIOCPARAMETERS)pDIOCParams;
if ( dwIoCtrlCode == DIOC_OPEN ||
dwIoCtrlCode == DIOC_CLOSEHANDLE)
return 0L;
// Init returned value
if (pD->lpcbBytesReturned)
*(DWORD *)pD->lpcbBytesReturned = 0;
if (dwIoCtrlCode==1) { // 1 = DeviceIoControl代码
GetMemory(dwDDB, pD);
}
else
return ERROR_NOT_SUPPORTED;
return 0;
}
将本文件加入到工程文件中.
第四步:
创建一个模块定义文件,此文件将要定义最终生成的VxD文件的段属性并输出设备
驱动
程序块,
输入下面的代码,并将文件存为 MiniVxd.def,同时将其加入到工程文件中.
VXD MINIVXD DYNAMIC
DESCRIPTION 'Written by Ding Kai'
SEGMENTS
_LTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LDATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TLS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_BSS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_ITEXT CLASS 'ICODE' DISCARDABLE
_IDATA CLASS 'ICODE' DISCARDABLE
_PTEXT CLASS 'PCODE' NONDISCARDABLE
_PDATA CLASS 'PDATA' NONDISCARDABLE SHARED
HEAPSIZE 10240
STACKSIZE 40960
EXPORTS
MINIVXD_DDB @1
当然,其中有些段在本程序中不存在,此处列出来是想将此文件做为一个模板以
供以
后使用。
第五步:
加入本工程文件需要的最后一个,VxdWarps.clb, 这个文件是DDK的LIB目录中.
第六步:
所需的四个文件已经全部加入完毕,它们是:
vxdstub.obj
minivxd.c
minivxd.def
vxdwarps.clb
在进行make之前请检查一下文件路径, 在 Include 中一定要包含 DDK 目录中的
INC3
2 目录.
确认无误后可以按 F7 键进行 Make 了。
结果会在 DEBUG 目录或 Release 目录 (依赖于工程文件的设置) 生成
MiniVxd.vxd
文件。
至此,一个小小的Vxd文件生成完毕, 可以在程序中用下面的代码与本VxD进行通
讯.
BYTE buffer[1024];
DeviceIoControl(hVxd, 1, NULL, NULL, buffer, 1024, NULL, 0);
结果,当前的中断向量表就会出现在 buffer 数组中。
本文不再提供完整的测试程序,需要的话请与作者联系
5.小结
本文用一个简单的示例介绍了如何在 Windows 95 下编写可动态安装的 VxD, 这
种方
法及用这种方法
编写的VxD在Windows 98 下同样适用。然而,在 Windows NT 不能采用这种方法
*****************************
**
*******************************************
标 题: VxD世界——虚拟的Windows世界
从事Windows系统编程的读者一定听说过VxD──这个在Windows(注:指Windows
3.1
/95/98,
非Windows NT)的世界里无所不能的超级武器。迄今为止,越来越多的人有了深
入了
解Windows的愿望,
这也使得VxD技术愈来愈受到重视。VxD,意即Virtual Something Driver,这里
的x就
是指Something。比如
说键盘驱动程序VKD、鼠标驱动程序VMD等。在许多人的脑海中,只有硬件开发者
才会
用到VxD,其实不
然,对于软件开发来说,VxD也是无所不能的。在Windows3.1下这点还不明显,
而在W
indows95中,随着
线程与局部化概念的引入,想控制系统的全部资源变得愈来愈困难。举个简单的
例子
,由于32位的Windows
应用程序拥有独立的4GB线性地址空间,在Windows 3.1下常用的Windows应用程
序之间
通信及共享变量的
方法不再完全适用了,想做些“出格”的事,比如说截获别的应用程序的消息,
变得
愈发困难了。就在您一
愁莫展的时候,采用VxD技术或许会令您“豁然开朗”──既然操作系统能控制
全部资
源,那么与操作系统
享有同样最高权限的VxD(或者干脆说作为操作系统一部分的VxD)也一定能够帮
助您
完成“出格行为”。
为什么VxD有如此神通?最根本的原因,是由于VxD运行在系统的Ring 0级,而用
SDK(
或现在大家熟知的
VC++、BC++、Borl and C++ Builder、VB、Delphi)开发出的应用程序运
行在
Ring 3级。在80x86
保护模式下,运行在Ring 0级的VxD拥有系统最高权限(操作系统也运行在
Ring 0级)
。
要想真正掌握VxD技术,您必须:
对80x86的保护模式有清楚的认识;
深入理解Windows的运行机制;
熟悉VxD本身的运行机制。
对于保护模式,目前国内市面上有一些参考书籍,希望您能硬着头皮看几遍。虽
说 不
能使人茅塞顿开,却也
对保护模式能有个比较清楚的认识。
让我们先来认识一下Windows本身吧。
虚拟的Windows世界
运行在Windows95下的几种应用程序:DOS应用程序、Win16应用程序、Win32应用
程序
。其中DOS 应用程
序大多运行在字符模式,而Win16/Win32 应用程序却是运行在图形模式下的。为
了使
这几种运作方式大相
径庭的应用程序能“和平共处”,Windows采用了虚拟机(Virtual machine)的
方式
。每个DOS应用程序
运行在一个独立的DOS VM中,使得这个愣头青似的DOS应用程序感到自己控制着
所有的
资源,它可以肆意
地在它认为的全屏上涂涂改改,在它以为自己独占的硬盘上读出写入(微软在设
计DO
S时怎么也想不到PC机
硬件的发展如此迅速)。而所有的Windows应用程序,不管是16位的,还是32位
的,都
运行在同一个System
VM中。这是由于Windows 应用程序守规矩得多,熟悉Windows编程的读者一定对
句柄
(Handle)这个概念
有非常深刻的印象。Windows应用程序的一举一动都是通过各种各样的句柄(
HWND、H
DC、HANDLE、
HMOUDLE等)来实现的,正是通过句柄这个中介,使得系统有机会在同一个
System VM
中协调多个Windows
应用程序。不信吗?好,让我们做个实验(我假定您已经安装了Softice for
Win95
),启动Windows95,启动
结束之后,不要运行任何DOS应用程序,按下Control+D激活Softice(在有的
Softic
e的缺省安装中,激活
Softice的热键是Alt+D),然后敲入指令:
:vm
VM Handle Status High Addr VM ID Client Regs
C39200E8 00001862 C3800000 00000001 C3449F70
从Softice的输出可以看到这时只有一个VM,它的ID是1。
按Control+D返回Windows桌面,这时,再运行几个Windows应用程序,不管是
16位的
,还是32位的。重复
刚才的操作,按下Control+D激活Softice,敲入指令VM。我们看到Softice输出
的结
果是一样的。也就是说尽
管有多个Windows应用程序在运行,可是系统中只存在一个VM,这个VM就是
System VM
。
按Control+D返回Windows桌面,这时,从“开始选单”中运行“MS-DOS方式”
,按
下Control+D激活
Softice,然后敲入指令:
:vm
VM Handle Status High Addr VM ID Client Regs
C8D200E8 00000802 C8C00000 00000002 C0F5FF70
C39200E8 00005A62 C3800000 00000001 C0F06F70
从Softice的输出可以看到这时系统中存在两个VM,其中一个就是我们刚才看到
的
System VM(VM ID=1),另外,又多了一个ID=2的VM,这个VM就是DOS VM,
也就是说刚才的那个“MS-DOS方式”就是运行在这个DOS VM中的。
按Control+D返回Windows桌面,从“开始选单”中再运行几个“MS-DOS方式”
,按下Control+D激活Softice,敲入VM指令,你会发现VM增多了,其数量等于
所
有的“MS-DOS方式”的数目加1。
现在您信了吧? 所有的DOS应用程序都是运行在各自的DOS VM中,而所有的
Windows 应用程序都运行在同一个System VM中。这是非常重要的概念。
******************************************
**
***************************
VxD世界——Windows的保护模式
一般来说,80x86(80386及其以后的各代CPU)可以在三种模式下运转:实模式
,
保护模式,V86模式。实模式就是古老的MS-DOS的运行环境。Win95只利用了两
种模式:保护模式和V86模式。
为什么要进入保护模式
保护模式有许多优越性。其中最最直接的好处就是:你的程序可以利用更多的内
存了
!
不要以为这是什么大不了的问题,我相信每一个曾在MS-DOS下写程序的人都有
一
个苦恼:怎样在程序中开个足够大的数组?动不动就会堆栈溢出,许多事都不能
做
了。不要怨Turbo C、MS Fortran、Turbo Pascal,它们也是心有余而力不足。
这些
烦恼都源自“你的程序是运行在实模式下的”。运行在实模式下的16位程序最多
只
能存取1M的内存。你也许会问:我的机器上不是有64M内存吗?是啊,如果说你
是
在MS-DOS下运行程序(或者说CPU运转在实模式下),那你只利用了1M内存,
其余的内存都“下岗”了,在这种情况下,你的386/486/586/PⅡ只相当于一个
跑得
快的8086。
但是保护模式给了我们一个惊喜。理论上,在保护模式下,CPU可以寻址4096M(
即4GB)内存。这就是说,只需把你的程序编译成32位的可执行程序(当然得借
助
32位编译器),你就可以在程序中充分利用内存了,这样做的直接结果是:你可
以
不用再为堆栈溢出或开不出5000×5000的数组而吃不下饭了。
正是4GB内存存取的实现,使得操作系统有了更加智能化的物质基础,多任务的
实
现才可以提到日程上来考虑了。
再深入一些
从硬件结构上说,386由三个寄存器CR0、CR1、CR2控制着CPU的运转。比如说
,CR0的第0位就是用来判断当前CPU是工作在保护模式还是实模式下。学过
8088/8086汇编语言的人一定熟悉AX、BX、CX、DX、SI、DI、SP、BP这些16位
的寄存器,在80386中,这些寄存器被扩展到了32位,即EAX、EBX、ECX、EDX
、ESI、EDI、ESP、EBP,如果CPU是运转在实模式下,那你只能利用这些32位寄
存器的前16位,而后面的16位就浪费了。
段的概念是我们理解保护模式的关键所在。在实模式下,段寄存器中存放着16位
的
段地址,这时,段地址是参与寻址的:把段地址左移4位,加上偏移地址,就是
20位
的物理地址了。在保护模式下,段寄存器中存放着16位的段选择器(Segment
Selector),这个值是不直接参与寻址的,而只是一个指向段描述表(
Segment
Descriptor Table)的索引。段描述表(Segment Descriptor Table)中存放着
段描
述符(Segment Descriptor)。段描述符中有关于段的描述,比如:段在内存中
的
位置、段的大小、段的类型(是数据段还是代码段)等等。
当CPU运行在保护模式下时,内存中往往有至少三张段描述表:全局描述表(
Global Descriptor Table,简称GDT)、局部描述表(Local Descriptor,简称
LDT)
、中断描述表(Interrupt Descriptor Table,简称IDT)。说到这里,我想提
醒读者
注意:记住GDT、LDT、IDT这三个词的含义,我们在后面会经常用到。
段描述表不可能超过64K(为什么?如果回答不上来,那就再看看前面的讲解)
,
每个段描述符(也就是段描述表中的一项)都是8byte长,所以说,每个段描述
表最
多只能包含8192个段描述符。
在今后的“走进VxD世界”中,我们将对段描述表、段描述符以及分页机制作进
一
步的讲解。
*************************
**
***************************
VxD世界——关于“描述表”
前面我们提到了在保护模式下,内存中往往至少有三张表:GDT,LDT,IDT。聪
明
的你可能要问:这几张表都在内存的什么地方呢?
图1 几个重要的寄存器的示意图
这三张表的位置是由三个寄存器记录的。这三个寄存器分别是:GDTR,LDTR,
IDTR。我们还要补充讲解一个寄存器,那就是TR(Task Register),这个寄存
器
与保护模式的任务管理有关。在386中,这几个寄存器如图1所示。
我们可以看到GDTR和IDTR都分别包含32位的物理地址和16位的权限级别,总共48
位。GDTR和IDTR中的32位地址是线性地址,不是段:偏移(Seg:Offset)的组合
形式,它表明了段开始的地方,如果说系统还没有启动内存分页管理机制的话,
那
这个线性地址就是物理地址,可是一旦系统启动了内存分页管理机制,GDTR和
IDTR中的32位线性地址就不再指向物理地址了。
CPU保留了GDT中的第一个描述符,任何试图通过GDT中的第一个描述符来访问内
存的操作都是非法的(还记得Win95下那可怕的蓝屏吗?)。如果指向GDT的段选
择器的Index域为0的话,那就指向空的段选择器。段选择器的示意如图2:
TI:Talbe Indicator
RPL:Requestor Privilege Level
图2 段选择器
这就是说,在保护模式下,far NULL指针是非法的。而在实模式下,NULL指针却
是有
意义的。
在内存中(当然是指CPU工作在保护模式下啦),GDT和IDT都只有一份,也就是
说,它们是全局的,任何一个任务改变了这两张表,都会对别的任务产生影响。
下面我们谈谈IDT、IDT中的每一项,也就是每一个描述符,都定义了256个中断
中
的一个。还记得实模式下MS-DOS环境中的中断向量表吧?在保护模式下,中断
描
述表IDT代替了中断向量表。虽说中断描述表可以容纳8192个中断描述符,可是
CPU能利用的只有处于前面的256个。所以中断描述表的长度限制应该是7FFh(23
×28-1)。
T=0:Interrupt gate描述符
1:Trap gate描述符
图3 Trap/Interrupt gate描述符
其实中断描述表(IDT)中可以有两种描述符:Interrupt gate描述符、Trap
gate描
述符。图3显示了Trap/Interrupt gate描述符的结构。这里提到了gate这个词,
一般
译作“门”。 “中断门”形像地直接表示了中断调用的过程:中断调用就像经
过一
扇门一样,这个门就是中断描述符,因为中断描述符中有DPL等权限盘查的标志
,
所以要想通过这扇门调用相应的中断服务程序是需要一定的资格的。
Trap和Interrupt gate非常相似,一般来说Trap gate是用来捕获系统异常,而
Interrupt gate用来响应中断。在具体的实现上,只有一点不同:Interrupt
gate会
将
IF置为0,这样可以屏蔽硬件中断。但是Trap gate不会改变IF的值。
寄存器LDTR中存有LDT的位置。与GDT不同的是,内存中可以有多份LDT。每一个
任务都可以有自己的LDT。从图1中我们可以看到,LDTR不包含“地址 / 权限”
位
。LDTR中所包含的是一个段选择器。这个段选择器指向GDT中的一项,但不是普
通的一项,这一项是指向某一LDT的描述符。你可能马上意识到,GDT中可以包含
指向不同LDT的描述符,不错,正是这样的。在Windows保护模式下,每一个MS-
DOS应用程序都有自己的LDT,而所有的Windows应用程序都共享一份LDT。是不
是想起些什么来了?对了,这与Windows中VM(虚拟机)的概念多少类似!
**********************************
**
***************************
VxD世界——分页机制
关于分页机制
在这种机制下,内存被划分为固定长度的“页面”。在保护模式下,“页面”是
受
到保护的,并可以被“虚拟”(说穿了,“虚拟”的东西就是说,本来没有的,
而
应用程序却误以为有。比如说,你只有64MB内存,而你的程序却傻乎乎地认为它
可
以有4GB的内存用,这就是“虚拟”)。
还记得前面讲过的保护模式下“段”的概念吗?在保护模式下,段的长度是可以
变
化的,而这里,“页面”的大小是不能变的,虽然它俩都受到保护模式的“保护
”
和“虚拟”。那么“页面”的大小是多少呢?答案是4KB。
假如现在有个32位的线性地址,我们来看一下怎么由这个线性地址获得物理地址
。
图1 线性地址通过页面映射为物理地址
请记住图1表示内容,因为它实在是太重要了。图中的CR3是指寄存器CR3的值。
如
果你装了Softice for Win95,那就按下Ctrl+D激活Softice,然后敲指令:
CPU;这时会看到CR3的值。
线性地址转换为物理地址的例子
假设现有线性地址8000DD88h,我们来看一下它到底指向何处物理地址。性线地
址
8000DD88h应作如下解析:
800 0D D88
Page Table Index Page Index Page Offset
(页面表索引) (页面索引) (页面偏移地址)
现在我们需要知道页面表目录的起始地址。于是我们查看寄存器CR3。假设寄存
器
CR3=891000h,那么891000h + 200h?4 = 891800h(想一想为什么要?4,答案
在文章后注释[1]处),现在我们要查看一下线性地址891800h处的值,在
Softice中
,敲如下的指令:
: d 891800
假设得到的是493227h,这个值与0FFFFF000h作“与”运算,就得到了我们要找
的Page Table的起始物理地址493000h,那么493000h + 4?0Dh = 49302Ch(想
一想为什么要?4,见注释[2]),然后我们要知道物理地址49302Ch处的值,但
是在
Softice中,我们无法直接获得存放在物理地址处的值,只能通过其对应的线性
地址
来查看。在Softice中,敲如下指令:
: phys 49302C
假设得到的是线性地址89302Ch(有时你会得到两个不同的值,我们取第一个值
。
想一想为什么会有两个不同的值?答案在注释[3]),现在我们查看一下线性地
址
89302Ch处的值,这也是物理地址49302Ch处的值。在Softice中,敲如下指令:
: d 89302C
假设得到的是3F5000h,这就是我们要找的Page(页面)的物理地址。然后,
3F5000h + 0D88h = 3F5D88h,这就是线性地址8000DD88h对应的物理地址。我
们想在Softice中查看物理地址3F5D88h处的值,那就得找到其相应的线性地址。
在
Softice中,敲如下指令:
: phys 3F5D88
假设我们得到线性地址7F5D88h,让我们看一下这个线性地址7F5D88h处的值。在
softice中,敲如下指令:
: d 7F5D88
让我们记下结果,然后再看一下线性地址8000DD88h处的值。
: d 8000DD88
你会发现线性地址7F5D88h与线性地址8000DD88h所指向的值是完全一样的。
一般来说,为了提高地址转换的效率,在CPU里都会有一个页面转换缓冲区(TLB
),里面存放着最近页面转换的信息。
MMU(Memory Management Unit)
从386开始,CPU都带有一个内存管理单元(Memory Management Unit,简称
MMU)。
MMU负责提供的功能:虚拟内存、重组段、进程分隔、页面保护、地址转换。
我们在上面的例子中所进行的线性地址到物理地址的转换就是由MMU来完成的。
模式转换
我们简单提一下保护模式与实模式之间的切换过程,有个概念就可以了。
由实模式切换到保护模式:检测一下当前CPU是否有能力运行于保护模式下;检
测
保护模式环境(DPMI或VCPI);建立IDT;建立GDT;禁止中断,包括NMI;加载
GDTR;设置IDTR;设置CR0的bit 0;清空指令队列;加载段寄存器;允许中断。
由保护模式切换到实模式:禁止中断,包括NMI;禁止页面转换;设置数据段寄
存
器的值为准实模式下的选择器;重置CR0的bit 0;清空指令队列;设置
IDTR=0000
:03FF;允许中断。
[1]因为在Page Table Directory中,每一个表项占4个字节。
[2]因为在Page Idex Table中,每一个表项占4个字节。
[3]因为线性地址到物理地址的映射关系不是一对一的,这就是说同一处物理地
址,
可能有多处线性地址指向它。
********************************
**
***************************
VxD世界——Win95 的线性地址分配
图1是一张描述Win95线性地址空间的图。下面我们对这张图加以详细说明。
图1 Windows 95线性地址分配图
0~4MB:
这部分在图上标的是DOS内存区,其实这是不确切的。我们知道16位的DOS应用程
序只能存取0~1MB的内存空间,那为什么还要把1MB~4MB的3MB内存也算作
DOS内存区呢?答案就在我们前面讲过的分页机制中。我们再温习一下:一个页
面
(Page)是4kB,一个页面表(Page Talbe)有1024个页面,一个页面表目录(
Page Table Directory)有1024个页面表。那么一个页面表目录项就可以映射
1024×
4k=4MB的线性地址。其实DOS只能利用到0MB~1MB的内存空间,那1MB~4MB
的地址空间留给谁了呢?关于这个问题,笔者曾经问过Karen Hazzah、Walter
Oney和Geoff Chappell,他们的回答是:这一部分是空的。Win95为了图省事,
就
把1MB~4MB的线性地址空间也当作DOS内存区,这样Win95在DOS VM之间切换
时,就可以页面表目录项(Page Table Directory Entry)为单位来进行。这样
虽说
浪费了3MB的地址,却换来了DOS VM切换的高效率。
那图1中标出的①又是指的什么呢?呵呵,Win95有趣得很,为了使系统、Win16
应
用程序能与DOS应用程序互相协作,于是在0~1MB之间,其实是紧挨着1MB的下
面,放了一个Win16全局堆(其实是Win16全局堆很小的一部分)。说到这里,笔
者
想起了一个深受大家喜爱的DOS下的编辑器Quick Edit 4.0,这个编辑器有一个
非常
有趣的功能:能与Windows共享剪贴板。当时我们猜想它一定用到了未公开的
DPMI
调用,现在从图1来看,肯定是①部分的Win16全局堆帮了它的忙啦。
同时请注意图1中还有一个②,这部分我们称作Win16全局堆的高端部分。为什么
要
在这里安置一个Win16的全局堆?它是用来作什么的呢?
这个问题的答案是:为了高效率地切换DOS VM。
每一个DOS VM在大于3GB的地址空间都有一个备份,Win95在DOS VM之间进行
切换时,只是简单切换一下页面表目录的第一项就可以了。所以说,如果一个
VxD
想访问某个DOS VM,没有必要一定要等到该DOS VM成为当前VM才能访问,它可
以直接去②访问那个DOS VM的备份。这个DOS VM备份的地址我们称作High-
linear address。
后面的文章中,我们将详细讲到如何在VxD中访问DOS VM。
***********************
**
***************************
VxD世界——Win95 内存揭秘
上期“走进VxD世界”中我们谈到了Win95中0~4MB线性内存空间分配情况。下面
我们接着讲余下的4MB~4GB线性内存空间的分配。
4MB~2GB
Win32应用程序的代码、数据和资源都存放在这段内存中了,这部分内存,对于
每
个Win32应用程序来说都是私有的。这里是最能体现保护模式分页机制作用的地
方
。两个Win32应用程序,对相同的线性地址(4MB~2GB范围内)进行读写,实际
上,它们是在对不同的物理地址进行读写。让我们算一下,这段内存对应多少个
Page Directory Entry。我们知道一个Page Directory Entry对应4MB线性内存
,那
4MB~2GB的内存就对应于1024 / 2 - 1 = 511个Page Directory Entry。也就
是说
Win95通过操纵这511个Page Directory Entry实现了Win32应用程序“独立”的
线性
地址空间。打个比方来说吧,现在有511个抽屉,上帝告诉A说:这些抽屉里都是
金
币,同时告诉B说:这些抽屉里都是银币。并规定只有上帝能打开抽屉。其实抽
屉里
可能只有一枚铜板,也可能什么都没有。这时,A想看看某个抽屉里到底是不是
金币
,于是上帝就背地里临时往那个抽屉中放一个金币,然后打开抽屉让A看,于是
A就
信了。这个伎俩同样作用于B,B也相信了那511个抽屉里都是他想要的银币。保
护
模式下的操作系统(这里当然指Win95)就相当于上帝,而被骗的A和B就相当于
运
行于保护模式下的Win32应用程序。那只操纵抽屉的上帝之手就是分页机制。
2GB~3GB
这部分内存空间我们称之为“应用程序共享内存区”,这里存放着Ring 3级应用
程
序需要共享的数据和代码。其中包括Win95系统DLL(如User32.dll,Kernel32.
dll等
)、内存映射文件、Win16应用程序以及DPMI调用分配的内存。
通过把要共享的数据映射到2GB~3GB之间的线性内存空间,可以实现Win32应用
程序间的数据共享。
Win16应用程序是需要共享线性地址空间的,这是Windows 3.1的历史遗留问题。
为
了使以前运行于Windows 3.1的Win16应用程序能在Win95下有同样的运行效果,
于
是Microsoft决定把Win16应用程序放到这段共享内存区来运行。应该说这是比较
合
理的决策:既省时省力,又保证了对上一代产品的良好兼容性。
再从分页机制的角度上来思考一下这段“共享内存区”是如何实现的。2GB~3GB
的线性空间对应着256个Page Directory Entry。无论谁在运行,Win95都不会改
变这
256个Page Directory Entry,也就是说2GB~3GB的线性地址空间对应的物理地
址
是一样的。这样,Win95什么也不用做就实现了2GB~3GB线性地址空间的内存共
享。
3GB~4GB
这部分内存空间称为“系统内存区”。只有Ring 0级的VMM和VxD可以访问这部分
内存空间。这部分内存也是共享的。虽说Ring 3的应用程序无法直接共享这部分
内
存空间(也就是说SDK中讲述的方法无法做到),但是我们还是称之为共享内存
区
,至少从分页机制的角度上说,对应于这部分内存的Page Directory Entry是不
变的
。其实Win32应用程序还是有办法访问这部分内存空间的。一般作法是,在VxD中
分
配一块内存,然后把指向那块内存的32位地址指针传给Win32应用程序,这样就
可
以在Win32应用程序中直接访问那块内存了。前面我们在讲0~4MB内存空间时,
提
到High-linear address,即DOS VM备份的地方,就在3GB~4GB内存空间中。
对于Windows应用程序开发者来说,实现内存共享有时是非常重要的。对于VxD开
发者来说,还应注意,对于经常需要存取的共享内存页面,必须调用VMM的
_LinPageLock服务进行锁定,否则会出现Out-Of-Context Memory Reference
。
********************
**
***************************
VxD世界——硬件虚拟与虚拟设备驱动程序
·硬件虚拟·
回忆一下,在实模式DOS下,实模式的MS-DOS应用程序调用runtime library中
的
fgets或—kbhit函数,这些函数就会直接通过软中断int 21h调用MS-DOS服务(
MS
-DOS服务会最终通过调用软中断int 16h调用BIOS服务),或者通过软中断
int 16h
调用BIOS服务。BIOS通过IN / OUT指令来直接操纵键盘或中断控制器。
我们通过下面这张图来理解一下什么是硬件虚拟的实现(见图1)。
两个不同的MS-DOS应用程序可能要同时访问键盘,它们都感到自己在直接操纵
着
硬件。其实从全局看,这两个MS-DOS应用程序对键盘的访问被VKD串行化了。
这两上MS-DOS应用程序其实是在操纵“虚拟的硬件”。
硬件虚拟的一个很关键的基础是:80386芯片的“port trapping”功能,这使得
VKD
可以捕获ring 3应用程序对键盘的访问。我们想起前面讲保护模式的内存管理时
,提
到“虚拟内存”的概念,其时“虚拟内存”的实现也是因为80386芯片具有“
paging trap ”功能。
·虚拟设备驱动程序·
Windows系统中,是VMM和VxDs实现了硬件虚拟(VMM本身就是一些VxD的集合
)。在Win95中,有两种VxD:static VxD(静态加载的VxD)和dynamic VxD(动
态加载的VxD)。在Windows 3.1中,只有一种VxD:static VxD。Static VxD的
加载
需要把VxD放在SYSTEM.INI或注册表中。比如说,你写了一个文件名为Fool.VxD
的
static VxD,那么你可以在SYSTEM.INI中的如下位置加上一句:
……
[386Enh]
device=Fool.VxD
……
或是在注册表的如下位置加入如下一项:
\HKEY—LOCAL—MACHINE\System\CurrentControlSet\services\VxD\Fool.VxD
这样Win95在启动时就会自动加载Fool.VxD。
相对于Windows 3.1而言,Win95又增加了dynamic VxD(动态加载的VxD)。在
Win95中,主要操纵dynamic VxD的是configuration Manager和Input/Output
Supervisor这两个功能模块(这两个模块自己却是static VxD)。
图1 键盘的虚拟
VKD:Virtual keyboard device VPICD:Virtual PIC device
*****************************
***************************
VxD世界——VxD文件格式
我们知道,MS-DOS下可执行文件.EXE是MZ格式,也就是说.EXE文件的前两个
字节是字符串“MZ”。而Windows 3.1的.EXE是NE(New Executable)格式,
Win95和WinNT的.EXE是PE(Portable Executable)格式,VxD则是LE(Linear
Executable)格式。但是有一点需要注意,在NE、PE和LE的头部,总嵌有一小段
DOS程序,它的作用是:当你在DOS下运行这几种.EXE文件时,它会提示你“This
program cannot be run in DOS mode”。
DEBUG TEST.VXD
LE文件格式最早出自OS/2 2.0。这种格式的文件可以同时包含16位和32位代码,
这
正是VxD需要的,因为VxD在加载的初始化阶段,需要进行一些实模式的操作,这
需要16位代码,而VxD的主要运行阶段是在32位环境中,这又需要32位代码,所
以
LE文件格式正好适合于VxD。
VxD不仅有16位和32位两种代码,而且,它把数据段和代码段搅和在一起,只是
通
过段前的标识来表明该段在运行时的特性。VxD之所以这样,是因为VxD所用的
Flat
mode的代码和Data selector有同样的基本地址和限制,这样当VxD想访问数据或
代
码时,用哪个段寄存器都可以。VxD中常用的Segment Class见表1。
在VxD中,LCODE、PCODE和PDATA段包含了主要的数据和代码。LCODE段所包
含的数据和代码必须总在内存中,我们在VxD中处理硬件中断的代码和相关数据
必
须位于LCODE段中,否则,在处理硬件中断时就会出现可怕的Page fault。
ICODE段包含的是VxD初始化时要完成的工作,当VxD完成初始后,ICODE段中的
代码和数据将被VMM抛弃。
RCODE中包含的是16位代码和数据,用作实模式初始化阶段。
SCODE中包含Static Code和Data,一般来说,SCODE对于动态加载的VxD尤其有
用。试想一下,如果可动态加载的VxD包含了一个回调函数,当你卸载这个VxD后
,又需要这个回调函数继续发生作用,那你就得把这个回调函数放到SCODE中。
再
者,如果你需要知道某个动态加载的VxD被加、卸载了几次,那可以在SCODE中放
个记数器,每次该VxD被加载时都把该记数器加一。
由于VxD的文件格式比较特殊,所以你必须使用可以产生LE格式的链接器(
Linker
)。如果你要开发Windows 3.1的VxD,那得用Windows 3.1 DDK带的链接器。但
是
如果开发Win95的VxD,那用MSVC 2.0及其以后版本的链接器就可以了。有一点需
要注意,MSVC 4.1的链接器由于存在一些小BUG,不能用于生成VxD。
表1
Segment Class 描 述
LCODE Page-locked code and data
PCODE Pageable code
PDATA Pageable data
ICODE Initialization-only code and data
SCODE Static code and data
RCODE Real-mode initialization
**********************************
***************************
VxD世界——VxD开发的利器SoftIce/VToolsD
SoftIce的安装
对于从事Windows系统开发的人来说,SoftIce是必不可少的利器。目前SoftIce
的最
新版本是3.24,有Win 95的版本,也有Win NT的版本。SoftIce 3.24 for Win
95可以
从国内的一些FTP站下载。
在SoftIce的安装过程中,有一步是很重要的,那就是对显卡的测试。新版本的
SoftIce提供对更多种显卡的支持。如果说显卡不被SoftIce支持,那就意味着
SoftIce将无法正常工作。
在安装过程中,如果Test通不过,那就试着选一下Universal Video Driver或者
Use
monochrome card/monitor。如果还通不过的话,那就在Display Adapter
Selection
列表框中选择Stand VGA再试。如果还不行的话,那就试着去下载一份new
SoftIce
video driver(在国内的FTP站上也可以找到),如果运气好的话,你的显卡会
被
SoftIce支持。
安装过程结束后,重新启动计算机,按下Ctrl+D进入SoftIce,如果SoftIce能
正常
工作,那我们的安装就成功了。你可以敲入help来看一下SoftIce的简要帮助,
或者
,按下Ctrl+D返回Windows桌面。
VToolsD的安装
在VToolsD出世之前,VxD的开发者面对DDK浩如烟海的古怪的asm代码,能忍受下
来的人不多。VToolsD就是把幸福带给VxD开发者的天使。就凭这一点,VToolsD
就
令VxD开发者趋之若鹜。VToolsD是Vireo公司的作品,目前国内的FTP站上有
VToolsD 2.03 for Win95下载(如果运气好的话,还可以找到2.04、2.05b)。
从国
内FTP站下载的VToolsD在安装时有一些需要注意的地方。
首先,在安装过程中会遇到对话框,询问你是否需要MASM6.11c。VToolsD并不包
含MASM 6.11c,但是,可以从DDK for Win95中找到MASM 6.11c。当你的程序中
需要嵌入汇编代码时,MASM 6.11c就不可缺少了(如果你只用C语言开发VxD,那
就用不着了)。
图1所示是对调试器的选择,请选择SoftIce。
图1 调制器的选择
在SoftIce的路径设置对话筐中,请选择SoftIce的Util16子目录。
再者,在安装过程中,图2所示的两个选项是不能选的,否则,安装过程将无法
正常结
束。
图2 不要选择的两个选项
在VToolsD安装完成之后,重新启动计算机,然后,进到VToolsD安装目录下的
examples\c\simple\下,敲如下的命令:
nmake-f simple.mak
如果一切正常的话,将生成simple.vxd。
如果不行的话,那就在DOS提示符下敲set命令,检查一下环境变量的设置。下面
是
我的系统中相应环境变量的设置,你需要根据自己系统的实际情况调整一下路径
设置
:
VTOOLSD=E:\VTD95
PATH= %PAH%;C:\MSDEV\BIN; E:\VTD95\BIN
INCLUDE=C:\MSDEV\INCLUDE; E:\VTD95\INCLUDE
LIB=C:\MSDEV\LIB;E:\VTD95\LIB
把这些环境变量的设置加到autoexec.bat中去,以使其每次开机都自动设置。
*********************************
**
*******************************
VxD世界——用VToolsD开发一个简单的VxD
这一次,我们讲一下如何用VToolsD开发一个最简单的VxD,以及用SoftIce进行
源程序
级的调试。
VToolsD的使用
在VtoolsD中,有一个最重要的VxD开发工具:QuickVxD。QuickVxD可以为我们自
动生成VxD源程序框架,而且QuickVxD提供了许多VxD的特性选项,例如可以选择
要生成的VxD是动态加载的或是静态加载的,要使用的编程语言是C还是C++等
等。
我们要利用QuickVxD自动生成的是一个可动态加载的、基于C语言的VxD框架。之
所以选用动态加载的VxD,是为了调试VxD的方便。每次修改代码,重新编译连接
之后,要使VxD重新生效,如果采用静态加载的VxD,那就不得不重新启动电脑,
而若采用了动态加载的VxD,那只须使用VToolsD带的另一个开发工具VxDLoad就
可以卸出或重新加载内存中的VxD。之所以采用C语言而不是C++,是因为其简
洁
易懂。请按照如图1~图4进行选择。按下Generate Now按钮,我们就获得了动态
加
载的、基于C语言的VxD的源程序。
如果您是按照上一篇文章中讲过的VToolsD的编译环境设置系统,那我们就可以
编
译刚才生成的这个最简单的VxD了。在DOS提示符下输入指令:
nmake -f myfirst.mak
看一下当前目录下是否生成了myfirst.vxd,如果有,那我们下面准备对这个
VxD进行
源程序级的调试。如果没有,那么很可能是您的编译环境没有正确配置,请找来
上
一篇文章好好读读。
用VxDLoad加载myfirst.vxd(见图5)
按下Load按钮,会出现VxD load successfully消息框。
用SoftIce调试VxD
对于SoftIce选单作如下选择:
(1)File→Open Module选择我们刚才生成的myfirst.vxd。
(2)Module→Translate,如果Symbol Loader提示无法加载一些asm文件,那就
跳过
所有的asm文件。
(3)Module→Load。
按下Ctrl+D,进入SoftIce运行环境中(如果您还没有按照上一篇文章中安装
SoftIce的话,那就无法再进行下面的测试)。输入如下指令:
:file ?
myfirst.c
:file myfirst.c
这时,在SoftIce中,您将会看到myfirst.c的源程序。
图1选项页面之一
图2选项页面之二
图3选项页面之三
图4可以生成VxD源程序了
图5用VxD Load加载myfirst.vxd
*********************************
**
*******************************
VxD世界——VxD的结构
在上一次“走进VxD”世界中,我们用VToolsD生成了一个最简单的可以动态加载
的
VxD——MYFIST程序。我们以这个例子为基础,不断地丰富其功能,并以此讲解
一些V
xD的基本技术。
下面是MYFIRST主要组成之一:MYFIRST.C。
// MYFIRST.c - main module for VxD MYFIRST
#define DEVICE_MAIN
#include "myfirst.h"
#undef DEVICE_MAIN
Declare_Virtual_Device(MYFIRST)
VOID_cdecl V86_Api_Handler(VMHANDLE hVM, PCLIENT_STRUCT pcrs) { }
VOID _cdecl PM_Api_Handler(VMHANDLE hVM, PCLIENT_STRUCT pcrs) { }
DefineControlHandler(SYS_DYNAMIC_DEVICE_INIT,
OnSysDynamicDeviceInit);
DefineControlHandler(SYS_DYNAMIC_DEVICE_EXIT,
OnSysDynamicDeviceExit);
DefineControlHandler(W32_DEVICEIOCONTROL, OnW32Deviceiocontrol);
BOOL _cdecl ControlDispatcher(
DWORD dwControlMessage,
DWORD EBX, DWORD EDX,
DWORD ESI, DWORD EDI, DWORD ECX)
{ START_CONTROL_DISPATCH
ON_SYS_DYNAMIC_DEVICE_INIT(OnSysDynamicDeviceInit);
ON_SYS_DYNAMIC_DEVICE_EXIT(OnSysDynamicDeviceExit);
ON_W32_DEVICEIOCONTROL(OnW32Deviceiocontrol);
END_CONTROL_DISPATCH
return TRUE;}
BOOL OnSysDynamicDeviceInit()
{ return TRUE; }
BOOL OnSysDynamicDeviceExit()
{ return TRUE; }
DWORD OnW32Deviceiocontrol(PIOCTLPARAMS p)
{ return 0; }
记得在前面的文章中提到过,VxD可以与应用程序实现相互通信。我们在用
QuickVxD生成这个例子时,又选中了支持动态加载和Real/V86 Mode API及
Protected Mode API等选项。上面程序中,函数V86_Api_Handler用来实现VxD与
16位的DOS应用程序通信,函数PM_Api_Handler用来实现VxD与16位的Windows
应用程序或DOS-Extended(DPMI)应用程序通信,函数OnW32Deviceiocontrol用
来
实现VxD与32位的Windows应用程序通信。函数OnSysDynamicDeviceInit和
OnSysDynamicDeviceExit自然是用来控制VxD动态加载和卸载啦。
上面的代码中有两个宏DefineControlHandler和ControlDispatcher,用来把这
些函数
与VxD的消息机制联系起来。好像我们搞清楚了,不,再仔细看一下,宏
DefineControlHandler和ControlDispatcher都只是定义了三个函数
OnW32Deviceiocontrol、OnSysDynamicDeviceInit和OnSysDynamicDeviceExit的
消
息映射关系。我们很自然地想到,函数V86_Api_Handler和PM_Api_Handler呢
,为什么能肯定VxD一定用这两个函数与16位应用程序通信呢?
让我们在VToolsD的include子目录下找一找,我们会发现VToolsC.h中有这两个
函数
的定义。下面的代码摘自VToolsC.h。
#define Declare_Virtual_Device_Ex(VName, RefData) \
extern _C_ void _cdecl V86_Api_Handler(VMHANDLE hVM, PCLIENT_
STRUCT pRegs); \
extern _C_ void _cdecl PM_Api_Handler(VMHANDLE hVM, PCLIENT_
STRUCT pRegs); \
extern _C_ void (?VXD_SERVICE_TABLE[])(); \
_EXC_ DDB The_DDB = { 0, DDK_VERSION, VName##_DeviceID, VName
##_Major, \
VName##_Minor, 0, {′ ′,′ ′,′ ′,′ ′,′ ′,′ ′,′ ′,′ ′},
\
VName##_Init_Order, (DWORD) LocalControlDispatcher, \
(DWORD) LocalV86handler, \
(DWORD) LocalPMhandler, 0, 0, RefData, (DWORD) VXD_SERVICE_TABLE, \
0, \
0, \
_SIG_} ;
// This is the standard macro for declaring a DDB, using all default
value
s.
#define Declare_Virtual_Device(VName) Declare_Virtual_Device_
Ex(VName,0)
从上面的代码中,我们可以看到,函数V86_Api_Handler和PM_Api_Handler被
宏Declare_Virtual_Device声明已在DDB(Device Descriptor Block)中,自然
不
用再在MYFIRST.C中进行消息映射了。
****************************************
**
*******************************
VxD世界__VxD的设备描述块与VxD API
VxD设备描述块
用汇编语言描述MYFIRST.VxD的设备描述块(DDB Device Descriptor Block)如下
(
其实,如果是用DDK来开发VxD,那我们在每个VxD的源程序中都会见到这些代码
,只是VToolsD替我们封装了这些费解的东西):
Declare_Virtual_Device MYFIRST,1,0,MYFIRST_Control,MYFIRST_
Device_ID,MYFIRST_Init_Order,MYFIRST_V86_API_Handler,
MYFIRST_PM_API_Handler
对于DDB的8个入口来说,只有前面4个是必须的,后面4个的缺省值为0,如果我
们
的MYFIRST.VxD不输出V86 API,那么上面的代码应这样写:
Declare_Virtual_Device MYFIRST,1,0,MYFIRST_Control,MYFIRST_
Device_ID,MYFIRST_Init_Order,,MYFIRST_PM_API_Handler
一般来说,MYFIRST_Init_Order是可以设为缺省值0的,因为我们一般不需要特
殊的初始化顺序。
你一定会奇怪MYFIRST_Control是怎么回事。读一下下面的代码,大概就明白了
。
BeginProc MYFIRST_Control
Begin_Control_Dispatch MYFIRST_Control
Control_Dispatch Sys_Dynamic_Device_Init, OnSysDynamicDeviceInit
Control_Dispatch Sys_Dynamic_Device_Exit, OnSysDynamicDeviceExit
.........
End_Control_Dispatch MYFIRST_Control
EndProc MYFIRST_Control
对比一下VToolsD为我们生成的C程序:
BOOL _cdecl ControlDispatcher(
DWORD dwControlMessage,
DWORD EBX,DWORD EDX,
DWORD ESI, DWORD EDI,
DWORD ECX)
{ START_CONTROL_DISPATCH
ON_SYS_DYNAMIC_DEVICE_INIT(OnSysDynamicDeviceInit);
ON_SYS_DYNAMIC_DEVICE_EXIT(OnSysDynamicDeviceExit);
END_CONTROL_DISPATCH
return TRUE;}
Windows是基于消息机制的操作系统,这一点在VxD中也体现了出来。MYFIRST_
Control就是接收Windows消息的入口点。Windows发给MYFIRST_Control的消息
与发给Windows应用程序的消息不完全一样,前者包含了一些系统信息。MYFIRST
_Control在收到消息后,调用相应的控制过程。
VxD API
在前面的文章中,我们说MYFIRST.VxD将支持Real/V86 Mode API及Protected
Mode API,这使得MYFIRST.VxD可以与V86应用程序或Win16应用程序通信。
MYFIRST.VxD输出的V86 API和PM API就是
VOID _cdecl V86_Api_Handler(VMHANDLE hVM, PCLIENT_STRUCT pcrs);
VOID _cdecl PM_Api_Handler(VMHANDLE hVM, PCLIENT_STRUCT pcrs);
一个问题很快就摆在我们面前:如何在我们的应用程序中调用到这两个API?
读一下这段代码:
DWORD NEAR PASCAL GetAPIEntry(WORD VxD_ID)
{DWORD Entry_Point;
_asm{
mov AX, 1684h
mov BX, WORD PTR SS: [VxD_ID]
sub DI, DI
mov ES, DI
int 2Fh
mov WORD PTR SS: [Entry_Point][0], DI
mov WORD PTR SS: [Entry_Point][2], ES
} return Entry_Point;}
这段代码可以用在MS_DOS应用程序或是Win16应用程序中,函数GetAPIEntry将分
别返回V86_Api_Handler的地址或PM_Api_Handler的地址。
等一下,函数GetAPIEntry的入口参数VxD_ID是怎么回事?嗯,问得好。如果你
一
直在读我的文章,那你会发现我们在前面有一个失误:在用QuickVxD生成
MYFIRST.VxD的源程序时,把MYFIRST.VxD的DeviceID置成了UNDEFINED_
DEVICE_ID。通过在VToolsD\include\Vmm.h中查找,可以看到:
#define UNDEFINED_DEVICE_ID 0x00000
也就是说所有UNDEFINED_DEVICE_ID的VxD的DeviceID都置成了0。如果我们
向函数GetAPIEntry传递MYFIRST_DeviceID,那我们很可能无法获得
MYFIRST.VxD中的API的地址,因为我们的DeviceID不是惟一的,Windows无法在
众多DeviceID为0的VxD中找到我们的MYFIRST.VxD。那怎么办呢?
解决方案有两个:
方案一:
再用QuickVxD重新生成MYFIRST.VxD的源程序。记着在Device Parameters页中填
写Device ID为某个值,这个值尽量大一些,因为比较小的DeviceID都让
Microsoft或
是别的硬件开发商注册了(注册是需要银子的),为了保证不与系统中现存的
VxD
的DeviceID发生冲突,我们只好把DeviceID设得大一些,比如说0xAAAA。
方案二:
编辑一下MYFIRST.H,把MYFIRST_DeviceID改了,改过之后的MYFIRST.h如下
:
#include 〈vtoolsc.h〉
#define MYFIRST_Major 1
#define MYFIRST_Minor 0
#define MYFIRST_DeviceID 0xAAAA
#define MYFIRST_Init_Order UNDEFINED_INIT_ORDER
好了,我们已经准备好与我们的MYFIRST.VxD通信了。
评论
查看更多