"SEGMENT"伪指令的标准用法应该如下所示:
段名 SEGMENT 定位类型 组合类型 类别名称
有些参考书称定位类型指得是段的起始位置,实际上定位类型可以理解为 "对两个相临段的段地址给予的一些规定"。定位类型可以有以下几种:
PARA:指定所定义的段开始于小段边界,实际是规定这个段的起始地址与前面一个段的起始地址之差必须是16字节的整数倍。这意味着相临两个段的段地址之差最小也得是1。给出的示例程序就是采用了PARA定位类型。
PAGE:指定定义的段开始于页边界,实际是规定这个段的起始地址与前面一个段的起始地址之差必须是256字节的整数倍。
BYTE:所定义的段开始于字节边界,实际上是指这个段可以从任何地址开始。
WORD:所定义的段开始于字边界,实际是指这个段只能从偶数地址开始。
如果源程序中指定了段的定位类型为PARA或PAGE,那么获得的可执行文件中相临段的段地址就有差异。但是如果源程序中指定了段的定位类型是BYTE或WORD,那么在可执行文件中相临段的段地址就有可能相同。为了说明这个问题,我们下面给出了一个程序例:
data segment para public 'data' assume ds:data msg db 'DEMO',0dh,0ah,24h data ends code segment para public 'code' assume cs:code main proc far push ds xor ax,ax push ax mov ax,data mov ds,ax mov dx,offset msg mov ah,9 int 21h ret main endp code ends end main
这个程序编译之后数据段地址为0F9CH,代码段地址为0F9DH,两者相差1。这恰好说明了数据段首与代码段首相差了16个字节。
C:\ASM\>debug demo.exe[Enter] -u0 10[Enter] 0F9D:0000 1E PUSH DS 0F9D:0001 33C0 XOR AX,AX 0F9D:0003 50 PUSH AX 0F9D:0004 B89C0F MOV AX,0F9C 0F9D:0007 8ED8 MOV DS,AX 0F9D:0009 BA0000 MOV DX,0000 0F9D:000C B409 MOV AH,09 0F9D:000E CD21 INT 21 0F9D:0010 CB RETF
如果把数据段与代码段的定位类型改成"BYTE",那么编译之后的程序就是下面这样了:
C:\ASM\>debug demo.exe[Enter] -u7 17[Enter] 0F9C:0007 1E PUSH DS 0F9C:0008 33C0 XOR AX,AX 0F9C:000A 50 PUSH AX 0F9C:000B B89C0F MOV AX,0F9C 0F9C:000E 8ED8 MOV DS,AX 0F9C:0010 BA0000 MOV DX,0000 0F9C:0013 B409 MOV AH,09 0F9C:0015 CD21 INT 21 0F9C:0017 CB RETF
可以看到代码与数据都在0F9CH段里了。这种情况有些象COM文件,不过这个程序不能转换成COM文件,因为代码的起始地址不是偏移0100H。
那么将源程序中的"PARA"改成"BYTE"后程序的起始地址为什么是0007H呢?这只要算清数据段内究竟有几个字节数据就可以搞清楚了。
这样一来就可以推断出如果再把"BYTE"改成"WORD",那么代码的偏移地址就应该是0008H,大家可以自行修改源程序进行验证。
组合类型与模块化程序设计技术有关。前面已经说过,每个模块都是一个独立的源程序,都有自己的段定义。设置一定的组合类型就可以通知LINK程序把一些分散在不同模块内的同类型段组织在一个段内,这样可以使最终形成的可执行文件结构比较清晰。组合类型共有5种:
PUBLIC: 凡是组合类型为PUBLIC的同名段在连接时都会被组织到一起,段的顺序按OBJ文件的先后顺序安排。
COMMON: 这种类型的段在连接时会被其它模块中的同名段覆盖掉,连接后段的长度以所有同名段中最长者为准。通常情况下这种类型的段可应用于在不同的模块之间共享数据,对于定义一般的数据定义而言不使用这种类型的段。
AT 表达式:编译程序把表达式的值当作该段的段地址,采用这种组合类型可以很方便地从内存中取得数据。比如采用下面定义的这个段就可以很方便地从BIOS数据区内取得系统时钟计数:
BIOSDATA SEGMENT AT 0040H ORG 6CH TIMER DD ? BIOSDATA ENDS CODE SEGMENT ASSUME CS:CODE START: MOV AX,BIOSDATA MOV ES,AX MOV DX,WORD PTR ES:TIMER ......
STACK: 指定该段运行时作为堆栈段的一部分。
MEMORY:指定该段在所有连接在一起的段的前面(高地址方向为"前")。如果连接时遇到多个MEMORY段,则第一个作为MEMORY段,后面的都作为COMMON段。
至于段的类别指得是连接时用于组成段组的名字。在前面给出的TEST程序例中,两个模块的数据段的组合类型均为"PUBLIC",段名均为"DATA",类别都是"DATSEG",这样的两个段在连接时会被LINK程序组织在一起共同一个段地址,各个数据的偏移地址都会自动处理好。这样只需在主模块TEST1中设置DS寄存器即可,在TEST2中无需再设置DS。
但是如果这两个数据段段名不同或类别不同,那么这两个段虽然也会被组织在一起,但是两个段不能使用同一个段地址,这样一来就不得不在两个模块中各自设置自己的DS段寄存器。
"SEGMENT"伪指令在模块化程序设计技术中的应用重点主要是"组合类型"这部分内容,模块化程序设计的另一个重点就是不同的模块之间如何相互调用以及如何共享数据。这部分内容主要应用的伪指令有两个:EXTRN和PUBLIC。
·EXTRN
当一个模块需要调用其它模块中的子程序或者需要使用其它模块中的数据,那么首先要使用这个伪指令说明调用的子程序或引用的数据是"外部的(External)"。这个伪指令的用法很简单,如果用于说明所调用的子过程,就应该写成:
EXTRN 过程名:属性
这里的属性是指"近程"(NEAR)或"远程"(FAR)。
如果用于说明所引用的数据,则应写成:
EXTRN 数据标号:类型
类型指得是"BYTE"(字节)或"WORD"(字)。
在给出的示例程序中,TEST1模块要调用TEST2模块中的"HEX2ASC"子过程,因此在TEST1中就有一行"EXTRN HEX2ASC:FAR",这一行说明了"HEX2ASC"子过程是在其它模块中,这样编译程序就不会因为在TEST1中找不到"HEX2ASC"子过程而显示出错信息了。
·PUBLIC
并非一个模块中所有的数据与子过程都可以由其它模块调用或引用,如果某个模块中的数据或子过程确实允许其它模块使用,那么这样的数据与子过程应该使用"PUBLIC"伪指令进行说明,格式为:
PUBLIC 过程名(或数据标号)
这里的"PUBLIC"伪指令与组合类型中的"PUBLIC"不是一回事,大家不可以将其混为一谈。
应用"EXTRN"与"PUBLIC"伪指令就可以方便的使不同的模块共享数据与子过程,不过这还不是唯一的方法。对于"共享数据"这个问题而言还有另一种方法就是使用组合类型为"COMMON"的段。请看下面给出的例子:
module1. asm data segment para COMMON 'data' assume ds:data num1 dw ? num2 dw ? data ends ...... module2. asm data segment para COMMON 'data' assume ds:data num1 dw 2 num2 dw 5 data ends ......
由于两个模块中的DATA段都是"COMMON"类型的,所以这两个模块在连接的时候模块1中的DATA段会被模块2中的DATA段"覆盖"。因此虽然模块1的数据段中有两个字的数据,模块2的数据段内也有两个字,但是最终生成的可执行文件中只有一个数据段且其中只有两个字,而不是有四个字。
正是由于"覆盖"的关系,所以模块1中的num1与模块2中的num1实际是在内存同一地址处,所以在模块1中引用num1就等于引用了模块2中的num1。这样就形成了数据共享的关系。
以上介绍的是模块化程序设计技术中最基本的内容,实际上模块化程序设计技术中还有两个重点内容:第一是汇编子程序库的建立与维护;第二是汇编语言与高级语言的联合编程。限于篇幅,有关这两方面的内容本书不再介绍。
本章对于程序设计技术所进行的讨论是很简单的,并不十分全面。有些重要的内容,如TSR程序的设计技巧我们没有进行更多的研究,实际上单单是TSR程序的设计技巧就足以写一本书来讲述。笔者在这最后一章内只是很初步地给大家开了个头儿,更深入的内容还要读者自己去探索。想学会一种本领不容易,想学精一种本领就更难了,探索是永无止境的。