ITEEDU

学过C语言的朋友一定接触过fprintf函数的这样一个用法:

fprintf(STDERR,"DANGER!!!......Found a VIRUS......");

本来,在STDERR这个位置上应该是指向一个文件的指针,这个指针的含义和我们现在讨论的文件句柄是一样的(我想也许汇编语言中的文件句柄就是C语言中文件指针的实质,我没有查过权威资料,这个看法是否正确还有待检验)。现在我们在这个位置上放了一个称为STDERR的常量,而且我们知道这样使用fprintf函数可以使引号中的文字出现在显示器上而不是某个文件中,而显示器又恰好是一个硬件设备。由此看来,文件、文件指针和硬件设备确实应该存在某种联系,这种联系并非在汇编语言程序设计中有体现,在C这样的高级语言中已经体现出来了。那么STDERR又是个什么东西呢?

这个常量出现在名为STDIO.H的一个包含文件中,在C语言中它被称为"标准错误输出设备(STanDard ERRor output device)"C语言中还有两个比较常用的常量,分别为STDIN和STDOUT,即标准输入和标准输出设备。这三种设备通常都与键盘和显示器有关,使用fprintf函数从STDIN读入数据时相当于等待键盘输入;而向STDOUT或STDERR输出的内容都会出现在显示器上。这就有点儿象我们刚刚给出的那个汇编程序,在那个汇编程序中我们在输出数据时使用了一个特殊的句柄,结果导致了所有的文字都出现于显示器上而没有写入什么文件中。

看来我们所使用的那个特殊句柄代表的是显示器而不是文件。我们所使用的DOS系统还有这样一个有趣的功能不知道你是否使用过,那就是"重定向"功能。如果在DOS状态下按下面这样的格式使用DIR命令,大家就会发现文件和设备确实具有某种不寻常的联系:

C:\DOS\>DIR>FILE.LST

本来会出现在显示器上的一行行文件名都跑到一个名叫"FILE.LST"的文件中去了。由此我们设想这样一个结论:我们通过调用DOS的文件句柄功能不仅可以操作文件,同样可以操作一些硬件设备。

这个结论是完全正确的,事实上文件与设备本身就具有一些相似的特点:文件可以读入或写出,而硬件设备同样可以"读入"(键盘)和"写出"(显示器、打印机)。既然它们具有这样相同的特性,当然可以使用同样的形式来操作,这就是DOS为我们提供了那五个特殊句柄的原因。我们下面就来详细谈谈这五个特殊的句柄:

表8-3列出了这五个句柄所代表的硬件设备,其中0、1、2三个句柄是我们最常用的。这五种设备在DOS启动之后就已经"打开",所以我们可以直接使用这五个句柄而不必再编制代码将其打开。有关这五个特殊句柄还有一些更值得深思的地方,比如,我们能否使用3FH功能从句柄2所表示的设备中读入信息?能否使用40H功能向句柄3所表示的设备输出信息?我们能否用3EH功能关闭某个句柄所表示的设备?分析这样的问题也非难事,编个程序试一试即可。

句 柄 设备名称逻辑设备名 缺省设备
00 标准输入设备CON 键盘
01 标准输出设备CON 显示器
02 标准错误设备CON 显示器
03 标准辅助设备AUX 串行口
04 标准列表设备PRN 打印机

至此有关文件操作的几个重要功能--建立、打开、读写、关闭--我们都已经讨论,不是很全面,没有深入的地方还要大家自行研究。我们下面的任务就是来讨论文件句柄的一些辅助功能,以此做为这一内容的结束。

8.1.2 一些辅助功能

我们前面讨论过使用FCB功能可以读写文件中任意一个记录,只需在FCB中设置一些数据即可。而在讨论文件句柄功能时我也曾谈到我们所处理的文件以不再按照记录块和记录的形式划分,而是被看作"字节流"来处理。也就是说使用FCB功能时我们所能处理的最小单位是"记录",而使用句柄功能时我们所能处理的最小单位就是字节。由此我们可以想到既然使用FCB功能操作文件时我们可以通过指定当前或随机记录号等方式处理文件中的任意记录,那么使用句柄功能时我们也应能通过某种方法处理文件中的任一字节。这个方法就是我们将要讨论的内容。

在使用文件句柄功能时DOS虽然不再将文件划分为记录,但是它在读写文件时也要知道下一步所要处理的数据在文件中的具体位置。所以DOS在其内部使用了一个"文件读写指针"指向将要处理的数据。指针的概念大家想必已不再陌生,这里所说的"指针"其实就是一个双字(32Bit)数据,这个数据存在于操作系统内部,当我们打开一个文件后,DOS会将这个数据设置为零,而后它返回给我们一个句柄并等待接受其它功能调用。如果我们使用了3FH功能读取了32字节的数据,那么DOS就会修改这个"指针",把它增加32。如果我们再次使用3FH功能读取数据,那么DOS就会从指针指向的位置读取数据,同时再次增加指针的值。写操作时同样遵守这个规则。可以说,这个指针是DOS读写数据的重要依据,通过设置这个指针指向不同的位置,我们就可以让DOS做到"指哪儿打哪儿"。为了更好地理解这个概念,请看下面这个程序:

        data  segment
              assume    ds:data
       fname  db        'config.txt',0	;待处理的文件名
      buffer  db        ?	;定义一个字节的文件缓冲区
        data  ends
        code  segment
              assume    cs:code
        main  proc      far
              mov       ax,data	;初始化DS寄存器
              mov       ds,ax
			
              mov       dx,offset fname	;DX寄存器指向文件名的ASCIIZ串
              mov       ax,3d02h	;按"读/写"方式打开文件
              int       21h
              jc        exit	;若文件打开错误,转EXIT结束
			
              mov       bx,ax	;BX寄存器取得文件指针
      loop1:
              mov       ah,3fh	;准备读取数据
              mov       dx,offset buffer	;DX寄存器指向文件缓冲区
              mov       cx,1	;读入一个字节
              int       21h
              jc        exit	;若文件读取出错,转EXIT结束
              cmp       ax,cx	;确实读入了一个字节吗?
              jb        exit	;没有读入数据,转EXIT结束
			
              mov       ah,40h	;将数据写出到原文件中
              int       21h
              jc        exit	;若写文件出错,转EXIT
              jmp       loop1	;转LOOP1继续处理下一个字节
       exit:
              mov       ah,3eh	;关闭文件
              int       21h
              mov       ah,4ch	;结束进程
              int       21h
        main  endp
        code  ends
              end       main

如果大家打算试试自己的能力,可以在执行这个程序之前仔细读一下这个程序,想一想它会产生一个什么样的结果。

这个程序运行之后大家会发现名为CONFIG.TXT的文本文件被弄得一团糟。笔者的CONFIG文件的第一行原本是"[MENU]",结果被改成了"[[EEUU"。规律很容易发现,所有在偶数位置的字符都被前面的字符代替了。

我们一起来研究一下上面这个程序中的"循环"部分,可以看到我们在使用了3FH功能读取一个字节之后马上又将这个字节写入了文件。由于DOS在打开了CONFIG.TXT之后首先设置文件读写指针为零,这样我们在第一次使用3FH功能时读入内存的就是文件的第一个字符。请注意DOS在执行完3FH功能后要修改文件读写指针使其加1,因此在执行完第一个3FH功能后文件读写指针将指向文件中的第二个字符。而后我们马上又将刚读入的字符写入文件,写入时DOS同样要查看文件读写指针指向何处,恰好此时文件读写指针已经指向了第二个字节处,所以DOS就将刚读入的字符写入文件的第二个字节处,原来的数据就被覆盖了。

对于笔者的CONFIG文件来说,第一次读入内存的是字符"[","["读入后文件读写指针指向了"M",因此当程序将"["写入文件时,DOS就会把"["写入"M"所在的位置,所以"M"变成了"["。DOS完成写入功能后它还要修改文件读写指针,使其再加1,这样文件读写指针就指向了第三个字节,所以当程序第二次调用3FH功能读入数据时,我们实际读取的是文件的第三个字节,这个字节随后又被写入文件的第四个字节处。可以看出,由于读、写功能交替修改文件读写指针,使得程序每次读入奇数位置的字符并将其写入偶数位置,因而使"[MENU]"变成了"[[EEUU"。

以上就是"文件读写指针"在文件处理过程中所起的作用,由于它是起一个"定位"的作用,所以人为修改这个数据使其指向文件中的任意一个位置,我们就能存取从相应位置开始的数据。现在的问题就是如何才能修改这个数据?

DOS为我们提供了一个功能调用专用于修改文件读写指针,表7-1中包括了这个功能,下面给出这个功能更详细的说明:

功能号:42H
用 途:移动文件读写指针
参 数:BX--文件句柄
CX--偏移值的高16位;DX--偏移值的低16位
AL--方式代码
00H--偏移量从文件头开始(绝对移动)
01H--偏移量从当前位置开始(相对机动)
02H--偏移量从文件尾开始(绝对倒移)
调 用:INT 21H
返 回:如果成功,CF标志置0,DX:AX--文件读写指针所指向的新位置;
如果失败,CF标志置1,AX--错误代码

有一点很关键的地方就是CX:DX寄存器中所设置的32位数据究竟是何含义?我们说随AL寄存器中的方式代码不同,CX:DX寄存器中的数据含义也有区别。当AL寄存器中的方式代码为00H时,CX:DX代表了从文件"头部"开始的偏移位置。比如CX寄存器为0,DX寄存器为0020H,这时用42H功能就可以使文件读写指针指向文件中的第32个字节处;当AL寄存器中的方式代码为01H时,CX:DX代表了从"当前"位置开始的偏移量。还以CX=0,DX=0020H为例,若当前文件读写指针正指向文件偏移0020H处,此时用方式1移动文件指针时DOS会将文件读写指针"增加"20H,使其指向距离文件首部64字节处。可见在文件读写指针为0的情况下方式0与1有相同的效果。至于方式3,CX:DX代表的是距文件尾部的偏移量。