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模块中启用了寄存器变量时,它们须在汇编模块中被保护,为了妥当,可一律保护它们。