8086汇编-模块化程序设计
9unk Lv5

1

段的完整定义

一个复杂的程序通常由若干个模块组成。源模块可用汇编语言编写,也可用高级语言编写。每个模块被单独汇编或编译成目标(OBJ)模块,最后由连接程序(LINKER)把各目标模块连接成一个完整的而可执行程序。
由于 8086/80808 采用分段的形式访问内存,所以一个模块往往又含有多个段。一个程序的若干个模块之间的段于段的定义,通常用以下两种方法:完整的段定义和简化段定义。

完整的段定义

完整的段定义提供了彻底控制段的机制,该机制可使得各模块的各个段严格按要求组合和衔接。

一般格式定义

完整段定义一般格式如下:

1
2
3
段名 SEGMENT [定位类型] [组合类型] ['类别']
语句
段名 ENDS

段名可以是唯一的,也可以与其他的段名相同。在同一模块中,如果已用相同的段名定义过,那么当前这个段被视为前一个同名段的继续,即同一个段。
对一个模块中的同名段而言,后续同名段的定义选项值应与前一个同名段相同,或者不再定义选项值而默认与前一个同名段相同。

例1:如下程序 T8-1.asm 中含有两个名为 DSEG 的段和两个名为 CSEG 的段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;程序名:T8-1.asm
;功能:打印字符串 HELLO
assume cs:code,ds:data
Sseg segment
Db 1024 dup(?)
Sseg ends

data segment
mess db 'HEL'
data ends

code segment
start:
mov ax,data
mov ds,ax
;
mov dx,offset mess
mov ah,9
int 21h
code ends
;
data segment
db 'LO',0dh,0ah,'$'
data ends

code segment
mov ax,4c00h
int 21h
code ends
end start

2
3

由于后面的同名段被视为前一个同名段的继续,所以汇编后只有 DSEG 和 CSEG 两个段。

定位类型

定位类型表示,当前段开始起始地址的要求,从而指示连接程序如何衔接相邻的两个段。
4

  1. 一般情况下(80386以下)默认的定位类型实 PARA,即段起始地址位于可用的第一个节(每节为 16 个字节)的边界处。
  2. 定位类型 BYTE 使得当前段紧接前一段,前后两个段之间没有空闲单元,所以是最节约的定位类型。
  3. 定位类型 WORD 使得段从偶地址开始,不仅较为节约,而且可以利用它把数据单元定位在偶地址。
  4. 定位类型 DWORD 常用于 80386 的 32 位段。
  5. 定位类型 PAGE(页)一页等于 256 字节,所以它可产生最大的段间隔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;程序名:T8-2.asm
;功能:略
assume cs:code,ds:data

data segment dword
mess db 'HELLO!',0dh,0ah,'$'
data ends

code segment dword
start:
mov ax,data
mov ds,ax
;
mov dx,offset mess
mov ah,9
int 21h
mov ax,4c00h
int 21h
code ends
end start

5

组合类型

不同模块的同名段的组合,为更有效更便利地使用存储器提供了方便。组合类型就是用于通知连接程序,如何把不同模块内段名相同地段组合到一起。有如下组合类型:

6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;程序名:T8-3-1.asm
;功能:略
assume cs:code,ds:data

data segment para public
mess db 'HELLO!',0dh,0ah,'$'
data ends

code segment para public
start:
mov ax,data
mov ds,ax
;
mov dx,offset mess
mov ah,9
int 21h
code ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;程序名:T8-3-2.asm
;功能:略

assume cs:code,ds:data

data segment para common
db 'ok'
data ends

code segment para public
mov ax,4c00h
int 21h
code ends
end

7

类别

类别用于表示段的分类。LINK 程序总是使类别相同的段相邻。实际上只有类别相同的同名段名才根据组合类型组合。
类别是由程序员指定的字符串,但必须用单引号括起。如果一个段没有给出类别,那么这个段的类别就为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
;程序名:MODULE1.asm
;功能:演示段的完整定义的类别属性
assume cs:cseg,ds:dseg

dseg segment byte public 'DATA'
MESS1 db 'he'
dseg ends

cseg segment para public 'CODE'
start:
mov ax,dseg
mov ds,ax
cseg ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;程序名:MODULE2.asm
;功能:演示段的完整定义的类别属性
assume cs:cseg,ds:dseg

dseg segment byte public 'XYZ'
MESS1 db '!','$'
dseg ends

cseg segment byte public 'CODE'
mov dx,offset MESS1
mov ah,9
int 21h
cseg ends
end
1
2
3
4
5
6
7
8
9
10
11
12
13
;程序名:MODULE3.asm
;功能:演示段的完整定义的类别属性
assume cs:cseg,ds:dseg

dseg segment byte public 'DATA'
MESS1 db 'llo','$'
dseg ends

cseg segment para public 'CODE'
mov ax,4c00h
int 21h
cseg ends
end

8
9
10

关于堆栈的说明

一个完整的汇编语言程序一般含有一个堆栈段,只有 COM 型程序例外。
当把某个段的组合类型指定为 STACK 时,这个段就被指定为堆栈段了。当然,如果在程序的其他模块中也有组合类型为 STACK 的同名段,那么连接时将以接续的方式组合到一起,这样会构成一个存储空间更大的堆栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;程序名:T8-1a.asm
;功能:使用段组合类型 STACK,为 T8-1.asm 增加一个大小为 1024 字节的堆栈段
assume cs:code,ds:data

data segment para STACK
db 1024 dup (?)
data ends

data segment
mess db 'HELLO!',0dh,0ah,'$'
data ends

code segment dword
start:
mov ax,data
mov ds,ax
;
mov dx,offset mess
mov ah,9
int 21h
mov ax,4c00h
int 21h
code ends
end start

11

LINK 程序会把组合类型为 STACK 的段的有关信息写入可执行程序文件中。于是在执行该程序时,操作系统的装入程序就会根据这些信息自动设置寄存器 SS 和 SP,从而构成物理堆栈。设置的 SS 的值是组合类型为 STACK 的段的段值,设置的 SP 值是堆栈段的大小,即 SS:SP 指向堆栈尾。

段组的说明和使用

程序中定义使用多个数据段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;程序名:T8-4.asm
;功能:略

assume cs:code,ds:data1 ;DS 关联 data1

data1 segment public ;数据段1
var1 db ?
data1 ends

data2 segment public ;数据段2
var2 db ?
data2 ends

code segment public ;代码段
start:
mov ax,data1
mov ds,ax ;DS 对应 data1
mov bl,var1
;
assume ds:data2 ;DS 重新关联 data2
mov ax,data2
mov ds,ax ;DS 对应 data2
mov var2,bl
;
mov ax,4c00h
int 21h
code ends
end start

如果频繁地使用上面的方法交叉访问两个数据段中的数据,这样不仅很麻烦,而且程序也会变得冗长。为了解决这个问题,编译器设计了段组的概念,让多个不同的段作为一个段来处理。
伪指令 GROUP 用于把源程序模块中若干不同名的段集合成一个组,并且赋予了一个组名。它的格式如下:

组名 GROUP 段名[,段名……]

其中,段名与段名之间用逗号隔开,段名也可由表达式 “SEG 变量” 或者表达式 “SEG 标号” 代替。

例6:段组演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;程序名:T8-4a.asm
;功能:演示段组的使用
Dsls2 group data1,data2
assume cs:code,ds:Dsls2

data1 segment
var1 db ?
data1 ends

data2 segment
var2 db ?
data2 ends

code segment para public
start:
mov ax,Dsls2
mov ds,ax
mov bl,var1
;..........
mov var2,bl
;..........
mov ax,4c00h
int 21h
code ends
end start

组名表示组,也代表组的起始地址。组名的使用和段名使用类似。段组名也可以使用在 ASSUME 语句中,表示使用某个段寄存器与某个段组相对应。
在定义段组后,段组内各段所定义的所有标号和变量除与定义它们的段起始点相关外还与组的起始地址相关。如果在 ASSUME 伪指令中使段寄存器与组内某个段对应,那么有关标号或变量就相对于改段的起始点计算。所以在使用段组后,程序员要谨慎地使用 ASSUME 伪指令,并保证具体置入段寄存器的值与之相适应。

例7:如下程序说明了如何把变量作为组的成员访问和把变量仅作为段内的成员访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;程序名:T8-5.asm
;功能:略
dgroup group cseg,dseg
assume cs:cseg,ds:dgroup

cseg segment
start:
mov ax,dgroup
mov ds,ax
mov bl,var1 ;作为组内成员访问
mov var2,bl
;
assume ds:dseg ;使DS与段DSEG对应
mov ax,dseg
mov ds,ax
mov bh,var1
mov var2,bh
mov ax,4c00h
int 21h
cseg ends

dseg segment ;数据段
var1 db 'A'
var2 db 'B'
dseg ends
end start

如果要用运算符 OFFSET 得到在段组内某个段中定义的标号或变量相对于段起始点的偏移,那么必须在标号或变量前再加上组名。例如:

MOV DX,OFFSET DGROUP VAR1

否则,只能得到相对于所在段起始点的偏移。

段的简化定义

完整的段定义使得程序员可以完全控制段,但较为复杂。新版汇编语言提供了段的简化定义方法,从而使得程序员能方便地定义段。

存储模型说明伪指令

12

简化地段定义伪指令

简化地段定义伪指令

简化地段定义伪指令均以 “点号” 引导。

(1) 定义代码段伪指令

.code

例1:写一个使系统喇叭发出 “嘟” 的程序。

1
2
3
4
5
6
7
8
9
10
11
12
;程序名:T8-6.asm
;功能:略

.model small ;采用小模型
.code ;说明代码段开始
start:
mov dl,7
mov ah,2
int 21h
mov ax,4c00h
int 21h
end start ;结束代码段

(2)定义堆栈段的伪指令

.stack [大小]

[大小] 表示字节大小,默认是 1024 字节。

(3)定义数据段伪指令

.data

例2:利用简化段定义指令改写 T8-1a.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;程序名:T8-1b.asm
;功能:略
.model small
.stack 1024
.data
mess db 'HELLO',0dh,0ah,'$'
.code
start:
mov ax,dgroup
mov ds,ax
mov dx,offset mess
mov ah,9
int 21h
mov ax,4c00h
int 21h
end start

在一个源程序模块中可以定义多个由伪指令 .data 开始的数据段,如同一个源程序中定义多个同名的数据段。
此外,还有伪指令 .data? 和 .const ,它们分别表示未初始化数据段和常量数据段。写纯粹的汇编程序时一般不使用这两条伪指令,因为 .data 中可以定义未初始化数据和常量数据。
宏汇编程序会自动把 .data、.const、.data?、.stack集合成一个段组。为了使这些段相互独立可以使用伪指令 .fardata 来实现。

(4)定义远程(独立)数据段的伪指令

fardata [名字]

“名字”时可选的,如果使用,则成就为该数据段的段名。

此外,还有伪指令 .fardata? 用于说明未初始化的独立数据段。在编写纯粹汇编程序时,无需使用 .fardata 伪指令,因为 .fardata 也可以定义未初始化数据。

缺省段名

在使用简化定义伪指令说明各段后,程序员一般不需要这些段的段名和它们的定位类型、组合类型等。但如果想把简化的段定义伪指令与标准的段定义伪指令混合使用,那么就需要直到以下内容。
13

存储模型说明伪指令的隐含动作

隐含的段组和段设定

存储模型说明伪指令 .module 除了说明程序采用的存储模型外,还起着相当于如下的作用:

dgroup group data,const,bbs,stack
assume cs:_text,ds:dgroup,ss:dgroup

由于 .module 的上述隐含动作,所以在使用伪指令 .module 后,可以直接引用段组 dgroup。而且大多数情况下也可以不使用伪指令 ASSUME。
但是少数情况下,程序仍需要安排 assume 语句来指示段寄存器与段的对应关系。我们可以使用如下方式来指示段寄存器与段的对应关系:
14

有关的预定义符

在上述程序片段中使用的符号 @code 等是汇编程序提供的若干预定义符。它们类似于用伪指令 EQU 所定义的符号。与简化的段定义伪指令相关的一些预定义符号有:

  1. 符号 @code 代表代码段的段名
  2. 符号 @data 表示由 .data 和 .stack 段等集合而成段组的组名
  3. 符号 @fardata 表示独立数据段的段名

模块间的通信

一个程序的若干模块在功能上是有联系的,不仅程序的运行次序可能要从一个模块转到另一个模块,而且程序处理数据和变量也会涉及不同的模块。以下介绍这方面的使用。

伪指令 PUBLIC 和伪指令 EXTRN

由于各模块被单独汇编,所以,如果模块A要访问其他模块的过程或变量,那么模块A必须告诉汇编程序这个符号名(标识符)在别的模块中,而别的模块也要告知汇编程序,这个模块是提供给其他模块使用的。
伪指令 EXTRN 和伪指令 PUBLIC 就是分别用于通知汇编程序上述信息。

伪指令 PUBLIC

伪指令 PUBLIC 用于声明在当前模块内定义的魔偶写标识符是公共标识符。它的一般格式如下:

PUBLIC 标识符 [,标识符…]

一条 PUBLIC 语句可声明多个这样的标识符,标识符之间用逗号隔开。一个源程序可以有多个 PUBLIC 语句。数据变量名和程序标号(包括过程名)均可声明为标识符。
15

伪指令 EXTRN

伪指令 EXTRN 用于声明当前模块使用的哪些标志是在其他模块中定义的。它的一般格式如下:

EXTRN 标识符:类型 [,标识符:类型,…]

上述语句中位于助记符 EXTRN 后的每一项 “标识符:类型” 声明一个在其他模块内定义的标识符。标识符和类型之间用冒号隔开。类型可以是NEAR、FAR、BYTE、WORD、DWORD等标识符类型属性。
一条 EXTRN 语句可声明多个这样的标识符,每项之间用逗号分隔。
16
17

声明一致性

各模块内的 PUBLIC 语句和 EXTRN 语句必须相互呼应,相互一致。而且所指明的类型也必须一致。否则 link 时出现错误。

模块间的转移

模块间的转移是指从一个模块的某个代码段转移到另一个模块的某个代码段。这种转移通常是以过程调用及返回形式出现。
若两个模块涉及转移的代码段在连接后不能组合为一个代码段,那么发生在这两个代码段之间的转移必须是段间转移,所以模块间的转移就称为远调用或远转移。
由于近调用或近转移的效率比原调用或远转移的效率高,所以程序员一般喜欢近调用或近转移。但是并非任何时候都能这样,因为分布在不同源程序模块中的代码段在连接时能被组合为一个段是有条件的,它们的段名及其类别必须相同,而且段组合类型也应为 PUBLIC。在实际编程时,不同的模块往往由不同的人员完成,所以很难做到段同名。为了避免考虑不同模块中的代码是否能组合成一个段,反而常常采用远调用或远转移。

例3:演示模块的转移

1
2
3
4
5
6
7
8
9
10
11
12
;程序名:T8-7.asm
;功能:演示模块间的转移

assume cs:cseg
extrn sub1:far
cseg segment para public 'code'
start:
call far ptr sub1
mov ax,4c00h
int 21h
cseg ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;程序名:T8-7ma.asm
;功能:作为T8-7的模块

assume cs:text
public sub1
extrn sub2:near
text segment para public 'code'
sub1 proc far
mov dl,'*'
mov ah,2
int 21h
call sub2
ret
sub1 endp
text ends
end
1
2
3
4
5
6
7
8
9
10
11
12
13
;程序名:T8-7mb.asm
;功能:作为T8-7的一个模块
assume cs:text
public sub2
text segment para public 'code'
sub2 proc near
mov dl,'+'
mov ah,2
int 21h
ret
sub2 endp
text ends
end

由于模块 T8-7.asm 和 T8-7ma.asm 两个代码段不同名,所以连接时不能组合成一个短,因此过程 SUB2 被定义为远过程;由于 T8-7ma.asm 和 T8-7mb.asm 中的两个代码段的段名和段类别同名,且组合类型时 PUBLIC,所以,在连接时它们能被组合成一个段。再由于,只有过程 SUB1 调用过程 SUB2,即只有段内调用,因此过程 SUB2 才被定义为近过程,再模块中也相应地声明为 NEAR 类型。

采用简化的段定义可避免考虑段名是否相同,把有关问题留给汇编程序解决。

1
2
3
4
5
6
7
8
9
10
11
;程序名:T8-7A.asm
;功能:简化段定义改写 T8-7.asm
extrn sub1:far
.model small

.code
start:
call far ptr sub1
mov ax,4c00h
int 21h
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;程序名:T8-7Ama.asm
;功能:

public sub1
extrn sub2:near
.model small
.code
sub1 proc
mov dl,'*'
mov ah,2
int 21h
call sub2
ret
sub1 endp
end
1
2
3
4
5
6
7
8
9
10
11
12
13
;程序名:T8-7Amb.asm
;功能:

public sub2
.model small
.code
sub2 proc near
mov dl,'+'
mov ah,2
int 21h
ret
sub2 endp
end

由于三个模块均是 small 模型,所以连接后的代码在一个段内,因此 SUB1 和 SUB2 均被作为近过程对待。

模块间的信息传递

模块间的信息传递主要表现为模块间过程调用时的参数传递。当有少量参数传递时一般使用寄存器传递,当有大量的参数传递时通常会利用堆栈来传参。
如果要利用约定的存储单元传递参数,情形稍微复杂些,需要把它们声明为公共标识符。

例5:写一个显示 DOS 版本号的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
;程序名:T8-8.asm
;功能:演示模块间利用寄存器和约定存储单元传递信息
assume cs:cseg,ds:dseg

dseg segment public 'DATA'
MESS db 'DOS Version is '
MESS1 db ?
db '.'
MESS2 DB 2 dup(?)
db 0dh,0ah,'$'
VERM db 0
VERN db 0
dseg ends

;声明 VERM和VERN是公共标识符
public verm,vern
;声明 GETVER 和 TODASC 在其他模块定义
extrn getver:FAR,todasc:FAR

cseg segment public 'code'
start:
mov ax,dseg
mov ds,ax
;
call getver ;获取 DOS 版本号
;
mov al,verm
mov bx,length MESS1 ;返回mess1重复操作符dup前的count值
mov si,offset MESS1
call todasc ;把主版本号转换成可显形式
mov al,vern
mov bx,length MESS2
call todasc ;把次版本号转换成可显形式
mov dx,offset MESS
mov ah,9
int 21h
mov ax,4c00h
int 21h
cseg ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
;程序名:T8-8ma.asm
;功能:作为程序T8-8的模块
assume cs:func
;声明GETVER和TODASC为公共标识符
public getver,todasc
;声明VERM和VERN在其他模块定义
extrn verm:byte,vern:byte
;
func segment public 'code' ;定义代码段
;子程序名:getver
;功能:获取DOS版本号
;入口参数:无
;出口参数:在其他模块的VERM单元中存放主板号
;在其他模块的VERN单元中存放次版本号
;说明远过程
getver proc far
mov ah,30h
int 21h
mov verm,al ;al=主版本号
mov vern,ah ;ah=次版本号
ret
getver endp

;
;子程序名:todasc
;功能:把一个8位的二进制数转换成相应十进制数的ASCII码串
;入口参数:AL=欲转换成的二进制数,BX=十进制数的最少位数
;DS:SI=存放ASCII码串的缓冲区首地址
;出口参数,ASCII码串在相应的缓冲区中
todasc proc far
mov cl,10
todasc1:
xor ah,ah
div cl
add ah,30h
mov [si+bx-1],ah
dec bx
jnz todasc1
ret
todasc endp
func ends
end

正确设置数据段或附加段寄存器是模块正确传递信息的保证。在访问定义在其他模块的变量前,必须保证已设置好相应的段寄存器。如有必要还可以动态地改变段寄存器的内容。
模块间传递信息的另一种方法是利用段覆盖,这个方法只适用于模块间的信息传递。
具体方法是:在两个模块中都定义一个同名同类别的数据段,规定段组合类型是 COMMON;把要传递的数据(变量)安排在这两个数据段的相同位置上。由于这两个在不同模块中的数据段同名同类别,且使用组合类型 COMMON,所以连接时它们就发生重叠。

例6:写一个显示当前系统日期的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;为了简单化,显示的日期只含月和日。主模块有一个代码段和一个数据段,子模块也有一个代码啊段和一个数据段
;程序名:T8-9.asm
;功能:演示利用段覆盖方法在模块间传递信息
assume cs:cseg,ds:dseg
extrn getdate:far ;声明getdata在其他模块定义
;
dseg segment common ;定义一个具有common类型的数据段
mess db 'Current data is'
mess1 db 2 dup(?)
db '_'
mess2 db 2 dup(?)
db 0dh,0ah,24h
dseg ends

cseg segment public
start:
mov ax,dseg
mov ds,ax
call getdate ;调用GETDATA取日期
mov dx,offset mess
mov ah,9
int 21h
mov ax,4c00h
int 21h
cseg ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
;模块名:T8-9ma.asm
;功能:作为T8-9的一部分

public getdate ;声明getdata作为公共标识符
;
dseg segment common ;定义一个具有common类型的数据段
mess db 'Currect date is:'
mess1 db 2 dup(?)
db '_'
mess2 db 2 dup(?)
db 0dh,0ah,24h ;这部分数据与模块T8-9中完全相同
year dw ?
month db ?
day db ?
dseg ends

cseg segment byte public ;定义代码段
assume cs:cseg,ds:dseg
;子程序名:getdate
;功能:取得当前日期并把月日转换成相应的十进制数ASCII码串
;入口参数:无
;出口参数:ASCII码串填入约定缓冲区
;说明:远过程

getdate proc far
mov ah,2ah
int 21h
mov year,cx
mov month,dh
mov day,dl
mov al,month
mov bx,length mess1 ;把月数转换成十进制数ASCII码串
mov si,offset mess1
call todasc
mov al,day
mov bx,length mess2 ;把日数转换成十进制数ASCII码串
mov si,offset mess2
call todasc
ret
getdate endp

;子程序名:todasc
;功能:把一个8位的二进制数转换成相应十进制数的ASCII码串
;入口参数:AL=欲转换成的二进制数,BX=十进制数的最少位数
;DS:SI=存放ASCII码串的缓冲区首地址
;出口参数,ASCII码串在相应的缓冲区中
todasc proc near
mov cl,10
todasc1:
xor ah,ah
div cl
add ah,30h
mov [si+bx-1],ah
dec bx
jnz todasc1
ret
todasc endp
cseg ends
end

模块 T8-9ma.asm 中的数据段比 T8-9.asm 中的数据段多了若干变量,在段覆盖时,以最长的段为段的实际长度。但必须注意,要传递的数据变量必须安排在相同的位置。由于模块 T8-9ma.asm 中含有要访问的数据段,所以过程 GETDATE 能够随便地访问呢想要访问地对象。

子程序库

子程序库是子程序模块的集合。库文件中存放着子程序的名称。,子程序的目标代码,以及连接过程所必须的重定位信息。当目标文件与库文件连接时,LINK 程序只把目标文件所需的子程序从库中找出来,并嵌入到最终的可执行程序中去,而不是把库内的全部子程序统统嵌入到可执行程序。

建立子程序库

为了给调用者提供方便,库中的子程序应该提供统一的调用方法,所以需要遵循如下约定:
(1)参数传递方式保持统一。
(2)过程类型保持相同,要么都是远过程,要么都是近过程。请特别注意,如果过程类型选择 NEAR,那么必须保证连接时调用者所在段能与子程序所在段组合成一个段,为此,调用者所在的段名和类别应该与子程序所在段的段名和类别相同,且组合类型同为 PUBLIC。
(3)采用一致的寄存器保护措施和可能需要的堆栈平衡措施。
(4)子程序名称规范。

建立子程序库的一般步骤如下:
(1)确定库所含子程序的范围,即库准备包含哪些子程序。
(2)确定参数传递方法
(3)确定子程序类型,还确定子程序所在段的段名、定位类型、组合类型和类别。
(4)确定寄存器保护措施等其他内容
(5)利用专门的库管理工具程序,把经过调试的子程序目标模块逐一加入到库中。

例1:编写一个把二进制数转换为对应十进制数 ASCII 码串的子程序,并把它添加到 BDHL.LIB 的库中。

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

利用如下命令汇编目标模块

masm T8L1
link BDHL.LIB+T8L1

例2:编写一个把二进制数转换为对应十六进制数 ASCII 码串的子程序,并添加到名为 BDHL.LIB 的库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
;程序名:T8L2.asm
;功能:略
public BHASCS ;声明BHASCS是公共标识符
.model small
.code ;代码段开始
;先定义一个内部使用的过程(i主程序说明信息等)
HTOASC proc near
and al,0fh
add al,90h
daa
adc al,40h
daa
ret
HTOASC endp
;
;子程序名:BHASCS
;功能:略
;入口参数:AX=欲转换的二进制数
; DS:DX=缓冲区首地址
;出口参数:略
;说明:(1)远过程
; (2)缓冲区至少长4个字节
BHASCS proc far
push di
push es
cld
push DS
pop es
mov di,dx
mov cx,404h
@@1:
rol ax,cl
mov dx,ax
call HTOASC
STOSB
mov ax,dx
dec ch
jnz @@1
pop es
pop di
ret
BHASCS endp
end

汇编目标模块

masm T8L2
link BDHL.LIB+T8L2

例3:写一个显示 16H 号中断向量的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
;程序名:T8-10.asm
;功能:略
vector = 16h
.model small
.stack 1024
.data
MESS label byte
MESS1 db 4 dup(0)
DB ':'
MESS2 DB 4 dup(0)
db 0dh,0ah,24h
.code
extrn BHASCS:far ;类型为far,但认为在相同段内
start:mov ax,@data
mov ds,ax ;设置数据段寄存器
mov ah,35h
mov al,vector
int 21h ;返回中断向量于 ES:BX 中
push BX ;保存中断向量中的偏移部分
mov ax,ES ;先转换中断向量的段值部分
mov dx,offset MESS1
call far ptr BHASCS ;转换
pop ax ;中断向量的偏移部分送AX
mov DX,OFFSET MESS2
call far ptr BHASCS
mov dx,offset MESS
mov ah,9 ;显示中断向量表
int 21h
mov ax,4c00h
int 21h
end start

汇编目标模块

masm T8-10
link T8-10+BDHL.LIB

例4:写一个显示系统常规内存量的程序。
系统常规内存量放在内存单元 40:13H 的字单元中,以 KB 为单位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
;程序名:T8-11.asm
;功能:略
extrn BDASCS:far ;认为BDASCS不在相同段内
sseg segment stack 'STACK' ;定义堆栈段
db 400h dup(0)
sseg ends

;
cseg segment public ;数据代码合为一段
MESS db 'Total = '
MESS1 db 5 dup(0)
db 'KB',0dh,0ah,24h
assume cs:cseg,ds:cseg ;段寄存器使用设定
start:push cs
pop ds
mov ax,40h
mov es,ax
mov ax,es:[13h] ;取常规内存量
mov dx,offset MESS1
call BDASCS
mov dx,offset MESS
mov ah,9
int 21h
mov ax,4c00h
int 21h
cseg ends
end start

汇编目标模块

masm T8-11
link T8-11+BDHL.LIB

至此8086汇编学习结束,后续学习 80386 汇编的同时,8086汇编中各章节练习题也会慢慢补上。

  • 本文标题:8086汇编-模块化程序设计
  • 本文作者:9unk
  • 创建时间:2022-10-23 18:53:00
  • 本文链接:https://9unkk.github.io/2022/10/23/8086-hui-bian-mo-kuai-hua-cheng-xu-she-ji-ji-zhu/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!