【简介】 PC中最主要的难题之一,也是最容易引起误解的,就是系统调用。系统调用所代表的那些函数实际上是计算机的所有底层操作——屏幕和磁盘的控制,键盘和鼠标的控制,文件系统的管理,时间,打印,这些只不过是系统调
PC中最主要的难题之一,也是最容易引起误解的,就是系统调用。系统调用所代表的那些函数实际上是计算机的所有底层操作——屏幕和磁盘的控制,键盘和鼠标的控制,文件系统的管理,时间,打印,这些只不过是系统调用所实现的一部分功能。
总的来说,系统调用往往涉及到BIOS(基本输入输出系统)。实际中有好几种不同的BIOS,例如主板的BIOS负责初始硬件检测和系统引导,VGA BIOS(如果有VGA卡的话)处理所有的屏幕处理函数,固定磁盘BIOS管理硬盘驱动器,等等。DOS是位于这些低级BIOS之上的一个软件层,并且提供了进入这些低级BIOS的基本接口。一般说来,这意味着有一个DOS系统调用可以调用几乎所有你想使用的系统功能。实际上,DOS将调用相应的一种低级BIOS来完成所要求的任务。在本章中,你将会发现你既可以调用DOS来完成一项任务,也可以直接调用低级BIOS来完成相同的任务。
14.1 怎样检索环境变量(environment variables)的值?
ANSI C标准提供了一个名为getenv()的函数来完成这项任务。getenv()函数很简单一把指向要查找的环境串的指针传递给它,它就返回一个指向该变量值的指针。下面的程序说明了如何从C中获得环境变量PATH的值:
# include <stdlib. h>
main(int argc, char * * argv)
{
char envValue[l29]; / * buffer to store PATH * /
char * envPtr = envValue ; / * pointer to this buffer * /
envPtr = getenv("PATH"); /* get the PATH */
printf ("PATH= %s/n" , envPtr) ; / * print the PATH * /
}
如果你编译并运行了这个程序,你就会看到与在DOS提示符下输入PATH命令完全相同的结果。事实上,你可以用getenv()检索AUTOEXEC.BAT文件中的或者系统引导后在DOS揭示符下输入的所有环境变量的值。
这里有一个小技巧。当运行Windows时,Windows设置了一个名为WINDIR的新的环境变量,它包含了Windows目录的路径全名。下面这段简单的程序用来检索这个串:
# include <stdlib. h>
main(int argc, char * * argv)
{
char envValue[l29];
char * envPtr = envValue ;
envPtr = getenv("windir");
/ * print the Windows directory * /
printf("The Windows Directory is %s/n" , envPtr);
}
这个程序还可以用来判断当前是否正在运行Windows,以及DOS程序是否运行在一个DOS shell下,而不是运行在“真正的"DOS下。注意,程序中的windir字符串是小写——这一点很重要,因为它对大小写是敏感的。如果你使用WINDIR,getenv()就会返回一个NULL串(表示变量未找到错误)。
用一putenv()函数也可以设置环境变量。但要注意,该函数不是一个ANSI标准函数,在某些编译程序中它可能不以这个名字出现,或者根本就不存在。你可以用一putenv()函数做许多事情。实际上,在上面那个例子中,Windows正是用这个函数创建了windir环境变量。
请参:
14.2 怎样在程序中调用DOS函数?
14.3 怎样在程序中调用BIOS函数?
14.2 怎样在程序中调用DOS函数?
其实,当调用printf(),fopen(),fclose(),名字以一dos开始的函数以及很多其它函数时,都将调用DOS函数。Microsoft和Borland还提供了一对名为int86()和int86x()的函数,使你不仅可以调用DOS函数,还可以调用其它低级函数。用这些函数可以跳过标准的C函数而直接调用DOS函数,这常常可以节省你的时间。下面的例子说明了如何通过调用DOS函数,而不是getch()和printf()函数,从键盘上得到一个字符并将其打印出来(该程序需要在大存储模式下编译)。
# include <stdlib. h>
# include <dos. h>
char GetAKey(void);
void OutputString(char * );
main(int argc, char * * argv)
{
char str[l28];
union REGS regs;
int ch;
/ * copy argument string; if none, use "Hello World" * /
strcpy(str, (argv[1]== NULL ? "Hello World": argv[1])),
while ((ch = GetAKey()) ! =27){
OutputString(str);
}
}
char
GetAKeyO
{
union REGS regs;
regs.h. ah = 1; /* function 1 is "get keyboard character" * /
int86(0x21, ®s, ®s);
return( (char)regs. h. al) ;
}
void
OutputString(char * string)
{
union REGS regs;
struct SREGS segregs;
/ * terminate string for DOS function * /
* (string + strlen(string)) = '$';
regs.h. ah = 9; / * function 9 is "print a string" * /
regs.x. dx = FP_OFF(string) ;
segregs. ds= FP_SEG(string) ;
int86x(0x21, ®s, ®s, &segregs);
}
上例创建了两个函数来代替getch()和printf(),它们是GetAKey()和OutputString()。实际上,函数GetAKey()与标准c函数getche()更为相似,因为它与getche()一样,都把键入的字符打印在屏幕上。这两个函数中分别通过int86()(在GetAKey()中)和int86x()(在OutputString()中)调用DOS函数来完成所要求的任务。
可供函数int86()和int86x()调用的DOS函数实在太多了。尽管你会发现其中许多函数的功能已经被标准的C函数覆盖了,但你也会发现还有许多函数没有被覆盖。DOS也包含一些未公开的函数,它们既有趣又有用。DOS忙标志(DOS Busy Flag)就是一个很好的例子,它也被称作InDos标志。DOS函数34H返回指向一个系统内存位置的指针,该位置包含了DOS忙标志。当DOS正忙于做某些重要的事情并且不希望被调用(甚至不希望被它自己调用)时,该标志就被置为1;当DOS不忙时,该标志将被清除(被置为O)。该标志的作用是当DOS正在执行重要的代码时,把这一情况通知DOS。然而,该标志对程序员也是很有用的,因为他们能由此知道什么时候DOS处于忙状态。尽管从DOS 2.0版开始就有这个函数了,但因为Microsoft最近已经公开了这个函数,所以从技术角度上讲它已不再是一个未公开的函数。有几本很不错的书介绍了已公开和未公开的DOS函数,对这个问题有兴趣的读者可以去阅读这些书。
请参见:
14.3 怎样在程序中调用BIOS函数?
14.3 怎样在程序中调用BIOS函数?
与前文中的例子一样,在使用象一setvideomode()这样的函数时,将频繁地调用BIOS函数。此外,就连前文例子中使用过的DOS函数(INT 21H,AH=01H和INT 21H,AH=09H)最终也要通过调用BIOS来完成它们的任务。在这种情况下,DOS只是简单地把你的DOS请求传给相应的低级BIOS函数。下面的例子很好地说明了这一事实,该例与前文中的例子完成相同的任务,只不过它完全跳过了DOS,直接调用了BIOS。
# include <stdlib. h>
# include <dos. h>
char GetAKey(void) ;
void OutputString( char * );
main(int argc, char * * argv)
{
char str[128];
union REGS regs;
int ch;
/ * copy argument string; if none, use "Hello World" * /
strcpy(str, (argv[1] == NULL ? "Hello World" : argv[1]));
while ((ch = GetAKeyO) !=27){
OutputString(str);
}
}
char
GetAKey()
{
union REGS regs;
regs. h. ah = 0; /* get character */
int86(0xl6, &xegs, ®s);
return( (char)regs. h. al) ;
}
void
OutputString(char * string)
{
union REGS regs;
regs. h. ah = 0x0E; /* print character * /
regs. h. bh = 0;
/ * loop, printing all characters * /
for(; * string !='/0'; string+ + ){
regs. h. al= * string;
int86(0xl0, ®s, ®s);
}
}
你可以发现,唯一的变化发生在GetAKey()和OutputString()自身之中。函数GetAKey()跳过了DOS,直接调用键盘BIOS来获得字符(注意,在本例这个调用中,键入的字符并不在屏幕上显示,这一点与前文中的例子不同);函数OutputString()跳过了DOS,直接调用了Video BIOS来打印字符串。注意本例效率不高的一面——打印字符串的C代码必须位于一个循环中,每次只能打印一个字符。尽管Vidoeo BIOS支持打印字符串的函数,但C无法存取创建对该函数的调用所需的所有寄存器,因此不得不一次打印一个字符。不管怎样。运行该程序可以得到与前文例子相同的输出结果。
请参见:
14.2 怎样在程序中调用DOS函数?
14.4 怎样在程序中存取重要的DOS内存位置?
与DOS和BIOS函数一样,有很多内存位置也包含了计算机的一些有用和有趣的信息。你想不使用中断就知道当前显示模式吗?该信息存储在40:49H(段地址为40H,偏移量为49H)中。你想知道用户当前是否按下了Shift,Ctrl或Alt键吗?该信息存储在40:17H中。你想直接写屏吗?单色显示(Monochrome)模式的视频缓冲区起始地址为B800:O,彩色文本模式和16色图形模式(低于640×480 16色)的视频缓冲区起始地址为B8000:0,其余标准图形模式(等于或高于640×480 16色)的视频缓冲区起始地址为A000:O,详见14.8。下面的例子说明了如何把彩色文本模式的字符打印到屏幕上,注意它只是对前文中的例子做了一点小小的修改。
# include <stdlib. h>
# include <dos. h>
char GetAKey(void) ;
void OutputString(int, int, unsigned int, char * );
main (int argc, char * * argv)
{
char str[l28];
union REGS regs;
int ch, tmp;
/ * copy argument string; if none, use "Hello World" * /
strcpy(str, (argv[1] == NULL ? "Hello World" : argv[1]));
/ * print the string in red at top of screen * /
for(tmp = 0;((ch = GetAKeyO) ! = 27); tmp+=strlen(str)) {
outputString(0, tmp, 0x400,str);
}
}
char
GetAKey()
{
union REGS regs;
regs. h. ah = 0; / * get character * /
int86(0xl6, ®s, ®s);
return((char)regs. h. al);
}
void
OutputString(int row, int col, unsigned int video Attribute, char * outStr)
{
unsigned short far * videoPtr;
videoPtr= (unsigned short far * ) (0xB800L <<16);
videoPtr + = (row * 80) + col; /* Move videoPtr to cursor position * /
videlAttribute & = 0xFF00; / * Ensure integrity of attribute * /
/ * print string to RAM * /
while ( * outStr ! = '/0'){
/ * If newline was sent, move pointer to next line, column 0 * /
if( (* outStr == '/n') || (*outStr == 'V') ){
videoPtr + = (80- (((int)FP-OFF(videoPtr)/2) % 80));
outStr+ + ;
continue;
}
/ * If backspace was requested, go back one * /
if( *outStr = = 8){
videoPtr -- ;
outStr++ ;
continue;
}
/* If BELL was requested, don't beep, just print a blank
and go on * /
if ( * outStr = = 7) {
videoPtr+ + ;
outStr++ ;
continue ;
}
/ * If TAB was requested, give it eight spaces * /
if ( * outStr == 9){
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
* videoPtr++ = video Attribute | ' ' ;
outStr+ + ;
continue;
}
/ * If it was a regular character, print it * /
* videoPtr = videoAttribute | (unsigned char) * outStr;
videoPtr+ + ;
outStr + + ;
}
return;
}
显然,当你自己来完成把文本字符打印到屏幕上这项工作时,它是有些复杂的。笔者甚至已经对上例做了一些简化,即忽略了BELL字符和一些其它特殊字符的含义(但笔者还是实现了回车符和换行符)。不管怎样,这个程序所完成的任务与前文中的例子基本上是相同的,只不过现在打印时你要控制字符的颜色和位置。这个程序是从屏幕的顶端开始打印的。如果你想看更多的使用内存位置的例子,可以阅读20.12和20.17——其中的例子都使用了指向DOS内存的指针来查找关于计算机的一些有用信息。
请参见:
14.5 什么是BIOS?
20.1 怎样获得命令行参数?
20.12 怎样把数据从一个程序传给另一个程序?
20.17 可以使热启动(Ctrl+Alt+Delete)失效吗?
14.5 什么是BIOS?
BIOS即基本输入输出系统,它是PC机的操作的基础。当计算机上电时,BIOS是第一个被执行的程序,DOS和其它程序都通过BIOS来存取计算机内部的各种硬件设备。
然而,引导程序并不是计算机内唯一被称为BIOS的代码。实际上,PC机上电时要执行的BIOS通常被称为主板BIOS,因为它被存放在主板上。直到不久之前,这个BIOS还被固化在一块ROM芯片上,因而无法为了修改错误和扩充功能而重新编写它。现在,主板BIOS被存放在一块叫做Flash EPROM的可重新编程的存储器芯片中,但它还是原来的BIOS。不管怎样。主板BIOS会读遍系统内存,从而找到系统中其它一些硬件设备,这些设备都带有自身要使用的一些基础代码(即其它的BIOS代码)。例如,VGA卡就有其自身的BIOS,通常被称为Video BIOS或VGA BIOS;硬盘和软盘控制器也有一个BIOS,并且也在系统引导时被执行。当人们提及BIOS时,或者是指这些程序的集合,或者是指其中单独的一个BIOS,这两种说法部对。
根据上述介绍,你应该知道BIOS并不是DOS——BIOS是PC机中最底层的功能软件。DOS刚好位于BIOS上面的一层,并且经常调用BIOS来完成一些基本操作,而这些操作可能会被你误认为是"DOS"函数。例如,你可能会用DOS函数40H来把数据写到硬盘上的一个文件中,而DOS最终还是要通过调用硬盘BIOS的函数03来把数据写到硬盘上。
请参见:
14.6 什么是中断?
14.6 什么是中断?
首先,中断分硬件中断和软件中断两种。中断为计算机的硬件设备和软件"部件"提供了一种相互交流的途径,这就是它的作用。那么,都有哪些中断呢?它们又是怎样实现这种交流的呢?
PC机中的CPU通常都是Intel 80x86处理器,它有几条引脚用来中断CPU的当前工作,并使它转去进行其它工作。每条中断引脚上都连接着一些硬件设备(例如定时器),其作用是为这条引脚提供一个特定的电压。当中断事件发生时,处理器会停止执行当前正在执行的软件,保存当前的操作状态•然后去“处理”中断。处理器中事先已经装有一张中断向量表,其中列出了每个中断号以及当某个特定中断发生时所应执行的程序。
以系统定时器为例——作为要完成的许多任务中的一部分,PC机需要维持一天的计时工作,其具体工作过程为:(1)一个硬件计时器每秒钟向CPU发出18次中断;(2)CPU停止当前的工作并在中断向量表中查找负责维持系统计时器数据的程序(这种程序叫做中断处理程序(interrupt handler),因为它的工作就是在中断发生时处理中断);(3)CPU执行该程序(将新的定时器数据存入系统内存),然后返回到刚才被中断的地方继续往下执行。当你的程序要求使用当前时间时,定时器数据就会按照你要求的格式被组织好并传给程序。以上的解释大大简化了定时器中断的工作情况,但它是一个很好的硬件中断的例子。
系统定时器只是通过中断机制发生的数百个事件(有时被称为中断)中的一个。在很多时候,硬仵并不参与到中断处理过程中去。换句话说,软件经常会通过中断来调用其它软件,并且可以不需要硬件的参与。DOS和BIOS就是这方面的两个主要例子。当一个程序打开一个文件,读/写一个文件,把字符写到屏幕上,从键盘那里得到一个字符,甚至询问当前时间时,都需要有一个软件中断来完成这项任务。你可能不知道发生了这些事情,因为这些中断都深藏在你所调用的那些无足轻重的小函数(例如getch(),fopen()和ctime())的后面。
在C中,你可以通过int86()和int86x()函数产生中断。int86()和int86x()函数要求用你想产生的中断号作为它们的一个参数。当你调用其中的一个函数时,CPU将象前面所讲的那样被中断,并俭查中断向量表,以找到需要执行的那个程序。在调用这两个函数时,通常将执行的是一个DOS或BIOS程序。表14.6列出了一些常见的中断,你可以通过它们设置或检索计算机的有关信息。注意这并不是一张完整的表,并且其中的每个中断都可以服务于数百种不同的函数。
表14.6 常见的PC中断
—————————————————————————————————————
中断(hex) 描述
————一————————————————————————————————
5 屏幕打印服务
10 视频显示服务(MDA,CGA,EGA,VGA)
11 获得设备清单
12 获得内存大小
13 磁盘服务
14 串行口服务
15 杂项功能服务
16 键盘服务
17 打印机服务
1A 时钟服务
21 DOS函数
2F DOS多路共享服务
33 鼠标器服务
67 EMS服务
--------------------------------------------------------------------------
当你知道了什么是中断后,你就会认识到:当计算机处于空闲状态时,它每秒可能要处理几十个中断;而当计算机紧张工作时,它每秒经常要处理数百个中断。在20.12中有一个例子程序,你可以参照该程序写出自己的中断处理程序,从而使两个程序通过中断进行交流。如果你觉得有意思,不妨试一下。
请参见:
20.12 怎样把数据从一程序传给另一个程序?
14.7 使用ANSI函数和使用BIOS函数,哪种方式更好?
两种方式各有利弊。你必须先回答几个问题,然后才能确定哪种方式适合你需要创建的那种应用。例如:你需要很快地实现你的应用吗?你的应用仅仅是用来“证实有关概念”,还是一个“真正的应用”呢?速度对你的应用重要吗?下面比较了使用ANSI函数和使用BIOS函数的基本优点:
使用ANSI函数的优点:
只需要printf()语句就可完成任务
改变文本的颜色和属性很方便
不管系统如何配置,都可以在所有PC机上工作
无需记忆BIOS iN数
使用BIOS函数的优点:
运行速度快
用BIOS可以做更多的事
不需要设备驱动程序(使用ANSI iN数需要ANSI.SYS)
无需记忆ANSI命令
刚开始时,你会发现用ANSI函数编程是很不错的,并且能使你写出一些漂亮的程序。然而,不久你就可能会发现ANSI函数“有些碍事”,此时你就会想用BIOS函数。当然,以后你又发现BIOS函数有时也会“碍事”,此时你就想使用一种更快的方式。例如,14.4中的一个例子甚至不通过BIOS来把文本打印到屏幕上,你也许会发现这种方法比使用ANSI或BIOS函数更有趣。
请参见:
14.4 怎样在程序中存取重要的DoS内存位置?