宏与多模块
1. 课程说明
本课程基于《汇编语言(第 2 版)》郑晓薇 编著,机械工业出版社,可以配合该教材使用。本课程由郑晓薇授权制作,提取教材中的实例以及实验内容,可以在实验楼环境中完成所有实例及实验。实验课程制作符合教材原版实例驱动教学以及实验训练贯穿始终的特点。
本教材可在 当当网 购买(点击链接即可进入购买页面)。
本课程实验列表中的最后模块为配套教材的习题参考答案,可供读者学习参考。
2. 实验环境
DOS 环境:
实验环境中安装有 dosemu
可以模拟 DOS 环境,并提供 DEBUG
、MASM
、LINK
等汇编语言开发程序。
3. 宏与宏库
(1)宏
宏是源程序中一段有独立功能的程序代码。调用宏的指令称为宏指令、宏操作。宏只需要在源程序中定义一次,就可以多次调用它,调用时只用一个宏指令语句。宏和子程序都可以在程序中多次调用,但是两者的调用方式不同,完成的形式也不同,编写程序时,要根据需要灵活使用宏和子程序。宏的使用需要经过三个步骤:宏定义、宏调用和宏展开。宏定义要放在用户源程序的前部,便于后面程序中使用宏。
(2)宏库
如果在程序中定义了多个宏,可以把这些宏一起或分类放在独立的文件中保存。这种文件与高级语言中的库文件类似,称为宏库。在程序中,可以用 INCLUDE 包含伪指令把宏库文件 .MAC
调入,也可以用 INCLUDE 将其它源程序 .ASM
包含进来。
3.1 建立宏库的例子
**建立宏库
**:把多个宏的宏定义放在一个文本文件中,为其起名并加上扩展名.MAC。
目标
建立宏库 8-1.MAC
文件。共有 5 个宏。
实验步骤
用记事本 gedit
,录入宏库 8-1.MAC
;8-1.mac 宏库
;1
input macro ; 宏input,键盘输入一个字符
mov ah,01H
int 21h
endm
;2
output macro x ; 宏output,显示一个字符
mov dl,x
mov ah,02h
int 21h
endm
;3
retsys macro ; 宏retsys,结束、返回DOS
mov ah,4ch
int 21h
endm
;4
addi macro x1,x2,result ; 宏addi,两数相加,结果保存
mov ax,x1
add ax,x2
mov result,ax
endm
;5
str_mov macro opr1,opr2,opr3 ; 宏str_mov,源串传送到目的串
mov cx,opr1
lea si,opr2
lea di,opr3
cld
rep movsb
endm
3.2 调用宏库的例子
在应用程序中使用宏指令之前,用 INCLUDE 伪指令把宏库调入,然后再使用这些宏。
**关注点
**:宏与宏库的用法
设计目标
宏库的使用。在程序中调用 8-1.MAC 宏库文件。
实验步骤
(1)用记事本 gedit
,录入 8-1.ASM
;8-1.asm 宏库的使用
include 8-1.mac
.model small
.stack 100h
.data
x db 33h,34h
y dw ?
mess1 db 1,2,3,4,5,6,7,8,9,0
mess2 db 10 dup(?)
.code
start:
mov ax,@data
mov ds,ax
mov es,ax
;
str_mov 10,mess1,mess2 ;mess1传送到mess2
str_mov 2,x,y ;x传送到y
;
input ;输入的小写字母变为大写输出
sub al,20h
output al
;
addi 34,25,y ;y=34+25
retsys ;结束,返回DOS
end start
要想查看宏展开的结果,可在汇编时生成 .LST 列表文件。打开列表文件观察宏调用情况。
4. 多模块结构
要编写复杂的大型程序,会有多人参与编程。每个程序员都编写自己的代码段,这就形成了多个代码段、多个模块的大系统。所谓多模块,是由多个汇编源程序 .ASM 经汇编后生成的 .OBJ 构成,一个 .ASM 中可以有一个代码段也可以有多个代码段。每个 .ASM 经过汇编后产生的目标文件 .OBJ 称为一个模块。要想实现多模块汇编,就要事先将各个参数进行说明和定义,使有关的参数能够关联起来。同时,各个模块中的源程序要独立汇编,生成各自的 .OBJ 文件,然后用 LINK 命令连接到一起,最终生成 .EXE 可执行文件。
4.1 模块的参数设置
(1)全局符号定义 PUBLIC
在各个模块间共用的变量、符号、标号、过程等要用 PUBLIC 伪指令事先说明为全局变量,以便能被其他模块引用。
格式:PUBLIC 符号1[,符号2,……]
功能:将本模块中的符号或过程定义为全局变量,供其它模块使用。
(2)外部符号说明 EXTRN
EXTRN 伪指令用来说明某个变量、符号或过程是其它模块定义的,在本模块中需要引用。
格式:EXTRN 符号1:类型 [,符号2:类型,……]
功能:对外部符号和其类型进行说明。类型为:BYTE、WORD、DWORD、NEAR、FAR 等。符号的类型要与它在定义模块中的一致。
(3)段属性与段组合
由于多个源程序分别在不同的代码段中使用,因此段的属性要设置正确,以便于段组合。在定义代码段时,代码段名相同时要加上 PARA’CODE’
,以使其类别相同;数据段也可以用 PARA’DATA’
加以说明。
在多模块程序设计中,至少定义一个堆栈段,一般在主模块中定义。主模块的最后一条结束伪指令 END START
必须加上标号(START),而其它模块的 END 语句不能带有标号。
(4)参数传递
多模块之间的参数传递方法与子程序传参类似,也可以用寄存器传参、存储单元传参、堆栈传参等。通过对变量的 PUBLIC / EXTRN 的声明,可以实现参数传递,但是要注意段的名字、类别要相同。还可以将数据段定义为共享数据段,即组合类型为 COMMON,利用公共数据段实现模块间的数据访问。
4.2 多模块设计
本例子为教材的示例 8-2。
**关注点
**:分别录入各个模块程序及库文件
本节从一个例子入手,来看一下如何利用宏简化编程;如何在两个代码段之间进行数据传送及子程序调用等模块化程序设计方法。
设计目标
从键盘输入 4 位十六进制数,转换成十进制数显示出来。
设计思路
(1)设计一个主程序 MAKE0、两个子程序 MAKE1 和 MAKE2;用两个代码段分别保存主程序和子程序;实现远程的访问与调用。
(2)将常用的功能设为宏,并用宏库 8-2.MAC 保存。宏库中共有 6 个宏。
(3)主程序 MAKE0 和子程序 MAKE2 在同一代码段中,保存在同一个模块(.ASM),作近程调用;另一个子程序 MAKE1 在另外一个代码段中,单独一个模块,作远程调用。
(4)主程序 MAKE0 的功能是调用两个子程序;
(5)子程序 MAKE1 功能是键盘输入,并把输入的数字变为十六进制数 X;
(6)子程序 MAKE2 功能是通过多次调用宏 DIVIS,分别除以万、千、百、十、个位,获得部分商,逐次查表显示十进制部分商。
程序框图
实验步骤:
(1)用记事本 gedit
,分别单独录入下列两个模块程序 8-2a.ASM 和 8-2b.ASM,再单独录入宏库 8-2.MAC :
- 模块 1:8-2a.ASM
;8-2a.asm 远程调用模块化程序。从键盘输入4位十六进制数,转换成十进制数显示出来
extrn make1:far ;外部符号说明,make1子程序是远程的
public x ;定义x为公共变量
include 8-2.mac ;宏库
data segment
x dw 0
mess1 db 0dh,0ah,'input HEX=$'
mess2 db 0dh,0ah,'out dec=$'
dectab db '0123456789'
data ends
stack segment para stack 'stack' ;堆栈段
dw 100h dup(0)
top dw ? ;栈底
stack ends
code segment para'code' ;代码段名类别相同
assume cs:code,ds:data,ss:stack
start:
mov ax,data
mov ds,ax
mov ax,stack ;堆栈段段地址→SS
mov ss,ax
mov sp,offset top ;栈指针SP指向栈底(顶)
;主程序make0
make0 proc far
mov x,0
display mess1 ;宏display,显示提示1
mov bx,0
call make1 ;调子程序1
call make2 ;调子程序2
jmp make0
make0 endp
;子程序make2:查表,显示十进制
make2 proc
display mess2 ;宏display,显示提示2
mov ax,x ;取出公共变量x
mov dx,0
divis 10000 ;宏divis,除法得到商并显示。由于最大
divis 1000 ;的十进制数为65535,所以先除以10000,
divis 100 ;得到万位,再依次做除法得到其它位
divis 10
divis 1
ret
make2 endp
code ends
end start ;模块1结束
2)模块 2:8-2b.ASM
; 8-2b.asm
public make1 ;定义make1子程序为公共类型
extrn x:word ;说明另一个模块中的x为字型
include 8-2.mac ;调入宏库
code segment para'code' ;代码段名类别相同
assume cs:code
;子程序make1:键盘输入、形成十六进制
make1 proc far
inc bx
cmp bx,4 ;键入4次?
jg exit
input ;宏input,键盘输入十六进制数
cmp al,0dh ;回车?
jz exit
cmp al,'0' ;判断是否0-9,A-F或a-f
jl out1 ;是其它字符,转out1
cmp al,'9'
jle smal1
cmp al,'A'
jl out1
cmp al,'F'
jle smal2
cmp al,'a'
jl out1
cmp al,'f'
jg out1
sub al,20h ;小写字母a~f减去57h
smal2: ;大写字母A~F减去37h
sub al,7
smal1: ;数字0-9减去30h
sub al,30h
mov ah,0
xchg ax,x ;形成十六进制数
mov cx,16
mul cx
add x,ax ;保存
jmp make1
exit:ret
out1:
retsys ;宏retsys,结束、返回DOS
make1 endp
code ends
end ;模块2结束
3)宏库 8-2.MAC
;8-2.mac 宏库
;1
input macro ; 宏input,键盘输入一个字符
mov ah,01H
int 21h
endm
;2
output macro opr1 ; 宏output,显示一个字符
mov dl,opr1
mov ah,02h
int 21h
endm
;3
retsys macro ; 宏retsys,结束、返回DOS
mov ah,4ch
int 21h
endm
;4
key_str macro opr1 ; 宏key_str,键盘输入一串字符
mov dx,offset opr1
mov ah,10
int 21h
endm
;5
display macro opr1 ; 宏display,显示一串字符
lea dx,opr1
mov ah,9
int 21h
endm
;6
divis macro opr1 ;宏divis,做除法并查表显示
mov cx,opr1
div cx ;ax除以cx,商在ax,余数在dx
mov bx,dx ;保存余数
mov si,ax ;ax中的部分商作为位移量
mov dl, dectab[si] ;查dectab表得到部分商的ASCII码
mov ah,2 ;显示部分商
int 21h
mov ax,bx ;余数→ax
mov dx,0
endm
运行结果:
(1)先将两个模块分别汇编
(2)用 LINK 命令将两个 OBJ 文件连接(用 + 号连接)。
(3)运行 EXE 文件
**小贴士
**:宏库
把多个宏的宏定义放在一个文本文件中,为其起名并加上扩展名.MAC。有关宏和宏库的用法参见本书 8.2.4 节。
4.3 一个段的模块
本例子为教材的示例 8-3。
在程序设计过程中,有时要编写一些小型程序,要求占用空间少、执行速度快。这样的程序只能有一个段。如果只有一个段,这个段必须是代码段。那么数据、堆栈都要在同一个段中。在这样的程序结构中,数据、堆栈、代码是混杂的,此时尤其要注意指针的改变。包括指令指针 IP 和栈指针 SP。
我们给出一个例子,只定义一个代码段,数据和堆栈包含在代码段中。用一个代码段编写程序,实现几个复合功能:
- 数据的串传送
- 指令代码的生成、复制和传送
- 对堆栈的操作。
**关注点
**:将代码段的一段程序复制到另一块存储区
设计目标
利用宏,编写对指令和数据的复制和传送,对堆栈操作的程序。
设计思路
(1)用简化程序格式。数据定义伪指令用来定义数据单元和堆栈单元;
(2)由于数据区和堆栈区在一起,栈指针定义在最高地址单元处;
(3)代码区以标号 START 为开始处,所有段的段地址都要定义为 CODE;
(4)利用宏 STR_MOV 实现串传送、数据传送、指令代码传送;代码存放的目的区应该事先预定义;
(5)对堆栈区的操作是以 SS:SP 所指出的位置入栈的,对 SS 设置和对 SP 设置要一起执行。
实验步骤
(1)双击桌面上的记事本gedit
,录入下列程序:
;8-3.asm 数据和堆栈在代码段中定义。利用宏,做数据、代码传送。
include 8-1.mac
.model small
.code
x db 33H,34H
y dw ?
mess1 db 1,2,3,4,5,6,7,8,9,0
mess2 db 10 dup(?) ;定义10个字节空单元
sss dw 10H dup(1) ;定义16个堆栈单元
eee db 'e' ;栈的底部
start:
mov ax,@code ;各个段都在代码段中
mov ds,ax
mov es,ax
mov ss,ax ;设置堆栈段段地址
mov sp,eee-x ;设置栈指针sp指向栈底
str_mov 10,mess1,mess2 ;宏,串mess1传送到串mess2
str_mov 2,x,y ;宏,数据x传送到y
mark1: ;要传送mark1~mark2之间的程序段
input ;宏,输入小写字母
sub al,20h ;变为大写
output al ;宏,显示
mark2:
addi 34,25,y ;宏,公式计算y=34+25
;宏,把mark1和mark2之间的若干条指令传送到mark3处
str_mov mark2-mark1,mark1,mark3
nop ;空操作指令
mark3:
db mark2-mark1 dup(90h) ;预定义n个单元,存放NOP(90H)指令
;堆栈操作
mov cx,5
mov bx,0
stacopr: ;从mess1中取出5个字
mov ax,word ptr mess1[bx] ;以字型取出
push ax ;入栈保存
add bx,2
loop stacopr
retsys ;结束,返回DOS
end start
(2)在 dos 子目录下保存为 8-3.asm,经过汇编 masm 8-3.asm
,连接 link 8-3.obj
,生成 8-3.exe。
D:\dos〉masm 8-3.asm
D:\dos〉link 8-3.obj
D:\dos〉8-3.exe
D:\dos〉debug 8-3.exe
(3)运行结果:
1)用 DEBUG 执行
用 U 命令查看程序,可看到从 0085 ~ 0091 存放的是 NOP 指令。0085 中的 NOP 是源程序中写的,后面的 12 个 NOP 是预定义指令单元时,在汇编时生成的。由于我们要传送的指令序列在 MARK2 到 MARK1 之间,即地址 0061 到 006B。在此处经过宏展开后共有六条指令,每条指令的长度都是 2 字节,因此是 12 个字节。再执行 G A3
命令并查看结果。
2)观察结果
先看堆栈,在栈底 0038 单元存放的是 e(65H),从 0037 ~ 002E 入栈保存了 5 个字数据 02、01,04、03,06、05,08、07,00、09。数据区中 Y 单元(0002H)是加法运算的结果 3BH;数据串 1234567890 也分别从原串(0004H)单元传送到了目的串(000EH)中。
再从 0085 处反汇编观察,可看到原来的 12 个 NOP 指令处(0086H~0090H)已经被 6 条指令所取代。
4.4 实验示例
**关注点
**:一个十进制与其他进制转换的小工具
在带符号数的运算中,如果从键盘输入负号,要求程序能够判断出“-”,并将数值求补。
设计目标
从键盘多次输入十进制数,无论正、负数,求出补码并用二进制和十六进制显示。
设计思路
(1) 主程序 MAIN 调用子程序 SUBR1,两次调用子程序 SUBR2 分别显示二进制和十六进制数。
(2) 子程序 SUBR1:功能为键盘输入,数字键 ASCII 码 → 十进制数(该十进制数保存为二进制);判断负号,求出负数的补码;用存储单元 X 传参;
(3) 子程序 SUBR2:取出 X,用循环左移 CL 位并保留要显示的数值,查 ASCII 表分别显示二进制数和十六进制数;
(4) 利用宏库 8-2.MAC 简化程序。
实验步骤
(1)双击桌面上的记事本gedit
,录入下列程序:
;8-4.asm 模块化程序。从键盘多次输入十进制数,无论正、负数,求其补码并用二进制和十六进制显示。
include 8-2.mac ;宏库
data segment
x dw 0
sign db 0
mess1 db 0dh,0ah,'input dec=$'
mess2 db 0dh,0ah,'binary=$'
mess3 db 0dh,0ah,'HEX=$'
coup dw ?
bin db '01' ;二进制ASCII码表
hex db '0123456789ABCDEF' ;十六进制ASCII码表
data ends
code segment
assume cs:code,ds:data
start:
mov ax,data
mov ds,ax
;主程序
main proc far
mov x,0
display mess1 ;宏display,显示提示1
mov bx,0
mov cx,0
call subr1
display mess2 ;宏display,显示提示2
lea bx,bin
mov cl,1 ;循环左移1位
mov ch,16 ;要显示16位数码
mov coup,0001h ;保留最低位
call subr2 ;显示二进制数
display mess3 ;宏display,显示提示3
lea bx,hex
mov cl,4 ;循环左移4位
mov ch,4 ;要显示4位数码
mov coup,000fh ;保留最低4位
call subr2 ;显示十六进制数
jmp main
out1:
retsys ;宏retsys,返回DOS
main endp
;子程序1:键盘输入、形成十进制
subr1 proc near
mov sign,0
see0:
input ;宏,键盘输入十进制数
cmp al,0dh ;回车?
jz exit
cmp al,'-' ;判断输入是'-'?
jnz see1 ;不是'-'跳到see1
mov sign,'-' ;保存负号
jmp see0
see1: ;判断输入是否0~9
cmp al,'0' ;其它字符?
jl out1 ;转out1
cmp al,'9'
jg out1
and ax,000fh ;保留ax的低4位,其余位清0
xchg ax,bx ;形成十进制数
mov cx,10
mul cx ;乘以10
add bx,ax
jmp see0
exit:cmp cx,0 ;先键入了回车,退出
jz out1
cmp sign,'-'
jnz see2
neg bx ;是负数,求补,变为补码
see2:
mov x,bx ;存储单元X传参
ret
subr1 endp
;subr2,子程序2:显示
subr2 proc near
mov di,x ;取出x
look1:
rol di,cl ;循环左移cl位
mov si,di
and si,coup ;保留最低m位
mov dl,[bx][si] ;查表显示高位、低位
output dl ;宏,显示
dec ch ;继续显示下一位
jnz look1
ret
subr2 endp
code ends
end start
(2)在 dos 子目录下保存为 8-4.asm,经过汇编 masm 8-4.asm
,连接 link 8-4.obj
,生成 8-4.exe。
D:\dos〉masm 8-4.asm
D:\dos〉link 8-4.obj
D:\dos〉8-4.exe
(3)运行结果:
实验结果分析:
(1) 输入数的范围应该在 -32768 ~ +32767 之间,即 16 位寄存器保存带符号数的范围。
(2) 输入其它字符则程序结束。
5. 宏与多模块
本节实验取自教材中第八章的《实例八 宏与多模块》
实验目的
通过分析和运行示例程序,观察宏在程序中的用法,加深对模块化结构设计的理解。
实验内容
参考示例 8-4,完成下列实验内容 :
- 对输入的负数求反码。
- 对输入的多个带符号数用补码做连续相加运算,按其他字符退出。
- 对 2)的运算结果分别用二进制、十六进制显示。
- 对 2)的运算结果用十进制显示。用十进制显示时,如果是负数,要用‘-’表示负号。提示:判断最高位(符号位)为 1 则为负数,要再求补,得到其真值显示。
实验要求
- 第 3、4 题选做
- 实验内容用截图形式记录实验结果
- 写出实验结果分析
实验拓展
- 如果将输入的数扩大范围,能用双字表示,程序应该怎样改写?
- 分析第七章的例 7-6,对键盘输入的学生姓名和成绩,按成绩排序;如果在子程序中采用宏,程序结构会大大精简。那么应该如何设计程序?