程序的模块化设计是现代大中型规模程序开发的基础,模块化设计思想使得程序设计框架变得清晰,开发团队能分工明细,整个开发项目管理才有可控制性,对程序的开发质量、再次开发源代码的可重用性提供了保障。
本章主要内容
& 模块化程序
& 子程序指令
& 参数的传递
& 汇编语言的编程
汇编语言程序开发的各模块可以独立汇编然后链接各自的目标文件,最后形成一个可执行文件,实现并行开发,提高开发进度和效率。多模块的程序设计也是现代程序开发最广泛使用的方法。
模块化程序设计是指将一个大程序按功能划分为许多小的、具有独立功能的模块,模块间按统一规范链接,每个模块单独汇编,最后链接成一个程序运行。
开发者要根据项目应用要求、程序运行环境来进行模块划分、模块设计及模块间的关系。下面仅提供一些应考虑的因素。
· 模块大小的划分主要根据其功能而定,每个模块的功能要明确、单一。
· 模块的独立性要强,即模块的功能由该模块自身完成,不依赖其他模块。
· 每个模块最好只有一个入口和一个出口。
· 模块间的关系要明确,即上层模块可调用下层模块,下层模块可返回上层模块;反之不可以。
· 程序中易变化的部分与不易变化的部分应分开,形成不同的模块。
模块化程序设计对大程序来说是一个很好的编程方法。汇编源程序采用模块化程序设计的结果是产生了许多OBJ的目标文件,它们都能独立地链接到任何代码程序中。充分利用这个特点将使模块化程序设计具有更大的用途。
多模块之间的链接需用到SEGMENT语句提供的链接信息。段定义的完整形式为:
段名SEGMENT[定位类型][组合类型]['类别']
段名ENDS
在第10章中已指出,段定义以后,段名就表示段地址,也就是说段名有段地址属性。实际上段定义以后,还有定位类型、组合类型和类别属性。若源程序中不指定某个属性,则汇编程序将使用该属性的默认值,下面分别进行介绍。
(1)定位类型。
定位类型表示当前段对起始地址的要求。链接程序(LINK.EXE)按表11-1的地址格式来定位段的起始地址,所以,各段之间就有可能出现一些空闲字节,即可能浪费几个字节单元。
段对齐类型PARA适用于所有段类型,它也是默认的对齐类型。对齐类型BYTE和WORD通常用于数据段的定位,对齐类型DWORD通常用于80386及其以后CPU代码段的定位。
表11-1 段对齐类型与段起始地址之间的关系
对齐类型 |
起始地址(二进制) |
功能说明 |
最多的空闲字节数 |
BYTE |
×××××××××××××××××××× |
下一个字节地址 |
0 |
WORD |
×××××××××××××××××××0 |
下一个字地址 |
1 |
DWORD |
××××××××××××××××××00 |
下一个双字地址 |
3 |
PARA |
××××××××××××××××0000 |
下一个节地址 |
15 |
PAGE |
××××××××××××0000 0000 |
下一个页地址 |
127 |
(2)组合类型。
组合类型告诉链接程序将本段与其他段链接的有关信息,具体的组合类型如表11-2所示。
表11-2 段的组合类型与段的链接之间的关系
组合类型 |
表示意义 |
NONE |
表示当前段在逻辑上独立于其他模块,并有其自己的基地址。NONE是默认的组合类型 |
PUBLIC |
表示当前段与其他模块中同段名的PUBLIC类型段组合成一个段。组合的先后次序取决于LINK程序中目标模块排列的次序。在组合时,后续段的起始地址要按其对齐类型进行定位,所以,同名段之间可能有间隔 |
续上表 | |
组合类型 |
表示意义 |
COMMON |
表示当前段与其他模块中同名段重叠,也就是说,它们的起始地址相同。最终段的长度是同名段的最大长度。由于段覆盖,所以前一同名段中的初始化数据被后续段的初始数据覆盖 |
STACK |
表示当前段是堆栈段,其组合情况与PUBLIC相同 |
AT数值表达式 |
该数值表达式是当前段所指定的绝对起始地址的段地址 |
通常用“AT数值表达式”定义的段地址不像其他段的段地址是运行时由操作系统分配的,而是由“数值表达式”指定的,所以也可以用该方式来调用系统中始终具有固定地址的程序代码,比如BIOS中的程序代码。
(3)类别。
类别可以使用任何一个合法的标识符,但必须用单引号括起来。链接时将把不同模块中同类别的各段在物理上相邻地链接在一起,其顺序与链接时提供的各模块顺序一致。当类别相同的各段的段名不同时,它们链接后虽然在同一物理段内,但它们不属于同一逻辑段,也就是它们的段基址不相同。这样做的一个目的是便于程序固化。
模块间的交叉访问是指一个模块要引用另一个模块的变量、标号等标识符。例如主模块中要调用的过程名是在另一个子模块中定义的,子模块要用到的变量可能是在主模块中定义的。这样,一个源模块中定义的标识符就可能有两类:一类是供本模块使用的,称为局部标识符;另一类是可供本模块和其他模块使用的,称为全局标识符。那么如何告诉汇编程序本模块中哪些标识符是全局的,哪些标识符是局部的呢?下面介绍相应的伪指令。
(1)伪指令PUBLIC。
伪指令PUBLIC用来说明当前模块中哪些标识符是能被其他模块引用的全局标识符。其说明的一般形式如下:
PUBLIC标识符1,标识符2,……
其中,“标识符”可以是变量名、过程名和标号,各标识符之间要用逗号分开。
上面的说明语句说明了标识符l、标识符2等是全局标识符,可以被其他模块引用。在一个模块中可用多条PUBLIC伪指令来说明全局标识符。
(2)伪指令EXTRN。
伪指令EXTRN用来说明在当前模块所使用的标识符中哪些标识符已在其他模块中被定义为指定类型的标识符。如果当前模块使用了其他模块的标识符,而对它又不加以说明的话,在汇编时,汇编程序将会给出出错信息。
伪指令EXTRN的一般说明形式如下:
EXTRN标识符1:类型1,标识符2:类型2,……
其中,“标识符”和“类型”之间要用冒号“:”链接。注意:伪指令EXTRN中所说明的标识符必须在其定义的模块中被PUBLIC伪指令说明为全局标识符,并且其说明的标识符类型要与该标识符在定义时的类型一致,否则,要么不能生成其可执行文件,要么其可执行文件不能正确运行。
上面语句说明了标识符l、标识符2等是全局标识符,它们在其他模块中已被分别定义为类型1、类型2等。类型说明符可以是NEAR、FAR、BYTE、WORD、DWORD等之一。如果在一条伪指令中说明了多个标识符,那么,各标识符之间要用逗号分开。
在一个模块中可用多条EXTRN伪指令来说明本模块所引用的全局标识符。
伪指令PUBLIC和EXTRN为不同模块间的有关变量和标号建立了联系,从而使模块间的交叉访问成为可能。但是在不同模块中引用同一个变量或标号时,它们的内存地址必须一致,这是多模块设计中应主要考虑的问题,否则程序的运行会出错。如果程序代码段、数据段链接起来分别不超过64KB,只需把不同模块中的数据段、代码段定义为同名,组合类型都为PUBLIC,这样就能保证不同模块的数据段链接起来后在同一逻辑段内,代码也在同一逻辑代码段内,从而保证了变量、标识符无论从哪个模块访问都具有相同的段基址和偏移地址,这就是一种近程模块调用。下面程序就是近程模块设计及调用的一个实例。
【例11-1】 近程模块设计及调用。
注意:按模块化程序设计要求,可把浮点数的显示编写成子程序模块,需要显示浮点数结果时,随时调用就可以完成其显示功能。
主程序:
.MODEL SMALL.386
.387
.STACK 200H.DATA
EXTRN dada:DWORD
L1 REAL4 0.000001
c1 REAL4 0.000001
two REAL4 2.0
.CODE
.STARTUP
EXTRN disp:NEAR
FLD l1
FMUL c1
FSQRT
FMUL two
FLDPI
FMUL
FLD1
FDIVR
FSTP dada
CALL disp
.EXIT 0
END
; disp子模块功能:显示内存单元dada(单精度)中的浮点数
; 入口参数:变量dada
; 出口参数:无
; 算法描述:
; (1)先判断dada值的正负,若为负,则显示“-”。取其整数部分给变量whole,取其小
; 数部分给变量fract
; (2)取其整数部分,采用除l0取余数的方法求出整数数码(先低位后高位),加30H压入堆栈;然后通过出栈显示,以达到先显示高位的目的
; (3)取其小数部分,把阶码变为原码,采用乘10取整法求出小数部分(先高后低),加30H;变为ASCII码显示
; 以下为disp子模块源程序
.MODEL SMALL.386
.387
.STACK 128
.DATA
PUBLIC dada
dada DD ?
status DW ?
whole DD ?
fract DD ?
.CODE
disps PROC
MOV AH,02
MOV DL,AL
INT 21H
RET
disps ENDP
PUBLIC disp
disp: FNINIT
MOV AX,@data
MOV DS,AX
FSTCW Status
OR status,0C00H
FLDCW status
FLD dada
FTST
FSTSW AX
AND AX,4500h
CMP AX,0100h
JNZ poSItive
MOV al,'-'
CALL disps
FABS
positive:
FLD ST
FRNDINT
FIST whole
FSUBR
FABS
FSTP fract
MOV EAX,whole
MOV EBX,10
MOV CX,0
PUSH BX
again1:MOV EDX,0
DIV EBX
ADD DL,30H
PUSH DX
INC CX
CMP EAX,0
JNZ again1
Displ:POP AX
CALL disps
LOOP displ
MOV AL,'.'
CALL disps
MOV EAX,fract
FSTCW status
XOR status,0c00h
FLDCW status
FLD fract
FXTRACT
FSTP fract
FABS
FISTP whole
MOV ECX,whole
MOV EAX,fract
SHL EAX,9
RCR EAX,CL
again2:MUL EBX
PUSH EAX
XCHG EAX,EDX
ADD AL,30H
CALL disps
POP EAX
CMP EAX,0
JNZ again2
RET
END
建立可执行文件的步骤是:首先,将各个源程序模块分别汇编,生成各自的目标模块(.OBJ),如图11-1所示;其次,用链接程序LINK命令将这些目标模块链接成一个可执行文件。键入命令:
link L11-1main+L14-1sub
图11-1 模块汇编界面
显示如图11-2所示的界面,生成可执行文件L11-1.exe。最后执行程序,运行结果如图11-2所示。这就是一个近程模块的设计及调用的实例。
图11-2 模块链接及执行界面
可见,当两个模块的代码段、数据段都同名,同为PUBLIC组合类型时,LINK链接程序就会将两个模块的代码段组合成一个逻辑段,将数据段也组合成一个逻辑段。那么主模块调用子模块,也就属于同一逻辑段的调用,即属于近模块调用。在主模块中的变量dada的引用也属于同一段变量的引用,所以就可把变量dada视为在主模块的数据段中编程。若两个模块的代码段超过了64KB,显然不能链接成一个逻辑段,即两个模块的代码段不在同一段,就需要考虑远程模块的设计要求,下节介绍相应知识。
远程模块的设计及调用实际上就是主模块与子模块的代码段不在同一逻辑段时,数据段可以在同一逻辑段,也可以不在同一逻辑段。用户只需把握两个原则:一是若代码段不在同一段,子模块就要设计为远程的,若数据段也不在同一逻辑段,在内存变量的交叉访问时就要用不同的段寄存器来指向不同的数据段;二是标号与变量在用PUBLIC、EXTRN声明时,该语句最好放在源程序的最前面,以免交叉访问时子模块与变量无法正确地调用与引用。下面通过举例说明。
【例11-2】 显示一字符串this is a routine。
分析:这个程序的功能按理说只用DOS的9号功能调用就可实现,但是为了说明远程模块的设计与调用方法,我们有意把显示功能的实现用显示单个字符的子模块实现。若要显示字符串,可通过循环调用子模块实现。
L11-2Main.ASM主模块程序:
EXTRN bChar:BYTE
EXTRN out_routine:FAR
stack SEGMENT para stack'stack'
DW 256 DUP(?)
Stack ENDS
code SEGMENT'code'
ASSUME CS:code,DS:code
messe DB 'this is a routine ',13,10
main: CLD
MOV AX,CS
MOV DS,AX
MOV SI,OFFSET messe
Loopl:LODSB
MOV ES:bChar,AL
CALL out_routine
CMP ES:bChar,10
JNE loopl
MOV AH,4CH
INT 21H
Code ENDS
END main
; out routine子模块功能:显示内存单元bChar中的字符
; 入口参数:变量bChar
; 出口参数:无
; 算法描述:通过DOS的2号功能调用来实现单个字符的显示
; 以下是out_routine子模块源程序
PUBLIC bChar
PUBLIC out_routine
code SEGMENT 'code1 '
ASSUME CS:code,ES:code
bChar DB?
out routine PROC FAR
MOV AX,CS
MOV ES,AX
MOV DL,ES:bChar
MOV AH,2
INT 21H
RETF
out routine ENDP
code ENDS
END
操作步骤及运行结果如图11-3所示。
图11-3 运行结果
库文件对学过C/C++语言程序设计的读者来说应该不会陌生,该语言的程序设计环境提供了大量的库文件,即提供了大量的标准函数。用户在调用某一库中的函数时,只需用#INCLUDE声明即可。本节将向介绍读者如何创建自己的库文件。
宏汇编MASM系统提供了建立库文件的命令文件LIB.EXE。显示命令LIB用法的命令如下:
…>lib?
命令用法如图11-4所示。
假设现有目标文件sub1.obj、sub2.obj和sub3.obj,要用它们建立库文件mylib.1ib,可用下列方法:
方法1,所有目标文件都准备好了,可一次性把它们加入到库文件中。
…>libmylib+sub1+sub2+sub3
方法2,随着目标文件的逐个生成,依次把它们加入到库文件中。
…>lib mylib+sub1
…>libmylib+sub2
…>libmylib+sub3
图11-4 LIB命令用法
这里我们用建立库文件的方法来完成例11-2。例11-2主程序及子程序的建立及宏汇编过程不变,现已生成了目标文件L11-2Main.obj及L112sub.obj(因为库命令输入的目标文件名不允许用“-”,所以取消了该字符)。用L112sub.obj建立库文件mylib.lib,用下列命令:
…>lib mylib L112sub
链接目标文件L11-2Main及库文件mylib.lib:
…>link L11-2Main
程序员在编写源程序时,通常采用模块化的思想来组织源程序。把各类不同的子程序分别编写在不同的源程序中,在各源程序中说明用到的小程序已在其他模块中定义,或说明本模块定义的子程序可被其他模块调用。这样组织后,就可以分别汇编它们而得到其相应的目标文件,有了这些目标文件后,就可生成最终的可执行文件。通常生成可执行文件的方法有两种。
(1)直接链接目标文件生成可执行文件。
这种方法简单、方便,也是常用的一种方法,但在链接时,LINK程序会把目标文件中的所有代码都嵌入到执行文件中,从而使得包含在某目标文件中但并没有被调用的子程序代码也出现在执行文件中。这种情况无疑增加了执行文件的字节数。
(2)采用子程序库的方法。
库文件可以看成是子程序的集合。库文件中存储着子程序名、子程序的目标代码以及链接所需要的重定位信息。当某目标文件与库文件相链接时,LINK程序只把目标文件所用到的子程序从库文件中找出来,并合并到最终的可执行文件中,而不是把库文件中所含的全部子程序都纳入最后的可执行文件中。