基本上我写的文章中的程序实例都是32位的,需要运行在保护模式下,但是不要祈求在DOS下可以写32位的设备驱动程序,因为DOS本身是16位实模式下的操作系统,当然其驱动程序的机制也只能是实模式下的,尽管在DOS下可以编写保护模式的程序,但这些程序亦可以通过DPMI调用实模式下的驱动程序,只是效率低一些,所以,按照DOS的规则编写设备驱动程序一样可以让你的32位保护模式下的程序使用。
DOS下的设备驱动程序(以下简称驱动程序),一般都需要使用汇编语言来编写,所以,看这篇文章需要汇编语言的知识,同时最好对MICROSOFT的汇编语法和格式比较熟悉。
1、DOS设备驱动程序的原理
DOS设备驱动程序采用一个单向链表串起来,第一个设备永远是NUL(一个空设备),它有一个指针指向下一个设备驱动程序,最后一个设备驱动程序的指针为-1,表示是驱动程序链的结尾,当DOS需要调用设备驱动程序时,他会从NUL驱动程序开始,根据设备名在链表里找到第一个匹配的设备驱动程序,然后调用,所以在DOS设备驱动程序的链表中,同样设备名的驱动程序可以有多个,但只有排在前面的有效。
当有一个新的设备驱动程序需要加入到设备链中时,总是把它插在NUL设备的后面,这样就可以保证一个新的设备驱动程序覆盖掉和自己重名的旧的设备驱动程序,旧的设备驱动程序因为在设备链的后面而失效。
DOS在调用设备驱动程序完成一个任务时,要调用两次驱动程序,第一次调用驱动程序的“策略例程”,第二次调用驱动程序的“中断例程”,关于这个问题,我们没有必要去深究,反正是要调用两次才能完成一个任务,而策略例程往往比较简单,主要的工作由中断例程完成。
所以,在后面我们会看到,编写设备驱动程序时不得不编写两部分程序,策略例程和中断例程。
2、DOS驱动程序的类型
有两种类型的驱动程序,一种叫字符设备设备驱动程序,比如:串口、打印机、键盘等,这些都归为字符设备;还有一种叫块设备驱动程序,比如:磁带机、软盘、硬盘、U盘等。这两种类型的驱动程序写法上有较大不同。
3、驱动程序的结构
简单地说,一个DOS设备驱动程序的结构分成三个部分:设备头、策略例程、中断例程。
我们知道,DOS中可执行文件的格式有两种,一种是EXE文件,可以编的比较大,还有一种是COM文件,只能在64K以内,大多数的设备驱动程序都是COM格式的文件,一般为避免混淆,后缀采用sys。
下面我们针对结构的三个部分分别进行说明。
4、设备头
设备头一共有18个字节,被分为五个字段,下面是其定义(本文的程序书写按照microsoft宏汇编的格式):
;************************************
; DEVICE HEADER
;************************************
next_dev dd -1
attribute dw ????
strategy dw ????
interrupt dw ????
dev_name db 'abcdefgh'
next_dev:指向下一个驱动程序的指针,置为-1,表明后面没有其它的驱动程序,DOS在装入时会做出相应处理;如果我们要在一个程序写多个设备的驱动动程序,这个字段可以指向下一个驱动程序。这是一个双字字段,第一个字(低地址)放偏移,第二个字放段地址。
attribute:属性字段,含义如下:
bit 含义(置1时)
0 标准输入设备
1 标准输出设备
2 NULL设备
3 时钟设备
4 特殊的设备
5-10 备用
11 支持OPEN/CLOSE/REMOVABLE MEDIA的设备
12 备用
13 非IBM格式设备
14 IOCTL
15 字符设备(块设备置0)
strategy:策略例程的入口偏移地址
interrupt:中断例程的入口偏移地址
dev_name:设备名称,一共8个字节,DOS的设备名不能超过8个字符,当设备名不足8个字符时,后面补空格。对于设备名称,还得多说两句,对于字符设备,这个字段如上所说,对于块设备,只有第一个字符有效,为块设备单元的数量,DOS不允许块设备驱动程序有名字。
5、策略例程
在DOS调用设备驱动程序时,总是把ES:BX指向一个叫Request Header(请求头)的,这个请求头中有调用驱动程序时需要传送的所有信息,然后调用设备驱动程序的策略例程,策略例程只完成一个任务,就是把这个请求头的地址保存起来,然后把控制交还给DOS,DOS得到控制权后再调用驱动程序的中断例程,中断例程根据请求头中的各种数据完成指定的任务。
不同的调用,请求头的结构会不同,但前面13个字节总是相同的,如下:
rh struc
rh_len db ? ;length of packet
rh_unit db ? ;unit code(block device only)
rh_cmd db ? ;device driver command
rh_status dw ? ;returned by device driver
rh_res db 8 dup(?) ;reserved
rh ends
策略例程一般总是由下面代码组成:
rh_off dw ?
rh_seg dw ?
dev_strategy:
mov cs:rh_seg, es
mov cs:rh_off, bx
ret
正如前面说的,策略例程在存储完请求头的地址后,将控制返还给DOS。
6、中断例程
这一部分是驱动程序的核心部分,几乎驱动程序的所有功能都是由这部分完成的。我们从请求头开始说起,请求头的前四个字段,rh_len不用说了,是请求头的长度,因为不同的调用请求头不一样,所以,需要这个字段告诉我们请求头的边界位置;rh_unit:块设备的设备号,对于字符设备没有意义;rh_cmd:命令代码,中断例程中有许多不同的功能模块,执行不同的操作,每个模块有一个唯一的命令代码,中断例程根据这个代码决定执行那个功能模块;rh_status:这是我们要强调的一个字段,在中断例程执行完后,将把执行结果以状态码的形式通过这个字段传回给DOS,就是说,在中断例程执行完即将返回DOS前要填这个字段,而DOS可以根据这个字段判断命令的执行情况,从而决定下一步的操作。
rh_status的说明:
名字 位 说明
------------------------------------------------------------------------------------
ERROR 15 0表示没有错误,如果为1,则要根据ERROR_CODE判断错误类型
DONE 8 驱动程序必须设置,表示已经完成
BUSY 9 必要的话,由驱动程序设置,以防止重复或不适当的操作
ERROR_CODE 0-7 DOS标准的错误代码,后面有说明
10-14 保留
错误代码ERROR_CODE说明:
错误代码 说明 错误代码 说明
--------------------------------------------------------------------------------------
0 写保护 1 不认识的单元
2 驱动程序没有准备好 3 不认识的命令
4 CRC错误 5 不正确的驱动程序请求结构长度
6 寻道错误 7 未知的介质
8 未找到扇区 9 打印缺纸
10 写失败 11 读失败
12 严重故障
下面我们再说说命令代码,DOS的驱动程序规范已经定义了一组命令码,比如0为初始化(initialization)命令;1为检查介质(media check)命令等等,下面是命令代码表。
命令代码 说明 命令代码 说明
----------------------------------------------------------------------------------------
00h 驱动程序初始化 01h 介质检查
02h 建立BIOS参数块 03h I/O控制读
04h 读 05h 非破坏性的读
06h 输入状态 07h 清空输入缓冲区
08h 写 09h 带校验的写
0Ah 输出状态 0Bh 刷新输出缓冲区
0Ch I/O控制的写 0Dh 打开
0Eh 设备关闭 0Fh 可移动的介质
10h 输出直到忙碌 11h 通用IOCTL
13h 通用IOCTL 17h 获得逻辑设备
18h 设置逻辑设备 19h IOCTL查询
DOS在调用驱动程序时,请求头的rh_cmd字段一定填的是上面的某个命令码,在驱动程序的interrupt部分通常有一个含有驱动程序功能指针的表,命令代码用作该表的索引,以定位所需的功能,这个表大致是下面的样子:
d_tdl:
dw s_init ;Initialization(初始化)
dw s_mchk ;Media check(检查介质)
dw s_bpb ;BIOS parameter block(得到BPB)
dw s_ird ;IOCTL read(IOCTL读)
dw s_read ;Read(读)
............
其中的s_init、s_mchk等都是相应命令处理程序的标号。这个表有点向DOS的中断向量表,只不过中断调用都是远调用,所以一个表项要4个字节,而这个表是段内调用,一个表项只需2个字节就够了。
在下来,通常interrupt部分的开始都会有类似下面的这段代码,以保证根据命令代码执行相应的功能。
mov dx, cs:rh_ seg ;请求头的段地址
mov es, dx
mov bx, cs:rh_off ;请求头的偏移地址
mov al, es:[bx] + 2 ;从请求头中得到命令代码
xor ah, ah ;ah=0
cmp ax, MAXCMD ;MAXCMD为允许的最大命令代码
jle ok ;合法的命令代码
mov ax, UNKNOWN ;非法的命令代码
jmp finish
ok:
shl ax, 1 ;命令代码乘以2,因为一个表项占2个字节
mov bx, ax ;bx=ax
jmp word ptr [bx+d_tbl] ;根据d_tbl表跳转到指定功能执行
finish:
...............
再回过头来说请求头,不同命令的请求头是不同的,但前13个字节(就是前面定义的rh结构)是一样的,不同的命令代码,在这13个字节后面的数据含义都各有不同,描述起来确实篇幅太长,目前写DOS设备驱动程序的资料几乎找不到,我从DOS程序员手册中把相应的章节给摘出来做了一份文档,文字是从网上找的,其中的图片是我从原文里给加上的,不然打那么多字会累死我的。
下载地址如下:
http://blog.hengch.com/article/dos-device-driver.pdf
这份资料基本上比较完整,除了我已经介绍的东西外,还有一个结构十分重要,就是BPB(Bios Parameter Block)的数据结构,在这份资料中也有详细描述,至多是比我啰嗦一点。
在这份材料里,好像没有关于介质描述符的说明,其实,这个描述符的大部分定义现在已经不用了,下面两个还在用:0f0h--其它介质,0f8h--各种容量的硬盘,其它有:0f9h--0ffh均表示各种不同类型的软盘介质,0f0h也用来表示1.44M和3.88M的3吋软盘。
我介绍DOS下设备驱动程序的写法,其主要目的是希望能编写出一个简单的U盘的设备驱动程序,为此目的,我在下一篇文章《如何写DOS下的设备驱动程序(下)》中准备给大家一个RAM DISK设备驱动程序的实例,因为它应该和我们想要做的事情比较类似,但前提是我得能找到那个程序,不用这个驱动已经很多年了。