56页
第4章DOS和BIOS接口
本章介绍了用户程序访问DOS内核和BIOS所提供的各种服务的方法。为了访问这
些服务,我们可以从任何编程语言中调用各个软件中断,这些中断便是我们在本书中要重
点地讨论的内容。用户当然不必了解访问系统资源的所有细节,但要入门,确实要学习相
当多的这方面知识。
本书重点介绍的是四种编程语言:汇编语言、C(极c++)、Pascal和BASIC。所讨论
的实现程序分别有Microsoft Macro Assembler(宏汇编)、Microsoft C/C++、Borland C/
C++、Turbo Pascal 7.1版(以及对早期版本的少量说明)和Microsoft Quick BASIC。所
有四种语言都带有允许直接访问DOS和BIOS功能的特性。但是,因为不是每一个功能
都是由语言内带特性所能提供的,因此,有些操作读者不得不自己去编程实现。在本章中,
我们将了解到哪些是由所学语言本身所具备的,哪些是读者必须自己去实现的,以及如何
去实现它。
4.1从程序中访问DOS和BIOS
要访问DOS和BIOS资源,只需按照下述简单的步骤来操作:
1.以相应的值装入CPU的寄存器。
2.产生一个软件中断来调用所需的硬件中断。
3.通过CPU的寄存器来返回中断结果(如果有)。
正常情况下,寄存器中的值都是8位或16位的数值参数或大型数据结构的地址。在
本书中所讨论的所有语言,都有一种约定的方式来装入CPU的寄存器,产生中断,并读取
返口值。有关CPU寄存器及其它们的用途,可参见第2章“DOS系统的结构”。
图4.1以图解方式列出了在一个系统调用之前或之后的寄存器中的内容。
根据所要调用的资源,在装入寄存器和产生中断之前,可能需要做一些额外的工作。
例如,许多面向文件的服务,都要求在中断产生之前,在寄存器中装入一个串或其它数据
结构的段:偏移值地址。(如果不熟悉表示地址的段:偏移值形式,可参阅第2章中的内存
分段与8086)。
首先,用户程序必须得到数据项的地址。得到一个项的地址是一个相对简单的过程:
而如果有什么复杂之处的话,恐怕是与用户所使用的编程语言有关。对本书所涉及的每一
种语言,本章后面都有一个节来专门介绍如何得到数据项地址的方法。
57页
图4.1一个典型的系统调用之前和之后,寄存器中的内容
数据串或其它的项必须以某种相应的固定形式组织起来。许多DOS功能都要求所使
用的带参数采用ASCIIZ(ASCII加零)格式:各个字符以ASCII代码格式设置,而该串的
最后一个字符则是ASCII字符零(在C语言中为\0,在BASIC中是CHR$(0),而在Pas-
cal里是chr(0)。图4.2显示了一个ASCIIZ的结构。
图4.2一个ASCIIZ串的结构
如果用户程序是用C来写成的,读者也许会知道ASCIIZ在C语言中是如何精确地
存放它的格式的。在BASIC或Pascal里,要创建一个ASCIIZ,还额外需要一个步骤。在本
章后面的一些例子则显示了是如何完成这些工作的。
因为汇编语言能提供对CPU和系统资源的最直接访问,因此,在后面各节中的介绍
性例子都用汇编语言来写成。有关访问DOS和BIOS资源基本原则的简介,可阅读下面
的各个小节;这些例子都非常清楚,哪怕是读者对汇编语言并不十分熟悉。
在本章后面,将介绍如何通过高级语言来访问操作系统。首先,让我们先看看一些简
单的汇编语言例子,来探讨一下DOS和BIOS接口。
4.1.1一个对DOS的简单调用
在本书中所介绍的每种编程语言,都有几个服务能提供对基本的DOS和BIOS中断
的访问。在这些语言中,最简单的语言是汇编语言,因为汇编语言允许程序直接访问Int
(中断)指令,而Int指令能直接地产生对BIOS或DOS功能的访问。
58页
下面的代码片断,使用Int 21h的功能02h,将字符X输出到控制台,这是一个典型的
对DOS的访问:
mov ah,2 ;字符输出功能
mov dl'X' ;字符X
int 21h ;执行DOS功能中断
使用Int 21h功能02h确实很简单:
1.以相应的值装入所需的寄存器:将值2装入AH来选择DoS功能2(控制台字符
输出)。字符X装入到DL中。
2.产生中断:汇编语言中的助记符Int后面所跟的值21h,是通用的DOS中断号。因
为Microsoft的Macro Assembler(宏汇编程序,MASM)是一种衡量汇编语言兼容性的标
准,因此,用户所选择的任何汇编语言中都应该可以使用Int指令。
因为Int 21h的功能2不返回任何值,因此这里就少了DOS调用模型的第三部分。下
面的例子则演示了第三步,并介绍了如何得到和传递比16位“宽”的数据项的地址给
DOS的基本技巧。
4.1.2传递字符串地址给DOS
前面已提到过,当数据项的内容比16位多时,就需要将数据项的段和偏移值地址放
入到cpu的寄存器中。下面的代码片断演示了传递字符串地址的一种方法:
;Path_name含有将要打开的文件的名字
mov ax, seg Path_Namee ;路径的段地址
mov ds,ax ;放入Ds
mov dx,offset Path_Name ;路径的偏移值
mov al, c2h ;打开文件的模式
mov ah,3dh ;打开文件
int 21h ;DOS中断
jc error ;如果出错,置进位位
mov file_Handle,ax ;保存文件句柄
此代码片断使用Int 21h,功能3Dh来打开一个文件。该功能要求DS和DX相应地包
含文件路径名的段地址和偏移值。该代码段的前三行按要求装入寄存器。接下来的一行
将C2h放入AL;AL中的值说明该文件被打开的模式(有关此值的模式编码将在本书的
“DOS参考手册”部分里详细描述)。将各寄存器正确地装入后,便随后产生DOS中断。
像大多数DOS服务一样,如果服务失败,就会设置进位位(此处表示文件不能打开);
如果已设置了进位位,程序于是便跳转到一个处理错误的例程(jc Error)。但是,如果进位
位清除,则表明打开文件成功,文件句柄(由AX返回)放入内存中的一个位置处,以备将
来使用。
注意最初的DOS服务(即由DOS1.0版提供的)并不使进位标志来指示出错。就像
cp/M中的服务(DOS的祖先是CP/M)一样,它们在AL寄存器中返回它们的信息。自
DOS1.0以来,一致性并不总是保持得那么坚定,因为,自1.0版以后,也并不是所有的随
59页
后服务都用进位标志来指示出错。某些服务(如3.0版中可用的Get PID服务)是不可能
出错的,因为它只不过返回由DOS保存在内存中的内容给调用者。这些调用有时会清除
标志位,但是有些DOS版本则直接从服务中返回,而将进位标志仍保持为调用DOS前的
状态。
因此有必要指出的是,标志位是否指示出错,要看调用的功能在文档中是否是这么说
明的。如果此功能并没有正式公开,那么可参看本书后面参考手册中的那一部分,看看此
标志位是否指示出错,如果是,再看看它指示的什么错误。
4.2高级语言资源
高级语言提供了许多不同的方法来调用DOS和BiOS例程。每种语言都有一种独特
的方法,在其它的语言中往往不能重复这一方法。甚至是在同一种语言内,不同的实现厂
家及不同的版本,也可能采用了不同的方法。 Turbo Pascal 3.0就提供了一种非内带的方
太式来访问所有的文件(满足通配符文件)(如C:\QTRLY\QTR?1993.DAT)。为了访问这
样一组文件,编程者不得不多写两个过程:一个是调用DOS Int 21h的功能4Eh来找出满
足匹配文件名说明的第一个文件,另一个则用于调用功能4Fh来找出满足匹配文件名说
明的余下文件。当Turbo Pascal 4.0推出后,它不仅允许用户自己去生成这样的例程,并
将它们保存到一个运行时刻库中,而且还提供了两个过程一FindFirst和FindNext来完
成这项工作(与其它一些过程一起,放在一个名为“unit”的运行时刻库中)。而到了Turbo
Pascal 5.0的问世,连这些例程的源代码也都可以得到了(尽管需要额外付费)。
在以下各小节中,每一节都给出了两个实例程序。对每种语言,第一个实例程序是一
个较简单的程序,用于说明访问操作系统资源的基本方法,它使用BIOS Int 17h功能2来
检查打印机的状态。第二个例子则相对地较复杂一些,它解释了从系统调用中返回的结
果。
较复杂的例子在不同的语言中差别很大。所遵循的原则是,用户不必再做无谓的工
作,给出的编程例子,在所选择的语言中不能提供这样的服务。BASIC例子显示了如何使
用DOS Int 21h的功能4Eh和4Fh来获取满足带有匹配符的文件,并将那些文件名赋值
给BASIC变量。因为类似的功能已由Turbo Pascal及C中内带,所以在这两种语言中给
出的例子就会执行不同的任务。Turbo Pascal的例子使用DOS Int 21h功能57h来获取一
个给定文件的日期和时间。C语言的例子则使用DOS的Int 21h功能43h来获得与设置
文件属性(归档、隐藏、系统及只读属性)。这两个例子根据需要使用了不同的功能来完成
必需的设置及清除工作。这些辅助的功能调用分别在程序文本和注释中作了说明。
避免做无谓的工作
许多高级语言都有预定义的函数、过程或变量来提供方便地访问系统资源的方式。在
大多数情形下,这些语言成份所访问的DOS和BIOS功能与用户自己在需要时直接编程
所调用的系统资源是相同的;只不过提供该编程语言环境的厂家已为使用者编好了这些
代码。如果认为在此语言中使用系统资源,对你来说是件新鲜事——或者它是你所熟悉语
60页
言的一种新的实现版本,那么必须注意的一点就是,不要做事复的工作。在本书中反复强
调的一个原则是,仅在必要时才去访问系统。因此,作为编程者,应该尽量使用语言中内带
的功能,除非有某些特别的要求,而所使用的语言资源满足不了这种要求。
应该总是仔仔细细地阅读用户手册。下述基本建议值得反复强调:去查手册!许多程
序员,特别是那些新接触某个语言或操作系统的人,常常将大量的时间花费在一些无谓的
工作上。如果他们更仔细地阅读过所使用的语言手册,就不会浪费时间去编制一些业已存
在的资源上。
4.2.1C语言
对与操作系统打交道来说,C是最合适的语言。如果读者读完了在其它节里介绍的其
它语言,就会发现,Pascal和BASIC中的高级特性都会导致使用者按照自己的想法去实
现,如果要深入地想看个究竟的话。因为这些语言在需要方便地访问DOS和BIOS资源
时,都禁止或限制用户访问系统级的数据结构和其它信息。
由于C语言在访问高级和低级资源时都很方便,因此曾有人将它称为“高级汇编语
言”。当需要在字符或位级进行详细地设置或操作时,C能让使用者“像一个微处理器那样
去考虑问题”。
C与操作系统有着一些类似的组成:许多C函数都是“穿着C语言外衣的DOS”。这
些函数通常都与DOS采用相同的参数,并返回相同的结果;确实,这些C函数作为输入的
数据结构和返回的输出值,都与在DOS中所用的参数相同。
接下来,在本节中要给出一个更复杂的实例程序,我们给出了两个版本:chmd.C直接
调用DOS,而chmc.c则调用等价的C函数。而只有一个版本的较为简单的例子名为
_Pronok.c;该例子所提供的功能,在Borland C++库中是不能直接得到的。
本节所给出的C程序都是由Borland C/C++编译器开发的,有关Borland c++与
Microsoft C之间的不同之处,可阅读例子代码片断中给出的注释。
访问寄存器和产生中断
在C和DOS接口中所使用的数据结构,是由REGS联合及SREGS与REGPACK结
构定义的。这些对象都是在头文件DOS.H中声明的。它们的声明在下面的代码片段中列
了出来:
struct WORDREGS{
unsigned int ax, bx, cx, dx, si, di, cflag, flags;
};
/* Microsoft C lacks flags element*/
struct BYTEREGS{
unsigned char al, ah, bl,bh, Cl, ch, dl, dh;
};
union REGS{
struct WORDREGS x;
struct BYTEREGS h;
};
61页
struCt SREGS{
unSigned int es;
unSigned int cs;
unSigned int ss;
unsigned int ds;
};
struct REGPACK{ /*Not defined in Microsoft c*/
unSigned r_aX,r_bX,r_cx,r_dx;
unSigned r_bp,r_Si,r_di,r_dS,r_es,r_flags;
}; 。
此外,在BorlandC++中CPU各个寄存器的内容也呵以通过伪变量AX、_AL、AH
等得到。每个8086的通用寄存器、偏移值及段寄存器(除了IP)都有相对应地伪变量。也
可使用对应于16位或8位的寄存器的变量(但却未声明),把它门当作相应的unsigned
int和unsigned char类型:
unsigned int _AX;
unsigned char _AL;
在前面介绍的伪变量是不能在Microsoft C中使用的。因为缺少这些伪变量会给程
序带来少许变化,因此必须仔细地阅读厂述程序片断,如果想要让它们通过Microsoft C
编译器的话。
DOS.H头文件中包含有下列C函数的原型,用于产生软件中断:
int int86(int intno,union REGS*inregs,
union REGS*outregs);
int int86x(int intno,union REGS*inregs,
union REGS*intregs,
struct SREGS*segregs);
int intdos(union REGS*inreg,
union REGS*outregs);
int intdosx(union REGS*inregs,
union REGS*outregs);
struct SREGS*segregs);
void intr(int int_type,struct REGPACKOpreg);
intdos()函数能产生Int 21h——最基本的DOS中断;int86()*intr()则产生由该函
数的第一个参数(intno或int_type)所指定的中断。每一个intdos和int86都有一个x版
本,它除了使用通用寄存器及偏移值寄存器外,还将使用段寄存器,在Microsoft C里是不
能使用intr()函数的。
列表4.1中所给出的PRNOK.C显示了如何使用int86()函数来验证LPT1是否联机
的技术。
62页
列表4.1
/*prnok.c
Listing 4.1 of DOS Programmer'S ReferenCe*/
#include<cOnio.h>
#include<dOS.h>
#define PRN_INT 0x17/*Printer·serviCes interrupt */
#define STAT_RQ 0x02/*Status·request Service number*/
int prnok(void)
{
union REGS regs;
regS.h.ah=STAT_RQ; /*AH=02 for printer status*/
regs.x.dX=0; /*DX=00 for LPT1*/
int86(PRN_INT,&regs, &regs);
return (((regs.h.ah&0x80)==0x80)?1:0);
}
main()
{
if(prnok())
cputs("Ready to print!\n");
else
Cputs("Please cheCk the printer!\n");
}
有关BIOs打印机状态请求的说明
每种语言的第一个实例程序都使用BIOS Int 17h功能2来验证LPT1是否已联机。
对于这一功能,将2放入AH,而将打印机号(0为LPT1,1为LPT2,依此类推)放入DX。
此功能在返回后,AH内存有打印机的状态,放在AH中的各位含义如下:
位 意义(如果置上,即为1时)
0 time-out(超时)
1 unused(未用)
2 unused(未用)
3 I/Oerror(I/O错)
4 printer is selected(打印机已选)
5 out of paper(无纸)
6 Acknowledge(确认)
7 printer not busy(打印机不忙)
只有位0和位3至位7的意义已定义好,但是在两个已配置好的硬件上进行测试,在
相同的环境下,会返回不同的结果。在每个测试情况下,当打印机已加电并已联机的情况
下,AH的高位都已置上。但是,当一台Toshiba(东芝)P351打印机与一台IBM Personal
System/2 Model 50(PS/2 50型)计算机相连接时,当打印机已连接但没有加电时,程序会
报告出“Ready to print(已准备打印)”的信息。而将一台Epson的RX-O打印机与一台
COMPAQ便携式计算机相连时,程序则以“please check the printert”的信息作为响应(如
果打印机已连接但却没有加电时)。这只是一个演示性的例程;一个真正起作用的状态程
序则需要更复杂的逻辑。
63页
获取和设置文件属性
本节给出了两个C的实例程序,用于改变一个指定文件的属性。这两个程序类似于
Norton Utilities中的很有用的FA(文件属性)程序;不同的是,它们都只接收一个确定的
文件名,而不是包含有通配符的文件说明。为了保证程序尽可能地简单,程序也不包括查
询文件属性的选项或一次改变多于一个属性的选项。这两个程序在运行时,要求在命令行
上指定一个要设置或清除的属性来作为参数,如果给了一个不正确的参数,程序就会给出
一条出错信息,并终止运行。
如果读者已精于C编程,则不难增加更进一步的选项(如“查询”),以扩展此程序的
功能。本书中已提供了足够的信息,因而也很容易加进处理带有通配符文件名的功能。
这两个程序的不同之处在于:一个直接地调用了DOS中断,而另一个则使用了相应
的C函数来调用此中断。第一个例子在列表4.2中给出,它直接调用了DOS的Int 21h功
能43h。
列表4.2
/*Chmod.c
Listing 4.2 of DOS programmer'S Reference */
#include <coniO.h>
#inClude<Stdio.h>
#include<ctype.h>
#inClude<process.h>
#include<dOS.h>
#include"attrmask.h" /*omit for Turbo c 2.0 */
#define GS_FATTR 0x43
#define GET_FATTR 0x00
#define SET_FATTR 0x01
/*For Turbo C 2.0,add these #define statements:
define ARCHIvE_BIT FA_ARCH
define HIDDEN_BIT FA_HIDDEN
define RDONLY_BIT FA_RDONLY
define sYsTEM_BIT FA_sYSTEM
*/
typedef enum{clr,set} clrorset;
void showattr(int attr);
int parsearg(char*thearg,clPorset*actiOn,char*selection);
main(int argc,char*argv[])
{
extern char*sys_errlist[];/*provided by Turbo
eXtern int errnO; /*Ditto*/
union REGS regs;
struct SREGS sregs;
clrorSet action;
char SelectiOn;
unsigned attPib,setting;
int goahead;
if(argc==3)goahead=parsearg(argv[2],&action,&selection);
64页
if(!goahead)
{
if(argc=3)
{
CPUtS("Can't parse");
cputs(argv[2]);
}
else
cputS("No input");
exit(1);
}
switch(selection){
caSe 'A':setting=ARCHIVE_BIT;break;
caSe 'H':Setting=HIDDEN_BIT;break;
Case 'R':Setting=RDONLY_BIT;break;
caSe 'B':Setting=SYSTEM_BIT;break;
default:
cputS("Bad input:");cputS(argv[2];cputS("\n\r");
exit(1);
}
regS.h.ah=GS_FATTR;
regS.h.al=GET_FATTR;
regS.x.dx=(unsigned)argv[1];/* offset of first argument*/
sregS.dS=_DS;
/* - - - - - - - - - - - - - - - - - - - - -
For Microsoft C,use the following in place of the preceding line:
segread(&sregs);
*/
intdosx(®s,®s,&sregs);/*Get the current attribute wOrd*/
if(!regs.x.cflag){/*If carry is clear,success*/
attrib=regS.x.Cx;
cputs("- - - - - - - - - - - - Initial - - - - - - - - - - - - \n\r");
showattr(attrib);
if(action==clr){
setting=((-setting)&attrib);
}else{
setting=lsetting|attrib);
}
regs.h.ah=GS_FATTR;
cegs.h.al=SET_FATTR;
regs.x.cx=setting;
regs.x.dx=(unsigned)argv[1];
SregS.ds=_DS;
/* - - - - - - - - - - - - - - - - - - - - - -
For Microsoft C, use the following in place of the preceding line:
segread(&sregs);
*/
intdosx(®s,®s,&sregs);/*Set the attribute*/
attrib=regs.x.cx;
cputs(" - - - - - - - - - - - - Final - - - - - - - - - - - - - \n\r");
showattr(attrib);
}else{/*That is,if carry is not set*/
char*msg;
cputs("function 0x43 failed:");
65页
switch(regs.x.ax){
case 1: msg="Bad function code\n";break;
case 2: msg="Bad file name\n";break;
case 3: msg="Bad path\n";break;
case 5: msg="can't change attribute\n";break;
default: msg="Unknown cauSe\n";
}
cputs(msg);
}
}/*End main*/
Int 21h功能43h需要下列输入值:
寄存器 值
AH 43h
AL 0表示获取文件属性,1表示设置文件属性
DS:DX 段:文件路径名偏
该程序使用了#define处理指示符来“命名"DOs功能以及所需的操作(获敢或设
置)。文件名是由命令行参数提供的。
对于一个在Turbo C下以小内存模式编译的程序,应使用下列语句来将路径名的段
和偏移值放入DS和DX:
regs.x.dx=(unsigned)argv(1);
sregs.ds=_DS;
在其它的内存模式下儒要用到其它的赋值方法。因为MicrosoftC缺少寄器伪变量,
因此应使用下列语句来取而代之:
segread(&sregs);
如果操作成功,就会清除进位标志,并且在cx中包含有文件的属性字。下面的表描
述了属性字中的每一位以及所对应的属性含义:
位 含义(如果置位,为1时)
0 Read Only(只读)
1 Hidden(隐藏)
2 System(系统)
5 Archive(归档)
如果某位已设置,则此文件就具备所对应的属性(设置归档位表示此文件自从被创建
成上次修改后还未备份)。
为了使用方便和便于阅读,我们使用了#有define指示符来创建属性字中各个含义位
的位屏蔽字(这些定义包含在ATTRMASK.H文件中;参见以下的代码片断)。我们在程
序中使用位屏蔽字来读取和修改文件的属性字。
/*attmask.h-from chapter 4 of DOS Programmer's Reference*/
66页
#define ARCHIVE_BIT 0x20 /*Bit 5 of CX is the archive bit*/
#define SYSTEM_BIT 0x04 /*Bit 2 of CX is the system bit*/
#define HIDDEN_BIT 0x02 /*Bit 1 of CX is the hidden bit*/
#define RDONLY_BIT 0x01 /*Bit 0 of CX is the red-only bit*/
列表4.3及列表4.4则给出了第二个实例程序版本所使用的一些杂用功能。
列表4.3
/*parsarg.c
Listing 4.3 of DOS Programmer's Reference*/
#include