您的位置: 网站首页 > 程序开发 > 汇编语言与微机原理教程 > 第11章 模块化程序设计 > 【11.1 模块化程序】

11.1 模块化程序

 

程序的模块化设计是现代大中型规模程序开发的基础,模块化设计思想使得程序设计框架变得清晰,开发团队能分工明细,整个开发项目管理才有可控制性,对程序的开发质量、再次开发源代码的可重用性提供了保障。

本章主要内容

&        模块化程序

&        子程序指令

&        参数的传递

&        汇编语言的编程

11.1  模块化程序

汇编语言程序开发的各模块可以独立汇编然后链接各自的目标文件,最后形成一个可执行文件,实现并行开发,提高开发进度和效率。多模块的程序设计也是现代程序开发最广泛使用的方法。

11.1.1  模块化程序设计的原则

模块化程序设计是指将一个大程序按功能划分为许多小的、具有独立功能的模块,模块间按统一规范链接,每个模块单独汇编,最后链接成一个程序运行。

开发者要根据项目应用要求、程序运行环境来进行模块划分、模块设计及模块间的关系。下面仅提供一些应考虑的因素。

·    模块大小的划分主要根据其功能而定,每个模块的功能要明确、单一。

·    模块的独立性要强,即模块的功能由该模块自身完成,不依赖其他模块。

·    每个模块最好只有一个入口和一个出口。

·    模块间的关系要明确,即上层模块可调用下层模块,下层模块可返回上层模块;反之不可以。

·    程序中易变化的部分与不易变化的部分应分开,形成不同的模块。

模块化程序设计对大程序来说是一个很好的编程方法。汇编源程序采用模块化程序设计的结果是产生了许多OBJ的目标文件,它们都能独立地链接到任何代码程序中。充分利用这个特点将使模块化程序设计具有更大的用途。

11.1.2  近程模块的设计及调用

多模块之间的链接需用到SEGMENT语句提供的链接信息。段定义的完整形式为:

段名SEGMENT[定位类型][组合类型]['类别']

段名ENDS

在第10章中已指出,段定义以后,段名就表示段地址,也就是说段名有段地址属性。实际上段定义以后,还有定位类型、组合类型和类别属性。若源程序中不指定某个属性,则汇编程序将使用该属性的默认值,下面分别进行介绍。

1)定位类型。

定位类型表示当前段对起始地址的要求。链接程序(LINK.EXE)按表11-1的地址格式来定位段的起始地址,所以,各段之间就有可能出现一些空闲字节,即可能浪费几个字节单元。

段对齐类型PARA适用于所有段类型,它也是默认的对齐类型。对齐类型BYTEWORD通常用于数据段的定位,对齐类型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)类别。

类别可以使用任何一个合法的标识符,但必须用单引号括起来。链接时将把不同模块中同类别的各段在物理上相邻地链接在一起,其顺序与链接时提供的各模块顺序一致。当类别相同的各段的段名不同时,它们链接后虽然在同一物理段内,但它们不属于同一逻辑段,也就是它们的段基址不相同。这样做的一个目的是便于程序固化。

11.1.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等。类型说明符可以是NEARFARBYTEWORDDWORD等之一。如果在一条伪指令中说明了多个标识符,那么,各标识符之间要用逗号分开。

在一个模块中可用多条EXTRN伪指令来说明本模块所引用的全局标识符。

伪指令PUBLICEXTRN为不同模块间的有关变量和标号建立了联系,从而使模块间的交叉访问成为可能。但是在不同模块中引用同一个变量或标号时,它们的内存地址必须一致,这是多模块设计中应主要考虑的问题,否则程序的运行会出错。如果程序代码段、数据段链接起来分别不超过64KB,只需把不同模块中的数据段、代码段定义为同名,组合类型都为PUBLIC,这样就能保证不同模块的数据段链接起来后在同一逻辑段内,代码也在同一逻辑代码段内,从而保证了变量、标识符无论从哪个模块访问都具有相同的段基址和偏移地址,这就是一种近程模块调用。下面程序就是近程模块设计及调用的一个实例。

11-1 近程模块设计及调用。

注意:按模块化程序设计要求,可把浮点数的显示编写成子程序模块,需要显示浮点数结果时,随时调用就可以完成其显示功能。

主程序:

.MODEL  SMALL.386

.387

.STACK 200H.DATA

EXTRN dadaDWORD

L1  REAL4 0.000001

c1  REAL4 0.000001

two REAL4  2.0

.CODE

.STARTUP

EXTRN dispNEAR

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,显然不能链接成一个逻辑段,即两个模块的代码段不在同一段,就需要考虑远程模块的设计要求,下节介绍相应知识。

11.1.4  远程模块的设计及调用

远程模块的设计及调用实际上就是主模块与子模块的代码段不在同一逻辑段时,数据段可以在同一逻辑段,也可以不在同一逻辑段。用户只需把握两个原则:一是若代码段不在同一段,子模块就要设计为远程的,若数据段也不在同一逻辑段,在内存变量的交叉访问时就要用不同的段寄存器来指向不同的数据段;二是标号与变量在用PUBLICEXTRN声明时,该语句最好放在源程序的最前面,以免交叉访问时子模块与变量无法正确地调用与引用。下面通过举例说明。

11-2 显示一字符串this is a routine

分析:这个程序的功能按理说只用DOS9号功能调用就可实现,但是为了说明远程模块的设计与调用方法,我们有意把显示功能的实现用显示单个字符的子模块实现。若要显示字符串,可通过循环调用子模块实现。

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     ESbChar,10

JNE     loopl

MOV     AH,4CH

INT     21H

Code    ENDS

END main

; out routine子模块功能:显示内存单元bChar中的字符

; 入口参数:变量bChar

; 出口参数:无

; 算法描述:通过DOS2号功能调用来实现单个字符的显示

; 以下是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  运行结果

11.1.5  子程序库

库文件对学过C/C++语言程序设计的读者来说应该不会陌生,该语言的程序设计环境提供了大量的库文件,即提供了大量的标准函数。用户在调用某一库中的函数时,只需用#INCLUDE声明即可。本节将向介绍读者如何创建自己的库文件。

1.建立库文件

宏汇编MASM系统提供了建立库文件的命令文件LIB.EXE。显示命令LIB用法的命令如下:

>lib?

命令用法如图11-4所示。

假设现有目标文件sub1.objsub2.objsub3.obj,要用它们建立库文件mylib.1ib,可用下列方法:

方法1,所有目标文件都准备好了,可一次性把它们加入到库文件中。

>libmylib+sub1+sub2+sub3

方法2,随着目标文件的逐个生成,依次把它们加入到库文件中。

>lib mylib+sub1

>libmylib+sub2

>libmylib+sub3

11-4  LIB命令用法

2.应用库文件

这里我们用建立库文件的方法来完成例11-2。例11-2主程序及子程序的建立及宏汇编过程不变,现已生成了目标文件L11-2Main.objL112sub.obj(因为库命令输入的目标文件名不允许用“-”,所以取消了该字符)。用L112sub.obj建立库文件mylib.lib,用下列命令:

>lib mylib L112sub

链接目标文件L11-2Main及库文件mylib.lib

>link L11-2Main

3.库文件与子程序

程序员在编写源程序时,通常采用模块化的思想来组织源程序。把各类不同的子程序分别编写在不同的源程序中,在各源程序中说明用到的小程序已在其他模块中定义,或说明本模块定义的子程序可被其他模块调用。这样组织后,就可以分别汇编它们而得到其相应的目标文件,有了这些目标文件后,就可生成最终的可执行文件。通常生成可执行文件的方法有两种。

1)直接链接目标文件生成可执行文件。

这种方法简单、方便,也是常用的一种方法,但在链接时,LINK程序会把目标文件中的所有代码都嵌入到执行文件中,从而使得包含在某目标文件中但并没有被调用的子程序代码也出现在执行文件中。这种情况无疑增加了执行文件的字节数。

2)采用子程序库的方法。

库文件可以看成是子程序的集合。库文件中存储着子程序名、子程序的目标代码以及链接所需要的重定位信息。当某目标文件与库文件相链接时,LINK程序只把目标文件所用到的子程序从库文件中找出来,并合并到最终的可执行文件中,而不是把库文件中所含的全部子程序都纳入最后的可执行文件中。