TURBO PASCAL是美国BORLAND国际公司的产品,在微机PASCAL市场上占有绝对优势。它克服了往常PASCAL编译系统占用大量内存的缺陷,并对标准PASCAL作了许多有益的扩充,如它具有与低层软件和硬件打交道的能力、具有强大的图形图象功能、支持面向对象的程序设计方法、支持WINDOWS程序设计等等。它是一个名副其实的通用系统程序设计语言,十分适合开发一些高级应用软件、数据库管理系统、编译程序等。另外,TURBO PASCAL还配备有一个高性能的集成软件开发环境,包括编辑、编译、调试、文件管理等一系列功能。
本章就使用TURBO PASCAL开发高级软件的实用技术进行阐述,介绍如何使用一些工具和技术,为TURBO PASCAL程序员提供方便。本章将讲述在程序设计时使用单元的技术、TURBO PASCAL与汇编语言和C语言混合编程技术、实现和使用动态数组的技术、编写中断例程的方法、在程序中使用扩展内存(EMS)和扩充内存(XMS)的方法以及将程序的标准数据作代码处理的方法等。
§1.1 单元及其使用
单元是能与TURBO PASCAL程序分开编译的一组TURBO PASCAL过程和函数。因为单元是单独编译的,所以使用单元的程序编译速度快。而且,一个独立的单元可以为多个程序使用。充分利用单元的优点,不仅可以加快软件的开发速度,而且可以提高程序可维护性。
§1.1.1 单元的结构
一个单元有两部分组成──接口部分和实现部分。如:
unit <标识符>; {单元头}
interface {接口部分开始}
uses <单元列表> {可选项}
{公共说明部分}
implementation {实现部分开始}
{私有说明部分}
{过程或函数的定义}
begin {初始化部分开始}
{初始化代码}
end.
1.接口部分
单元的接口部分由保留字interface开始,在单元头和实现部分之间。在此部分,说明公用的常量、类型、变量与过程和函数的头部。一个程序如果使用了一个单元,那么它就能访问该单元的接口部分所定义的所有变量、数据类型、过程和函数。
接口部分仅包含过程和函数的头部。过程和函数的实现部分在单元的实现部分定义。在程序中使用一个单元只需要知道怎样调用单元中的过程,而不需要知道过程是怎样实现的。
2.实现部分
实现部分是由保留字implementation开始。实现部分定义所有在接口部分声明的过程和函数的程序体。另外实现部分可以有自己的说明,这些说明是局部的,外部程序是不知道它们的存在的,也不能调用它们。
因为在实现部分中声明的一切对象在作用域上是局部的,所以实现部分的改变对其它单元和程序来讲是不可见的。因此,修改一个单元的实现部分,并不需要重新编译使用该单元的单元,只需要编译这个修改单元和使用此单元的程序。然而,如果接口部分做了修改,所有使用该单元的单元和程序,均需要重新编译,甚至需要修改。
在实现部分,如果有uses子句,则必须紧跟在保留字implementation之后。
如果过程说明为external类型,则需用{$L 文件名.OBJ}编译指令将其连入程序。
在接口部分说明的函数或过程,除了inline类型之外,都必须在实现部分再现,它们的头部必须和接口部分一致或用简写格式。
3.初始化部分
单元的整个实现部分通常包括在保留字implementation和end之间。然而,如果把保留字begin放在end之前,在它们中间写一些语句,这些语句就是单元的初始化部分。
在初始化部分可以初始化任何变量,这些变量可由单元使用,也可通过接口部分由程序使用。可以在这部分打开文件供程序使用。例如,标准单元Printer用它的初始化部分使所有输出调用都指向文本文件Lst,这样在write语句中就可以使用它。
当使用单元的程序执行时,在程序的主体执行之前,它所使用的所有单元的初始化部分按uses子句中说明的先后依次被调用。
§1.1.2 单元的使用
当使用单元时,需在uses语句中将使用的所有单元的名字列出来,单元与单元之间用逗号(,)隔开。如:
uses dos,crt;
当编译器扫描到uses子句时,它把每个单元的接口信息加到符号表中,同时又把实现部分的机器码与程序代码连接起来。
1.单元的直接引用
一个模块(程序或单元)的uses子句只须列出该模块直接使用的单元名。例如:
program prog;
uses unit2;
const
a = b;
begin
writeln('a=',a);
end.
unit unit2;
interface
uses unit1;
const
b = c;
implementaion
end.
unit unit1;
interface
const
c = 1;
implementation
const
b = 2;
end.
unit2用了unit1,主程序用了unit2,间接地使用了unit1。
单元的接口部分如果有改动,则所有使用该单元的单元或程序必须重新编译。但如果改动了单元的实现部分,则用到它的单元不必重新编译。在上例中,如果unit1的接口部分改动了(如C=2),unit2就必须重新编译;如果只改动实现部分(b=1),则unit2不必重新编译。
编译一个单元时,TURBO PASCAL计算出该单元的版本数,这个数是单元的接口部分的校验和。上例中,在编译unit2时,unit1的当前版本数存入unit2的编译版本中,编译主程序时,unit1的版本数就和存在unit2中的版本数比较,若二者不同,说明unit2编译后,unit1的接口部分改动过,编译器给出错误信息并重新编译unit2。
2.单元的循环引用
由于在实现部分使用的单元对用户是不可见的,因此把uses子句放在单元的实现部分,进一步隐藏了单元的内部细节,而且有可能构造出相互依赖的单元。
下面的例子说明两个单元如何相互引用。主程序Circular使用Display单元,而Display单元在接口部分说明了Writexy过程,它有3个参数:坐标值x和y和要显示的文本信息,若(x,y)在屏幕内,Writexy移动光标到(x,y)并显示信息,否则,调用简单的错误处理过程ShowError,而ShowError过程反过来又调用Writexy来显示错误信息,这样就产生了单元的循环引用问题。
主程序:
program circular;
uses
crt,display;
begin
writexy(1,1,'Upper left corner of screen');
writexy(100,100,'Way of the screen');
writexy(81-length('Back to reality'),15,'Back to reality');
end.
display单元:
unit display;
interface
procedure Writexy(x,y:integer;Message:string);
implementation
uses CRT,Error;
procedure Writexy;
begin
if (x in [1..80]) and (y in [1..25]) then
begin
gotoxy(x,y);
writeln(message);
end
else
ShowError('Invalid Writexy coordinates');
end;
end.
Error单元:
unit Error;
interface
procedure ShowError(ErrMessage);
implementation
uses display;
procedure ShowError;
begin
Writexy(1,25,'Error: '+ErrMessage);
end;
end.
Display和Error单元的实现部分的uses子句互相引用,TURBO PASCAL能完整编译两个单元的接口部分,只要在接口部分不相互依赖,在实现部分可以相互调用。
§1.1.3 单元与大程序
单元是TURBO PASCAL模块化编程的基础,它用来创建能够为许多程序使用但不需要源程序的过程和函数库,它是把大程序划分为多个相关的模块基础。
通常,一个大程序可以划分为多个单元,这些单元按过程的功能将其分组。例如,一个编辑程序可以划分成初始化、打印、读写文件、格式化等若干个部分。另外,也可以有一个定义全局常量、数据类型、变量、过程及函数的“全局”单元,它能被所有单元和主程序使用。
一个大程序的框架如下:
program Editor;
uses
dos,crt,printer,
EditGlobal;
EditInit;
EditPrint;
EditFile;
EditFormat;
......
begin
...
end.
在大程序开发中使用单元的另一个原因是与代码段的限制有关。8086处理器要求代码段长度最大为64K。这意味着主程序及任何单元都不能超过64K。TURBO PASCAL将每个单元放在一个单独的段中来解决这个问题。
§1.2 与汇编语言混合编程
TURBO PASCAL以编译速度快、生成的目标代码高速和紧凑而著称。在大多数情况下,只使用TURBO PASCAL即可以完成各种各样的程序编制,但是,在硬件接口程序、实时控制程序及大规模浮点运算时,都需要用汇编语言来编程。虽然TURBO PASCAL提供了INLINE语句和命令,以及内嵌式汇编语言(TURBO PASCAL 6.00),但这是远远不够的。本节详细讨论TURBO PASCAL与汇编语言混合编程的技术,并列举了大量的实例。
§1.2.1 TURBO PASCAL的调用协定
TURBO PASCAL程序与外部汇编子程序混合编程时,要涉及到子程序的调用方式、函数或过程的参数传递方法和函数如何返回值的问题,现分述如下。
§1.2.1.1 调用子程序的方式和子程序的返回方式
TURBO PASCAL程序在调用汇编子程序时,可以是近调用也可以是远调用,因此,TURBO PASCAL程序在对汇编子程序进行调用时,根据调用方式的不同,有两种不同的保存返回地址的方法:①近调用时,因是段内调用,仅将偏移地址IP入栈,占2字节;②远调用时,因是段间调用,要将代码段值CS和偏移地址IP入栈,占4字节。
在主程序中直接调用汇编子程序时,一般采用近调用,汇编子程序采用近返回方式,用RET指令;在TURBO PASCAL的单元中使用汇编子程序时分两种情况:①在单元接口部分说明的子程序,在汇编子程序中要用远返回,用RETF指令;②在单元解释部分说明的子程序,汇编子程序要用近返回方式,用RET指令。
汇编子程序在运行结束后,为了能正确地返回到调用程序,栈顶指针必须指向正确的返回地址,它通过在返回指令RETF(或RET)中给出参数入栈时所占的字节数的方法实现的。
§1.2.1.2 参数传递的方法
TURBO PASCAL是利用堆栈向过程和函数传递参数的,参数按从左到右的顺序(说明顺序)被压入堆栈中,例如调用过程PROC(A,B,C : INTEGER; VAR D)时,其堆栈情况见图1-1。
殌 ┌────────────┐
│+0E│ 参数A的值 │ ↑
│ ├────────────┤ │
参│+0C│ 参数B的值 │ │参
│ ├────────────┤ │
数│+0A│ 参数C的值 │ │数
│ ├────────────┤ │
压│ +8│ 参数D的段地址 │ │出
│ ├────────────┤ │
栈│ +6│ 参数D的偏移地址 │ │栈
│ ├────────────┤ │
顺│ +4│ 过程返回的段地址 │ │顺
│ ├────────────┤ │
序│ +2│ 过程返回的偏移地址 │ │序
↓ ├────────────┤ │
│ BP寄存器的值 │
└────────────┘
殣 图1-1.TURBO PASCAL远调用汇编程序PROC的堆栈情况
TURBO PASCAL在调用子程序时,有两种传递参数的方法,即传值和传地址的方法。下面分别说明这两种参数传递方法。各种类型参数入栈的方法见表1-1。
(1)传值方式
在TURBO PASCAL的过程或函数的形式参数表中,以值参形式定义的参数,且类型是记录、数组、字符串、指针等复合类型以外的各种类型,如字节型(BYTE)、短整型(SHORTINT)、整型(INTEGER)、字型(WORD)、长整型(LONGINT)、字符型(CHAR)、布尔型(BOOLEAN)、实数型(REAL)等,TURBO PASCAL在调用子程序时,直接将实参值依次从左到右顺序压入堆栈中,汇编子程序可以直接从堆栈中取得实参的值。
(2)传地址方式
在TURBO PASCAL的过程或函数的形式参数表中,以变量形式定义的参数,及以记录、字符串、数组、指针等复合类型定义的值参,TURBO PASCAL在调用子程序时,是将调用程序的实参地址依次按从左到右的顺序压入堆栈的。汇编子程序从堆栈中取得实参的地址,即可得到参数的值。同样汇编子程序可以把运算结果存放到对应的变量中,以便传回调用程序。
表1-1.各种类型参数入栈的方法
殌┌───────┬────┬─────┐
│形参类型 │传递方式│栈中字节数│
├───────┼────┼─────┤
│char,boolean │ │ │
│byte,shortint,│ 传值 │ 2 │
│integer,word │ │ │
├───────┼────┼─────┤
│longint,single│ 传值 │ 4 │
├───────┼────┼─────┤
│real │ 传值 │ 6 │
├───────┼────┼─────┤
│double │ 传值 │ 8 │
├───────┼────┼─────┤
│string,pointer│ 传地址 │ 4 │
│变量 │ │ │
└───────┴────┴─────┘殣
§1.2.1.3 函数返回值的传递
TURBO PASCAL函数返回值的传递方式根据函数返回值类型的不同而异,有采用传地址的方式进行,也有采用寄存器方式进行,如采用传地址的方式,其地址(4字节)首先入栈,然后才压入函数参数,最后压入函数的返回地址。各种函数返回类型的传递方式见表1-2。
表1-2.各种函数返回类型的传递方式
殌┌───────┬──────────┬──────┐
│ 函数返回类型 │ 返 回 方 式 │ 所占字节数 │
├───────┼──────────┼──────┤
│boolean,byte, │ 在寄存器AL中 │ 1 │
│char,shortint │ │ │
├───────┼──────────┼──────┤
│word,integer │ 在寄存器AX中 │ 2 │
├───────┼──────────┼──────┤
│longint │ 高位在DX,低位在AX │ 4 │
├───────┼──────────┼──────┤
│real │由高到低在DX,BX,AX中│ 6 │
├───────┼──────────┼──────┤
│pointer │段地址在DX,偏移在AX │ 4 │
├───────┼──────────┼──────┤
│string │ 在DS:SI指的地址中 │ 不限 │
└───────┴──────────┴──────┘
殣
§1.2.2 汇编子程序的编写格式
根据TURBO PASCAL的调用协定,外部汇编子程序的通用编写格式如下:
TITLE 程序名
DOSSEG
LOCALS @@
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
PUBLIC 过程或函数名
过程或函数名:
PUSH BP
MOV BP,SP
…
POP BP
RETF 参数占堆栈字节数
END
上述汇编子程序是TURBO ASSEMBLER的格式,本文汇编子程序均采用这种格式。对此汇编子程序格式说明如下:
. 汇编模块要采用TPASCAL模式;
. 在汇编模块中,必须把TURBO PASCAL调用的过程或函数说明为PUBLIC属性;
. 子程序返回指令视具体情况而定,近调用用RET,远调用用RETF;
. 返回指令后的参数是指该子程序形式参数表中所有参数入栈后所占堆栈的字节数;
. 汇编模块结束要写END。
§1.2.3 TURBO PASCAL程序的编写格式
在TURBO PASCAL中,声明外部子程序的格式如下:
procedure prc(a, b : integer; var c : real);external;
function func(a, b : integer) : real; external;
即在通常的TURBO PASCAL过程或函数的声明后加上external关键字。在声明了外部过程或函数的主程序或程序单元中,要用编译指令{$L},把汇编好的目标模块加载进来。
在TURBO PASCAL程序中使用外部汇编过程或函数时,方法和一般的TURBO PASCAL过程和函数没有两样。
§1.2.4 主程序中使用外部汇编子程序的典型例子分析
在TURBO PASCAL主程序中直接使用外部汇编子程序时,一般采用近调用方式,所以汇编子程序返回指令为RET,在特别指明采用远调用方式时,要用RETF返回指令。
1.无参数传递的过程
program prog1;
{$L prog1.obj}
procedure DisplayOk;external;
begin
DisplayOk;
end.
Title PROG1
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
OkMsg db 'OK !',0dh,0ah,'$'
; Procedure DisplayOk
PUBLIC DisplayOk
DisplayOk:
push ds ;保存数据段
push cs ;代码段入栈
pop ds ;弹出数据段
mov ah,09 ;显示字符串
mov dx,offset OkMsg ;字符串地址
int 21h ;DOS功能调用
pop ds ;恢复数据段
ret ;近返回
end ;汇编子模块结束
2.传递字符型值参的过程
program prog2;
{$L prog2.obj}
procedure DisplayInt(ch : char);external;
begin
DisplayInt('a');
end.
Title PROG2
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure DisplayInt
PUBLIC DisplayInt
DisplayInt:
push bp
mov bp,sp
mov ah,02 ;显示字符
mov dl,[bp+4] ;从栈中取参数
int 21h ;DOS功能调用
pop bp
ret 2 ;形式参数在栈中占2字节
end
3.传递字符型值参和整型变参的过程
program prog3;
{$L prog3.obj}
procedure ProcArg(ch : char;var i : integer);external;
var i : integer;
begin
ProcArg('d',i);
writeln(i);
end.
Title PROG3
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure ProcArg
PUBLIC ProcArg
ProcArg:
push bp
mov bp,sp
xor ax,ax
mov al,byte ptr [bp+8] ;取字符参数
les si,[bp+4] ;取整数变量的地址
mov es:[si],al
pop bp
ret 6 ;形式参数在栈中占6字节
end
4.传递字符值参返回整型的函数
program prog4;
{$L prog4.obj}
function func(ch : char) : integer; external;
begin
writeln(func('a'));
end.
Title PROG4
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
xor ax,ax
mov al,byte ptr [bp+4] ;取字符参数值
pop bp
ret 2 ;形式参数在栈中占2字节
end ;子程序返回值在寄存器AX中
5.传递字符串型值参和返回字符串型的函数
program prog5;
{$L prog5.obj}
function func(s : string) : string; external;
begin
writeln( func('abcd') );
end.
Title PROG5
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
push ds
xor ch,ch
lds si,[bp+4] ;取字符串S的地址
les di,[bp+8] ;取返回值地址
mov cl,[si]
inc cl
cld
@@1:
lodsb
stosb
loop @@1
pop ds
pop bp
ret 4 ;字符串S的地址在栈中占4字节
end
6.传递长整型值参和返回长整型的函数
program prog6;
{$L prog6.obj}
function func(li : LongInt) : Longint; external;
var i : longint;
begin
i := func(111111110);
writeln(i);
end.
Title PROG6
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
mov ax,[bp+4] ;取长整型数高位
mov dx,[bp+6] ;取长整型数低位
les si,[bp+8] ;取函数返回地址
mov es:[si],dx
mov es:[si+2],ax
pop bp
ret 4 ;长整型数LI在栈中占4字节
end
7.传递实型数值参和返回实型数的函数
program prog7;
{$L prog7.obj}
function func(r : real) : real; external;
var r : real;
begin
r := func(11111.1110);
writeln(r);
end.
Title PROG7
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
mov ax,[bp+4] ;取实数R的值
mov bx,[bp+6] ;
mov dx,[bp+8] ;
les si,[bp+0ah] ;取函数的返回地址
mov es:[si],dx
mov es:[si+2],bx
mov es:[si+4],ax
pop bp
ret 6 ;实数R在栈中占6字节
end
§1.2.5 单元中使用汇编模块的情况
在下面的演示单元DEMOU中声明了两个外部汇编函数,P1是在单元接口部分定义的,在汇编模块中采用远返回方式,P2是在单元的解释部分声明的,在汇编模块中采用近返回方式。在单元DEMOU的过程P3中又调用了函数P1和P2,调用P2采用近调用,没有问题;当调用P1时,因其是在接口部分定义的,必须采用远调用方式,这可以用编译指令{$F}来完成,在调用 P1之前,加上 {$F+},在调用之后,加上{$F-}即可。
program prog8;
uses demou;
begin
if p1(1) then Writeln('Far call complete !');
p3;
end.
unit demou;
interface
function p1(a : integer) : boolean;
procedure p3;
implementation
{$L demou.obj}
function p1( a : integer) : boolean; external;
function p2( a : char ) : boolean; external;
procedure p3;
begin
if p2('a') then writeln('Near call complete !');
{$F+} ;打开远调用编译指令
if p1(1) then Writeln('Far call again !');
{$F-} ;关闭远调用编译指令
end;
end.
Title DEMOU
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; function p1
PUBLIC p1
p1:
push bp
mov bp,sp
xor ax,ax
cmp ax,[bp+4]
jnz @@1
mov al,0
jmp @@2
@@1: mov al,1
@@2: pop bp
retf 2 ;此函数在单元接口部分定义
; function p2
PUBLIC p2
p2:
push bp
mov bp,sp
mov ax,'a'
cmp ax,[bp+4]
jnz @@3
mov al,0
jmp @@4
@@3: mov al,1
@@4: pop bp
ret 2 ;此函数在单元解释部分定义
end
§1.2.6 小结
本节详细地介绍了TURBO PASCAL与汇编语言混合编程的技术,并给出了许多典型实例,读者可以参照实例的格式进行混合语言编程,解决工作中的具体问题。高级语言和汇编语言混合编程是一个比较复杂的事情,只有在实践中不断细心体会,积累经验,才能有所提高。在对混合语言编写的程序进行调试时,不妨多使用TURBO DEBUGGER,它可以帮助你发现许多不易发现的汇编语言的错误,有利于加快程序的开发进程。
§1.3 与C语言混合编程
一般来说,高级语言间的相互调用比较困难。对TURBO系列软件来说,BORLAND公司提供了语言之间相互调用的接口机制,下面介绍TURBO PASCAL和TURBO C/C++混合编程的方法步骤。TURBO PASCAL的调用协议在上一节中已经叙述,这里不再赘述。
§1.3.1 TURBO C/C++程序的编写与编译
用TURBO C编写的供TURBO PASCAL调用的模块的一般形式如下:
/* C moduler for Turbo PAscal */
类型1 far 函数名1(形参表) /* 在单元接口部分定义 */
{
...
}
类型2 near 函数名2(形参表) /* 在程序或单元实现部分定义 */
{
...
}
其中,第一个函数的说明部分使用了调用模式说明far,它是在TURBO PASCAL单元的接口部分定义的,需要使用远调用。第二个函数用了near调用模式,它是在单元的实现部分或程序中定义的,采用近调用。
编写供TURBO PASCAL程序使用的TURBO C模块应当遵循如下的规则:
(1)在TURBO PASCAL单元的实现部分或主程序中直接定义的C函数,调用类型应当说明为near;在TURBO PASCAL单元的接口部分定义的C函数,调用类型应当说明为far;
(2)公用数据应当在TURBO PASCAL程序中定义,TURBO C模块中定义的数据不能被TURBO PASCAL程序引用;
(3)由于没有正确的段名,TURBO C/C++的运行库例程不能在TURBO C模块中使用。但是,当你拥有TURBO C/C++运行库例程的源码时,可以在你的C模块中包含进库例程的原型,重编译单个库例程模块,这样即可使用相关的库例程;
将编写好的TURBO C/C++程序模块编译成目标文件,需要遵循如下的规则:
(1)任何C模块均用小内存模式(SMALL)编译;
(2)把TURBO C/C++的代码生成编译开关设置为PASCAL;
(3)段名设置如下:
CODE names的Segment name设置为CODE,Group name和Class name设为空;
DATA names的Segment name设置为CONST,Group name和Class name设为空;
BSS names的Segment name设置为DATA,Group name和Class name设为空;
或者,用TURBO PASCAL系统盘上提供的TURBO C/C++的配置文件TURBOC.CFG来编译C模块的源程序。方法有二:
(1)在包含TURBOC.CFG和C模块子目录下,执行:
TCC C模块名.C
(2)执行:
TC /CCTOPAS.TC C模块名.C
把C模块编译为目标模块,即可在TURBO PASCAL程序中引用。其中CTOPAS.TC和TURBOC.CFG都可以在TURBO PASCAL或TURBO C的系统盘上找到。
§1.3.2 TURBO PASCAL程序的编写
TURBO PASCAL程序与普通的TURBO PASCAL程序没有两样,只是把有关的C函数定义为外部函数,并用编译开关{$L 文件名}将C模块的目标模块(.OBJ)连接到PASCAL程序中即可。
§1.3.3 程序中使用TURBO C函数的实例
在TURBO PASCAL的主程序中使用TURBO C模块定义的函数, 则C模块中的函数一般均定义为near调用类型。实例如下:
PASCAL主程序CPASDEMO.PAS:
program CPASDEMO;
uses Crt;
var
Factor : Word;
{$L CPASDEMO.OBJ}
function Sqr(I : Integer) : Word; external;
{ Change the text color and return the square of I }
function HiBits(W : Word) : Word; external;
{ Change the text color and return the high byte of W }
function Suc(B : Byte) : Byte; external;
{ Change the text color and return B + 1 }
function Upr(C : Char) : Char; external;
{ Change the text color and return the upper case of C }
function Prd(S : ShortInt) : ShortInt; external;
{ Change the text color and return S - 1 }
function LoBits(L : LongInt) : LongInt; external;
{ Change the text color and return the low word of L }
procedure StrUpr(var S : string); external;
{ Change the text color and return the upper case of S-Note that }
{the Turbo C routine must skip the length byte of the string. }
function BoolNot(B : Boolean) : Boolean; external;
{ Change the text color and return NOT B }
function MultByFactor(W : Word) : Word; external;
{ Change the text color and return W * Factor - note }
{ Turbo C's access of Turbo Pascal's global variable. }
procedure SetColor(NewColor : Byte);
{ A procedure that changes the current }
begin
{ display color by changing the CRT }
TextAttr := NewColor;
{ variable TextAttr }
end; { SetColor }
var
S : string;
begin
Writeln(Sqr(10));
{ Call each of the functions defined }
Writeln(HiBits(30000));
{ passing it the appropriate info. }
Writeln(Suc(200));
Writeln(Upr('x'));
Writeln(Prd(-100));
Writeln(LoBits(100000));
S := 'abcdefg';
StrUpr(S);
Writeln(S);
Writeln(BoolNot(False));
Factor := 100;
Writeln(MultbyFactor(10));
SetColor(LightGray);
end.
C模块CPASDEMO.C:
typedef unsigned int word;
typedef unsigned char byte;
typedef unsigned long longword;
extern void setcolor(byte newcolor); /* procedure defined in
Turbo Pascal program */
extern word factor; /* variable declared in Turbo Pascal program */
word sqr(int i)
{
setcolor(1);
return(i * i);
} /* sqr */
word hibits(word w)
{
setcolor(2);
return(w >> 8);
} /* hibits */
byte suc(byte b)
{
setcolor(3);
return(++b);
} /* suc */
byte upr(byte c)
{
setcolor(4);
return((c >= 'a') && (c <= 'z') ? c - 32 : c);
} /* upr */
char prd(char s)
{
setcolor(5);
return(--s);
} /* prd */
long lobits(long l)
{
setcolor(6);
return((longword)l & 65535);
} /* lobits */
void strupr(char far *s)
{
int counter;
for (counter = 1; counter <= s[0]; counter++) /* Note that the routine */
s[counter] = upr(s[counter]); /* skips Turbo Pascal's */
setcolor(7); /* length byte */
} /* strupr */
byte boolnot(byte b)
{
setcolor(8);
return(b == 0 ? 1 : 0);
} /* boolnot */
word multbyfactor(word w)
{
setcolor(9); /* note that this function accesses the Turbo Pascal */
return(w * factor); /* declared variable factor */
} /* multbyfactor */
§1.3.4 TURBO PASCAL单元中使用TURBO C函数的实例
在TURBO PASCAL单元中使用TURBO C模块定义的函数,则C模块中的函数的调用方式可以是near和far两种类型。下面给出一个简单的例子。
PASCAL主程序CPDEMO.PAS:
program CTOPASDEMO;
uses CPUNIT;
begin
writeln(add2(3));
DisplaySub2(3);
end.
PASCAL单元CPUNIT.PAS:
unit CPUNIT;
interface
function add2(x : integer) : integer;
procedure DisplaySub2(x: integer);
implementation
{$L CTOPAS.OBJ}
function add2; external;
function sub2(x : integer) : integer; external;
procedure DisplaySub2;
begin
WriteLn(Sub2(x));
end;
end.
C模块CTOPAS.C:
int far add2( int x)
{
return (x + 2);
}
int sub2(int x)
{
return(x - 2);
}
TURBO PASCAL和TURBO C均是目前比较流行的编程语言,广大编程人员如果能正确熟练地使用TURBO PASCAL和TURBO C进行混合编程,可以达到事半功倍的效果,使软件的开发得以加速。
§1.4 过程类型及其使用
TURBO PASCAL允许使用过程类型,把过程或函数当做能赋给变量或传给参数 的对象。在过程类型说明中,定义过程或函数的参数和函数的返回类型。
§1.4.1 过程类型的说明
过程类型的说明方法如下:
TYPE
Proc0 = Procedure;
Proc1 = Procedure(x : integer);
func0 = function : integer;
func1 = function(x : integer) : boolean;
过程类型说明中的参数名完全是装饰性的,并无实际意义。
§1.4.2 过程类型常量
过程类型常量必须指定过程或函数的标识符,而且过程或函数必须与常量的类型赋值兼容。例如:
Type
ErrorProc = Procedure(ErrorCode : integer);
Procedure DefaultError(ErrorCode : integer); far;
begin
Writeln('Error ',ErrorCode,'.');
end;
const
ErrorHandler : ErrorProc = DefaultError;
§1.4.3 过程类型变量
过程类型说明之后,就可以用来定义变量,这种变量叫做过程变量。如:
Var
f01,f02 : func0;
f11,f12 : func1;
p1 : proc1;
象整型变量一样,过程变量能赋以过程值。过程值可以是另一过程变量,一可以是过程或函数的标识符。比如有如下过程和函数说明:
function f1 : integer;far;
begin
f1 : = 1;
end;
则下面的语句是成立的:
f01 := @f1;
f02 := f01;
把函数或过程赋给过程变量,必须满足下列要求:
. 函数或过程必须是far调用类型
. 不能是标准过程或函数
. 不能是嵌入式过程或函数
. 不能是INLINE过程或函数
. 不能是INTERRUPT过程
过程类型的使用不限于简单变量,象其它类型一样,过程类型变量可以作为结构类型的分量。如:
type
GotoProc = procedure(x,y:integer);
procList = array[1..10] of GotoProc;
WindowPtr = ^WindowRec;
WindowRec = Record
Next : WindowPtr;
Header : string[31];
Top,Left,Bottom,Right : integer;
SetCursor : GotoProc;
end;
var
p : ProcList;
W : WindowPtr;
这样说明之后,下面语句是合法的过程调用:
p[3](1,1);
W^.SetCursor(10,10);
过程值赋给过程变量,实际上是把过程的地址存放到变量中。过程变量很象指针指针量,只是指针变量指向数据,过程变量指向过程或函数的入口。过程变量占4个字节,第一个字存放地址的偏移量,第二个字存放段地址。
§1.4.4 表达式中的过程类型
通常,在语句或表达式中使用过程变量表示对变量中储存的过程或函数的调用,但也有例外,当TURBO PASCAL在一个赋值语句的左边发现一个过程变量时,它就认为右边一定是一个过程值。例如:
type
IntFunc = function : integer;
var
F : IntFunc;
N : integer;
function ReadInt : integer; far;
var i : integer;
begin
readln(i);
ReadInt := i;
end;
begin
F := ReadInt;
N := ReadInt;
end.
主程序中的第一个语句将过程值ReadInt赋给过程变量F,第二个语句调用ReadInt,将返回值赋给N。
对语句:
if F = ReadInt then Writeln('Equal');
的解释是,调用F和ReadInt,然后比较它们的返回值,如果相等,则打印Equal。若要比较F和ReadInt的过程值,必须使用以下的结构:
if @F = @ReadInt then Writeln('Equal');
当给出过程变量或过程、函数标识符是时,地址操作符@可以阻止编译器调用该过程、函数,同时将参量转换为一个指针,@F将F转换为包含地址的无类型指针,@ReadInt返回ReadInt的地址。
当要取得过程变量的内存地址时,必须连用两个地址操作符(@@)。如,@F表示将F转换为一个无类型指针变量,@@F则表示返回变量F的物理地址。
§1.4.5 过程类型参数
由于过程类型可在任何场合使用,所以可把过程类型作为过程或函数的参数进行传递,这样在某些场合可以大大简化程序的编制工作。下面两例说明了过程参数的使用。
利用Simpson公式,求S=∫攩b攪攬a攭f(x)dx。采用逐步近似逼近方法:S=h/3*(f攬0攭+4*(f攬1攭+f攬3攭+...+f攬n-1攭)+2*(f攬2攭+f攬4攭+...+f攬n-2攭+f攬n攭),其中f攬i攭=f(a+i*h),h=(b-a)/n。
program Simpson_Method;
type
Func = function(x: real) : real;
{$F+}
function Simpson(f : Func; a,b : real; n : integer) : real;
var
h,s : real;
i : integer;
begin
h := (b-a) / n;
s := f(a) + f(b);
for i := 1 to n-1 do
if odd(i) then s := s + 4 * f(a + i*h)
else s := s + 2 * f(a + i*h);
Simpson := s * h / 3;
end;
function f1(x : real) : real;
begin
f1 := sqr(x) * sin(x);
end;
function f2(x : real) : real;
begin
f2 := 1 / (1 + x);
end;
begin
Writeln('∫1,2(x^2*sin(x) = ',SimpSon(f1,1,2,10):10:2);
Writeln('∫1,10(1/(1+x)) = ',SimpSon(f1,1,10,20):10:2);
end.
利用过程参数,设计一个通用的打印自然对数和常用对数表的程序。
program PrintLogTable;
type Func = function(x : integer) : real;
{$F+}
function log(x : integer) : real;
begin
log := ln(x) / ln(10);
end;
function lne(x : integer) : real;
begin
lne := ln(x);
end;
procedure PrintTable(i,w : integer; f : func);
var
m,n : integer;
begin
m := 0;
n := 0;
repeat
inc(m);
inc(n);
write(m:3,f(m):7:4,' ');
if n = w then
begin
writeln;
n := 0;
end;
until m = i;
writeln;
end;
begin
PrintTable(1000,7,log);
PrintTable(1000,7,lne);
end.
§1.5 中断例程的编写方法
TURBO PASCAL的运行库和由编译程序产生的代码是可中断的。大多数运行库是可重入的,这允许你用TURBO PASCAL编写中断例程。
§1.5.1 编写中断例程
中断过程可用INTERRUPT指令声明。每个中断程序必须用下列过程头来说明:
procedure IntHandle(Flags,CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP : WORD);
INTERRUPT;
BEGIN
...
END;
所有寄存器都是作为参数传递的,所以在源代码中可以使用和修改,并且可以省略部分和全部参数。
在入口时,中断过程自动保存所有的寄存器,并初始化DS寄存器;出口代码恢复所有寄存器,并执行中断返回指令。
§1.5.1.1 常驻型中断例程(ISR)的框架
Program 程序名;
{$M 栈长度,0,0}
uses dos;
var
...
procedure 中断过程名(Flags,CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP : WORD);
INTRRUPT;
begin
...
end;
begin
SetIntVec(中断号,@中断过程名);
keep(k);
end.
其中编译指令$M是Keep过程要求的,栈段的长度值为1024~65520,最大和最小堆长度经常都说明为0。
§1.5.1.2 暂驻型中断例程的框架
Program 程序名;
uses dos;
var
p : pointer;
...
procedure 中断过程名(Flags,CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP : WORD);
INTRRUPT;
begin
...
end;
...
begin
GetIntVec(中断号,p);
SetIntVec(中断号,@中断过程名);
...
SetIntVec(中断号,p);
end.
在暂驻型程序中,在设置自己的中断向量之前,首先把系统原有的中断向量存放到有关的变量中,等到程序运行结束时,要把程序中所有重新定义过的中断向量恢复到系统原有的状态。否则程序返回操作系统后随时都会造成死机。
§1.5.2 设置中断向量、调用中断功能、程序驻留内存
在使用自己编写的中断例程的程序中,可用SetIntVec来设置自己的中断服务程序:
SetIntVec(IntNo,@IntHandle);
IntNo为中断号,@IntHandle为中断服务过程的地址。
在程序中可以使用Intr过程来调用自己的中断过程:
Intr(IntNo,Regs);
当用TURBO PASCAL编写的中断例程要驻留内存时,可以使用Keep过程来实现:
Keep(ExitCode);
§1.5.3 建立通讯数据区
由于中断例程的代码段地址可以从中断向量表中直接得到,用这个地址加上适当的地址偏移量,作为预定义内存数组Mem/MemW和MemL的下标,即可直接访问这些特定的内存单元,因此可以建立ISR的通讯数据区。
例如,为了防止同一个ISR在内存中重复驻留,应当设置一个"已驻留"的标志。如果在ISR最前面的过程的第一条语句中,把一条在驻留之前显示的屏幕提示,赋给一个动态串。通常这条提示串在显示完成后,其内容已不再需要,但可以通过引用内存数组元素MEMW[中断入口段地址:1],对其中的内容加以比较,即可知道ISR是否驻留,新的ISR是否可以驻留。
§1.5.4 释放驻留内存
一个设计优秀的ISR,在撤离时,它所占用的内存也应该被释放。在用TURBO PASCAL 4.0或更高版本编译的程序中,有一个预定变量PrefixSeg,保存着该程序的程序段前缀PSP的起始段地址。把这个段地址作为调用参数送入ES寄存器之后,再调用INT 21H的49H号功能,即可把ISR自身占用的内存予以释放。
除了ISR自身占用的驻留内存空间外,DOS还为它另外分配了一块内存空间,作为它的环境块。其中存放着DOS环境参数的副本。虽然环境块不大,但它们同样也驻留着。在PSP的地址偏移量为2CH的位置上已经存放着上述环境块的段地址。将内存数组元素MEMW[Prefixseg:$2C]的值送入ES寄存器,再一次调用INT 21H的49H号功能,就能够把ISR的环境块也释放掉,从而回收ISR的全部驻留空间。
值得注意的是:如果在回收到的内存空间的高端地址处,还有其它未释放的驻留块,则已经回收的内存空间就会成为待用自由链中的“碎块”。这些碎块将会影响到以后的内存分配情况。在重新启动系统之前,其中的一部分有可能不能再次进行分配。因此在使用过程中应当先引导那些不需要撤换的ISR,需要反复撤换的ISR则放在最后引导。
此外,应当牢记:释放ISR的驻留内存之前,一定要恢复原来的中断向量,否则会造成系统混乱,导致死机或其它故障。
能够释放驻留内存,就可以在用户需要的时侯,任意调换不同的ISR,以满足各种不同的使用要求,并能随时保持较小的内存开销。
§1.5.5 程序实例
本程序(INTR.PAS)演示常驻型中断例程(ISR)的编写,并演示了怎样建立通讯数据区和程序的撤离方法,释放所有驻留内存。
{$M $1000,0,0}
program intrrupt_example;
uses dos;
const
MyMark:string[8] = 'MyInt500';
var
OldInt5,MyIntr5 : longint;
Mark : string[10];
reg : registers;
procedure First;
begin
Mark := 'MyInt500';
end;
procedure MyInt5(flags,cs,ip,ax,bx,cx,dx,si,di,ds,es,bp:word);
interrupt;
begin
inline($fa);
myintr5 := meml[0:5*4];
memw[0:5*4] := $a;
inline($fb);
if ax and $ff00 = $fe00 then
begin
inline($fa);
meml[0:4*5] := oldint5;
reg.es := prefixSeg;
reg.ah := $49;
msdos(reg);
reg.es := memw[PrefixSeg:$2c];
reg.ah := $49;
msdos(reg);
inline($fb);
end
else
begin
writeln('Call Interrupt 5');
inline($fa);
meml[0:5*4] := myintr5;
inline($fb);
end;
end;
procedure SetInt5;
begin
setIntvec($1b,saveint1b);
inline($fa);
OldInt5 := meml[0:4*5];
SetIntVec(5,@MyInt5);
mem[memw[0:4*5+2]:$a] := $cf;
inline($fb);
end;
function CheckInt5 : boolean;
var
i,j : word;
int5 : pointer;
begin
getIntVec(5,int5);
checkInt5 := true;
j := ofs(mark);
for i := 1 to 8 do
begin
if (chr(Mem[seg(int5^):i]) <> MyMark[i]) then
CheckInt5 := false;
end;
end;
procedure RemoveInt5;
begin
if CheckInt5 then
begin
reg.ah := $fe;
intr(5,reg);
writeln('Intrrupt 5 has been removed for memory');
end
else
writeln('Not find external Intrrrupt 5 routine');
end;
begin
first;
if paramcount = 0 then
begin
if CheckInt5 then
begin
writeln('Int 5 has kept');
halt(1);
end
else
begin
SetInt5;
keep(0);
end
end
else
if (paramstr(1) = 'R') or (paramstr(1) = 'r') then
RemoveInt5;
end.
§1.6 动态数组及其使用
§1.6.1 TURBO PASCAL的内存分配
TURBO PASCAL将计算机的可用内存划分为4个部分,如图1-2所示。代码段用于存放编译后程序指令;数据段用于存放程序和单元的常量和全程变量;栈段用于存放程序中过程和函数的局部变量;堆段用于存放程序的动态变量。
殌 ┌────┐← 最高可用内存
堆段 →│动态变量│↑向上分配
├────┤←
栈段 →│局部变量│
├────┤
数据段→│数据段 │
├────┤
代码段→│程序指令│
└────┘← 最低可用内存
殣
图1-2 TURBO PASCAL程序的内存分配图
堆空间是一个由用户控制的动态数据区域,它是利用指针变量在程序运行时动态分配的。也就是说,程序在编译时并不为指针变量在堆上分配空间,而在程序运行时,执行有关的语句时,才为其在堆上分配空间。堆的空间虽然能在0~640K之间变动,但在其中建立的每个动态变量的体积均不能大于65521字节。
§1.6.2 构造动态数组的方法
根据TURBO PASCAL对内存的管理方法,对总体积不大于64K的数组,可以直接在堆空间进行分配,在程序运行中构造动态数组。利用指针在堆中建立动态数组的步骤如下:
1.数组及其指针类型的说明
Type
IntArrPtr = ^IntArray;
IntArray = array[1..100] of integer;
2.指针变量的说明
Var IntArr : IntArrPtr;
3.申请内存
在使用动态数组之前,用New或GetMem过程在堆中分配内存空间,建立动态数组及它们的指针值。如:
GetMem(IntArr,Sizeof(IntArr^));
4.引用
在程序中按一般的TURBO PASCAL动态变量引用规则使用动态数组。如:
writeln(IntArr^[10]);
5.释放内存
动态数组使用完毕,立即用Dispose或FreeMem过程释放堆空间。如:
FreeMem(IntArrPtr,Sizeof(IntArr^));
下面的程序演示了上述方法,它在堆中建立了一个10000个元素的实型数组A。
program Dynamic_Array;
type
Arr1 = array[1..10000] of real;
var
A : ^arr1;
i : integer;
begin
GetMem(A,sizeof(a^));
for i := 1 to 10000 do a^[i] := i;
for i := 1 to 10000 do write(a^[i]:8:0);
FreeMem(A,sizeof(a^));
end.
§1.6.3 构造大于64K的数组
将整个大数组当做若干个小于64K的同类型、较低维数组的组合,在堆中建立这些子数组。然后,将这些子数组的指针以一个指针数组的形式组织起来。在形式上,该指针数组的数组名就是要定义的大数组的数组名,通过此名来统一引用该数组的各元素,从而达到能按通常的编程习惯在表达式中通过下标直接引用大数组的元素的目的。
下面的程序给出了如何具体应用上述方法的示例。它在堆中建立了一个8x100 x100的三维实数数组A,约占用480K内存。该数组被看成由8个100x100的二维子数组组成,因为每个子数组的体积为60000字节,故可用一动态数组表示。指向这8个子数组的8个指针组成了指针数组A。这样可通过A[i]^[j,k]引用上述三维数组的元素,不需要作任何下标变换,可直接参加表达式的运算,与静态数组的用法非常接近。从而保证了原来的程序设计风格,给程序的设计、编写、阅读、调试和修改都带来了方便。
program Huge_Array;
const
n = 100;
m = 8;
type
Arr2 = array[1..n,1..n] of real;
var
A : array[1..m] of ^Arr2;
i,j,k : integer;
begin
for i := 1 to m do GetMem(A[i],sizeof(a[i]^));
for i := 1 to m do
for j := 1 to n do
for k := 1 to n do
a[i]^[j,k] := 100*i + 10*j + k;
for i := 1 to m do
begin
for j := 1 to n do
begin
writeln('*****i=',i,',j=',j,'*****');
for k := 1 to n do write(a[i]^[j,k]:8:0);
writeln;
end;
writeln;
end;
for i := m downto 1 do FreeMem(A[i],sizeof(a[i]^));
end.
§1.6.4 动态可调数组的实现
当程序中要多次进行某种数组运算,如矩阵的转置、矩阵相乘或求解线性方程组时,程序员总希望把某种数组运算编写成一个通用过程,让过程中的数组是可调数组,即数组的维数和元素类型固定,每维的上下界可变。
动态可调数组的实现方法如下:
1.类型说明
按照所需的数组类型建立一个数组类型,为了达到数组规模可调,说明时不必给定数组各维的界限。如:
Type RealArray = array[1..1] of real;
2.变量说明
动态可调数组变量的说明和动态数组的说明一样,采用指针的形式。如:
var ra : ^RealArray;
3.动态可调数组的建立
当需要使用数组时,首先计算所有数组元素所占用的空间AraaySize的值,然后用New或GetMem分配ArraySize个字节的堆空间,用FillChar函数将此空间填入0,即完成数组的建立。
4.数组的引用
与动态数组的引用方法一样。
5.数组的撤消
为提高堆空间的利用率,数组用完后应及时将其撤消,其方法是利用Dispose或FreeMem函数释放分配给动态数组的堆空间。
下面的程序演示了上述方法,首先说明一个两维数组类型,数组的界限不定;然后说明一个具有此数组类型的指针变量;在程序开始,要求用户输入这个两维数组的各维的大小,接着计算数组的大小,申请堆空间,而后通过指针变量实用数组,最后撤消数组。
program Changable_Array;
Type
ArrayInt2 = array[1..1,1..1] of integer;
var
P : ^arrayInt2;
ArraySize : word;
I,j,n,m : integer;
begin
write('n = ');readln(n);
write('m = ');readln(m);
ArraySize := n * m * Sizeof(integer);
GetMem(p,ArraySize);
FillChar(p^,Arraysize,'0');
For i := 1 to n do
for j := 1 to m do
begin
randomize;
p^[i,j] := Random(j);
write(i:3,' ',p^[i,j]:5);
end;
FreeMem(p,ArraySize);
end.
§1.7 扩展内存
§1.7 扩展内存(EMS)及其使用
为了突破DOS管理640K自由内存空间的限制,1985年Lotus/Intel和Microsoft公司联合开发了一种扩展内存规范,简称LIM-EMS。它的主导思想是避开CPU模式切换的难题,采用了当时硬件上已相当成熟的“存储体切换”技术:将扩展内存卡插入机器的扩展槽,卡上的内存处于CPU的正常寻址空间之外;通过建立特定的映射关系由硬件将其以页面为单位重定位到CPU寻址空间中的某处,DOS程序中可以自由地改变这种映射关系,从而对整个扩展内存进行存取。这种用存储体开关技术扩展了的那部分内存空间称为扩展内存(Expanded Memory)。对扩展内存的管理由扩展内存管理程序EMM(Expanded Memory Manager)提供,具体是由INT 67H提供的。EMM 3.2支持最大为8MB的EMS内存,EMM 4.0则可支持16MB。
EMS技术在基于8088/80286的机器上得到了广泛的使用,绝大多数优秀的商业软件,如电子表格、CAD等等,都支持EMS规范。在用TURBO PASCAL编写的应用程序中如何使用EMS内存呢?可以设计一种通用的TURBO PASCAL使用EMS的程序单元(见§2.6),应用程序使用此程序单元,即可使用扩展内存。下面阐述在程序中如何使用EMS。
§1.7.1 扩展内存的工作原理
使用扩展内存需要一块内存扩展卡。该卡由扩展内存管理器(EMM)软件来存取。计算机启动时,加载扩展内存管理软件,这需要在CONFIG.SYS文件中指定。
扩展内存管理软件把扩展内存卡上的存储器划分为16K的页。这样,2M的扩展内存相当于128个扩展内存页,但这些页不能同时使用。每次能够使用的扩展内存页数由页框的大小决定。
§1.7.2 扩展内存页框
PC机中8088/86微处理器能寻址1M字节,通常称为常规内存。然而在启动计算机时,DOS保留了很多内存(384K)自用,只为用户留下640K。扩展内存管理器在DOS保留的384K中划分出64K作为扩展内存页使用。
页框就好象是扩展内存的窗口。该窗口有4个16K的“窗口片”,每个片对应一页扩展内存。在使用一页扩展内存之前,先要把该页影射到或称移动到页框中。例如,如果想在扩展内存的0页中存储一些信息,就要把0页影射到页框中,然后把数据移动到页框中。
当程序要使用更多的扩展内存时,就需要将一些新的页移动到页框中。当将一页影射到页框中时,页框中原来的页就要被影射出去,也就是要保存在扩展内存中。当以后再把它移回页框时,其中的信息同移出去以前是一样的。
§1.7.3 逻辑页和物理页
同扩展内存相关的两个经常出现的术语是物理页和逻辑页。物理页是组成页框的4个页,其编号是0到3。而逻辑页是影射到页框中的扩展内存页。页框中的页是“物理”的,是因为可以直接往页框中存储数据,但不能直接向扩展内存的页传递数据。
§1.7.4 扩展内存句柄
在使用一个扩展内存页之前,要调用扩展内存管理器进行分配。可以申请最少1页,最多全部可用页。当扩展内存管理器满足申请分配页时,返回一个扩展内存句柄,即一个与所分配的页相关联的整数。
每个句柄有自己的一套扩展内存页。例如,句柄可以分配3页,编号为0到2。同时,另一个句柄可能有5页,编号为0到4。
§1.7.5 扩展内存功能
使用扩展内存功能与使用DOS和BIOS中断服务一样。扩展内存管理软件在装入内存时,占用中断67H。所有扩展内存服务均通过该中断完成。
扩展内存管理器几经修改,其最新版本是4.0版,它提供了30个扩展内存服务,其中只有15个在旧的版本中能工作。而在这15个中,又只有少数几个为大多数扩展内存程序所必需。表1-3列出了最常用的扩展内存服务。
表1-3.最常用的扩展内存服务
殔 ┌───┬──────┬───────────────────┐
│ 功能│描述 │ 说 明 │
├───┼──────┼───────────────────┤
│ 40H │取EMM状态 │ 确定是否加载了扩展内存管理程序和 │
│ │ │ 正常工作。结果返回在AH寄存器中, │
│ │ │ 0表示安装了EMM并且没有检测到硬件 │
│ │ │ 错误 │
│ 41H │取页框地址 │ 取页框段地址在BX中。如果AH不等于 │
│ │ │ 0,则BX中的值无效 │
│ 42H │取未分配页数│ 得到计算机中扩展内存总页数(在DX │
│ │ │ 中)和程序可用页数(在BX中) │
│ 43H │分配内存 │ 通知EMM程序分配扩展内存,供用户程 │
│ │ │ 序使用。BX中放需要的页数。EMM中 │
│ │ │ 断返回时将句柄放在DX中。 │
│ 44H │影射内存 │ 将一个扩展内存页(BX指定)影射到一 │
│ │ │ 个页框中的页(由AL指定)。EMM页句 │
│ │ │ 柄由DX指定 │
│ 45H │释放内存 │ 释放一个EMM句柄(DX指定)的所有页。 │
│ │ │ 一旦释放,这些页就可以再分配给一 │
│ │ │ 个新的句柄。 │
│ 46H │取EMM版本 │ 返回当前所用EMM版本。返回时,AL中 │
│ │ │ 高4位是主版本号,低4位是版本的小 │
│ │ │ 数部分。 │
└───┴──────┴───────────────────┘
殣
§1.7.6 判断扩展内存是否安装
扩展内存服务40H,报告是否加载了扩展内存管理器以及硬件功能是否正常。用户可能会用这一功能确定运行程序的计算机是否安装了扩展内存,然而这是错误的,因为只有在安装了扩展内存管理器后才能使用40H号服务。如果在没有扩展内存的情况下使用40H号服务程序,计算机可能会死锁。
那么40H号服务是干什么的?而且怎样知道计算机中是否安装了扩展内存?第一个问题的答案很简单,要时检测扩展内存是否在正常工作。40H服务能提供这种周期性的状态报告。
DOS的35H号功能用来取某一中断程序的入口地址,调用它可以确定是否安装了扩展内存。该服务程序返回指定中断的中断服务程序的段地址(ES)和偏移地址(BX)。因为EMM使用中断67H,因此,如果安装了EMM的话,这一DOS调用会返回EMM的入口地址。
如果在系统启动时装入了扩展内存管理程序(EMM),在内存中的一个固定地址就会存放一个字符串──“EMMXXXXX0”。该地址为EMM所在段,偏移量为0AH处。
DOS服务35H返回EMM段地址在ES中,偏移量在BX中。看一下在内存ES:000AH处的内容,就可以判断是否安装了EMM。如果ES:000AH处是字符串“EMMXXXXX0”,那么就安装了EMM,否则就没有安装这一管理软件。检测EMM是否存在的程序段如下:
; 检测EMM是否存在
;
mov ax,3567h
int 21h
mov di,10
push cs
pop ds
mov si,offset DevName
mov cx,8
rep cmpsb
;
DevName DB 'EMMXXXX0'
;
§1.8 扩充内存(XMS)及其使用
EMS技术在基于8088/80286的机器上得到了广泛的使用,但是几乎没有一台386机上会装扩展内存卡,因为386芯片与286不同,它的内存管理功能更为强大,模式切换也非常方便,它的页式管理功能可以很容易把内存映射到任何地址;另外386数据总线为32位,32位的内存卡的存取速度要比16位的EMS卡快,而且价格便宜;因此在386机上由软件利用扩充内存(XMS)仿真扩展内存(EMS)就十分划得来。
扩充内存(Extended Memory)是指物理地址位于1MB(100000H)以上的那部分内存,它只适用于配备80286以上档次的CPU的机器上。如果应用程序使用扩充内存,不仅运行速度快,而且效率高。通常,在80386和80486系统中,MS-DOS还提供了EMM386.EXE程序,使扩充内存仿真扩展内存。
在MS-DOS 4.0及WINDOWS 3.0中,提供了一个名为HIMEM.SYS的扩充内存管理程序XMM(Extended Memory Manager),它是按照Lotus/Intel/Microsoft/AST的扩充内存管理规范2.0版本编制的,使得应用程序对扩充内存的使用变得非常方便。HIMEM.SYS是一个设备驱动程序,可以在系统配置文件(CONFIG.SYS)中用DEVICE命令加以说明,在机器启动时便可装入。
XMS的使用通过INT 2FH的43H子功能提供管理。下面具体介绍XMS管理功能和使用方法。
§1.8.1 扩充内存管理规范(XMS)简介
扩充内存管理规范(XMS)是Lotus/Intel/Microsoft/AST公司的合作成果。它为286/386微机定义了一个软件接口,可以允许实模式程序以一种与硬件无关的方式使用扩充内存。如果不同厂商开发的程序按照这种协议来申请扩充内存,那么它们之间就能和平共处,不会发生冲突。
XMS规范定义了3种内存块的申请、修改和释放功能:
. 上位内存块(UMB):地址位于640K和1024K之间
. 高内存区 (HMA):地址位于1024K和1088K之间
. 扩充内存块(EMB):地址位于1088K以上
这3部分的关系如图1-3所示。
殌 ────┬────────┐16MB/4GB
↑ │EMB(扩充内存块) │
扩充内存├────────┤1MB+64KB
↓ │HMA(高内存区) │
────┼────────┤1MB
↑ │ROM BIOS │
│ ├────────┤
│ │UMB(上位内存块) │
│ ├────────┤
│ │EMS页框地址 │
├────────┤
传统内存│外设口地址 │
├────────┤
│ │视频刷新缓冲区 │
│ ├────────┤640KB
│ │ 常规内存 │
│ │----------------│
↓ │ MS DOS内核 │
────┴────────┘0KB
殣
图1-3 286/386/486内存映象图
所以扩充内存是指80X86机器1MB寻址空间之外的内存。在扩充内存规范中,扩充内存也指高内存区(HMA)和上位内存块(UMB)。
UMB是指在DOS内存640KB和1MB之间的内存。在DOS5.0以前,程序员只有通过XMS驱动程序才能使用这一区域,从DOS 5.0开始,可以通过DOS内存服务来访问UMB。实际上DOS内存服务例程代为访问了XMS驱动程序。
HMA的存在比较特殊。当CPU运行在实模式并且第21条地址线(A20)处于激活状态时,CPU就可以访问一块65520B的内存(64K少16B),这块内存就叫HMA。HMA的存在与CPU的寻址方式有关。CPU根据段地址:偏移地址来寻址,首先将段地址乘以16,再加上偏移地址,形成物理地址。如果此值超过20位,则截去其高位,使物理地址在000000H-0FFFFFH之间。如果A20线不激活,地址0FFFF:0010H就是物理地址000000H;若A20线激活,0FFFF:0010H就是物理地址010000:0000H,这样就有了额外的65520B的内存。也就是说地址0FFFF:0010H-0FFFF:0FFFFH通常映象到物理地址000000H-00FFFFH,当A20激活后,映象到的物理地址就为010000H-010FFEFH。
XMS驱动程序提供了五组功能:驱动程序信息、HMA管理、A20地址线管理、扩充内存管理和上位内存块管理。另外的两个功能是检查XMS驱动程序是否存在和XMS驱动程序控制功能的地址。表1-4给出了XMS功能调用。
§1.8.2 XMS的使用
使用扩充内存,需要判定扩充内存是否可用。首先执行如下代码,判定XMS程序是否存在。
MOV AX,4300H
INT 2FH
CMP AL,80H
JNE XMS_NOTPRESENT
; XMS IS PRESENT
如果存在,再取XMS驱动程序控制功能的地址,用如下的代码段即可完成此功能。
;
XMS_CONTROL DD (?)
;
MOV AX,4310H
INT 2FH
MOV WORD PTR [XMS_CONTROL],BX
MOV WORD PTR [XMS_CONTROL],ES
;
之后,就可以用远调用的方式来使用XMM提供的功能了。如执行取EMM版本号功能的程序如下:
;
mov ah,0
call XMS_control
;
表1-4.XMS的功能调用
殔┌──────┬───┬──────────┐
│ 功 能 │功能号│ 描 述 │
├──────┼───┼──────────┤
│驱动程序信息│ 0 │ 取XMS版本号 │
├──────┼───┼──────────┤
│管理高内存区│ 1 │ 请求高内存区HMA │
│ (HMA) │ 2 │ 释放高内存区HMA │
├──────┼───┼──────────┤
│ 操纵 │ 3 │ 全程启用A20 │
│ │ 4 │ 全程停用A20 │
│ A20 │ 5 │ 局部启用A20 │
│ │ 6 │ 局部停用A20 │
│ 地址线 │ 7 │ 查询A20的状态 │
├──────┼───┼──────────┤
│ │ 8 │ 查询自由扩充内存 │
│ 管理 │ 9 │ 分配扩充内存块 │
│ │ AH │ 释放扩充内存块 │
│ │ BH │ 移动扩充内存块 │
│ 扩充内存块 │ CH │ 锁住扩充内存块 │
│ │ DH │ 解锁扩充内存块 │
│ │ EH │ 取EMB句柄信息 │
│ (EMBs) │ FH │ 重新分配扩充内存块 │
├──────┼───┼──────────┤
│ 管理 │ 10H │ 请求上位内存块UMB │
│ 上位内存块 │ 11H │ 释放上位内存块UMB │
└──────┴───┴──────────┘
殣
§1.8.3 扩充内存管理功能
1.取版本号
入口参数:AH=00H
出口参数:AX=二进制版本号;BX=内部XMM版本;DX=1,存在HMA
2.请求高存区(HMA)
入口参数:AH=01H;DX=请求长度
出口参数:AX=1,HMA分配成功;否则BL返回错误码,错误码见表1-5
3.释放高存区(HMA)
入口参数:AH=02H
出口参数:AX=1,HMA释放成功;否则BL返回错误码
4.全程打开A20
入口参数:AH=03H
出口参数:AX=1,A20已激活;否则BL返回错误码
5.全程关闭A20
入口参数:AH=04H
出口参数:AX=1,A20已关闭;否则BL返回错误码
6.局部打开A20
入口参数:AH=05H
出口参数:AX=1,A20已激活;否则BL返回错误码
7.局部关闭A20
入口参数:AH=06H
出口参数:AX=1,A20已关闭;否则BL返回错误码
8.查询A20状态
入口参数:AH=07H
出口参数:AX=1,A20已激活
9.查询自由扩充内存大小
入口参数:AH=08H
出口参数:AX=最大扩充内存块的长度(KB),DX=自由扩充内存总数(KB),BL=错误码
这里查到的扩充内存总数是系统中实际安装的扩充内存数减去HMA的内存数。
10.分配扩充内存块
入口参数:AH=09H
出口参数:AX=1,分配成功;DX=扩充内存块句柄;BL=错误码
扩充内存的管理是通过扩充内存控制块来实现,扩充内存控制块的数据结构如下:
DB 标志(01:自由块;02:分配块;04空闲块) DW 内存块始址 (KB)
DB 加锁标志(0 : 加锁;非0 : 解锁) DW 内存块长度 (KB)
扩充内存控制块的地址称为扩充内存块句柄。从数据结构中可以看出,扩充内存最基本的管理单位是1KB,即最大可存取的物理地址为128MB。扩充内存控制块的数量即句柄的数量可在系统配置文件中说明,默认值为21,最大值为128,即最多可使用的内存块是128个。
11.释放扩充内存块
入口参数:AH=0AH,DX=扩充内存块句柄
出口参数:AX=1,扩充内存块已释放;否则BL=错误码
12.移动扩充内存块
入口参数:AH=0BH,DS:SI=参数表地址
出口参数:AX=1,移动成功;BL=错误码
本功能可以在常规内存和扩充内存之间双向传送数据。DS:SI所指参数表的格式:
DD 传送长度(必须是偶数) DW 目标块句柄
DW 源块句柄 DD 目标块内偏移
DD 源块内偏移
其中当句柄为0时,则相应的偏移量以SEGMENT:OFFSET的形式表示,数据由BL返回错误码。
13.扩充内存块加锁
入口参数:AH=0CH;DX=句柄
出口参数:AX=1,加锁成功;DX:BX=32位加锁的内存地址;否则BL=错误码
14.扩充内存块开锁
入口参数:AH=0DH;DX=句柄
出口参数:AX=1,开锁成功;否则BL=错误码
15.取扩充内存控制块句柄信息
入口参数:AH=0EH;DX=句柄
出口参数:AX=1,信息块已获得;BH=加锁信息;BL=自由句柄数;DX=内存块长度(KB);否则BL=错误码
16.重新分配扩充内存
入口参数:AH=0FH;DX=句柄;BX=新的长度(KB)
出口参数:AX=1,重分配成功;否则BL=错误码
表1-5.XMM错误码一览表
殔┏━━━┯━━━━━━━━━━━━┳━━━┯━━━━━━━━━━━━━┓
┃错误码│ 含义 ┃错误码│ 含义 ┃
┠───┼────────────╂───┼─────────────┨
┃ 80H │ 功能未实现 ┃ 91H │ HMA已使用 ┃
┃ 81H │ 已安装虚拟盘 ┃ 92H │ 请求长度小于最小请求长度 ┃
┃ 82H │ A20地址线处理错 ┃ 93H │ HMA未使用 ┃
┃ 8EH │ 一般驱动程序错 ┃ A0H │ 无自由扩充内存 ┃
┃ 90H │ 不存在HMA ┃ A1H │ 无扩充内存句柄可用 ┃
┃ A2H │ 扩充内存控制块句柄无效 ┃ A8H │ 传送时有无效的地址重叠 ┃
┃ A3H │ 源句柄无效 ┃ A9H │ 奇偶校验错 ┃
┃ A4H │ 源偏移量无效 ┃ AAH │ 内存块已解锁 ┃
┃ A5H │ 目标句柄无效 ┃ ABH │ 内存块已加锁 ┃
┃ A6H │ 目标偏移量无效 ┃ ACH │ 加锁内存块数已溢出 ┃
┃ A7H │ 传递长度无效 ┃ ADH │ 不能加锁内存块 ┃
┗━━━┷━━━━━━━━━━━━┻━━━┷━━━━━━━━━━━━━┛
殣
§1.9 程序的标准数据作代码处理的方法
很多程序在开始运行时,要把一些标准的数据文件读到内存中。这些数据文件包含一些不变的信息,如字库和特殊的表。TURBO PASCAL使用程序BINOBJ能使用户直接把这些数据放在程序中,避免等到程序运行时再读取。
用BINOBJ把数据装入程序需要3步:
. 创建数据文件;
. 用BINOBJ将数据文件转换为.OBJ文件;
. 在程序中将数据文件作为外部过程引用。
把数据文件作为外部过程看待,成为程序的一部分,才能在程序启动时就自动装入内存。这样做有如下优点:首先,由于不需要打开和读取文件,加快了程序运行速度;其次,如果程序作为商品出卖,则可以减少磁盘文件数目;其三,增加了程序的保密性。
§1.9.1 创建数据文件
在用BINOBJ之前,要有一个准备好的二进制数据文件。下面的过程产生一个含有1到100的自然数及其自然对数的二进制数据文件。该文件的结构由数组类型LogArray定义。
Program MakeBinaryDataFile;
Type
LogArrayPtr = ^LogArray;
LogArray = Array[1..100] of Record
I : Integer;
LnI : Real;
End;
Var
I : Integer;
LogA : LogArrayPtr;
F : File of LogArray;
begin
GetMem(LogA,sizeof(LogA^));
for i := 1 to 100 do
begin
LogA^[i].I := I;
LogA^[i].LnI := ln(i);
end;
Assign(f,'LogData.bin');
Rewrite(f);
Write(f,Loga^);
close(f);
end.
该程序产生的LOGDATA.BIN二进制数据文件可作为BINOBJ的输入文件,因文件具有LogArray类型,所以在程序中存取这些作为外部过程的数据时,必须采用相同的数据类型。
§1.9.2 转换数据文件
用BINOBJ把二进制数据文件转换为目标文件(.OBJ)。
使用BINOBJ的一般格式是:
BINOBJ <源文件名.BIN> <目标文件名[.OBJ]> <公用数据名>
源文件是二进制数据文件,BINOBJ不能自动加.BIN,所以使用时要写上数据文件的扩展名。目标文件是BINOBJ产生的输出文件,如果不指定该文件的扩展名,BINOBJ会把标准的扩展名(.OBJ)加上。最后,公用数据名是在访问这些数据时所用的过程名。
用BINOBJ把上述程序产生的数据文件LOGDATA.BIN生成目标文件。
BINOBJ LOGDATA.BIN LOGDATA LOGDAT
§1.9.3 访问外部过程
将产生的数据文件转换为目标文件后,就可以连接到程序中。实现的一般形式是:
Procedure <公用数据名>; erternal;
{$L 目标文件名.OBJ}
过程名和运行BINOBJ的公用数据名相同。{$L}编译指令使用的名字与BINOBJ使用的目标文件名相同。所以,要使用上面生成的数据文件,必须在程序中作如下的声明:
Procedure LogDat; external;
{$L LOGDATA.OBJ}
在运行TURBO PASCAL编译程序时,LOGDATA.OBJ被连接到程序中,相应的数据放在LogDat指明的地址上。
访问存储在代码中的这些数据很简单,首先,声明一个与创建的数据文件相同数据类型的指针变量;其次,将指针变量指向存储这些数据的代码。
下面举例说明如何访问存储在LOGDATA.OBJ中的数据:
Program TestBin;
Type
LogArrayPtr = ^LogArray;
LogArray = Array[1..100] of Record
I : Integer;
LnI : Real;
End;
Var
I : Integer;
LogA : LogArrayPtr;
procedure LogDat; external;
{$L LOGDATA.OBJ}
begin
LogA := @LogDat;
for i := 1 to 100 do
begin
Write(LogA^[i].I);
writeln(LogA^[i].LnI:10:4);
end;
end.
LogA是与所创建的数据文件有相同的数据类型的指针变量。在运行时,LogA通过这个语句指向所连接的数据:
LogA := @LogDat;
这个语句取出LogDat的地址,赋给LogA。这样就可以访问LogA中的所有数据,就象在堆中动态分配来的一样。
注意,LogA没有申请任何内存,因为是在代码段中。不要试图释放LogA或其它任何指向代码段的指针。
虽然利用BINOBJ把数据存放在代码是个有效的方法,但是它也具有一些弊端。假设数据存放在程序的代码段,如果数据文件很大,则代码会超过64K限制,且数据在程序启动后永远保存在内存中,无法象在堆上申请空间一样释放。另外,如果修改数据文件,就必须重新运行BINOBJ,并编译程序。