《80X86汇编语言程序设计教程》七 模块化程序设计

时间:2022-09-01 07:59:36

1、 这部分内容也不是重点,学8086编程主要是为了过渡到80386,这里对汇编程序的模块化设计稍做了解。模块化设计优点:单个模块易于编写、调试及修改;程序员可分工合作;代码可重利用;提高易读性;程序修改局部化。模块化设计步骤:确定系统边界;模块化,确定模块之间的关系;选择合适编程语言;编译各模块,链接;整理文档资料。

 

2、 段的完整定义
  1) 一般格式
    段名 segment [定位类型] [组合类型] [‘类别’]
    ;语句
    段名 ends
    段名可以重复,同一个模块,如果段名相同,则被认为是追加,但是后一个段的可选选项必须与前一个相同,或者不再给出可选项值。其中可选项各意义解释如下:
    a) 定位类型
      表示当前段对其实地址的要求,从而指示链接器如何衔接相邻的两段。

 

定位类型

起始地址(二进制表示)

含义

BYTE

XXXX XXXX XXXX XXXX XXXX

使用下一个可用字节地址

WORD

XXXX XXXX XXXX XXXX XXX0

使用下一个可用字地址

DWORD

XXXX XXXX XXXX XXXX XX00

使用下一个可用双字地址

PARA

XXXX XXXX XXXX XXXX 0000

使用下一个可用节地址

PAGE

XXXX XXXX XXXX 0000  0000

使用下一个可用页地址

      一般80386以下采用PARA,即段起始地址位于可用的下一个节(每节为16个字节)的边界处。BYTE类型是紧紧衔接,没有间隔,最节约空间。WORD使得从偶数地址开始,可节约空间,也利用数据单元定位在偶地址。DWORD用于80386的32位段。一页等于256字节,所以PAGE是可能导致最大间隔的段。
    b) 组合类型
      表示如何把不同模块的同名段组合到一起,有如下组合类型:
       PUBLIC
        当前段与其它模块同名段(也为PUBLIC)组合为一个段。顺序由编译器决定,衔接时间隔大小由定位类型决定。
       COMMON
        当前段与其它模块同名段重叠。采用的是覆盖方式,重叠后段长是2着较长的一个。
       STACK
        当前段是堆栈段,组合情况同PUBLIC。
       MEMORY
        同PUBLIC。
       AT 表达式
        当前段按绝对地址定位,表达式的值就是段地址。一般AT段不含代码和数据,它仅表示已在内存中的代码和数据地址的样板,如显示缓冲区和其它硬件定义的绝对存储单元,LINK不对AT段生成任何代码和数据。
       PRIVATE
        不与其他段进行组合。MASM不识别该关键字。
    c) 类别
      用于表示段的分类,LINK总是使类别相同的段相邻,也只有类别相同的段才能进行组合。类别是一个由程序员指定的字符串,但必须使用单引号括起来。如不给出,则类别为空。
  2) 堆栈段的说明
    除了COM,一个完整程序应该有堆栈段。组合类别中的STACK指定堆栈段,组合多个堆栈段只会加大堆栈段空间。LINK会把组合类型为STACK的段信息写入可执行文件,执行时,操作系统根据这些信息自动设置SS和SP,从而构成物理堆栈。如果不指定STACK,那么程序员必须自行在程序中取得堆栈段的段值送入SS和SP。如:

 

 1 SSEG    SEGMENT    PARA
2 DB 1024 DUP(?)
3 STOP LABLE WORD
4 SSEG
5
6 CLI
7 MOV AX,SSEG
8 MOV SS,AX
9 MOV SP,OFFSET STOP
10 STI

 

    由于硬件中断与程序使用同一个堆栈段,所以切换堆栈段时要关中断。如果不指出堆栈段,汇编编译器会给一个缺省的堆栈段,大小为64KB。
  3) 段组的说明和使用
    定义在不同数据段的变量访问总是要切换段值,为了减去这种麻烦,程序员可以在源代码的各独立段中安排几种数据类型,且要在执行时能通过一个独立的、公用的段寄存器访问它们,就可以使用段组。GROUP把不同段集合为一个组,并赋予组名,格式为:
    组名 GROUP 段名[,段名…]
    段名与段名之间用逗号分隔。段名也可用“SEG 变量”或“SEG 标号”来取代。组名的使用与段名使用类似,也可使用ASSUME语句,此时组内各段的变量的偏移将相对与段组起始地址来计算。此时,如果要用OFFSET得到段组内的偏移,须加上组名,如“MOV DX,OFFSET GROUP1 VAR1”,不然只能取得对应段的段内偏移。

 

3、段的简化定义
  为了使程序设计更为简便。
  1) 存储模型说明伪指令
    描述程序采用的存储模型,在使用简化的段定义伪指令前必须用它指定存储模型。格式:
.    MODEL 存储模型
    常用存储模型有:
      a)SMALL:全部数据限制在单个64KB内,全部代码也如此。独立汇编语言常用的模型。数据段寄存器可保持不变,而所以跳转都是段内转移。
      b)MEDIUM:全部数据限制在单个64KB内,但代码段长大于64KB。数据段寄存器不变,但会出现段间转移的情况
      c)COMPACT:全部代码限制在单个64KB内,数据总量大于64KB,但一个数组不可大于64KB。
      d)LAGER:代码和数据均可独立超过64KB,但是数组不能大于64KB
      e)HUGE:代码,数据和数组均可独立超过64KB
  2) 简化的段定义伪指令
    a) 定义代码段的伪指令:.CODE
    b) 定义堆栈段的伪指令:.STACK [大小]
      大小以字节为单位,不指定则默认1024字节。
    c) 定义数据段的伪指令:.DATA
      说明:定义多个.DATA开始的数据段同在一个源文件模块内定义多个同名段性质一样。此外伪指令.DATA?和伪指令.CONST表示未初始化数据段开始(.DATA数据也可不初始化)和常数数据段开始。宏汇编程序自动把.DATA、.CONST、.DATA?、.STACK自动组成一个段组。
    d) 定义远程独立数据段伪指令
      表示一个独立数据段,不加入上述所说的段组。格式:
      .FARDATA [名字]
      名字可选,如果选用,则为段名。对应的有.FARDATA?。它们的关系类似.DATA和.DATA?
    e) 缺省段名
      在简化段定义下,程序员可以不关心段类型、组合类型等,但是与完整段定义的文件混用时,就要关注它们。实际上,汇编编译器给它们预取了段名,下表列出了在小(SMALL)内存模型下,各段的段名信息:

伪指令

段名

定位类型

组合类型

类别

组名

.code

TEXT

WORD

PUBLIC

‘CODE’

 

.fardata

FAR_DATA

PARA

PRIVATE

‘FAR_DATA’

 

.fardata?

FAR_BSS

PARA

PRIVATE

‘FAR_BSS’

 

.data

DATA

WORD

PUBLIC

‘DATA’

GROUP

.const

CONST

WORD

PUBLIC

‘CONST’

GROUP

.data?

BSS

WORD

PUBLIC

‘BSS’

GROUP

.stack

STACK

PARA

STACK

‘STACK’

GROUP

      在MEDIUM、COMPACT、LAGER、HUGE等内存模型下,段名前面还会加上带下划线的模块名,如“MNAME_TEXT”,所以,要注意模块名不能以数字开头。在使用伪指令FARDATA时加了名字,那么该名字就成为独立数据段的段名。
  3) 存储模型说明伪指令的隐含动作
    a) 隐含的段组和段设定
      存储模型说明伪指令指示汇编把可能的.DATA、.CONST、.DATA?、.STACK集合为一个名为DGROUP的段组,同时指示汇编程序把数据段寄存器DS和堆栈段寄存器SS与段组DGROUP对应(ASSUME语句)。使代码段寄存器CS与代码段对应。所以,一般使用存储模型伪指令后可直接引用DGROUP,而且多数情况下不再需要ASSUME语句。
    b) 有关的预定义符
      为了间断段定义伪指令
       符号@CODE表示代码段段名
       符号@DATA表示由.DATA段和.STACK段等集合而组成段组的组名
       符号@FARDATA表示独立数据段的段名

 

4、模块间的通讯
  1) 伪指令PUBLIC和伪指令EXTRN
    a)伪指令PUBLIC:用于声明当前模块内定义的某些标志符(数据变量、程序标号或者过程名)是公共标识符,即可供其他模块使用的标识符。可以有多条PUBLIC语句,每条PUBLIC语句可声明多个标识符。格式为:
      PUBLIC 标识符 [,标识符…]
    b)伪指令EXTRN:用于声明当前模块使用的哪些标识符在其他模块内定义。一般格式如下:
      EXTRN 标识符:类型 [,标识符:类型…]
      汇编程序根据类型产生合适的代码或者保留恰当的单元,类型可以是NEAR、FAR、BYTE、WORD或者DWORD等。同样,可有多条EXTRN语句。须注意:把EXTRN放段内时,声明的标识符属于那个段,而放段外,则从属段无定义。
    c)声明一致性:凡是有PUBLIC声明的标识符必须在其他模块通过EXTRN被引用,反之亦然,否则编译出错。此外,PUBLIC与EXTRN语句经常放在模块首。
  2) 模块间的转移
    指从一个模块的某个代码段转到另一个模块的某个代码段(通常形式是过程调用)。当连接时,模块组合为同一个段是有条件的,可是如果组合为同一个段,那么过程就成为了近过程,它比远过程高效。但实际开发中段同名比较难以做到,反而常常采用远过程。须注意的是:使用简化段定义,可以避免考虑段名是否相同而使用NEAR声明为近过程,相关问题留给编译器去解决。
  3) 模块间的信息传递
    表现为模块间过程调用时的参数传递,在“三 子程序设计与DOS功能调用<一>:http://user.qzone.qq.com/703016035/blog/1382361408”中调用参数传递原则和方法依然有效。少量参数可以利用寄存器或利用堆栈传递,大量参数先组织在一个缓冲区,然后利用寄存器或堆栈传递相应指针。如果要利用约定存储单元传递参数(不可重入),那么它们需要被声明为公共标识符。此外,正确设置数据段或附加段寄存器是正确传递信息的保障,在访问其它模块变量时,必须保证已设置好相应的段寄存器。

 


5、子程序库
  为了实现代码的重复利用,有如下几个方案:
    1) 源码库:把子程序源码集中到一个文件,使用时用INCLUDE导入。问题:符号冲突;重复汇编。
    2) 目标文件库:把如上源码库文件单独编译,只需将使用到的子程序声明为EXTRN即可。问题:目标文件库整体被连接,其中包好大量未使用子程序。
    3) 子程序库:库文件中存放子程序名称,目标代码,连接时重定位信息。优点:连接时只从目标文件库中提取需要用到的函数。
  子程序库中的函数需要遵守如下约定:
    1) 参数传递方法保持一致
    2) 过程类型保持相同,即均为近过程或者远过程。为近过程时保证组合条件被满足。
    3) 采用一致的寄存器保护措施和可能需要的堆栈平衡措施
    4) 子程序名字规范
  建立子程序库步骤如下:
    1) 确定子程序库所含子程序范围,即库准备包含哪些子程序
    2) 确定参数传递方式
    3) 确定子程序类型,还确定子程序所在段的段名,定位类型,组合类型和类别
    4) 确定寄存器保护措施等内容
    5) 利用专门的库管理工具程序,把经常调试的子程序目标模块逐一加入到库中
  下面来举一个例子:写一个把二进制数转换为十进制数码的ASCII码串子程序,并加入库BDHL.LIB库中。
  分析:为了方便确定子程序类型,采用简化段定义方式,存储模型定位SMALL;过程定义为FAR,于是调用时只需用“EXTRN FAR XXX”声明即可,而无需考虑段组合问题;采用寄存器传递参数;保护必要寄存器内容。源代码如下:

 1         public    BDASCS
2 .model small
3 .code
4
5 ;子程序名:BDASCS
6 ;功 能:把二进制数转换为十进制数码的ASCII码串
7 ;入库参数:AX = 欲转换的二进制数
8 ; DS:DX = 缓冲区首地址
9 ;出口参数:无
10 ;说 明:(1)远过程
11 ; (2)缓冲区至少长5个字节
12 BDASCS proc far
13 push si
14 mov si,dx
15 mov cx,5
16 mov bx,10
17 @@1:
18 xor dx,dx
19 div bx
20 add dl,30h
21 mov [si + 4],dl
22 dec si
23 loop @@1
24 pop si
25 ret
26 BDASCS endp
27 end

 

  以上可通过另一种算法实现,即通过BCD码调整指令与带进位加法指令结合。这里不是关注要点,先不做讨论。使用如下方式加入子程序库(加上文件名为T8L1.ASM,只用的是TASM):

    TASM T8L1.ASM
    TLIB BDHL.LIB + T8L1.LIB

  使用例程:

1 
2 .code
3 extrn BHASCS:FAR ;声明
4 mov AX,XXX ;欲转换的二进制数
5 mov DX,OFFSET XXX ;缓冲区首址
6 call FAR PTR BHASCS ;函数调用
7

 

 

6、 编写供TurboC调用的函数
  通常将C语言与汇编语言结合方式是:先各自写出独立模块并汇编出目标代码,然后统一邻接。须注意两点:汇编模块中的函数名符合C语言约定以及汇编函数能恰当处理C风格函数调用(包括参数传递、返回值以及C要求寄存器保护规则)。
  1) 汇编模块应遵守的约定
    a) 关于内存模式和段的约定
      采用简化的段定义模式,兼容内存模式与段名由汇编解释器完成。
    b) 关于函数名的约定
      TurboC希望所有外部符号以下划线开头,所以汇编模块的符号须同样以下划线开头。此外,TurboC大小写敏感,故汇编模块也须注意(使用/ml告诉编译器对所有标识符大小写敏感,而/mx只对公共标识符大小写敏感)。
  2) 参数传递和寄存器保护
    a) 获取入口参数cdcel调用方式。
    b )8,16位返回值通过AX寄存器返回,32位通过DX:AX寄存器返回。
    c) TurboC约定必须保护好BP、SP、CS、DS以及SS的内容。可随意改变AX、BX、CX、DX以及ES和标志寄存器的内容。寄存器SI和DI在C模块中启用了寄存器变量时,它们须在汇编模块中被保护,为了妥当,可一律保护它们。