DOS程序员参考手册内容分析

DOS程序员参考手册内容分析

304页
第12章设备驱动程序
计算机早期的程序员直接在硬件水平上编写各类程序。每个程序都必须直接涉及卡
的读写器、打印机和其它一些与计算机相联系的设备。于是程序员不得不掌握有关处理错
误类型、处理正确输入和输出等方面的各种知识。
随着计算机的发展,程序员发现这种重复工作所花的时间完全是没有必要的。外部设
备的处理程序渐渐成为加到程序上的标准项目。不久,这些处理程序被选进了原始操作系
统之中,在这个系统内,所有程序都可以使用设备处理程序或驱动程序的相同配置。
在最早的操作系统中,不同的设备驱动程序编码组成了该系统的整个部分,并以复杂
的方式与系统其它部分之间发挥作用。要使设备驱动程序更加独立,系统程序员可在启动
过程中,把设备驱动程序作为必要的部分来装入。
尽管大多数重要的操作系统都有一定的灵活性,但只有DOS才为用户提供了最大程
度的灵活性,可方便地安装各类驱动程序。许多微机操作系统都需要冗长的修补来完成提
前编写的驱动程序和系统配置文件所能完成的事情。
对于大多数用DOS进行工作的人来说,他们与驱动程序的唯一联系就是从发布磁盘
来安装它们,并在CONFIG.SYS文件中创建所需项(稍后将介绍这些页)。用户则只需遵
循程序所给出的指导,一行一行地进行修改。在有些系统中,甚至不需要这种联系,因为安
装程序可以自己完成这种改变。
最后大多数程序员开始对他们自己的技术相当信赖并决定编写一个设备驱动程序。
富有经验的汇编语言程序员发现这个任务相对较容易—他们只需遵循一个公式般的约
定。如果能正确遵守此约定,驱动程序就能正常运行。许多程序员没有成功,因为他们没
有坚持严格的规则,即执行驱动程序必须做的事情,以及必须以某种方式使用驱动程序。
本章的目的就是要说明编写设备驱动程序的方法。
为了获得最高速度和编程的便利性,在PC机上的设备驱动程序通常用汇编语言来
编写。尽管设备驱动程序的一些部分能用C语言编写,但在获得正确结构及减少额外开
销方面会产生问题。设备驱动程序有严格的结构,而用C编译的模型只能提供功能性的
服务。若使用C来为驱动程序建立功能,就必须从汇编语言部分开始,该部分获得初始控
制,然后调用所需的C例程。不能调用C的库函数,因为其中的许多都是DOS功能(不允
许驱动程序调用DOS服务。本章的“驱动程序初始化”一节将更多地介绍这方面的内容)。
用汇编语言编写整个驱动程序有3个理由:
·设备驱动程序是所有的设备访问的核心,所以必须把它紧凑地编程以节约执行时
间和内存空间。

305页
. 驱动程序布局是严格定义的;只有汇编语言才能提供所需的布局控制。
. 必须在特定的时间去操纵特定的CPU寄存器,从C中很难做到这一点。
用C来编写设备驱动程序在UNIX世界中是很普遍的,但接口要求却不一样。汇编
语言前端链接并控制了操作系统。用C编写驱动程序则可能很好笑(并且是失败的)。
在建立设备驱动程序之前,应该知道设备驱动程序的使用方法和它的工作方式,然后
才能建立该程序。
知道了驱动程序,就要建立驱动程序的外壳程序,在其中增加另外的程序来为真实设
备编写真实驱动程序。首先让我们看看驱动程序的类型以及它们的工作方式。
12.1 驱动程序的类型
设备驱动程序有两个基本类型——字符设备和块设备,二者是根本不同的。现在先看
看它们的差异。
12.1.1字符设备驱动程序
字符设备是面向字节的设备,如打印机端口或串行端口。所有与设备的通信都发生在
一个个字符的基础之上。
字符设备的输入和输出在两个基本模式之一内进行:处理过的和原始模式。在处理过
的模式中,DOS从驱动程序每次要求一个字符并在内部缓冲输入。特殊的键组合如Ctrl-
C和回车键由DOS处理。在原始模式中,DOS不缓冲数据,也不寻找和对Ctrl-C或回车
键产生反应。但DOS请求输入固定数目的字符,该请求直接传递给驱动程序;回答则是由
驱动程序所读取的字符组成的。
字符设备名(如CON、AUX和LPT)能达到8个字符那么长(像文件名一样)。8个字
符的限制是因为设备头的驱动程序名只有8个字符(“设备头”一节将讨论头)。这个限制
在DOS中是故意设计成这样的,目的是使用相同的I/O例程来处理命名的文件和设备。
它也使访问与系统中已存在的驱动程序具有相同名字的文件成为不可能;如果遇上了与
设备同名的文件,就由这些例程来访问驱动程序。
12.1.2块设备驱动程序
块设备处理磁带和磁盘上的数据块。每次访问块设备都把数据以合适的块大小进行
传递。在块设备中,没有与字符设备驱动程序那样的处理过的模式和原始模式。
驱动器字母分配给了块设备,块设备变成了一个或多个系统逻辑驱动器(如A、B和
c)。单个块设备驱动程序能操纵多个硬件单元或把一个硬件单元映射成多个逻辑驱动
器。每个逻辑驱动器都有基本文件系统这样的结构,它包含一个FAT(文件分配表)和根
目录(这些结构的其它情况,请看第2章和第8章)。
306页
12.2设备驱动程序的工作方式
应用程序调用DOS的Int 21h来执行任何I/O功能时,设备驱动程序会涉及几乎每
种情况(除非这些系统功能用作扩展出错处理)。让我们看一个实例,在其中我们要向磁盘
上的文件里输入内容。
不论是把文件写操作当作DOS调用,还是使用函数库中的函数,应用程序都要设置
寄存器,并调用Int 21h(DOS服务例程)来操纵磁盘I/O。服务例程获得控制时,它反过来
设置并调用Int 26h(绝对的磁盘写)。 Int 26h在内存的保留区域设立请求头(驱动程序的
命令缓冲区),并调用操纵磁盘的设备驱动程序策略例程。该例程只是保存请求头的地址,
并把控制返回到中断处理程序。
然后,DOS调用驱动程序的中断部分(它的名字,像策略例程一样并不反映它的功
能)。这个部分读请求头。并确定请求的内容。接着,它把控制传递给合适的内部例程,并
调用BIOS磁盘写功能—Int 13h来执行磁盘写操作。完成磁盘写操作后,控制就从链中
返回到应用程序,并调整状态码来反映每个调用例程所希望的状态。
图12.1显示了这些事件的系列。每一步都涉及把控制传递给更低水平的例程,直到
发生磁盘写操作。
图12.1请求磁盘写
307页
这些步骤都代表了磁盘的单一操作;这样对驱动程序的调用可能很多。若在文件调用
中使用高级语言源代码,可能需要对磁盘进行另外的访问来在磁盘上阅读FAT、分配空
间并更新参数。这样的话,驱动程序可能极忙。
尽管这个例子很复杂,但可以用对BIOS的调用来操纵它。不必担心“地下又黑又暗”
的接口细节(硬件操作)。在每个PC机和兼容机上,都假定BIOS能够使所有设备看起来
像一套标准的设备。但加上一个自定义的片段又怎么样呢?图12.2显示了所发生的事情。
驱动程序必须直接操纵新的硬件,因为没有BIOS来处理接口的细节。
图12.2常规设备驱动程序
向系统添加一块插板,即加上一项新功能的话,必须加上一个新的设备驱动程序。如
加上cD-ROM驱动器、鼠标、局部网络卡或音乐合成器一类的硬件,则加上的硬件就是设
计原始PC系统软件时未曾考虑过的设备。MS-DOS没有软件来处理鼠标设备。与鼠标一
起所带的驱动程序必须直接与硬件打交道。复杂性便从这里开始。要把这个硬件与系统
连接起来,就需要驱动程序。
通过驱动程序进行磁盘访问的实例(参看图12.1)隐藏了一个重要事实:BIOS已经
注意到硬件的细节,并处理所有的定时、位处理等操作。对于一个自定义的添加设备驱动
程序,必须直接处理有关硬件的细节问题。
向系统添加硬件的新片段,并为它编写驱动程序时,必须考虑到所有的接口细节。例
如,如果是添加一个模拟数字转换器,以便读取某个仪器的数据,就必须在硬件水平上服
务于芯片。如果要观察定时限制或处理其它问题就必须了解它们。
有关特殊插板的工作接口的介绍超出了本书的范围。要编写好一个成功的接口,必须

308页
具有想运行的硬件的详细知识。有一点考虑不到也是不可接受的。本书提供了一个框架
能让你的驱动程序运行起来,利用它,既可以为加进系统中的硬件新片段编写驱动程序
也可以为已存在的硬件编写驱动程序。
12.3设备驱动程序的结构
设备驱动程序由三个部分组成:设备头、策略例程和中断例程。在DOS 2.0版中驱
动程序必须是没有起始地点(ORG0,或没有语句)的内存映象(或COM程序)。而且它必
须编写成一个FAR程序。EXE2BIN程序可把汇编过的驱动程序变成一个映象文件,而且
系统在引导操作过程中安装该映象。常规情况下,驱动程序常带有扩展名.SYS(或者有时
是.BIN)。文件扩展名变成.SYS,可防止人们偶尔把驱动程序当作一个程序来执行。
在DOS3.0版和更高的版本中,驱动程序以EXE格式而成为目标文件。操作系统同
样地能正确安装它们。但要保持与DOS 2.0版的向下兼容性,大多数编写驱动程序的人
都用coM格式来工作(DOS 1.0版没有准备可安装的驱动程序)。本章的实例都是COM
类型的驱动程序。
本节讨论驱动程序的结构。首先看看设备头——驱动程序的最重要部分。
表12.1驱动程序属性字
位 意 义
FEDCBA98 76543210
........ .......1 标准输入设备
........ .......0 非标准输入设备
........ ......1. 字符设备:标准输出设备
块设备:能处理32位扇区数(V4独有)
........ ......0. 字符设备:非标准输出设备
块设备:不能处理32位扇区数(V4独有)
........ .....1.. NUL设备
........ .....0.. 非NUL设备
........ ....1... 时钟设备
........ ....0... 非时钟设备
........ ...1.... 驱动程序服务Int 29h
.....000 000..... 在V3.2之前保留(设置成0)
........ ..0..... 在V3.2和更高版本中保留(设置成0)
........ .1...... 驱动程序支持通用的IOCTL(V3.2和更高版本)
........ .0...... 驱动程序不支持通用的IOCTL(V3.2和更高版本)
.....000 0....... 在V3.2和更高版本中保留(设置成0)
....0... ........ 支持OPEN/CLOsE/可移动的介质(V3和更高版本)
....1... ........ 支持OPEN/CLOSE/可移动的介质(v3和更高版本)
...0.... ........ 保留(设置成0)
..1..... ........ 字符设备:设备不支持“输出直到忙”操作
块操作:IBM块格式
309页
(续)
位 意义
FEDCBA98 76543210
..0..... ........ 字符设备:设备支持“出直到忙”操作
块操作:非IBM块格式
.1...... ........ 支持IOCTL
.0...... ........ 不支持IOCTL
1....... ........ 字符设备
0....... ........ 块设备
12.3.1设备头
设备头是一个18个字节的区域,被划分成五个域(见图12.3)。
图12.3设备头的组成
下面介绍这五个域的含义。
.下一个驱动程序指针。4个字节初始化为-1(FFFFFFFFh)。 DOS用这个域装入
驱动程序表中下一个驱动程序的指针。表中的最后一个驱动程序标记为-1(只有
该指针的偏移值部分必须是-1;而段值部分则可以为0)。
.驱动程序属性字。这两个字节定义了驱动程序特性(见表12.1)。
.策略例程偏移值。它是驱动程序内策略例程的两字节偏移值。
.中断例程偏移值。它是驱动程序内中断例程的两个字节偏移值。
.设备名。若设备为字符设备,后面就会出现8个字符、向左边对齐、不足部分补空
格的设备名。如果该设备名与已存在的设备的名字相同,新的驱动程序就会取代
已存在的设备。如果该设备为块设备,本域的第一字节就是与驱动程序相关的逻
辑驱动程序器号;其它字节被忽略(DOS的一些版本还包括可引导块设备的特殊
信息)。
DOS在初始化自己后,便建立了标准设备驱动程序的链表;在每个驱动程序中的下
一个驱动程序的指针提供了链表中下一个驱动程序的地址(见图12.4)。链中的最后一个
驱动程序有一个-1指针来指示链的结束。

310页
图12.4驱动程序链
DOS在最后读取CONFIG.SYS文件时,已建立起了一条驱动程序链。新的驱动程序
会加到链头上(见图12.5)。
图12.5向驱动程序链添加一个新的驱动程序
DOS先寻找字符设备驱动程序,它搜索驱动程序链找到与所请求的名字相匹配的驱
动程序名。 DOS从驱动程序链表的起点开始检查驱动程序名字看看是否与所请求的名字
匹配。若不是,则查寻下一个驱动程序指针域来找到表中的下一个驱动程序,并且DOS也
在那里检查。DOS在链中检查每个驱动程序(它跳过块设备驱动程序)直到发现所需的驱
动程序或链表的末端(在下一个驱动程序指针域中标记为-1)
新的驱动程序总是加到链表的开始(参见看图12.5)。然后,DOS搜寻驱动程序时,它
首先检查新的驱动程序。如果加上的驱动程序,其名字与已存在的驱动程序名字相同(如
加上一个新驱动程序名字为PRN——与打印机驱动程序名字相同),则新的驱动程序将
“取代”已存在的那一个,因为一次搜索总会导致使用新的驱动程序,而不是旧的同名的驱

311页
动程序。
ANSI.SYS驱动程序包含在CONFIG.SYS文件中时都是以这种方式来工作的。它
与控制台驱动程序有相同的名字(CON);把ANSI.SYS驱动程序加到驱动程序链上
时,无论何时进行控制台操作,都会首先找到该驱动程序。然后所有控制台操作都通过
ANSI.SYS驱动程序而不是通常的DOS控制台驱动程序来进行。
要说明建立驱动程序及用它进行工作的方式,让我们建立一个实际例子(或至少是真
实驱动程序的工作外壳程序)。列表12.1就是一个叫做drvr.asm的真实驱动程序头。在
本章后面将介绍把这个驱动程序汇编成实际上不做任何事情的驱动程序。该驱动程序是
一个可添加更多实际应用程序的外壳程序。
列表12.1
CR EQU 0Dh ;Carriage return
LF EQU 0Ah ;Line feed
MAXCMD EQU 16 ;DOS3.0,12DOS2.0
ERROR EQU 8000h ;Set error bit
BUSY EQU 0200h ;Set busy bit
DONE EQU 0100h ;Set completion bit
UNKNOWN EQU 8003h ;Set unknown status
cseg segment public'code' ; Start the code segment
org 0 ; Zero origin
assume cs:cseg,ds:cseg,es:cseg
该驱动程序的第一部分由指示汇编程序的伪指令组成。首先,为获得程序的便利性,
要定义许多常量如CR(回车)、LF(换行)、MAXCMD(最大命令号:16用于V3.0和
V3.1,12用于(V2.x),等等。这些定义可在继续工作时简化编程。
我们在前面已提到,列表12.1已定义成0起点(ORG 0)的代码段。所有段寄存器都
设置成与代码段相同,以便能把驱动程序作为一个二进制映象(COM格式)文件来汇编。
影响内存的驱动程序的第一部分是以标号drvr开始的,在这里将它声明为一个FAR
程序。这一点很重要,因为DOS要用一个FAR子例程请求来调用所有的驱动程序。FAR
子例程请求能跨越段界限——把返回地址(段和偏移值地址)放到堆栈上作为调用的一部
分。声明它是一个FAR程序以后,就能保证汇编程序使用FAR返回(它能获得离开堆栈
的段和偏移值地址)来把控制返回给DOS。
头的第一个域(一个双字,或8个字节)原始值被声明为-1。 DOS在驱动程序链中把
这个字设置成下一个驱动程序的地址。然后把属性字设置成8000h,指示该字符设备驱动
程序没有特殊功能(参看表12.1)。这后面是驱动程序的策略指针(只有偏移值)和中断程
序,然后是驱动程序的8个字节的名字。
驱动程序头对于正常驱动程序操作是很关键的。 DOS在需要某个驱动程序时,它先
检查属性字节看看该驱动程序能做的事情,然后再用策略和中断指针来定位这些例程。如
果驱动程序头不正确,驱动程序在启动之前就会失败。因为驱动程序头是登记式的,且汇
编程序进行登记工作,所以还是让我们先看看策略例程。

312页
12.3.2策略例程
策略例程与通常所提到的“策略”没有什么关系——它不是试图找到最好的途径来驱
动设备或诸如此类的其它事情。可以只用5行程序来编写策略例程;它的目的就是要“记
住”操作系统在内存中分配给设备的请求头(RH)的位置。 RH有以下两种功能:
·它是DOS内部操作的数据区。
·它是一个通信区,在其中,DOS告诉驱动程序要做的事情,而驱动程序则要汇报操
作的结果。
请求驱动程序输出数据时,数据地址由RH提供。驱动程序则执行输出任务,然后在
请求头中设置标志或状态字节未指示完成。
DOS要调用驱动程序时,请求头就建立在内存的保存区域中,其地址传递给ES:BX
中的策略例程。尽管对驱动程序的每次调用可能都有一个新地址,但实际上这些地址都是
相同的,策略例程把这个值保存起来以备驱动程序的中断例程将来使用。
请求头的长度会改变,但却总有一个固定的13字节的请求头(有时叫作请求头的“静
态部分”)。请求头的结构见表12.2。
表12.2请求头的开始部分
字书偏移值 域长度 意 义
00h 字节 请求头的长度
01h 字节 单元代码:块设备的设备号。
02h 字节 命令代码:送给驱动程序的最近命令号。
03h 状态:每次调用后驱动程序设置的状态代码。如果设置了15位,则
错误代码在低顺序的8个位中。0状态代码意味着已成功地结束。
05h 8个字节 保留起来以备DOS使用
0Dh 变量 驱动程序请求的数据
请求头中的大多数域已在表中解释清楚。但状态字(字节03-4h)需要说明一下。
状态字把请求的完成状态传递回DOS所用格式见表12.3。
表12.3请求头的状态字
位 意义
FEDCBA98 76543210
........ 00000000 写保护违反错误
........ 00000001 未知的单位错误
........ 00000010 驱动程序没准备好错误
........ 00000011 未知
........ 00000100 CRC错误
........ 00000101 不正确的驱动程序请求结构长度错误
........ 00000110 寻道错误
........ 00000111 未知的介质错误
313页



FEDCBA98 76543210 意义
........ 00001000 未找到扇区错误
........ 00001001 打印缺纸错误
........ 00001010 写失败
........ 00001011 读失败
........ 00001100 严重故障
........ 00001101 保留
........ 00001110 保留
........ 00001111 无效的磁盘改变
.......x ........ 已完成
......x. ........ 忙碌
.xxxxx.. ........ 保留
0....... ........ 无错误
在状态字中设置出错位是为了指示驱动程序操作中发生的出错类型。出错码返回到
状态字的较低8个位中。如未设置出错位,则出错码应设置为0以表示操作的圆满完成。
设置忙碌位是指示调用设备时,该设备正在忙碌。驱动程序完成操作时应设置已完成
位。驱动程序在请求操作时设置以上那些位来指示状态;所有功能都要设置已完成位来指
示已完成。
用于样本驱动程序的策略例程(drvr.asm)如列表12.2所示。
列表12.2
rh_seg dw ? ;RH segment address
rh_off dw ? ;RH offset address
strategy:
mov cs:rh_seg,es
mov cs:rh_off,bx
ret
列表12.2分配空间来保存请求头的段和偏移值。整个策略例程,仅由保存请求头指
针(段地址在ES寄存器中,偏移值地址在bx寄存器中)的操作组成。为什么不做更多的
事情?一个更突出的问题可能是“为什么有两个入口点?”为什么不把指针传递给ES:BX
中的中断例程并用它来完成工作?
答案涉及操作系统的兼容性和内部机制。驱动程序结构设计成希望能与将来的扩展
版兼容,该扩展版打算把DOS变成多任务的结构。操作系统运行多任务时,在单个请求被
处理之前可能会输送多个请求给某个特定的驱动程序。换言之,这些请求可能不得不排
队。
例如,如果要请求磁盘扇区的读操作,在第一个请求得到满足之前可能到达了多个请
求。特别是如果所请求的扇区远离磁盘的当前位置,则更有可能会出现以上的情况。可以
改进策略例程使它把多个请求排序,减少磁头移动,从而优化对磁盘设备的访问。但这些

314页
功能无一能用于DOS V4.01及更高版本。
DOS是一个单用户、单任务的系统,所以它没有多个进程访问驱动程序的潜在能力
但其结构却允许在这个方向上进行扩展(如果这类扩展确实是必需的)。
安全地保存了请求头的地址后,可以返回到DOS并等着调用中断例程:在单任务系
统中它立即就会调用。
12.3.3中断例程
驱动程序的重要部分叫做中断例程,由它来完成所有工作。采用这个名字并不恰当,
因为它并不像一个中断,而且它以RET终止而不是以IRET终止。但这个名字却反映了:
没有具体化的计划;它的意图就是中断处理程序要处理那些排诚队列的请求。当某个设备
要处理下一个任务时,它会中断DOS,然后处理程序会把控制指向中断例程。但像策略例
程的设计一样,这一想法在DOS上同样未具体实现。
中断例程包括DOS系统所需的21个功能那么多的代码(DOS2.0版上为13,
DOS3.0版则为20,DOS5.0版为21)。无论何时调用设备驱动器程序,客观上都会获得请
求头的地址并去查看其中的偏移值为02h处的字节,以便找到命令代码,该代码指示驱动
程序所要执行的功能。
大多数驱动程序都会创建一个含有驱动程序功能指针的表。命令代码用作该表的索
引,以定位所需的功能。列表12.3显示了样本驱动程序的调度表。
列表12.3
d_tDl:
dW s_init ;Initialization
dw s_mchk ;Media check
dw s_bpb ;BIOS parameter block
dw s_ird ;IOCTL read
dw s_read ;Read
dw s_nrd ;Nondestructive read
dw s_inst ;Current input Status
dw s_infl ;Flush input buffer
dw s_write ;write
dw s_vwrite ;Write with verify
dw s_ostat ;Current output status
dw s_oflush ;Flush output buffers
dw s_iwrt ;IOCTL write
dw s_open ;Open
dw S_close ;Close
dw s_media ;Removable media
dw s_busy ;Output until busy
这个表特别易于设计,因为汇编程序跟踪这些功能并自动把正确的偏移值地址插入该
表中。这个驱动程序不支持DOS V3.0之后产生的特殊功能:通用的IOCTL;Get Logical
Device(获取逻辑设备);Set Logical Device(设置逻辑设备)和IOCTL Query(查询)。但这
不是问题,因为大多数运行DOs的DOS旧版本的程序并不使用依赖于这些功能的调用。
中断例程的主体决定了要服务的请求的本质。它通过调度表来分别走向相应的功能。
列表12.4显示了中断例程余下的部分。
315页
列表12.4
interrupt:
cld ;Save machine state
push es ; Save all registers
push ds
push ax
push bx
push cx
push dx
push si
push di
push bp
mov dx , cs : rh_ seg
mov es , dx
mov bx , cs :rh_off
mov al,es: [bx]+2 ; Command Code
xor ah , ah
cmp ax ,MAXCMD ; Legal command?
jle ok ;Jump if okay
mov ax, UNKNOWN ; Unknown command
jmp finish
ok:
shl ax, 1 ; Multiply by 2
mov bx , ax
jmp word ptr [bx+d_tbl]
finish :
mov dx , cs : rh_ seg
mov es , dx
mov bx , cs : rh_off
or ax , DONE ; Set the DONE bit
mov es : [bx] +3, ax
pop bp ; Restore the registers
pop di
pop si
pop dx
pop cx
pop bx
pop ax
pop dS
pop es
ret ; Back to DOS
中断例程开始时,先在堆栈中保存当前的机器状态。然后它从策略例程所保存的请求
头指针的地方获得该请求头的指针。中断例程接着查看请求头中的偏移值02h处的字节,
决定要做的情况。该例程接着检查一下,确定该命令是否是合法的:如果是,该例程就执行
分支转列它完成该功能的地方。一个非法的命令(比最大的命令号还大)会导致驱动程序
返回一个出错标志,设置这个标志即表示它是一个未知的命令。
当命令号小于MAXCMD(这是驱动程序所支持的命令个数)时,驱动程序就把命令
号乘以2(借助左移,这与乘以之是相同的)来在调度表中获得命令代码的偏移值。这一移
位操作必不可少,因为在调度表中为每个表项保留了两个字节。然后把偏移值加到调度表

316页
的基地址上,然后驱动程序跳到指定的例程上。
功能结束时,驱动程序重新获得请求头的指针并在状态字中设置已完成位,表示操作
已经完成。接下来,保存在驱动程序开始处的寄存器重新恢复,控制返回到DOS内核。
在查看每个独立的驱动程序功能之前,让我们先看看该样本驱动程序的余下部分代
码。在任何驱动程序中,只有一些功能是必须实现的。在不需要某个功能的情况下,驱动
程序可以只返回一个状态代码而不必做任何事情。一些人主张返回一个0代码来指示圆
满的操作。其它人则建议返回一个错误代码3(命令未知)。如果要编写自己的驱动程序来
使用自己的软件,就要进行自己的选择。但是,如果编写一个驱动程序来代替已有的驱动
程序,新返回的代码应该与原来的驱动程序返回的代码相同。
列表12.5提供了drvr.asm剩下的部分。
列表12.5
s_mchk: ;Media check
s_bpb: ;BIOS parameter block
s_ird: ; IOCTL read
s_read : ; Read
s_nrd : ; Nondestructive read
s_inst : ; Current input status
s_infl: ;Flush input buffers
s_vwrite: ;Current output status
s_ostat : ; Current output status
s_oflush : ; Flush output buffers
s_iwrt : ; IOCTL write
s_open : ; Open
s_close : ; Close
s_media : ; Removable media
s_ busy: ; Output until busy
MOV AX,UNKNOWN ; Set error bits
jmpfinish
ident:
db CR,LF
db ' Sample Device Driver- - Version'
dD '0. 0'
db CR,LF,LF,'$'
S_init:
mov ah,9 ;Print String
mov dx , offset ident
int 21h
; Retrieve the rh pointer
mov dx , CS : rh_seg
mov es , dX
mov bx,CS: rh_off
lea ax, end_driver ;Get end of driver address
mov es : [bX] +14 , ax
mov es : [bX ] +16, CS
xor ax,ax ;Zero the AX register
jmp finish
s_write:
xor ax , ax ;Zero the AX register
jmp finish

317页
end_driver:
drvr endp
cseg ends
end
编写驱动程序时,可以忽略那些在完成你的工作时不需要的功能。例如,在drvr. asm
中,只有初始化功能和写功能才起作用。所有剩下的功能都由同一个部分来处理,该部分
也只返回一个代码告诉DOS所请求的功能是未知的。只需读和写的驱动程序可以只提供
这些功能。
唯一必须包含在所有驱动程序内的功能是初始化功能:它必须把驱动程序的最后地
址放进请求头的偏移值0Eh处,以便操作系统的初始化程序使用它。如果该地址为0,对
于一个块设备驱动程序来说,整个驱动程序头就在安装过程中从内存中移走了。如果在初
始化时遇到了致命的错误,那么这种移动是可取的;一个大胆的开发者用这个特点来开发
了一个在屏幕偶尔显示一下数据然后立即消失的驱动程序。但是字符设备驱动程序做不
到这一点;它的“结束”地址必须大到最少能容纳整个驱动程序头。如果它不这样做,则所
有后面的字符设备都会失效。
图12.6下一个驱动程序添加在当前驱动程序的结束地址之后
要忽略这些功能,只需返回一个代码告知你不知道DOS在请求什么。然后跳到关闭
操作的中断例程的部分,并在请求头的状态字(偏移值03h)中设置已完成位(第8位)。
然后是初始化工作。打印一个字符串给屏幕(以便知道驱动程序确实在那里),接着确
定驱动程序的最后地址(标号end_driver处就是该例程的末尾)。这个地址必须保存在请
求头中,这样DOS才知道装入下一个驱动程序的位置。由于驱动程序是从低内存到高内
存进行装入的,所以下一个驱动程序在当前驱动程序的结束地址之后(见图12. 6)。
最后写例程把AX寄存器设置为0(驱动程序功能的返回状态)来指示驱动程序的操

318页
作中没有出错。这类简单的功能设置对于大多数驱动程序来说是典型的。大多数驱动程
序都只需要几个功能来完成其操作。其中一些功能只对某类驱动程序才有意义。例如
BIOS参数块功能对涉及键盘的驱动程序来说就毫无意义。
表12.4列举了设备驱动程序的功能,并指出哪些能用于DOS的特定版本。该表还详
细说明了每个功能的工作方式。
一、驱动程序初始化
初始化功能是必须存在于所有驱动程序之中的一个功能。它为驱动程序执行所有必
要的设置。它有一项任务对DOS来说是必不可少的:它必须把驱动程序的结束地址设置
到请求头的偏移0Eh字节处。如果驱动程序正在驱动一个硬盘系统,初始化功能将检查
磁盘的存在及它的正确操作,对磁盘参数进行初始化,等等。对于串行端口,它应该对端口
进行初始化并建立缺省值设置。
驱动程序的初始化部分是唯一能合法调用DOS的部分。其它部分都不许调用DOS
(记住DOS是不能重入的。当程序处在驱动程序里面时,它就是DOS!)只有功能01h到
0Ch(有限的控制台I/O)和30h(获得DOS版本号)才是可用的。当驱动程序进行初始化
时,DOS的其它部分还未进行初始化,所以对磁盘驱动器之类的调用(等等)会失败并使
系统死锁。
要允许参数通过CONFIG. SYS命令地来传递给驱动程序,就要传递一个命令行指
针给驱动程序。该指针指向“DEVICE=”行中等号后的第一个字符;只允许初始化程序去
读数据,不允许改变它。
表12。4设备驱动程序的各个功能
功能 意 义 DOS版本
00h 驱动程序初始化 2,3,4,5,6
01h 介质检查 2,3,4,5,6
02h 建立BIOS参数块 2,3,4,5,6
03h I/O控制读 2,3,4,5,6
04h 读 2,3,4,5,6
05h 非破坏性的读 2,3,4,5,6
06h 输入状态 2,3,4,5,6
07h 清空输入缓冲区 2,3,4,5,6
08h 写 2,3,4,5,6
09h 带校验的写