为了避免不必要的损失,我们在执行或跟踪这个程序之前需要讨论一下建立文件时要注意的一些东西。这个功能调用是在当前目录下新建一个目录项并使相应的FCB处于打开状态,所以使用这个功能后无需再用0FH功能。很重要的一点,如果我们建立的文件恰好和磁盘上已存在的某个文件重名,那么执行此操作后磁盘上己有的文件其长度将被置成0,如果对这个文件执行写操作,那么旧的内容将被新内容覆盖。所以在执行或跟踪这个程序之前请确认当前目录下没有CONFIG.SYS,否则下次启动系统时恐怕就要出麻烦。下面我们就来跟踪这个程序:
C:\ASM\>DEBUG FCB1.EXE[Enter] G=0 C[Enter] AX=1600 BX=0000 CX=004F DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=2589 ES=2579 SS=2589 CS=258C IP=000C NV UP EI PL NZ NA PO NC 258C:000C 0AC0 OR AL,AL -d0 24?当前驱动器为"C:" 文件名及扩展名 2589:0000 03 43 4F 4E 46 49 47 20-20 53 59 53 00 00 80 00 .CONFIG?SYS.... 2589:0010 00 00 00 00 46 21 CD 8B-40 02 42 00 00 61 B8 0F ...F!..@.B..a.. 2589:0020 00 00 00 00 00 .....
跟踪结果和0FH功能几乎一样,不过DOS返回到FCB中的数据有很多与0FH功能不同,比如建立文件的时间和日期,大家可以自己分析一下日期和时间,对对手表,看看是否一致。
继续将这个程序执行完,然后退出DEBUG,再次执行DIR操作,看看当前目录下是否出现了一个长度为0的新文件。
在扩展FCB的支持下,这个功能还可以建立具有特定属性的文件,继续修改FCB1.ASM,将数据段定义改为下面的形式(代码段不变):
DATA SEGMENT ASSUME DS:DATA MY_FCB LABEL BYTE DB -1 ;扩展的FCB起始 DB 5 DUP (0) ATTRIB DB 1 DRIVE DB 0 ;驱动器号,0:当前 1:A 2:B ... FILE_NAME DB 'CONFIG ' ;文件名 EXT_NAME DB 'SYS' ;扩展名 CUR_BLOCK DW ? ;当前记录块号 REC_SIZE DW ? ;记录长度 FILE_SIZE DW 2 DUP(?) ;文件长度,由系统填入 CREA_DATE DW ? ;建立或最后修改的日期,由系统填入 POSITION DB 10 DUP(?) ;保留空间,由系统填入 CUR_REC DB ? ;当前记录号 REL_REC DW 2 DUP(?) ;相对记录号 ERRMSG DB 'Error',0DH,0AH,24H DATA ENDS
运行这个程序,然后用DEL命令删除新建的CONFIG.SYS,看看有何结果。利用ATTRIB命令查一下这个文件,看看它是否具有"只读"属性。
打开与建立只是对文件作进一步处理的准备工作,对文件的读写操作才称得上是真正的文件处理。下面我们就来讨论DOS提供的文件读写功能。
利用FCB完成对文件的访问时,DOS一共提供了三类文件读写功能,第一类称为"顺序读写",第二类称为"随机读写",还有一类称为"随机块读写"。这三种功能各具特色,我们现在先讨论第一种方法--顺序读写。
观察FCB的结构,可以看到这样两个数据,第一个是位于FCB偏移0CH处的一个字,这个字指示了当前要处理的记录块号;另一个是位于FCB偏移20H的一个字节,这个字节指示了当前要处理的记录号。所谓顺序读写,指得就是在调用DOS的功能之前首先设置好这两个数据,DOS依靠这两个数据完成读写操作。我们现在来看这个功能的具体用法:
功能号:14H
用 途:顺序读文件
参 数:DS:DX指向打开的FCB
调 用:INT 21H
返 回:AL = 0 读成功
AL = 1 未读入数据,已经到了文件末尾
AL = 2 数据传输区超界
AL = 3 读入数据不足一个记录已到文件末尾
这个功能返回的信息较多,我们先来看看它是如何应用的。请看下例:
; 数据段定义与程序FCB1.ASM相同,在此省略 CODE SEGMENT ASSUME CS:CODE MAIN PROC FAR MOV AX,DATA ;初始化DS寄存器 MOV DS,AX MOV DX,OFFSET MY_FCB ;DX寄存器指向未打开的FCB MOV AH,0FH ;打开一个文件 INT 21H OR AL,AL ;AL寄存器返回0吗? JNZ ERR_EXIT ;未返回0,转ERR_EXIT结束 MOV AH,14H ;准备从文件中读取数据 MOV DX,OFFSET MY_FCB ;DX寄存器指向打开的FCB INT 21H ;读取文件 OR AL,AL ;AL寄存器返回0吗? JNZ ERR_EXIT ;未返回0,说明读取不正确,转ERR_EXIT MOV AH,4CH ;结束进程 INT 21H ERR_EXIT: MOV AH,9 ;输出错误信息 MOV DX,OFFSET ERRMSG INT 21H MOV AH,4CH ;结束进程 INT 21H MAIN ENDP CODE ENDS END MAIN
这个程序只供跟踪执行用,在跟踪这个程序之前请查看当前目录下是否存在CONFIG.SYS。下面是这个程序的跟踪结果
C:\ASM\>DEBUG FCB4.EXE[Enter] G=0 15[Enter] AX=1400 BX=0000 CX=005A DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=2589 ES=2579 SS=2589 CS=258C IP=0015 NV UP EI PL ZR NA PE NC 258C:0015 CD21 INT 21
我们一次性执行了读操作前的所有代码,此时文件已经打开了,我们可以观察一下打开后的FCB:
-d0 24?当前记录块号记录长度 2589:0000 03 43 4F 4E 46 49 47 20-20 53 59 53 00 00 80 00 .CONFIG?SYS.... 2589:0010 9B 02 00 00 38 21 4C 85-40 02 42 F4 04 61 B8 06 ....8!L.@.B..a.. 2589:0020 00 00 00 00 00 ..... 当前记录号
请注意打开后的FCB中当前记录块设置为0,当前记录也是0,在这种情况下我们要是执行读操作那么必然读入这个文件的第一块中第一个记录。记录长度我们取默认值128,执行一次读操作后就会有128字节数据被读进内存:
P AX=1400 BX=0000 CX=005A DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=2589 ES=2579 SS=2589 CS=258C IP=0017 NV UP EI PL ZR NA PE NC 258C:0017 0AC0 OR AL,AL
用P命令执行中断,如果被读取的CONFIG文件长度大于128字节,那么就应该看到AL寄存器返回了0,说明此时已有128字节数据正确地读入了内存中。那么这128字节读到内存的什么地方了呢?我们现在把ES:80H处的数据列出:
-DES:80 2579:0080 5B 4D 45 4E 55 5D 0D 0A-4D 45 4E 55 49 54 45 4D [MENU]..MENUITEM 2579:0090 3D 47 45 4E 45 52 41 4C-2C 57 49 4E 44 4F 57 53 =GENERAL,WINDOWS 2579:00A0 39 35 20 43 4F 4E 46 49-47 55 52 41 54 49 4F 4E 95 CONFIGURATION 2579:00B0 0D 0A 4D 45 4E 55 49 54-45 4D 3D 44 4F 53 2C 44 ..MENUITEM=DOS,D 2579:00C0 4F 53 20 50 52 4F 4D 50-54 20 43 4F 4E 46 49 47 OS PROMPT CONFIG 2579:00D0 55 52 41 54 49 4F 4E 0D-0A 4D 45 4E 55 49 54 45 URATION..MENUITE 2579:00E0 4D 3D 45 4D 4D 2C 44 4F-53 20 26 20 45 4D 53 20 M=EMM,DOS & EMS 2579:00F0 4D 45 4D 4F 52 59 20 43-4F 4E 46 49 47 55 52 41 MEMORY CONFIGURA
大家应该看到自己的CONFIG文件的内容。笔者在前言中说过,学习汇编语言对读者的记忆力是有要求的,这并非是笔者没事找事,如果现在问大家一句:"ES寄存器指向的段内有什么重要的数据?"大家能否不加思索地说出来呢?
如果大家能马上反应出ES寄存器指向PSP所在段,那么下面的讨论就很容易被大家所接受了。我们前面说过,PSP是DOS调入一个可执行程序时在内存中预存的一些重要的数据,PSP共有256个字节,其中后面的128个字节就是提供给文件处理功能使用的,我们把这128个字节的空间称为"缺省的DTA"。
所谓DTA,英文全文为"Data Trastor Area",翻译过来就是"数据传输区"。当我们进行文件读写操作时这操作系统就要使用DTA和应用程序交换数据,操作系统对DTA的要求是至少能够容纳下一个记录的数据。由于DOS默认一个记录长度为128字节,所以在我们这个示例程序中我们使用了系统默认的DTA。如果程序改变了记录的长度,那么就必须设定新的DTA。相应的系统功能我们将在讨论完这个示例程序后介绍。
执行了"顺序读"操作后我们来看看FCB有什么变化:
-d0 24[Enter] 2589:0000 03 43 4F 4E 46 49 47 20-20 53 59 53 00 00 80 00 .CONFIG?SYS.... 2589:0010 9B 02 00 00 38 21 4C 85-40 02 42 F4 04 61 B8 06 ....8!L.@.B..a.. 2589:0020 01 00 00 00 00? ..... 当前记录号已改变
列出新的FCB,我们可以看到"当前记录号"由0变成了1,操作系统自动将记录指针指向了下一个记录。如果我们继续使用"顺序读"功能,那么文件的下一个记录将被送至DTA,刚才读入的数据将被覆盖。所以如果我们想要读入文件的所有数据,就可以利用一个循环,边读入数据边处理。这样做虽可行,但它并非是一个最佳的方案,我们一般的作法是设置更大的DTA取代缺省DTA,这样我们就有可能一次读入文件中所有的数据来处理,请看程序FCB5.ASM:
;数据段定义与程序FCB1.ASM相同,此处省略 CODE SEGMENT ASSUME CS:CODE MAIN PROC FAR MOV AX,CS ;设置DS寄存器指向代码段 MOV DS,AX MOV DX,OFFSET BUFFER ;DX寄存器指向新的DTA MOV AH,1AH ;设置一个新的DTA缓冲区 INT 21H MOV AX,DATA ;重新设置DS寄存器指向数据段 MOV DS,AX MOV DX,OFFSET MY_FCB ;DX寄存器指向未打开的FCB MOV AH,0FH ;打开一个文件 INT 21H OR AL,AL ;打开操作正确吗? JNZ ERR_EXIT ;没有正确打开文件,转ERR_EXIT结束 MOV BX,DX ;BX指向打开的FCB MOV AX,[BX+10H] ;从FCB中取得文件长度 MOV WORD PTR [BX+0EH],AX ;设置记录长度等于文件长度 MOV AH,14H ;从文件中读入一个记录 MOV DX,OFFSET MY_FCB INT 21H OR AL,AL ;正确读入数据了吗? JNZ ERR_EXIT ;没有正确读入数据,转ERR_EXIT MOV AH,4CH ;结束进程 INT 21H ERR_EXIT: MOV AH,9 ;输出错误信息 MOV DX,OFFSET ERRMSG INT 21H MOV AH,4CH ;结束进程 INT 21H MAIN ENDP BUFFER LABEL BYTE ;在代码段中设置新的DTA CODE ENDS END MAIN
在此例中我们使用了一个新的系统功能--1AH,这个功能用于设置应用程序自己的DTA,用法很简单:
功能号:1AH
用 途:设置应用程序的DTA
参 数:DS:DX = 应用程序的DTA起始"段:偏移"地址
调 用:INT 21H
返 回:无
使用这个功能之后,DOS就不再使用缺省的DTA了。之后所有的磁盘服务都使用新的DTA,直至应用程序设置了另一个DTA为止。我在设计这个程序时用了一点小技巧,当应用程序打开一个文件后,DOS会把有关这个文件的一些特征信息存入FCB中。其中FCB偏移10H的四个字节(我们习惯称为一个"双字")填入了文件的长度,我的CONFIG文件长度只有667字节,所以我将这个双字的低16位取出作为一个记录的长度存入FCB偏移0EH处,这样一来一个记录的长度恰好等于文件的长度,所以应用程序只需顺序读一次就能读入整个文件的内容。
当然,使用这样的技巧是有限制的,那就是被处理的文件长度要小于64KB,这是因为DOS只用一个字表示记录的大小。如果需要处理更大的文件,则可根据需要设置合适的记录长度和DTA。
这个程序中没有使用DB伪指令定义DTA缓冲区,而是将DTA设置在代码段中,并起始于代码段的后面,这样做的目的是为了缩短编译后可执行文件的长度。大家也可以在数据段中用于DB伪指令定义一个数千字节的DTA空间,看看编译后成EXE文件的长度和这个程序有无差别,差别有多大。
可以肯定地说这个示例程序的长度一定要比使用DB伪指令的程序短。当然这样做还有另一个原因,那就是我们需要让这个程序适应各种长度的文件,所以我们要定义一个尽可能大的DTA。这个技巧同样可以应用到COM类程序,不过需要很好地保护程序的堆栈才行,方法很简单,将SP寄存器设成100H即可。
如何使用这个程序,不必多问,在DEBUG环境下运行这个程序,而后用D命令观察读入的数据,这样即可很好地理解1AH功能与程序设计技巧。在上学时老师总爱让我们总结学习方法,笔者不敢妄言自称是大家的老师,但还是希望大家也能自觉地研究一下我们讨论程序设计的思路,即使只是在脑子中总结也行。