第10章程序和内存管理 为了“使作业做完”,程序员已开发了许多装置和设备来从程序中获得内存所能包含 的更多信息。程序的连接、覆盖和其它技术长期以来一直是程序员技巧的主要成分。例如, 我们第一个使用的系统是只有32K内存的IBM 7040;有些程序没有覆盖技术就不能运 行(参见本节覆盖技术的讨论)。 特殊化的技术能转变成调试时讨厌的东西,使程序高度地不可移植。因为大的系统可 以有增加的内存和处理机速度,所以计算机程序员已开发了一些技术来从其它程序内部 处理程序。命令外壳,如COMMAND.COM就采用这些技术来按需要执行程序。 在一个DOS系统中,可以使用COMMAND.COM所使用的相同技术来从一个程序 的内部控制另一个程序的执行(这种能力与在UNIX系统中提供的性能相似)。这种操纵 程序功能的方式是相对清楚的。系统的重要模式能以单一标准的程序的形式存在。每个 这样的程序都能作为一个独立的实体而完整地测试和调试。UNIX程序员已经发现这项 技术对于程序开发是极为有效的。 本章先介绍有多少内存可发挥作用,当需要时怎样获得更多内存以及怎样让DOS保 存不需要的内存。当在高级语言中工作时,编译程序管理着这个过程。而在汇编语言中, 就应该明确要对内存做些什么。本章还要看看扩展内存和扩充内存并检查一下能对它们 做什么。 然后讨论程序执行——一个进程(父进程)怎样执行另一个进程(子进程)然后重新获 得控制。下面给出了一个简单的实例来说明这个过程的工作方式。 最后解释一种特殊程序—TSR(终止和驻留)。TSR在许多方面与“通常的”程序 (应用程序如字处理程序和电子表格)不同。最明显的是TSR停止后,它们仍留在内存中, 并且不会被覆盖。TSR能以许多途径从另一个程序获得控制,其中最重要的是涉及一个 中断(见第11章“中断处理程序”)。本章将视野放在要遵循的程序上。 然后,让我们继续看看内存管理。 〈程序覆盖〉 程序覆盖是在内存中没有稳定地保持住的一部分代码或数据。该程序可能 以许多二元图象的形式存在,其中每个图象都操纵一些特异的功能。主程序段操 纵了整个坐标,并且常常拥有覆盖所需的功能。 在一个有覆盖的典型程序中,主菜单和普通功能在主覆盖块中,后者总是保 204页 存在内存中。无论程序何时需要一个子菜单,覆盖就安装在内存中的一个拥有覆 盖代码的区域内。然后主模式将控制传递给覆盖块,子菜单及其功能就能操作 了。 用户结束了菜单,返回主菜单时,控制就传递回一级覆盖。因为子菜单的覆 盖在内存中不再需要,所以其内存空间能被另一个覆盖所使用(子菜单的程序代 码被新代码覆盖)。 10.1内存的工作方式 基本的PC机或兼容机都有一个拥有1M内存的地址空间(记住一兆的字节是 1024K)。第3章“动态的DOS”指出该内存中只有640K能用于程序使用。剩下的384K分 配给ROM BIOS、显示适配器和盒式磁盘。 但是较低的640K内存不只用于用户程序。其中,1024个字节的中断向量表由处理器 保存。只有中断向量在这些位置上,才能硬连线到处理器中(见第11章)。然后是BIOS和 DOS参数表、DOS内核、系统驱动程序,最后是命令处理程序的驻留部分。所有这些内存 都来自内存的用户区域。 要计算失去的内存数量是很困难的,因为它要依据所使用的系统和巳安装的驱动程 序而定。安装的第一批用户程序(在内存图中)是TSR如Borland的SidekicK(10.1)是 已安装的TSR所使用的内存图示)。必须从有效空间中减去它们的内存使用。如果增加一 个与DESQview相似的窗口包,就只有大约350K内存可能从640K的起点上保存下来 ——并且没有真正地启动程序去做什么! 启动之后剩下的空间(通常用于安装Sidekick,大约500K到550K)是空着的,并且可 以使用。这个空间叫作暂用程序区域(TPA),这个名称也用在CP/M系统的等价区域上。 这个名称是合适的,因为用户程序在这个区域中暂驻——它们来或者去都是依据在系统 中工作的用户的需要而定的。 PC机产生时,机器的内存640K的限制对于一个工作区域看起来是有限的。当时,具 备640K内存的PC是标准的;那些拥有128K内存及少数拥有256K内存的人是令人嫉 妒的。随着需求的增长,640K限制变成了一条非常有趣的笑话——它成了重要的系统限 制。Murphy法则的变化用于计算机内存:“不论你有多少,你总是需要更多的”。就成了一 个大笑话。 有了80286芯片之后,PC系统就可能拥有16M的内存(有人说:“万事大吉”,其它人 则说:“现在我们有了一个更重要的系统”)。但DOS不能以相应的操作来利用这个内存。 超过1M限制的内存,已知是扩展内存,它对于80286处理器和后来的模型都是有用的, DOS却不能使用它;要访问这种内存,80286芯片必须在保护方式中工作,DOS做不到这 一点。在DOS下面,只有特别编写的、能够打开CPU方式的程序才能使用扩展内存。 205页 图10.1已安装的TSR所使用的内存 〈保护方式〉 80286和80386处理器所具有的保护方式能访问控制多任务操作的特殊处 理器功能。实地址方式提供8088/8086处理器只用来访问1M内存的同样的环 境。将这个处理器移到保护方式中(通常需在一个操作系统中进行),该系统能控 制多个程序在内存中的操作以及从一个任务到另一个任务的切换。 本章不会详细介绍保护方式的性能,因为DOS不使用这种方式。要了解保 护方式的更多信息,可查阅有关80286/80386 汇编语言程序设计方面的书籍。 DOS在处理器的实地址方式中运行;于是1M以上的内存对于那些通过普通的DOS 功能来运行的程序来说是不可访问的。即使当一个系统有额外内存时,用户也不能有效地 使用它。 尽管一些程序如DESQview能将它们的一些操作码插进使用80286扩展内存(像磁 盘一样)的RAM——磁盘驱动程序内,该内存对于大多数程序还是不能访问的(程序必 须在访问内存并完全返回实地址方式之前将处理器切换到保护方式)。 RAM磁盘在扩展 内存中操作时,它们也不必那么快,因为处理器从实地址方式切换到保护方式并返回来获 206页 得数据——这个过程可能相对较慢。在80286处理器上,换到硬盘可能更快一些。 自从产生了个人计算机的AT型号以来,BIOS提供了两个功能来帮助程序确定有多 少内存是有用的(Int 15h,功能88h),然后将数据块移向或离开1M标记以上的扩展内存 (Int 15h,功能87M。这些例程的使用会带来严重的问题,因为没有对扩展的内存的空间 进行管理。一个程序能很容易地在另一个程序(或一个RAM——磁盘驱动程序)保存在 扩展内存空间中的数据上进行写操作;没有什么能说明这个问题或报告这个错误。 扩充内存的第一次出现,是作为一个联合工作组提出的。该工作组由Louts和Intel 在1985年春季的COMDEX展示会介绍给人们的(3.0版本),它提供了一条途径,允许访 问8M那么大的内存,而不需要在处理器方式中进行特别的移动。扩充内存使处理器能通 过16K页来访问额外的内存,该页能映射进640K与1M之间的内存空间里未用的区域 之中。4个16K页映射进64K框架,其位置由安装板时用户的系统决定(见第3章“动态 的DOS”,那里深入地讨论了扩充内存)。 扩充内存管理(EMM)使扩充内存活动起来像一个带有句柄的文件。当程序在扩充内 存中请求空间时,EMM就将这个空间放在一边,并返回能用来获得对该空间进行访问的 一个独一无二的“句柄”。 要想从扩充内存中获得16K页,可以使用Int 67h来调用EMM,并告诉它将该页放 进页框架之中。然后就能直接从程序中定位此内存。图10.2显示了所发生的事情。 图10.2扩充内存的访问 将下列任意一行加到CONFIG.SYS文件中就能安装EMM.SYS(扩充内存管理器 驱动程序): DRIVER=EMM.SYS DRIVER=EMM386.SYS 这些驱动程序运行起来部分像一个真实的驱动程序(真实的驱动程序在第12章“设 备驱动程序”中介绍;目前,就先认为扩充内存管理器驱动程序不能正常工作)。不能像对 待其它驱动程序那样去访问EMM.SYS或EMM386.SYS,而是通过Int 67h来访问它的 207页 功能。可参看“DOS参考手册”一节中有关Int 67h各种功能的详细列表。 EMM.SYS包括以下这些有用功能: ·报告扩充内存的状态 ·在扩充内存中分配页 ·在扩充内存中释放分配页 ·诊断 ·多任务支持 ·将物理页映到扩充内存中,变成分配给程序的逻辑页。 这种内存分页技术长期以来用于计算机中。当有扩充内存可用时,没有理由不去利用 它。 扩充内存产生后不久,Microsoft宣布它支持该标准的3.2版本,其中包括了对于多 任务操作系统有用的设备。3.2版本变成了LIM EMS(Lotus-Intel-Microsoft扩充内存规 范)。Ashton-Tate, Ast Research和Quandram注意到这种标准的局限性:只能将一个16K 页映射到内存的一个未使用区域之中。那么为什么不将来自TPA的内存的大区域映射 进去呢?这就引导人们开发了增强的扩充内存规范(EEMS)。 用增强的扩充内存就能将一个完整的程序移到扩充内存区域中并代替另一个程序。 这种功能意味着一台PC机能变成多任务的系统。1988年产生的LIM4.0,除了包含旧的 LIM 3.2以外,还包括EEMS。所有涉及扩充内存技术的公司都在一定程度上支持这个新 的LIM,尽管并非所有的公司都执行了该标准中要求的所有功能。 只要我们使用基本的PC机,就必须学会与分配给我们的640K的空间区域一起生 活。当然,新的机器型号突破了这个限制。 10.2内存管理 DOS之下的内存管理涉及到TPA中的自由区域。要保持程序和未来出现的操作系 统之间的兼容性,就应该将DOS调用用于所有的内存分配和释放的请求之中, 尽管当前我们能在内存中使用各种技巧,但当移向多任务系统时,这些使用了特殊技 巧的程序会停止运行。了解了在DOS局限性下工作,就能编写出更便利的程序。遗憾的 是,一些技巧(如直接访问视频显示内存)是DOS编程的主要成分。没有它们,系统不会敏 感到能给用户提供专业程序所必需的“感觉”类型。尽管这些花招使程序没有那么方便,但 有时要编写好程序,失去一点便利性也是必要的。 而在内存分配中却没有这些技巧。甚至一个小的错误也会使该系统死锁。让我们先 看看DOS控制TPA中内存的方式。 TPA安排在一个叫做“内存场(memory arena)”的结构之中。 DOS维护着这个被称为 场项的内存块的链表,每一项都有它自己的特殊控制块,叫做“场头(arena header)”(第3 章讨论这些题目,表3.3显示了场头的布局)。3个DOS功能(Int 29h,功能48h、49h和 4Ah)用于请求或释放内存:“DOS参考手册”一节将详细讨论每个功能。 208页 场链(见图10.3)将每个内存块链接成内存块表。不论两个空白内存块何时碰到了一 起,它们都结合成单一的较大的,且在链中只有一个场头的块。 图10.3内存分配链 请求内存时,DOS搜索内存场链来分配一个能满足请求的块。要分配这些块,DOS采 用了最先适配策略,其中它利用了第一个够大的能满足需要的块。如果该块所含内存超过 了需要,就将它分成两部分,多余的内存作为一个独立内存块放回到链表中。 DOS之所以使用这个策略,是因为它是通常使用中最有效的。从DOS 3.0版开始,可 将分配策略变成下列替代方法之一: ·最佳适配在这个策略中,要搜索整个内存并且用与要求最紧密匹配的内存块来 填满它。 ·最后适配使用链中能满足分配需要的最后一个块(该块带有最高的内存地址)。 内存分配策略不需改变,因为最有效的方法已经在使用(用DOS 3.0版进行的非常 有限的测试,看起来是指示DOS在它安装任何程序时都继续使用最先适配策略,也不 管设置了哪种方法。这个结果并不是最后的——因为随着所涉及的变化因素的增多,就需 要更多的测试来提供有限的回答。一个问题是DOS的装入程序开始时,总是要试着获得 所有保留的RAM)。 无论DOS何时得到一个分配请求,它都会检查内存场链表,看看是否存在什么问题。 如果在写内存时,冲掉了这个场头,或者损坏了这个表,那么程序就会废弃并且看到这样 一条信息:Memory Allocation Error(内存分配出错)。 209页 10.2.1压缩程序内存 当COM程序启动时,系统会将所有内存都分配给它,因为DOS生来就是一个单用 户的操作系统,每次只能运行一个程序。装入程序一开始就请求65,535个段(比可能得到 的要多)来找到有多少可用的段,然后它再要求精确的数目。 EXE程序也分配了所有内存,但在这里如果愿意,就可以减少所分配的内存。EXE程 序的程序头有2个参数:MINALLOC和MAXALLOC。前者是程序运行所需的最小的内 存分配。如果一个块有这个最小数量的内存,那么这个块有效时,该程序就能运行。 但DOS总是在可能的情况下,努力将一个带有MAXALLOC内存字节的块分配给 程序。 Microsoft的连接程序,总是设置MINALLOC为0,MAXALLOC为FFFFh(1048560 个字节),除非用户告诉它使用别的值。无论何时,在没有特异化这些值的情况下连结一个 EXE程序,都要保证它获得所有有效的内存。 注意Turbo C和Microsoft C都在启动时自动释放多余的内存。但Turbo Pascal像 Microsoft的连接程序一样不能自动地完成这件事。在Turbo Pascal 4.0版和5.0版中(它 们能产生EXE文件),所有有效内存都被默认值所使用,将最高区域管理成一个堆。但是 如果想自己控制它,有一个编译程序指令,就能设置大小。另外,内部变量(在手册中已清 楚地公开了)精确告知使用内存的方式。 旧的Turbo Pascal版本能产生COM文件,这些版本之所以能限制程序所用内存数量 的唯一方式,是因为在编译过程中为堆设置一个最大值的容量。由于内存分配的方式,以 及程序之上的堆栈和堆,所以不存在有效的方法来确定Pascal COM程序结束的地方。 BASIC也向程序员提出了这一问题,因为没有有效的方式来确定程序在内存中的结 束地址,也没有办法启动一个程序,除非访问EXE功能,特殊的MEMSET功能让程序设 置内存上限,但该功能有意允许BASIC程序安装汇编语言支持例程而不是提供其它程序 的执行。CHAIN和RUN(BASIC语句用来执行其它BASIC程序)用一个新程序代替业巳 存在的程序,而不是保持当前程序的状态。 在高级语言中,C和Turbo Pascal的较新版本都适于动态的内存分配工作。C给内存 分配和释放提供了较好的功能;从来不必为此目的而访问DOS功能。最近的Turbo Pascal 版本综合了传统(或标准)Pascal的许多扩展名,并以略有不同的名字提供了像C一样的 功能。 只有以汇编语言编写的程序必须明确地将内存在启动处返回到内存池中。列表10.1 显示了这样做的方法。 列表10.1 ;Freemem.asm mov sp,offset stack ; Move stack to safe area mov ah,4Ah ; Setblock mov bx,280h ;Retain 10K of space int 21h jc alloc_error ; Allocation error 210页 ; [MORE PROGRAM CODE] dw 64 dup(?) stack equ $ 10.2.2获得更多的内存 如果程序需要额外的内存,它可以用修改内存分配功能(Int 21h,功能4Ah)来请求 额外的内存。作为回报,如果内存是可用的,进位标志会清零,AX寄存器含有内存基的段 地址,如果没有足够的内存来满足请求,就会设置进位标志,AX含有一个出错标志(7= 控制块破坏,8=内存不够),寄存器BX拥有最大有效块的大小。 C和Turbo Pascal的较新版本都提供有用的函数以获得额外的内存并释放内存。旧 的Turbo Pascal版本(V4之前的)请求另外内存的能力最小或不存在,除非使用特殊的编 译程序开关。BASIC未提供方法来改变给BASIC程序的内存分配,也没有提供方法来确 定从哪里进入内存,去限制内存使用。 只有在汇编语言中才必须直接访问DOS分配功能。在C和TurboPascal的较新版本 中,由库分配例程去访问DOS功能。列表10.2提供了一种方法来获得内存,并确定如果 禁止第一次尝试,将留下多少内存。 列表10.2 ;Getmem.asm getmem: mov ah,48h ;Allocate memory mov bx,bufsize ;16K memory int 21h jc nomem ;Cannot allocate mov bufSeg,ax ;Save pointer jmp pgm ;Continue program nomem : Cmp aX,8 jnz quit ;Major alloc error mov bufsize,bx ;Save buffer size mov ah,48h ;Allocate memory int 21h jC quit ;Still cannot allocate pgm: ; [MAIN PART OF PROGRAM] done: mov ah,49h ;De·allocate memory mov es,bufseg ;Point to buffer segment int 21h quit : [EXIT CODE GOES HERE] bufsize dW 400h bufseg dw 0 在没有访问DOS有关内存分配的请求的情况下,许多内存分配和释放的功能对于用 C或Turbo Pascal的较新的版本编写的程序都是有用的。使用这些有效的功能很有意思, 因为它们可明显简化操作,并将它保持在语言系统的控制之下。Turbo Pascal V4.0以前 的版本在堆中拥有所有可用的内存,并且提供了程序空间的分配和释放功能。这些Pascal 211页 分配和释放例程不会从内存场中获得或返回空间。BASIC不请求也不返回什么。 使用BASIC和Pascal(Turbo Pascal 4.0之前)的程序员能用编译程序开关来人工限 制内存,然后用DOS功能调用来请求释放另外的内存。但这个练习会带来混乱和内存处 理不力。必须有动态内存处理的程序,应使用C来编写,或用新的Turbo Pascal,或者干脆 用汇编语言来编写。 10.3扩充内存 或迟或早(当在PC上用完内存并已添加所有必要的芯片来使程序达到640K上限 时),用户可能要购买一块扩充内存板。有了扩充内存,就能对大量数据存储(EMS)或多 任务区域(EEMS)进行快速而有效的访问。 10.3.1确定扩充内存的有效性 要确定是否已安装了扩充内存,可使用下列方法之一: ·试着打开文件EMMXXXX0(设备驱动程序的推荐名)。如果能成功能地打开,那 么就有驱动程序存在或存在有相同名字的文件。要看看是否有驱动程序,可用 IOCTL功能来给出一个“获得输出状态”的请求。驱动程序返回FFh;文件就返回 00h。关闭该文件,以便重新使用这个句柄。 ·考察在Int 67h向量位置的地址。该地址是驱动程序的中断入口点。如果有EMM. SYS(或一个可替代的驱动程序),所给的段地址就是该驱动程序的基地址;该段 地址中的偏移值00Ah处,驱动程序名作为驱动程序头的一部分而出现。尽管该 过程比打开文件的过程(打开和关闭的开销是显著的)要快,该方法要依赖程序去 访问其正常内存范围以外的内存。 列表10.3和列表10.4给出了两个用C编写的独立的例程,检查EMS驱动程序是 否存在。 列表10.3 /*emmtest.c Listing 10.3 of DOS Programmer's Reference*/ #include <dos.h> int emmtest() { union REGS regs; struct SREGS sregs; short int result; unsigned int handle; regs.h.ah=0x3d; /*Open file*/ regs.h.al=0; /*Read mode only*/ regs.X.dX=FP_OFF("EMMxxxX0");/*File name*/ sregS.dS=FP_SEG("EMMXXxx0");/*Set the DS register*/ intdosx(&regs,&regs,&Sregs); handle=regs.x.ax; /*File handle*/ 212页 /*If opened OK,then close the file*/ if(result=(regs.x.cflag==0)){ regs.h.ah=0x3e; regs.x.bX=handle; intdoS(&regs,&regs); } return (result); } 列表10.3用打开文件的方法去确定EMM是否已安装,它不会去检查IOCTL调用, 看看该方法返回了一个文件或是一个驱动程序。大多数情况下,这不是一个问题。但为了 安全起见,应该向该功能增加一个IOCTL调用的检查。 注意:用Borland C++编译该函数,要在if()语句中产生一个警告,该语句设置re- sult。这个警告能安全地忽略,因为所期望的效果是设置功能返回值为TRUE或FALSE, 根据测试结果而定,编译程序虽抱怨它但仍能处理它。 列表10.4对于某些不习惯涉及远(far)指针的C程序员,它变成了一个真正的问题。 当在PC机上工作时,必须意识到各种指针之间的区别:在一个段内进行指向操作的指针 (near指针);能够指向内存、所给定的段和偏移值地址的任何地方的指针(far指针);以及 能指向操作内存任何位置的指针,就像内存没有划分成段一样(huge指针)。不论何时使 用指针变量,都必须谨慎并且应一直保持与它们之间的匹配;否则意外的结果会阻止程序 运行。 列表10.4 /*emmchk.c LiSting 10.4 of DOS Programmer's Reference*/ #define FALSE 0 #define TRUE !FALSE #include<Stdlib.h> #include<Stdio.h> #include<dos.h> #include<alloc.h> int emmchk() { union REGS regs; struct SREGS sregs; char far*emptr,far*nameptr,far*fmptr; nameptr="EMMxxxx0"; regs.h.ah=0x35; /*Get Interrupt vector*/ regs.h.al=0x67; /* Get it for the EMM */ intdosx(&regs,&regs,&sregs); /*Make a FAR pointer to access the driver*/ emptr=MK_FP(sregs.es,0); fmptr=farmalloc(sregs.es); if((emptr=fmptr)==NULL{ printf("Unable to allocate far pointer.\n"); exit(0); } 213页 /*Return TRUE if they are the same for eight CharacterS*/ return (farcmp(emptr+10,nameptr,8)); } int farcmp(Str1,str2,n) char far*str1, far*str2; int n; { while(*str2 && n>0){ if(*str1!=*str2)return(FALSE); n--; str1++;Str2++; } return (TRUE); } 一些书籍介绍扩充内存的方式会产生一个更为严重的错误。因为许多扩充内存方面 的讨论都错误地解释说Int 67h处的指针指向驱动程序的开始,并且所给的在Int 67h向 量上的内存地址的10个字节是名字EMMXXX0。马上就会知道这种说法不会是真的;中 断向量在被调用时会指向控制传递的位置,但驱动程序的起始字节是驱动程序头的一部 分,它不能是可执行的代码(这个问题将在第12章里讨论)。 该向量提供了地址的段部分,但在段内部,在绝对偏移值000Ah处能找到这个名字, 因为驱动程序的内存总是在段边界上对齐(只能分配多个完整的段)。任何已装入的驱动 程序基地址的段地址都要规范化——即段地址要调整,直到偏移值为0。 列表10.5是一个用两种方法来检验扩充内存是否存在的小程序,它还显示每种方法 返回的内容。 列表10.5 /*testemm.C Listing 10.5 of DOS programmer'S Reference*/ #include<Stdio.h> VOid main() { int emmchk(void); int emmteSt(void); if(emmchk()) printf("MEM:Expanded memory is present\n"); else printf("MEM:Expanded memory is NOT present\n"); if(emmteSt()) printf("OPEN:Expanded memory is present\n"); else printf("OPEN:Expanded memory is NOT present\n"); } 10.3.2使用扩充内存 知道有扩充内存可用时,就可以使用扩充内存功能(与Int 67h紧密相关)来获得并使 214页 用内存(附录部分的EMS一节将详细介绍扩充内存的功能)。列表10.6给出了一个使用 Int 67h的内存功能的一个简单实例。 列表10.6 /*emstest.c Listing 10.6 of DOS Programmer's Reference*/ #include #include #include #include #define FALSE 0 #define TRUE !FALSE #define EMM 0X67 Char far*emmbase; void main() { unsigned int emmhandle; char teststr[80]; int i; int emmtest(void); int emmok(void); unsigned int emmalloc(int n); unsigned int emmmap(unsigned int handle,int phys,int page); void emmmove(int page,char*str,int n); void emmget(int page,char*str,int n); unsigned int emmclose(unsigned int handle); /*Is there any expanded memory?*/ if(!emmtest()){ printf("Expanded memory is NOT present\n"); printf("Cannot run this program\n"); exit(0); } /*Is the expanded memory manager functional?*/ if(!emmok()){ printf("Expanded memory manager NOT available\n"); printf("cannOt run this program\n"); exit(0); } /*Get ten pages of expanded memory for the demo.*/ if((emmhandle=emmalloc(10))<0){ printf("There are not enough pages available\n"); printf("cannot run this program\n"); exit(0); } /*Write the test string into each of the ten pages.*/ for(i=0;i<10;i++){ Sprintf(teststr,"This info is in EMS page %d\n",i); emmmap(emmhandle,i,0); emmmove(0,teststr,strlen(teststr)+1); } 215页 /*Now read them back in and recover the test string.*/ fOr(i=0;i<10;i++){ emmmap(emmhandle,i,0); emmget(0,teststr,strlen(teststr)+1); printf("Reading from block %d:%s",i,teStstr); } /*Finally,release the expanded memory*/ emmclose(emmhandle); } int emmtest() { union REGS regs; struct SREGS sregs; short int result; unSigned int handle; regs.h.ah=0x3d;/*open file*/ regS.h.al=0;/*Read mOde Only*/ regS.x.dx=(int)"EMMXXXX0"; /*File name*/ sregS.ds=_DS; /*Set the DS register*/ intdosx(®s,®s,&sregs); handle=regS.x.ax;/*File handle*/ if(result=(regs.x.cflag==0)){ regs.h.ah=0x3e; regs.x.bx=handle; intdos(®s,®s); } return (result); } int emmok() { union REGS regs; regs.h.ah=0x46;/*Get manager status*/ int86(EMM,®S,®s); if(regs.h.ah!=0)return (FALSE); regs.h.ah=0x41;/*Get page frame segment*/ int86(EMM,®S,®s); if(regs.h.ah!=0)return (FALSE); emmbase=MK_FP(regs.x.bx,0); return (TRUE); } unsigned int emmalloc(n) int n; { union REGS regs; regs.h.ah=0x43; /*Get handle and allocate memory*/ regs.x.bx=n; int86(EMM,®s,®s); if(regs.h.ah!=0) return (-1); return (regs.x.dx); } unsigned int emmmap(handle,phys,page) unsigned int handle; 216页 int phys,page; { union REGS regs; regS.h.ah=0x44; /*Map memory*/ regs.h.al=page; regS.x.bX=phys; regS.x.dX=handle; int86(EMM,&regS,&regs); return (regS.h.ah==0); } vOid emmmOve(page,str,n) int page,n; Char *str; { char far*ptr; ptr=emmbaSe+page*16384; while(n-->0) *ptr++=*Str++; } void emmget(page,str,n) int page,n; char*str; { char far*ptr; ptr=emmbase+page*16384; while(n-->0) *Str++=*ptr++; } unSigned int emmclose(handle) UnSigned int handle; { union REGS regs; regs.h.ah=0x45; /*Release handle*/ regs.x.dX=handle; int86(EMM,&regS,&regs); return (regs.h.ah==0); } 该程序分析了扩充内存的基本操作。如下所示: ·检验扩充内存是否存在。 ·然后看看扩充内存管理程序是否在正常工作。 ·试着分配10页扩充内存。 ·每次将一页映射到页框架中,并将一个测试字符串写进每一页中。 ·将这些页映射回页框架中,然后读出并打印测试字符串。 这些是扩充内存上的最基本操作。它们使你能在多个程序中利用内存区。可利用这 类技术将电子表格或数据库放进扩充内存:要作到这一点,就要在页框架中限定一个值数 组,并用它们的指针填满它们。记住:要用far指针或大指针来访问页,因为这个区域将超 出当前段。 217页 程序的主要部分后面是主程序所使用的许多小的扩充内存功能。将这些功能组合起 来这些功能便覆盖了扩充内存的操作,尽管还可能想向它们添加错误检查。 程序所包含的函数有: emmtest 检验内存中是否存在扩充内存管理程序。 emmok 检验扩充内存管理程序的功能并确定页框架的基地址。 emmalloc 从扩充内存管理程序请求指定数量的页数。 emmmap 将一个扩充内存页映射进页框架中。 emmmove 把数据移进页框架中的指定页中。 emmget 从页框架中的指定页中获得数据。 emmclose 将扩充内存句柄的控制返回给扩充内存管理程序。 扩充内存的简单应用程序能用这些基本功能来启动,然后适当地增加错误检查和新 的功能,就可以发展成羽翼丰满的程序。 10.4扩展内存 扩展内存是指超过8086的1M限制的内存。在扩展内存规范(XMS)的范围内,扩展 内存还指高位内存区(HMA)和上位内存块(UMB)。 UMB是DOS640K内存界线与8086 1M界线之间的有用内存区域。它们通常不与 640K有用内存相连接,并且在DOS5.0之前,它们只有通过XMS驱动程序才对程序员 有用。从DOS5.0开始,UMB能通过DOS的内存服务来访问;DOS内存服务则可为你访 问XMS驱动程序。 HMA是一个奇怪的东西,在其中当CPU位于实地址方式中时,第21个地址线(A20 线)被激活,增加65520个字节,增加可用内存的数量,所增加的内存在实地址方式中的 CPU也能直接访问它。这个过程的工作方式如下:考察一下段值加上偏移值对物理内存 的映射,就会发现段值乘以16再加偏移值,就是所访问的物理地址。如果这个值超过20 位,那么就丢掉较高的几位,将物理地址映射在000000H到0FFFFFH的范围内。于是,地 址0FFFF:0010H在A20为0时,映射的物理地址是000000H,在A20为1时,映射的物 理地址是0100000H。额外的65520个内存字节正是来自这里;从0FFFF : 0010H到 0FFFF : FFFFH这些地址通常映射到物理地址000000H到00FFEFH上,而在这里,它 们映射到了物理地址010000H到010FFEFH上。 XMS驱动程序提供了5组服务:驱动程序信息功能;HMA管理功能;A20管理功 能;扩展内存管理功能以及上位内存管理功能。两个另外的服务确定XMS驱动程序是否 存在并且获得XMS驱动程序的控制功能的地址。 10.4.1确定扩展内存的有效性 要确定XMS驱动程序是否存在,可以执行下列代码: mov AX,04300H 218页 int 02FH ·= cmp AL, 080H = :: jne XMS_NotPresent ;XMS is present 如果XMS存在,就需要获得XMS驱动程序的控制功能的地址。这可借助下列代码 片段来完成: code segment mov AX,04310H ” int 02FH mov word ptr[XMS_Control],BX mov word ptr[XMS_COntrol],ES cOde ends data segment XMS_Control dd (?) data ends 10.4.2使用扩展内存 如果扩展内存可用,就可以用扩展内存功能来获得和使用扩展内存(XMS参考手册 一节位于本书的第5部分,那里介绍了扩展内存功能)。本节中的两个列表是扩展内存功 能方面的简单实例。 列表10.7中的程序分析了扩展内存的基本操作。如下所示: ·检验一下扩展内存是否存在。 ·获得XMS版本号,检查一下HMA是否存在,并获得扩展内存最大空白块的大 小。 ·试着分配一个扩展内存块。 ·向扩展内存块写数据。 ·从扩展内存块读回数据。 ·释放扩展内存块。 ·试着分配一个上位内存块。 ·释放上位内存块。 这是在扩展内存中的基本操作。它们使你能在多个程序中运行内存区。可用这类技 术将数据放入扩展内存中。 列表10.7 /*xmstest.C Listing 10.7 of DOS Programmer's Reference*/ #include<ctype.h> #include 219页 #include #include Struct XMS_Status{ unsigned short version; unsigned short revision; unsigned short Hma_existence; }; Struct XMS_memory_StatuS{ unsigned short largest_block; unSigned ShOrt tOtal_free_memOry; unsigned short error_status; }; struct bstat{ unSigned Short lock_count; unSigned shOrt free_handle_count; }; union memory_address{ unSigned long long_offset; struct conventional_address{ unsigned short conventional_offset; unsigned short conventional_segment; } C_a; }; Struct XMS_move_data{ unsigned long transfer_size; unsigned short source_handle; UniOn memOry_address sOurce_OffSet; unsigned short deStination_handle; union memory_addreSS deStination_offset; }; Struct umb_data{ unsigned short segment_number; unsigned short segment_size; }; extern unsigned short far a20_stat(void); extern unsigned short far alter_a20(unSigned Short change_code); extern unsigned short far hma_alloc(unsigned ShOrt size_in_byteS); extern unsigned short far hma_free(void); extern void far hma_move(unsigned short count, unsigned short direction, unsigned short hma_offset, vOid far*data); extern unsigned short far umb_alloc(unsigned short size struct umb data far*ptr); extern unsigned short far umb_free(unsigned short segment); extern unsigned short far xms_alloc(unsigned short size,unsigned short far*handle); extern void far xms_avail(struct XMS_memOry_StatuS far*ptr); extern unsigned short far xms_bstat(unsigned short handle, struct bstat far*ptr); extern unsigned short far 220页 xms_free(unsigned short handle); extern unsigned short far xms_lock(unsigned short handle, unsigned long *linear_address); extern unsigned short far xms_move(Struct XMS_move_data far *ptr); extern unsigned short far xms_realloc(unsigned short handle, unsigned short size); extern void far xms_stat(struct XMS_status far *ptr); extern unsigned short far xms_test(void); extern unsigned short far xms_unlock(unsigned short handle); /* Return Codes */ #define A20_DISABLED (0*0000) #define A20_ENABLED (0*0001) #define FAILURE (0*0000) #define SUCCESS (0*0001) #define FUNCTION_NOT_IMPLEMENTED (0*0080) #define VDISK_DEVICE_DETECTED (0*0081) #define A20_ERROR_OCCURRED (0*0082) #define HMA_DOES_NOT_EXIST (0*0090) #define HMA_ALREADY_IN_USE (0*0091) #define NOT_ENOUGH_BYTES_TO_ALLOC_HMA (0*0092) #define HMA_NOT_ALLOCATED (0*0093) #define A20_LINE_STILL_ENABLED (0*0094) #define ALL_EXTENDED_MEMORY_ALLOCATED (0*00A0) #define ALL_EXTENDED_MEMORY_HANDLES_USED (0*00A1) #define INVALID_HANDLE (0*00A2) #define INVALID_SOURCE_HANDLE (0*00A3) #define INVALID_SOURCE_OFFSET (0*00A4) #define INVALID_DESTINATION_HANDLE (0*00A5) #define INVALID_DESTINATION_OFFSET (0*00A6) #define INVALID_LENGTH (0*00A7) #define INVALID_OVERLAP (0*00A8) #define PARITY_ERROR (0*00A9) #define BLOCK_NOT_LOCKED (0*00AA) #define HANDLE_IS_LOCKED (0*00AB) #define LOCK_COUNT_OVERFLOW (0*00AC) #define LOCK_FAILED (0*00AD) #define SMALLER_UMBAVAILABLE (0*00B0) #define NO_UMBS_AVAILABLE (0*00B1) #define INVALID_UMB_SEGMENT (0*00B2) #define ILLEGAL_PARAMETER (0*FFFF) /* Codes for alter_a20() */ #define GLOBAL_A20_LINE (0) #define LOCAL_A20_LINE (2) #define ENABLE_A20_LINE (0) #define DISABLE_A20_LINE (1) /* Codes for hma_move() */ #define COPY_FROM_HMA (1) #define COPY_TO_HMA (0) #define KILOBYTE (1024) unsigned char test_buffer[KILOBYTE]; void test_hma(void) 221页 { int HMA_allocated; int A20_status; int original_a20_status; unsigned short result; HMA_allocated=0; result = hma_alloc(0*FFF0); switch(result) { case SUCCESS: printf("HMA successfully allocated to current process\n"); HMA_allocated++; break; case FUNCTION_NOT_IMPLEMENTED: printf("HMA allocate not implemented by XMS\n"); break; Case VDISK_DEVICE_DETECTED: printf("VDISK device detected; couldn't allocate HMA\n"); break; Case HMA_DOES_NOT_EXIST: printf("HMA cannot be allocated; doesn't exist\n"); return; case HMA_ALREADY_IN_USE: printf("HMA cannot be allocated; already in use\n"); break; case NOT_ENOUGH_BYTES_TO_ALLOC_HMA: printf("Not enough bytes to allocate HMA were specified\n"); return; default: printf("Invalid result of hma_alloc(): 0*%.4X\n", result); return; } if(HMA_allocated) { Original_A20_Status = A20_StatuS = -1; result = a20_stat(); switch(result) { case A20_DISABLED: original_A20_Status=A20_Status = 0; break; case A20 ENABLED: Original_A20_Status = A20_Status = 1; break; case FUNCTION_NOT_IMPLEMENTED: printf("Can't get A20 line status; function not implemented\n"); break; case VDISK_DEVICE_DETECTED: printf("Can't get A20 line Status; VDISK device detected\n"); break; default: printf("Can't get A20 line Status; unexpected result 0*%.4X\n", result); break; } if(A20_status == 0) /* Disabled */ { result = alter_a20(GLOBAL_A20_LINE|ENABLE_A20_LINE); switch(result) { case SUCCESS: 222页 printf("A20 line globally enabled\n"); A20_status=1; break; case FUNCTION_NOT_IMPLEMENTED: printf("Function not implemented; couldn't enable A20 line\n"); break; case VDISK_DEVICE_DETECTED: printf("VDISK device detected; couldn't enable A20 line\n"); break; case A20_ERROR_OCCURRED: printf("Error occurred while attempting to enable A20 line\n"); break; case ILLEGAL_PARAMETER: printf("Illegal argument to alter_a20()\n"); break; default: printf("Unexpected result 0*%.4X from alter_a20()\n", result); break; } } if(A20_status==1) { int k; memset(test_buffer,0*55,KILOBYTE); hma_move(KILOBYTE,Copy_to_HMA,0,test_buffer); memset(test_buffer, 0*AA, KILOBYTE); hma_move(KILOBYTE,copy_FROM_HMA,0,test_buffer); result=1; for(k=0; k>8, status.version&0x00FF); printf("XMS Driver Revision:%.2X.%.2x\n", Status.revision>>8, status.revision&0x00FF); printf("HMA existence: %.4X\n",status.HMA_existence); if(status.HMA_existence==1) test_hma(); test_xms(); test_umb(); return 0; } 列表10.8是一个小小的函数库,它含有在列表10.7中所使用的扩展内存函数。经过 组合后的这些函数覆盖了各种扩展内存的操作。下面列出了此库中所包含的函数: xms_test 测试扩展内存管理程序是否存在 xms_stat 获取扩展内存管理程序的版本号并检查HMA是否存在 xms_avail 获取最大的自由扩展内存块的大小 xms_alloc 向扩展内存管理程序请求一个扩展内存块 xms_realloc 重新分配一个扩展内存块,改变其大小 xms_lock 锁定一个扩展内存块 xms_unlock 解锁一个扩展内存块 xms_bstat 获取一个扩展内存块的状态 xms_move 往一个扩展内存块中移入数据或从中移出数据 xms_free 返回一个扩展内存块给扩展内存管理程序 hma_alloc 分配HMA hma_free 释放HMA hma_move 往HMA中移入数据或从中移出数据 alter_a20 允许或禁止A20线 a20_stat 获取A20线的状态 umb_alloc 分配一个UMB umb_free 释放一个UMB 一些简单的扩展内存应用程序可将这些函数作为起点,并在此基础上添加出错检查, 234页 或添加一些新的功能。 列表10.8 page 55 ,132 ; XMSLib.asm ; XMS library functions XMS_CODE SEGMENT'CODE' public _xms_test public _xms_stat public _xms_avail public _xms_alloc public _xms_realloc public _xms_lock public _xms_unlock public _xms_bstat public _xms_move public _xms_free public _hma_alloc public _hma_free public _hma_move Public _alter_a20 public _a20_stat public _umb_alloc public _umb_free ASSUME CS:XMS_CODE ASSUME DS:NOTHING ASSUME ES:NOTHING ASSUME SS:NOTHING ;Storage for XMS call address XMS_Control LABEL DWORD XMS_Control_offset dw (?) XMS_Control_Segment dw (?) ; Test for the presence of the XMS driver and,if present, ; get the address of the XMS function call ; Returns AX=1 if XMS found;otherwise AX=0 ; Borland C++ prototype:unsigned short far xms_test(void); ALIGN 16 _xms_test PROC FAR push ES ; Save ES mov AX,04300H ; See if XMS is present int 2FH cmp AL,080H ; if it is,AL=80H jne __x_t2 ; XMS is present;get the function address mov AX,04310H int 2FH mov CS:[XMS_Control_offset],BX mov CS:[XMS_Control_Segment],ES ASSUME DS:NOTHING 235页 mov AX,1 ;Return 1 ;Return. . . __x_t1: pop ES ; Restore ES ret ; Return error __x_t2: xor AX,AX ; Return 0 jmp __X_t1 xms test ENDP ; Get XMS status ; Stores the XMS version number, driver revision number, ; and HMA existence flag at the address on the stack ; Borland C++ prototype: ; void far xms_stat(struct XMS_status far*ptr); ; Data structure:struct XMS_status ;{ ; unsigned short version ; unsigned short revision; ; unsigned short HMA_existence; }; ALIGN 16 _xms_stat PROC FAR push SI;Save registers push DS push BP mov BP,SP ; Set argument pointer mov AH,0 ; call XMS call CS:[XMS_Control] lds SI,[BP+10] ; Get address of pointer mov [SI],AX ; Store XMS version number mov [SI+2],BX ; Store driver revision number mov [SI+4],DX ; Store HMA existence flag pop BP ; Restore registers pop DS pop SI ret ; Return _xms_stat ENDP ; Get XMS memory status ; Stores the size of the largest free XMS block and the total ; amount of free XMS memory at the address on the stack ; Borland C++ prototype: ; void far xms_avail(struct XMS_memory_status far*ptr); ; Data Structure:Struct XMS_memory_status ;{ ; unsigned short largest_block; ; unsigned short total_free_memory; ; unsigned short error_status; ; }; 236页 ;Note:the block size and the amount of free memory are in K bytes ALIGN 16 _xms_avail PROC FAR push SI ; Save registers push DS push BP mov BP,SP,Set argument pointer xor BX,BX mov AH,8; Call XMS call CS:[XMS_Control] lds SI,[BP+10] ; Get address of pointer mov [SI],AX ; Store largest block size mov [SI+2],DX ; Store total memory available xor BH,BH mov [SI+4],BX ; Store error status pop BP ; Restore registers pop DS pop SI ret ; Return xms_avail ENDP ; Allocate a block of extended memory ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK device detected ; 00A0 All extended memory is allocated ; 00A1 All eXtended memory handles are in use ; Borland C++ prototype: ; unsigned short far xms_alloc(unsigned short size, ; unsigned short far*handle); ALIGN 16 _xms_alloc PROC FAR push SI ; Save registers push DS push BP ; Make pointer to arguments mov BP,SP mov DX,[BP+10] ; Get size in kilobytes mov AH,9 ; Request memory call CS:[XMS_Control] or AX,AX ; Error? jz__X_a2 ; Set handle and return __X_a1: lds SI,[BP+12] ; Store handle mov [SI],DX pop BP ; Restore registers pop DS pop SI ret ; Return ; Return error __x_a2: mov AL,BL ; Get return code xor DX,DX ; Create NULL handle jmp __X_a1 237页 _xms_alloc ENDP ; Resize an XMS block ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK device detected ; 00A0 All extended memory is allocated ; 00A1 All extended memory handles in use ; 00A2 Invalid handle ; 00AB Block is locked ; Borland C++ prototype: ; unsigned short far xms_realloc(unsigned short handle, ; unsigned short size); ALIGN 16 _xms_realloc PROC FAR push BP ;Point to arguments mov BP,SP mov DX,[BP+6]; Get handle mov BX,[BP+8]; Get size mov AH,0FH call CS:[XMS_Control] or AX,AX ; Error? jz__x_r2 ; return __x_r1: pop BP ; Restore BP ret ; Return ; return error __x_r2: mov AL,BL ; Get error code jmp__x_r1 _xms_realloc ENDP ; Lock a block of XMS memory ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK device detected ; 00A2 Invalid handle ; 00AC Lock count overflow ; 00AD LOCk failed ; Borland C++ prototype: ; unsigned short far xms_lock(unsigned short handle, ; unSigned long*linear_address); ALIGN 16 _Xms_lock PROC FAR push BP ; Save registers mov BP,SP push DS push SI mov DX,[BP+6];Get handle mov AH,0CH 238页 or AX,AX ; Error? jz __x_l2 lds SI,[BP+8] ; Get address of pointer mov [Si],BX ; Store linear address mOv [SI+2],DX ; Return __x_l1: pop SI ; Restore registers pop DS pop SP ret ; Return ; Return error __x_l2: mov AL,BL ; Get error code jmp __X_l1 _xms_lock ENDP ; Unlock a block of XMS memory ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK device detected ; 00A2 Invalid handle ; 00AA BlOCk was nOt lOCked ;Borland C++ prototype: ;unsigned short far xms_unlock(unsigned short handle); ALIGN 16 _xms_unlock PROC FAR push BP ; Get pointer to argument mov BP,SP mov DX,[BP+6] ; Get handle mov AH,0DH call CS:[xms_Control] or AX,AX ; Error? jz __X_u2 ; Return __X_u1: pop BP ; Restore BP ret ; Return __x_u2: mov AL,BL ; Get error code jmp __X_u1 _xms_unlock ENDP ; Get status of an XMS block ; Returns StatuS in AX: 0001 success ; 0080 Function not implemented ; 0081 VDISK device detected ; 00A2 invalid handle ; Borland C++ Prototype: ; unsigned short far xms_bstat(unsigned short handle, ; struct bstat far*ptr); ; Data structure:struct bstat 239页 ; { ; unsigned short lock_count; ; unsigned short free_handle_count; ;}; ALIGN 16 _xms_bstat PROC FAR push BP ; Save registers push DS push SI mov BP,SP ; Point to arguments mov DX,[BP+10] ; Get handle mov AH,0EH call CS:[XMS_Control] or AX,AX;Error? jz __x_b2 lds SI,[BP+12] ; Get pointer to structure xor DX,DX ; Clear DX mov DL,BH ; Get lock count mov [SI],DX ; Store it mov DL,BL ; Get number of free handles mov [SI+2],DX ; Store it ; Return __x_b1: pop SI ; Restore registers pop DS pop BP ret ; Return __x_b2: mov AL,BL ; Get error code jmp __X_b1 _xms_bstat ENDP ; Move data to or from an extended memory block ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK device detected ; 0082 A20 error occurred ; 00A3 Invalid source handle ; 00A4 Invalid source offset ; 00A5 Invalid destination handle ; 00A6 Invalid destination offset ; 00A7 Invalid length ; 00A8 Invalid overlap on move ; 00A9 Parity error occurred ; Borland C++ prototype: ; unsigned short far xms_move(struct XMS_move_data far*ptr); ; Data Structure:Struct XMS_move_data ; { ; unsigned long transfer_size; ; unsigned short source_handle; ; unsigned long source_offset; ; unsigned short destination_handle; ; unsigned long destination_offset; ; }; ALIGN 16 240页 _xms_move PROC FAR push BP ; Save registers push DS push SI mov BP,SP ; Get argument pointer lds SI,[BP+10] ; Get address mov AH,0BH call CS:[XMS_Control] or AX,AX ;Error jz __X_m2 ;Return __x_m1: pop SI ; Restore registers pop DS pop BP ret ; Return ;Return error __x_m2: mov AL,BL ; Get error code jmp __x_m2 _xms_move ENDP ; Free a block of extended memory ; Returns status in AX: 0001 Success ; 0080 FUnctiOn not implemented ; 0081 VDISK device detected ; 00A2 Invalid handle ; 00A4 Handle is locked ; Borland C++ prototype: ; unsigned short far xms_free(unsigned short handle); ALIGN 16 _xms_free PROC FAR push BP ; Make pointer to argument mov BP,SP mov DX,[BP+6] ; Get handle mov AH,0AH ; Free it call CS:[XMS_Control] or AX,AX; Error? jz __x_f2 ; Return __x_f1: pop BP ; Restore BP ret ; Return ; Return error __x_f2: mov AL,BL; Get error code jmp __x_f1 xms_free ENdP ; Allocate the HMA 241页 ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK deviCe detected ; 0090 HMA does nOt exist ; 0091 HMA already in uSe ; 0092 NOt enOUgh byteS reqUested ; Borland C++ prototype: ; unsigned short far hma_alloc(unsigned short size_in_byteS); ALIGN 16 _hma_alloc PROC FAR push BP ; Get pointer to arguments mov BP,SP mov DX,[BP+6] ; Get number of bytes mov AH,1 ; Request HMA call CS:[XMS_Control] or AX,AX; Failure? jz __h_a2 ; if so, get return code ;Return __h_a1: pop BP ; Restore BP ret ; Return ; Set error code __h_a2: mov AL,BL ; Return error code jmp __h_a1 _hma_alloc ENDP ; Free the HMA ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 0081 VDISK device detected ; 0090 HMA does not exist ; 0093 HMA not allocated ; Borland C++ prototype: ; unsigned short far hma_free(void); ALIGN 16 _hma_free PROC FAR mov AH,2 ; Release HMA call CS:[XMS_ContrOl] or AX,AX; Error? jz __h_f2 ; If so, get code ; Return __h_f1: ret ; Return ; Return error __h_f2: mov AL,BL ; Store error code jmp __h_f1 _hma_free ENDP 242页 ; Move data intO Or OUt Of the HMA ; Borland C++ prototype: ; void far hma_move(unsigned short count, unsigned Short direction, ; unsigned short hma_offset, void far*data); ; Note: If the direction is zero, data is copied to HMA;otherwise, ; it is Copied from the HMA ALIGN 16 _hma_move PROC FAR push BP ; Get pointer to arguments mov BP,SP push DS ; Save registers push ES push DI push SI mov CX,[BP+6] ; Get byte count mov AL,[BP+8] ; Get direction flag Or AL,AL ; If set, copy from HMA mov AX,0FFFFH ; Set HMA segment jz __h_m2 ; if clear, COpy to HMA ; Copy data from HMA mov DS,AX ; Set Source=HMA mov SI,[BP+10] ; Get offset add SI,10H ; Add 10H to offset les DI,[BP+12]; Get destination address ; Do move and return __h_m1: cld ; clear direction flag shr CX,1 ; Convert bytes to words rep movsw ; Copy data adc CX.0 ; Add carry rep movsb ; Copy data pop SI ; Restore data pop DI pop ES pop DS pop BP ret ; Return ; Copy data to HMA __h_m2: mov ES,AX ; Set destination=HMA mov DI,[BP+10] ; Get offset add Di,10H ; Add 10H to offSet lds SI,[BP+12] ; Get source address jmp __h_m1 _hma_move ENDP ; Alter the A20 line status ; Valid argumentS are(GLOBAL_A20_LINE|ENABLE_A20_LINE)==0 ; (GLOBAL_A20_LINE|DISABLE_A20_LINE)==1 ; (LOCAL_A20_LINE|ENABLE_A20_LINE)==2 ; (LOCAL_A20_LINE|DISABLE_A20_LINE)==3 243页 ; Returns status in AX: 0001 Success 0080 Function not implented ; 0081 VDISK device detected ; 0082 A20 error occurred ; 0094 A20 line still enabled(args 1 and 3) ; FFFF Illegal argument ; Borland C++ prototype: ; unsigned short far alter_a20(unsigned short change_code); ALIGN 16 _alter_a20 PROC FAR push BP ; Get pointer to argument mov BP,SP mov AX,[BP+6] ; Get argument cmp AX,3 ;Check range ja __a_a2 mov AH,AL ; Create function code add AH,3 . call CS:[XMS_Control] Or AX,AX ; Failure? jz __a_a3 ;Return __a_a1: pop BP ; Restore BP ret ; Return ; Illegal argument __a_a2: mov AX,-1; Return ILLEGAL_ARGUMENT jmp __a_a1 ; Bad result __a_a3: mov AL,BL ; Return error code jmp __a_a1 _alter_a20 ENDP ; Get A20 line status ; Returns status in AX: 0000 A20 disabled ; 0001 A20 enabled ; 0080 Function not implemented ; 0081 VDISK device detected ; Borland C++ prototype: ; unsigned short far a20_stat(void); ALIGN 16 _a20_stat PROC FAR xor BX,BX mov AH,7 ; Get line status call CS:[XMS_Control] Or BL,BL ; Error? jnz __a_S2 ; Return __a_s1: ret 244页 ; Return error __a_s2: xor AX,AX ; Clear AX mov AL,BL ; Get error code jmp __a_s1 _a20_Stat ENDP ; Allocate an upper memory block ; Returns status in AX: 0001 Success ; 0080 Function nOt implemented ; 00B1 A smaller UMB iS available ; 00B1 NO UMBS are available ; Borland C++ prototype: ; unsigned short far umb_alloc(unsigned short size, ; struct umb_data far*ptr); ; Data structure: struct umb_data ;{ ; unsigned short segment_number; ; unsigned short segment_size; ;}; ALIGN 16 _umb_alloc PROC FAR push BP ; Save registers push DS push SI mov BP,SP ; Point to arguments mov DX,[BP+10]; Get size mov AH,10H call CS:[XMS_Control] or AX,AX ;Error jz __u_a2 lds SI,[BP+12] ; Get address of data structure mov [Si],BX ; Store segment number ;Return __u_a1: mov [SI+2],DX ; Store size in paragraphs pop SI ; Restore registers pop DS pop BP ret ; Return ; Return error __u_a2: mov AL,BL ; Store error code ldS SI,[BP+12] ; Point to data structure jmp __u_a1 umb_alloc ENDP ; Free UMB ; Returns status in AX: 0001 Success ; 0080 Function not implemented ; 00B2 Invalid segment number ; Borland C++ prototype: ; unsigned short far umb_free(unsigned short segment); ALIGN 16 245页 _umb_free PROC FAR push BP ;Get pointer tO argument mov BP,SP ; mov DX,[BP+6] ;Get segment number mov AH,011H call CS:[XMS_Control] or AX,AX ;Error? jz _u_f2 ;Return _u_f1: pop BP ;Restore BP ret ;Return ;Return error __u_f2: mov AL,BL ;Get error code jmp __u_f1 _umb_free EnDP XMS_CODE ENDS END 10.5程序执行 DOS最为有用的功能之一就是它能从一个程序(父程序)中运行另一个程序(子程 序),而不丢失初始程序或父程序的当前状态。在UNIX系统上,函数能运行并行的进程, DOS也具有类似的功能,只不过当新的进程运行时,父进程会进入睡眠状态(停止执行功 能)。 ExEC功能的一些基本特征是让程序执行以下几项任务: .在系统的自由内存中运行子程序。 ·等待子程序操作完毕。 .接收子程序返回的代码,该代码表示正常完成、出错终止或状态码。 可能读者已听说过EXEC很难使用或用起来很危险。但是,只有事实才最具有说服 力。虽然ExEc的V2版的确有些让人头疼,这点我们稍后讨论(以及如何避免这些问 题),但随后的所有版本都能出色地执行各项指定的功能。尽管执行EXEC功能要求按规 则进行,但这些规则写得清楚明白。 要执行ExEc 功能,必须保证有足够可用的内存来运行新程序(参见本章前面的关 于如何释放内存的内容)。 10.5.1 ExEC功能 只要有足够的内存可供当前程序使用,就可以用ExEC功能来执行另一程序。在调 用EXEC功能(Int 21h,功能4Bh)时,寄存器必须设置成这样: 246页 寄存器 用 途 AL=0 装载和执行程序 DS:DX 指向完整路径名的指针 ES:BX 指向参数块的指针 可将AL置为03h来告诉EXEC功能以覆盖方式装入到父进程已拥有的内存。因为 覆盖不需考虑,因而只需集中精力来执行程序。 DS:DX寄存器对指向可执行文件的路径名。可执行文件的路径名可以是下面这些 名字中的任意一个: ·要执行的程序的文件名,包括.EXE或.COM扩展名(在这种情况下,程序必须处 于当前目录内)。 ·与此文件相关的路径名,开头为一圆点(·)并且其对应的目录必须位于程序当前 正使用的路径中。 .此文件的完整路径名。该路径名以根目录(\)开始,且一直延伸到该文件上,其中 还可包括驱动器字母和冒号。 现在,假定想在\UTIL目录中执行DEMO.EXE程序。若已径进入\UTIL目录,则可 按如下方式来使用它: DEMO.EXE 若当前在\APPL目录中,则可通过与该程序相关的路径名来使用它: .\..UTIL\DEMO.EXE 在上面这一情况下,可在目录前使用“..”来简化路径名: ..\UTIL\DEMO.EXE 最后,无论当前在哪一位置,都可按下面的格式来执行DEMO.EXE程序: \UTIL\DEMO.EXE 路径名中必须包括找到该文件所需的一切内容;但其中不能出现通配符。文件名不仅 要清楚、完整,而且还一定要带有.EXE或.COM扩展名。用EXEC功能不能直接执行批 处理文件(带.BAT扩展名),但可用EXEC命令来执行COMMAND.COM,然后把\C选 项开关和该批处理文件的名字作为参数块的部分而传给COMMAND.COM,其结果是批 处理文件得到了执行。 你可能已用惯了某些功能,如文件名中的通配符或该文件的路径搜索,但这些是属于 COMMAND.COM的功能,而不是EXEC的功能。COMMAND.COM通过搜索path(路 径)环境变量来查找文件。它通过使用内部批处理文件的执行过程来执行批处理文件。然 而,所有这些都不能直接用EXEC功能来实现。 EXEC功能的实现原理同样适用于COMMAND.COM内部命令。COMMAND. COM的内部命令如DIR,不能被单独执行。但是,用EXEC命令来执行COMMAND. 247页 cOM本身却为突破上述限制提供了一种方法。只是有一件事情是这一方式所不允许的, 那就是不允许改变环境;我们会看到用EXEC命令拷贝的每一程序都获取了它自己的环 境拷贝,并且对环境进行改变不可能影响到原始拷贝,这些原始拷贝才是随后一切命令将 用到的东西。 ES : BX寄存器对指向含有下述四种辅助地址的参数块: ·环境块(两字节,段地址) ·命令尾(四字节,偏移值跟段) ·文件控制块1(四字节,先偏移值后跟段) ·文件控制块2(两字节,先偏移值后跟段) 环境块是程序环境变量的集合,这些程序环境变量是由DOS设置的或通过SET指 令以VARIABLE=VALUE格式加进的环境变量。例如,可以把TMP变量设置为等于 TMp——一个暂时的目录区域,创建暂时文件的一些程序将在这一区域中创建文件。 AUTOEXEC.BAT·文件中可能有下面这样的行: SET TMP=C:\TMP 所有这些变量加在一起就构成了环境块。在大多数情况下,之所以要保存这种环境, 是因为目前的程序要在其中运行。将此环境块置为零会导致EXEC把文件程序的环境拷 贝传递给子程序。但这种拷贝的大小恰恰只可容纳下现有的数据,而没有更多的空间可供 你加入更多的内容(如果想这样试试的话那只能是白费精力,因为当子进程终止时,它的 环境就会被释放父程序看不到它的环境)。 命令尾指针指向跟随在命令行中命令之后的部分。例如,若输入这样的命令: \BIN\COMMAND/C DIR the command tail is/C DIR 所保存的命令尾中,包括有指示该命令尾长度的计数字节,命令尾自身以及回车符 (代码(0Dh)。实际使用的完整命令尾应是这个样子: 1 2 3 4 5 6 7 8 06h / C D I R C/R 注意,回车不包括在计数范围内。 后两个参数是程序段前缀(PSP)中文件控制块(FCBs)的初始设置值。除非使用FCB 功能来维持与DOS 1.0版的兼容性,或者让EXEC执行使用FCB的其它程序,否则可以 安全地让它们指向任何地方,因为你用不到它们。如果打算使用FCB,则必须像第9章“目 录和文件”中介绍的那样来设置它们。可以通过EXEC把FCB复制到程序段前缀(PSP)。 列表10.9的程序示出前面提及的COMMAND.COM的用法;本程序执行带有/C DIR命令尾的COMMAND.COM。请注意如下的特性: ·程序中没有明显地保存堆栈指针,虽然该指针被破坏,但intdos会自动地恢复这 一指针。 ·必须获取段寄存器值,以便正确地设置所有的指针。 248页 我们的程序只是简单地假定COMMAND.COM位于当前驱动器的根目录下;我 们建议,为了使程序更通用,应该再加进一段代码,搜索环境,找到COMSPEC= 串,然后拷贝跟在等号后面的所有字符。这样,如果当前驱动器不包含COM- MAND.COM,该程序也能运行。 列表10.9 /* exectest.c Listing 10.9 Of DOS Programmer's Reference*/ #include #inClude #inClude void main() { union REGS regs; struct SREGS segs; struct { int envblk;/*Environment block*/ int ocmd;/*Command tail*/ int scmd; int ofcb;/*FCB #1*/ int sfcb; int ofcb2; /*FCB #2*/ int SfCb2; }pblock; char buffer[256]; char name[128]; segread(&segs); strcpy(name,"\\command.com"); printf("Executing program %s\n",name); /*Set up the command tail*/ buffer[0]=6; /*Number of characters*/ strcpy(buffer+1,"/c dir\015");/*Command tail*/ pblock.ocmd=(int)buffer; pblock.scmd=segS.ds; /*USe parent's environment*/ pblock.envblk=0; /*Set up FCBs #1 and #2*/ pblock.ofcb=(int)buffer; pblock.sfcb=segs.ds; pblock.ofcb2=(int)buffer; pblock.sfcb2=segs.ds; /*Execute the designated program using the EXEC function 4Bh*/ regs.h.ah=0x4b; regs.h.al=0; regs.x.dX=(int)name; segs.es=segs.ds; regs.x.bx=(int)&pblock; intdosx(®s,®s,&segs); /*If the carry flag is set,an error code is in the AX register*/ if(regs.x.cflag==1) 249页 printf("Error Code=%d\n",regs.x.ax); } 程序在知道了段寄存器值以后,它就会设置用于EXEC功能调用的每一个参数、命 令尾、环境(缺省值)和FCB。然后,程序执行各种功能,并检查是否有返回的错误代码。 在C语言中,有一个很好的能用于运行子进程的exec()函数,几乎可用于所有的子 进程运行场合。 Turbo Pascal也提供了这样的exec()函数,但用法略有不同。前面曾提到 过,BASIC和Turbo Pascal 4.0版以前的版本都不具备这些函数来运行子进程。 10.5.2程序退出 DOS EXEC的强大功能之一就是给它的父进程传回一个表明状态的代码。通过使用 INT 21h的功能4ch(终止功能),程序就能传回一个退出状态,以备将来作出决定时的条 件码。 uNIX系统一直使用这一功能来管理程序的系统执行。 例如,最近有些程序通过一连串的操作来管理一系列其他程序的执行情况。根据程序 的结束方式,主程序提供相应的分支程序来改变进程。在这方面,最复杂的莫过于通过接 口与IBM主机连接,以及如何对数据库作智能决策。若IBM主机数据传送行为并不发 生,就可跳过“把记录录入数据库”这一步,而直接使用数据库。 程序的返回代码在批处理文件内可做为ERRORLEVEL变量使用,从而在该文件中 进行逻辑分支。若从一个程序(父程序)中执行另一个程序(子程序),那么在EXec功能 返回之后,父程序就可以调用Int 21h,功能4Dh来获取子程序的返回代码。寄存器AH中 有如下几种退出类型: 退出代码 含义 00 正常终止 01 由Ctrl-C命令终止 02 关键设备出错 03 TSR返回(Int 21h,功能31h) 寄存器AL中含有子进程的返回代码 10.5.3 潜在的ExEC问题 本章前面曾提到,DOS 2.0版中的EXEC存在一些潜在的问题,并答应在后面为读 者提供一些避免这类问题的方法。本小节介绍的就是EXEC中的问题以及克服这些问题 的方法。 只有使用汇编语言时,EXEC中的问题才较明显,作为EXEC的常规DOS接口的一 部分,高级语言中似乎未出现这些问题。那么,这些问题的根源究竟在哪儿?看看下面这 个事实相信我们能找出答案。2.0版本中执行EXEC功能的DOs例行程序是用块移动指 令来把全部数据块传送到它自己的工作环境中。但块移动是由CPU芯片中的“方向”标志 控制的。若设置此标志,它会假设要传送的数据块位于所有块的末端,因而将末端的数据 250页 块移走。若清除此标志,则从开头部分开始传送数据块。这样,前后传送的数据块就会不 同。 EXEC功能允许清除此标志,但在标志清除后却不采取任何安全措施。因而,若在调 用EXEC时设置这种方向标志,则接下来所发生的一切是DOS设计者们根本不愿看到的 内容。最常见的情形就是系统被直接锁定。因为它此时正在把一套二进制机器指令当作 文件名来使用,这样,它会去装载并执行一个实际上根本不存在的文件。 在3.0版本中,可在EXEC代码的前面加上一种CLD指令,以确保正确的清除该方 向标志,因此避免了此类问题的发生。要使2.0版本中不至出现这种问题,可用Int 21h调 用EXEC功能之前,人为地加上CLD指令。 在所有已公开的DOS版本中,还存在着一个潜在的问题,即除CS和IP之外,整个调 用期间没有保存其他的寄存器。这意味着在返回时,只能把SS和SP保存在能用CS定址 的位置,或者换句话说,用户将失去对程序的控制。 10.5.4往DOS中输入命令 往DOS中输入命令时,DOS首先将命令名与命令参数分开。随后,DOS检查此命令 是不是一条内部命令。用于执行内部命令(如CLS或DIR)的代码位于DOS命令行解释 程序内。由于首先检查的是内部命令,因此用户自己所写的命令不能是DIR,也不能在命 令行上执行它。但是,有些办法可让用户添加DOS内部命令,或者将DOS内部命令替换 掉,具体情况要看使用的是哪一个DOS版本。 现在假定找到的不是一条DOS内部命令,随后就会搜索磁盘系统,期望找到相应的 文件。在当前工作目录中搜索文件名时,顺序是找具有.COM、.EXE及BAT扩展名的文 件。如果在当前目录中找不到相匹配的文件,就会继续搜索PATH字符串中环境变量中 的每一个目录。若搜遍PATH后,仍未找到匹配的文件名,则显示出“Bad filename or com- mand"(错误的文件名或命令)信息;并且DOS会返回到提示符处。 若找到批处理文件(具有.BAT扩展名),则授权给DOS批处理文件解释程序来处理 该文件。否则,调用DOS EXEC功能(功能043H,子功能000H)。若此文件的头两个字节 为MZ,就把此文件当作EXE型文件,在而不管后跟的扩展名是什么。因此,可以保证,用 COM扩展名来重命名该文件时,就可装载一个EXE文件,随后创建新的程序段前缀,将 这种COM文件逐字逐句地复制到内存中,把段寄存器设置在内存(程序就是在这里写成 的)中的节上,并且把控制权交给CS:00100H。而发生在EXE文件上的一切则更复杂 (参见图10.4)。 由于使用COM文件,创建了新的PSP,因而,除EXE文件头外,可将整个EXE文件 装入内存中位于PSP之上的位置,我们称此位置为装载地址。随后,把DS和ES寄存器 设置成指向此PSP,通过EXE头的值来设置CS:IP和SS:SP寄存器,并把装载地址处 的段值放入CS和SS寄存器中。 最后,用EXE头中的重定位表修改内存映象中的段引用。重定位表由段:偏移值指 针组成。在这里,指针就是内存映象中的字的地址,该地址是相对于装载地址的。程序的 段值依次指向内存映象中的每个字,被指向的字为加进的装载地址段相应的段值。采用这 251页 图10.4 EXE文件头 种方式,可调整程序的段引用以平衡内存(装载程序的地方)中的地址。 10.5.5替换DOS的内部命令 替换DOS的内部命令可采用三种主要方法。第一种方法适用于DOS的全部版本,即 直接用DEBUG或第三方厂家的文件编辑程序来修改COMMAND.COM,并改变修改后 的命令的名称。这种方法并不难,因为很容易找到COMMAND.COM中的命令名列表。 把初始内部命令名中某个字母换成另一大写字母,如把COPY换成KOPY,就能保留与 此名字对应的应用程序。若想取消此命令,只需把命令名中某个字母改成小写就行了,如 果把COPY改为cOPY。此cOPY决不会与敲入的命令相匹配。 第二种方法适用于DOS3.0及更高的版本,先为命令名环境(name-alike)规定出完 整的路径,例如: C:\DOSCLONE\COPY*.* 252页 然后,调用COPY(拷贝)版本。 第三种方法适用于DOS3.3及更高的版本,这种方法就是创建一种TSR,此TSR链 接DOS Interrupt(中断)2F,并截获功能0AEH,子功能000H和001H。DOS在检查它自己 的内部命令列表前调用这些中断功能。 把DS和ES设置为指向DOS的暂驻存储区,BX指向含有两个计数字节和一个DOS 命令行的缓冲区,并且让SI指向一个含有命令行(已转换成大写字母,且未超出字符计 数)中命令字的缓冲区,这样便可发出第一个调用(调用功能0AEH,子功能000H)。若命 令(或命令之一)刚好与Si缓冲区匹配,则此命令必须把AL置为0FFH,并随即返回。否 则,它就会链接到前面的中断处理程序上。 在第二个调用里(调用功能0AEH,子功能001H),寄存器的设置与第一个调用相同, 只是暂时改变BX所指的数据。这个主意不错,因为这样便可在调用子功能000H时,把命 令行参数写入内部缓冲区。执行完代码后,必须清除DS:SI命令缓冲区中的字符计数。 以便让DOS知道已执行过此代码。 对于所执行的代码,唯一要做的事就是清除字符计数。尽管对代码来说,这样作毫无 用处,但DOS满意,因为对DOS来说,有用的工作已完成。用这种方法来消除出故障的 DOS内部命令如DEL或ERASE,使用起来很简便。 10.5.6为什么有些EXE文件不能被转换成COM文件 若EXE文件不符合以下几条准则,EXEZBIN就不把EXE文件转换成COM文件: ·EXE文件必须在其文件头上带有MZ标志。 ·在文件头中,SS:SP引用值必须为0000:0000。 ·CS:IP引用值只能是0000:0100或0000:0000(用于设备驱动程序)。 ·重定位表中的项数必须为零。 10.5.7程序段前缀(PSP) 程序段前缀是一个有256字节的数据块,DOS就是用其中的数据来构造它所运行的 每一个程序的。PSP除含有DOS必须维持的信息外,还含有DOS程序要使用的信息。表 10.1显示了PSP的结构。 位于PSP:0000和PSP:0005处的代码是与CP/M兼容的早期DOS版本遗留下来 的;它们的作用与cp/M CALL0000和CALL0005相同。 表10.1 PSP结构 偏移值 内容 含义 000H-001H CD20 调用DOS终止例程 002H-003H 可变的 位于分配给程序的内存块顶部的段地址 004H 00 填充程序 005H-009H 可变的 对Int 21H调度程序的远程调用 00AH-00DH 可变的 Int 23H向量 253页 (续) 偏移值 内容 含义 00EH-011H 可变的 Int 24H向量 012H-015H 可变的 Int 22H向量 016H-017H 可变的 父进程PSP的段 018H-028H 01 01 01 00 02 系统文件表的索引 FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF 02CH-02DH 可变的 环境拷贝的段地址 02EH-031H 00 00 00 00 SS:SP存储器 032H-033H 14 00 可用的文件句柄数 034H-037H 可变的 指向文件句柄表的指针 038H-03BH 可变的 SHARE的前一个PSP 03CH-04FH 00 00 00 00 00 未使用 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 050H-052H CD 21 CB 对Int 21H和RETF的调用 053H-053H 00 00 00 00 00 未使用 00 00 00 00 05CH-06BH 可变的 缺省FCB1 06CH-07FH 可变的 缺省FCB2 080H 可变的 命令尾计数 081H-0FFH 可变的 命令尾 程序可用PSP:0002处的段地址来确定分配给它的内存是否足够。 位于PSP:000A、PSP:000E以及PSP:0012处的向量地址是MS-DOS在退出时 恢复的地址。程序得到的警告是不要修改低于PSP:00TC的值,但可以修改psP:000E 和PSP:0012(分别代表Ctrl-C和关键出错处理程序向量),而且不会造成危害。psp: 000A处的向量实际上是紧跟在DOS EXEC调用之后的父进程的返回地址,可将地址调 整为指向用户定义的过程,这一过程可与程序已安装的一切特殊处理程序脱钩。然后,此 过程必须返回到位于PSP:000A处的初始向量处。只要调用用户定义的过程,就会释放 所有的程序内存,关闭文件,并且可把PSP调整为指向父进程的pSp。在此过程中,无文 件访问或其他I/O行为发生。 父进程PSP可用来往回跟踪链接父COMMAND.COM的PSP,而父COMMAND. COM的PSp则把它自身显示为自己的父进程。这种COMMAND.COM不能是主COM- MAND.COM;它可以是外壳程序(shell)。 可用系统文件表的索引来打开stdin、stdout、stderr、stdaux和stdprn的句柄,用值FF 来关闭这些句柄。可改变PSP:0032处的值和PSP:0034处的指针来反映不同的句柄 表PSP:0034处的值则被初始化为PSP:0018。 254页 可用环境段地址来访问程序的环境拷贝。可以往回跟踪父进程PSP,以访问并修改父 进程的环境。而且,可以肯定,这一环境不是主环境。 DOS用PSP:002E处的SS:SP存储器来存储程序在Int 21H入口处的SS:SP。这 一功能允许DOS执行多重任务,但此功能至今仍未正式使用。 当SHARE(共享)在DOS 3.3及以后的版本中投入使用时,就可用PSP:0038H的 指针来保存父进程的PSP。在DOS 3.3以前,往往把PSP:0038H处的指针,设置为指向 FFFF:FFFF。 PSP:0050H处代码提供了另一种机制来访问Int 21H,这一机制使用的是了种远程 调用。 FCB可供使用FCB式文件I/O的程序使用。假设命令尾中的参数是文件名,那么,可 用命令尾中的头两个参数来装载FCB。这种装载方法是从CP/M时代继承下来的。 命令尾计数就是命令尾的长度。命令尾是命令的剩余物,从命令名后面一直延伸到处 于末端的回车,这段代码长度都属于命令尾。命令尾计数未把回车代码计算在内。若此命 令无参数,则命令尾计数为0,并且其回车代码(0DH)处于PSP:0081处。 DOS将命令尾和命令尾计数设置成缺省磁盘传送区(DTA)。这一功能也是从CP/M 中继承下来的,若使用FCB文件功能,则必须使用这一功能。 10.6内存常驻软件的编程 TSR(内存常驻软件)实用程序是一个很热很热的项,被大量地用于PC类编程。自 Borland的SideKick问世后,PC软件市场就一片热闹。有那么多的TSR实用程序可用,但 若将它们全部都用起来,就没有更多的内存空间来运行可执行的程序。人们似乎在不断地 想要很多很多的TSR,而不管他的机器实际上能否容纳得下这么多。 大多数人并未意识到他们眼中的TSR实质上可分为两种能设置和使用的TSR。TSR 实用程序(像SideKick和其他的实用程序一样),并不是TSR功能的真正用途。因为最初, 人们只是把TSR看作是操作系统的扩展。 有如下两种能设置和使用的TSR: ·活动的TSR这些弹出式实用程序如SideKick,是TSR中最普遍使用的类型。它 们通常申相应的特定按键——人们称之为热键的方法来操作。激活这些实用程序 后,它们会取代计算机。并且在把控制权归还给最初控制此机器的程序之前,执行 它们的功能。 ·不活动的TSR当调用程序调用指定的中断时,这一类TSR会作出应答,当调用 这类TSR时,它们将执行已定义好的功能,然后,类似于子程序,把控制权还给调 用程序。 活动的TSR给用户造成一种印象,即在PC机上可进行多任务执行。实际上,TSR并 不执行多任务,但是用户就是得到这种印象,好像不止一种操作在同时进行。要解释这种 现象只能说TSR较复杂。正如读者知道的那样,DOS是不可重入的(不可能中断正在进 255页 行中的内部DOS程序并从别的地方来重启动它)。当使用TSR往磁盘中写入某一内容 时,若DOS也正在处理某一操作(例如,磁盘访问),那么,将引起严重的问题,并可能把磁 盘弄得乱七八糟。 活动的TSR必须监视DOS和用户双方的行为。在第11章的描述中,读者会看到采 用的策略就是把TSR作为中断服务程序。 不活动的TSR在一种温和的环境下工作。在程序调用它之前,这种不活动的TsR并 不做任何事情。由于DOS是一种单任务的环境,在处理过程中,一次只能进行一种操作, 并且我们知道,调用TSR时,无DOs操作行为发生。因而可以安全地从不活动的TSR中 使用DOS调用。 自从DOs问世以来,TSR的概念经历了几次重大的改变。最初的TSR 功能(Int 27h)已被Int 21h的功能31h所取代。这种Int21h的功能31h用起来更方便,因为它允许 传递返回代码,并让TSR 使用64K以上的内存。这两方面的因素使得人们更喜欢使用后 一种TSR调用:Int 21h的功能31h。 当TSR运行时,TSR先设置它自己的内存表,并作好连接DOS中断的准备。一切准 备就绪后,TSR程序确定需要保留多少内存;然后把AH置为31h,AL置为返回代码,并 把DX置为分配给TSR的节(16字节的块)数。程序退出时,会把用于执行程序的内存数 量减少到TSR指定的数量,并且把退出代码返回到父程序。 听起来很简单,不是吗?对非常简单的TSR来说可能是这样。但是,若继续去编写类 似于sideKiek一类的程序时,就会发现事情远远不只是执行TSR那样简单。 首先,必须找到某种方法来激活TSR的行为。可以为TSR加上计时器中断,并且每 隔一个规定的秒数,就去激活一次TSR的操作。更常见的情况是,把TSR与键盘中断服 务例程结合在一起,并找到一种固定的击键。由于TSR的猛增,不可避免地会出现一些冲 突,因为大多数击键可用于好几个地方。 无论采用何种方式一把TSR加给某个系统,TSR都必须意识到有这种可能,即此系 统内还有其它的TSR出现。要允许同一触发器上出现其它的操作,TSR必须调用中断服 务例程,该例程在TSR启动以前处理中断。更进一步地讲,要防止与其它功能重叠,必须 能改变触发TSR的击键。 处理DOS功能时,TSR和中断服务例程(ISR)同样都有危险。 MS-DOS系统一直只 容许单用户、单任务的系统,因此是不可重入的。由于TSR或ISR对应的事件与DOS功 能的操作有可能不同步,因此在DOS内部功能的行使过程中,TSR或ISR就有可能控制 计算机。有几种未公开的DOS 功能可帮助你确定何时使DOS功能是安全的,以及何时不 能安全使用。这些未公开的DOS功能在第11章“中断处理程序”中介绍。 编写TSR是一个范围广泛的话题,足以写一整本书。复杂的TSR可能涉及到许多予 功能、窗口,以及其它一些足以让众多高级程序员用很长时间才能实现的功能,因此,这里 只能将它们略去。如果你已从11章里学到足以能使用中断的时候,你就有能力构造简单 的TSR。 256页 10.7小 结 本章已介绍了内存管理、扩展内存和程序执行等知识。从中可掌握用DOS分配TPA (传送程序区)来满足那些需要内存的程序的请求。程序启动时,它们通常获取运行时要用 到的全部内存,并且必须释放所有不使用的内存。 使用汇编语言的程序员必须明确地释放内存。而那些使用C语言或Turbo Pascal最 新版本的程序员们则可通过使用程序的启动代码来完成这一工作。除了编译期间以外,其 余的时间里BASIC和其它的Pascal程序员均不能释放内存。 扩展内存允许通过把大量的内存分页装入1M的PC可寻址内存来访问附加内存。 可用16K页中的内存来保存和获取数据。要做到这一点,必须使用Int 67h功能调用,在 本书的末尾对“EMS参考手册”的讨论中详细地谈到了此功能调用。 扩充内存则允许通过使用80286、80386和80486的功能来寻址超过1M的PC常规 内存,从而达到访问附加内存的目的。可以使用任意倍数的1K内存来保存和获取数据。 可使用XMS功能调用来实现这一点,有关XMS功能调用的详细情况可见本书后面 “XMS参考手册”一节的内容。 一旦拥有足够可用的内存,DOS便允许从其它程序中运行程序(COMMAND.COM 就是这么做的)。可以用这些内存来把各个单独的程序连接成软件系统。TSR(终止并驻留 程序)允许在程序的操作过程中,在同一内存里创建其它程序。使用第11章“中断处理程 序"中介绍的技术可创建TSR,从键盘中调用TSR能给人以一种多任务操作的映象。