汇编语言程序设计
Goal:
- 了解汇编语言源程序的结构.
- 深入理解伪指令系统.
- 深入理解DOS功能调用
- 掌握汇编语言源程序的设计方法.
4.1 汇编语言源程序
4.1.1 汇编语言源程序的结构
一个完整的汇编语言源程序由若干个逻辑段(Logic Segment)组成, 包括DS, CS, SS, ES.
- 每个逻辑段以 SEGMENT语句开始
- 以ENDS语句结束
- 整个源程序用END语句结尾
下面给出一个完整的ASM程序的结构框架:
下面以一个具体的例子来说明一个完整ASM语言的结构.
E.g. 编写一个两个字相加的程序:
4.1.2 ASM语句类型及格式
可分为两大类: 指令性语句 和 指示性语句.
指令性语句 - Instruction Statement : 由指令助记符(Ch.3学到的哪些)等组成的 可被CPU执行的语句.
一般格式:
[标号:] [前缀] 助记符 [操作数[,操作数]][;注释]
[label:] [prefix] mnemonic [operands],[operands] [;comment]
指示性语句 - Directive Statement : 用于告诉asm程序如何对程序进行汇编, 是CPU不执行的指令. 又称 伪操作语句 或 伪指令.
一般格式:
[名字] 伪操作 操作数 [,操作数,...] [;注释]
[name] mnemonic(directive) operands [,operands,…] [;comment]
其中+[]的是可选项.
对于一个语句Statement的构成要素(constituent elements) :
- 标号 - Label : 标号表示指令的符号地址, 要有:
- 名字 - Name :通常表示 变量名, 段名, 和 过程 名, 不要有:
- 助记符 - Mnemonic Instruction :
- 操作数 - Operand :
- 指令性语句 的操作数 : 0, 1, 2
- 指示性语句 的操作数 : 不限
操作数以 “,” 隔开.
- 注释 - Comment : 从;开始, 注释不参加程序汇编, 不生成目标程序, 只是为程序员阅读程序提供方便.
4.1.3 数据项及表达式
1 - 常量 - Constant
包括 数字常量 和 字符串常量
2 - 标号 - label
标号具有3种属性 : 段值, 偏移量 和 类型.
-
段值 : 标号所在段的短地址, 当程序种引用的一个标号时, 该标号应在代码段CS.
-
偏移量 : 标号所在段的段首 to 定义该表好的地址之间的字节数(EA) -> 16bits unsigned.
-
类型 : 两种, NEAR 和 FAR, NEAR 为 近标号, 只能在段内被引用, 地址指针为2个字节; FAR 为远标号, 可在其他段被引用, 地址指针为 4 个字节.
3 - 变量 Variable
与标号同样具有3种属性 : 段值, 偏移量 和 类型
变量的类型 : BYTE(字节), WORD(字), DWORD(双字), QWORD(4字), TBYTE(十字节).
变量是存储器种某个数据区的名字.
使用时 :
- 变量类型 与 指令的要求必须相符.
- 在定义变量时, 变量名对应的时数据区的首地址, 例如:
4 - 表达式 - Expression
Expression 本身不是指令, 本身不能执行. 在我们的程序进行汇编时, 汇编程序对表达式进行相应的运算, 得出一个确定的值.
算术运算符 +,-,*,/和MOD
表达式汇编结果就是一个数值, 具体比如说 :
E.g.
逻辑运算符 AND, OR, NOT, XOR
逻辑运算符只用于数值表达式, 用来对数值进行按位逻辑运算, 并得到一个数值结果.
关系运算符 EQ(=), NE(!=), LT(<), GT(>), LE(<=), GE(>=)
参与关系匀速那的必须为2个数值或同一段 中的两个存储单元地址.
运算结果为一个逻辑值 :
- 关系为真 -> 0
- 关系为假 -> 0FFFFH
取值运算符 OFFSET, SEG & 属性运算符 PTR
-
OFFSET : 获取 一个标号Label/变量Var 的偏移地址.
MOV SI, OFFSET DATA1 ;将 变量DATA1 的EA送SI
这条指令等价于
LEA SI, DATA1 ;取 DATA1 的EA送SI
-
SEG : 得到 一个标号Label/变量Var 的段地址.
MOV AX, SEG ;将 变量DATA的短地址送AX
MOV DS, AAX ;DS <- AX -
PTR : 指定 位于其后的 存储器操作数 的类型
CALL DWORD PTR[BX] ;说明存储器操作数为4个字节长
MOV AL, PTR[SI] ;将 SI 指向的一个字节数送 AL如果变量已经定义为了字变量, 可以用PTR修改属性.
MOV AL, VAR ;指令非法, VAR为WORD, AL为Byte, 两操作数不等长.
MOV AL, BYTE PTR VAR ;指令合法,
BYTE PTR VAR强制将VAR变为字节操作数.
4.2 伪指令
提供非机器指令的功能,用于定义变量、段、存储器等。
常用伪指令 : ASSUME、DB(定义字节), DW(定义字)等.
4.2.1 数据定义伪指令
1 - 格式
[变量名] 伪操作 操作数 [,操作数 ...]
常用 的 伪操作指令 如下:
DB(Define Byte) : 定义变量为字节类型。DB伪指令也常用来定义字符串
DW(Define Word) : 定义变量为字类型。DW 伪指令后面的每个操作数都占用2个字节。在内存中存放时,低字节在低地址,高字节在高地址。
DD(Define Double Word) : 用来定义双字类型的变量。DD 伪指令后面的每个操作数都占用4个字节。在内存中存放时,同样是低字节在低地址,高字节在高地址。
DQ(Define Quad Word) : 定义四字(QWORD,8个字节)类型的变量。在内存中存放时,低字节在低地址,高字节在高地址。
(5)(TBYTE)类型的变量。DT伪操作后面的每个操作数都为10个字节的压缩BCD数。
2 - 操作数
可为常数, 表达式 or 字符串.
一个数据定义伪指令可以定义多分数据元素, 但 每个数据元素的值 不能超过 由伪操作 所定义的数据类型限定的范围.
E.g.
3 - 重复操作符 DUP
格式 :
[变量名] 数据定义伪操作 n DUP(,初值 ...)
- 圆括号中 -> 重复的内容
- n -> 重复次数
E.g.
DATA1 DB 20 DUP(?)
DATA3 DB 20 DUP(30H)
重复操作符 主要用于需要预留存储区域且不关心初始值 的场合.
4.2.2 符号定义伪指令 - EQU
给一条表达式赋予一个名字.
格式 :
名字 EQU 表达式
E.g. 下面的表达式左右等价.
CR EQU 0DH
TEN EQU 0AH
VAR EQU TEN*2+1024
ADR EQU ES:[BP+DI+5]
用EQU可以用一个名字定义一个数值 or 用一个名字定义另一个名字. 但不能重复定义
=> 若要重复定义, 用"="伪指令. E.g.
FACTOR = 10H ;FACTOR 代表了数值 10H
...
FACTOR = 25H ;从现在开始, FACTOR 代表 25H
4.2.3 段定义伪指令 - SEGMENT
段名 SEGMENT [定位类型] [组合类型] ['类别']
.
.
.
段名 ENDS
源程序中 每个逻辑段由 SEGMENT语句开始,到ENDS语句结束. 对于DS, ES, SS来说, 段体一般为变量, 符号定义等伪指令; 对CS则存放代码.
1 - 定位类型 - Align
告诉汇编程序如何确定逻辑段的地址边界.
-
PARA - Paragraph : 逻辑段从一个节(16bits)的边界开始. -> 段起始物理地址为 XXXX0H.
-
BYTE : 从字节边界开始.
-
WORD : 从字边界开始. -> 段起始物理地址为 偶数
-
PAGE : 从页边界开始. 256字节为一页. -> 段起始物理地址为 XXXX00H.
2 - 组合类型 - Combine
用在具有多个模块的程序中. 告诉汇编程序, 当一个逻辑段装入存储器时 它与其他段如何进行组合.
-
NONE(default) : 本段与其他段不组合 -> 即对不同程序模块, 对不同程序模块中的逻辑段, 即使具有相同的段名, 也分别作为不同的逻辑段装入内存.
-
PUBLIC : 不同程序模块中用PUBLIC说明的同名逻辑段, 汇编时会将其组合构成一个大的逻辑段.
-
STACK : 含义与 PUBLIC 基本一致, 但仅作为堆栈的逻辑段使用 -> 在汇编时 将不同的逻辑段集中在一个大的堆栈段, 由各模块共享.
-
COMMON : 连接时从同一个地址开始装入 -> 各个逻辑段重叠在一起.
-
MEMORY : 当几个逻辑段连接时, 本逻辑段定位在地址最高大的地方. 如果存在多个MEMORY -> 只将首先遇到的段作为MEMORY段, 其余的视作COMMON.
3 - 类别 - Class
以单引号’‘括起来的字符串, e.g. 代码段’CODE’, 堆栈段’STACK’…
作用为 :当几个程序模块连接时, 同CLASS的逻辑段装入连续的内存区内, 按出现的先后顺序排列.
E.g.
汇编成功后, 存储器中的分配情况:
4.2.4 设定段寄存器伪指令 - ASSUME
用于向ASM程序说明所定义的逻辑段属于何种类型的逻辑段.
ASSUME 段寄存器 : 段名 [,段寄存器 : 段名 [, ...]]
8088的存储器采用分段结构, 每个逻辑段最大为64KB, 可以有多个逻辑段. 但每个程序模块最多只允许有4个逻辑段(CS, DS, SS, ES).
=> ASSUME 用来告诉ASM程序当前正在使用的各段的名字 -> 告诉ASM程序用SEGMENT伪操作定义过的段的段地址将要存放在那个段寄存器中.
E.g.
4.2.5 过程定义伪指令 - CALL & RET
过程 <=> 子程序 <=> 函数 <=> 方法
一般格式 :
过程名 PROG [NEAR/FAR]
. ; ↓
. ;→ 过程体
. ; ↑
RET ; 返回指令(也算过程体)
过程名 ENDP
过程名实际上是过程入口的符号地址. 过程体内必须要有一条返回指令RET -> 使在程序调用结束后能返回源地址. 过程可以嵌套, 也可以递归.
过程体可以是 :
- 近地值 - NEAR(default) : 与调用程序在同一个代码段
- 原地址 - FAR : 在不同的代码块中
E.g. 编写一个10ms延时的子程序
DELAY PROC ;定义一个过程
PUSH BX ;保护 BX 原来的内容
PUSH CX ;保护CX原来的内容
MOV BL, 2 ;外循环次数, 2
NEXT: MOV CX, 4167;内循环次数(实现延时5ms)
W10MS: LOOP W10MS ;CX != 则循环
DEC BL ;修改外循环计数值
JNZ NEXT ;
RET ;过程返回
DELAY PROC ;过程结束
4.2.6 宏命令伪指令 - MACRO
将程序段定义为一个宏指令, 然后每次需要时 -> 简单用宏指令名来代替 - 宏调用
格式:
宏命令名 MACRO [形式参数, ...]
(宏定义体)
ENDM
汇编语言的宏定义 与 C语言基本没差, 下面给个例子以供简单理解
宏调用 与 过程调用的区别:
- MACRO 由宏汇编程序MASM在汇编过程中进行处理, 在每个MASM都用其对应的宏定义体替换
- CALL 与 RET 则是CPU指令, 执行CALL指令时, CPU使程序的控制转移到子程序的入口地址.
4.2.7 模块定义 与 连接伪指令
在编写较大的汇编语言程序时, 通常将其划分为几个独立的源程序(/模块), 分别汇编生成各自的目标程序, 最后将其连接成为一个完整的可执行程序.
1 - NAME
格式 :
NAME 模块名
NAME 伪指令用于给汇编后得到的目标程序一个名字. NAME前不允许加标号, 如下的语句非法:
BEGIN : NAME 模块名
2 - TITLE
格式:
TITLE 标题名
标题名最多允许60个字符.
- 如果程序中无 NAME -> 汇编程序将TITLE伪指令后面的 “标题名” 中的前6个字符作为模块名.
- 无 NAME 也无 TITLE -> 将源程序的文件名作为目标程序的模块名.
3 - END
-> 表示源程序停指, 指示汇编程序停止汇编.
END [标号]
只有主模块的END语句允许使用标号.
E.g. 求从TABLE开始的10个unsigned字节数的和, 结果放在SUM字单元中.
DATA SEGMENT ;
TABLE DB 12H, 23H, 34H, 45H, 56H
DB 67H, 78H, 89H, 9AH, 0FDH
SUM DW ?
DATA ENDS
;
CODE SEGMENT
ASSUME CS:CODE, DS:DATA, ES:DATA
START: MOV AX, DATA
MOV DS, AX
MOV ES, DATA ;DS, ES段初始化
LEA SI, TABLE ;
MOV CX, 10
XOR AX, AX
NEXT: ADD AL, [SI]
ADC AH, 0
INC SI
LOOP NEXT
MOV SUM, AAX
HLT
CODE ENDS
END START ;汇编结束, 起始运行地址为START
4.3 BIOS 和 DOS 功能调用 (主要是DOS)
系统软件中提供的调用功能有两种 :
-
低级调用 - BIOS / Basic Input and output System : 是被固化在计算机主机板上Flash ROM型芯片中的一组程序, 与系统硬件有直接依赖.
-
高级调用 - DOS / Disk Operation System : 负责管理系统的所有资源, 协调微机的操作, 其中包括大量的可供用户调用的服务程序. DOS功能调用不依赖于具体的硬件系统
在调用 BIOS/DOS的系统服务程序时, 不适用CALL命令, 而是采用软中断指令INT n来实现(又称BIOS or DOS 中断), n 表示 中断类型码 -> 对应不同的中断模块.
4.3.1 BIOS 功能调用 - 考试不涉及, 不看
4.3.2 DOS 功能调用
所有的DOS系统功能调用都是利用 软中断指令 INT 21H 来实现.
可实现的子功能可分为: 设备管理Equipment ,Management, 目录管理Directory Management, 文件管理File Management 和 其他Other.
DOS 系统功能调用的使用方法如下:
- AH <- 功能号(Function Number)
- 在指定寄存器中放入该功能所要求的入口参数
- 执行 INT 21H
- 分析出口参数
下面介绍 INT 21H 的几个常用功能:
- 键盘输入 - Keyboard Input
- 显示器输出 - Display Output
1 - 键盘输入
按键分 3 种 :
- 字符键 - 字母, 数字
- 功能键 - Del, Enter
- 组合键 - Shift, Alt
DOS系统功能通过调用字符输入子功能, 可以接收从键盘上输入的字符, 输入的字符将以对应的ASCII码的形式存放.
1. 单字符输入
功能号1, 7, 8都可以接收键盘输入的单字符 -> 接收的字符以ASCII码存于累加器AL.
E.g. 从键盘输入一个"Y"或"N"
...
KEY : MOV AH, 1
INT 21H
CMP AL, 'Y'
JE YES
CMP AL, 'N'
JE NOT
JMP KEY
YES :
...
NO :
...
2. 字符串输入
调用 0AH号 功能来实现. 该功能要求用户指定一个输入缓冲区来存放输入的字符串. 缓冲区一般定义在数据段, 定义格式严格要求:
- 首字节为 用户定义的缓冲区长度
- 2nd字节为 实际输入的字符串(不包括Enter)个数, 由0AH号功能自动填入
- 3rd字节开始 存放输入的字符, 在调用本功能前, 应该把输入缓冲区的起始偏移地址预置入DX寄存器.
E.g. 从键盘上输入字符串"HELLO", 并在串尾+‘$’
DATA SEGMENT
STRING DB 10, 0, 10 DUP(?)
DATA ENDS
;
CODE SEGMENT
ASSUME CS:CODE DS:DATA
START: MOV AX, DATA
MOV DS, DATA ;初始化
LEA DX, STRING ;缓冲区偏移地址 预送入 DX
MOV AH, 0AH ;功能号21H送AH
INT 21H ;执行功能
MOV CL, STRING+1 ;实际输入的字符个数送CL -> CL = 5
XOR CH, CH ;清零CH -> 构成完整的 CX 寄存器
ADD DX, CX ;缓冲区起始地址DX加上输入字符数 -> DX = STRING + 5
MOV BX, DX ; BXL = STRING + 5 -> 指向第二个'L' (STRING, STRING+1 = 缓冲区长度n,实际读入的字符个数)
MOV BYTE PTR[BX+2],'$' ;插入串结束符'$', 串尾位置 = STRING+n+2 = STRING + 7 = BX +2
MOV AH, 4CH ;设置功能号4CH -> 程序退出功能
INT 21H ;程序退出
CODE ENDS
END START
2 - 显示器输出 - Display Output
显示器上显示的内容都为 字符形式ASCII码.
将一字符串送到显示器显示 -> 可调用 DOS 功能
- 2, 6 - 单字符显示
- 9 - 字符串显示
单字符显示
功能2 :
...
MOV DL, <要显示的字符> ;注意为ASCII码
MOV AH, 2 ;功能号送AH
INT 21H ;执行
...
功能6 :
...
MOV DL, <要显示的字符> ;注意为ASCII码, 不可以为0FFH
MOV AH, 6 ;功能号送AH
INT 21H ;执行
...
E.g. 用单字符显示功能实现 依次显示123ABC 6个字符
DATA SEGMENT
STRING DB '123ABC'
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA
START: MOV AX, DATA
MOV DS, AX
LEA BX, STRING
MOV CX, 6
LPP: MOV DL, [BX]
MOV AH, 2
INT 21H
LOOP LPP
MOV AH, 04CH
INT 21H
CODE ENDS
END START
字符串显示 - 功能号 9
要求被现实的字符串必须以’$'字符作为结束符, 否则会引起屏幕混乱.
E.g. 在屏幕上显示欢迎字符串"Hello, World!"
DSEG SEGMENT
STRING DB 'Hello, World!', ODH, OAH, '$'
DSEG ENDS
CSEG SEGMENT
ASSUME CS:CSEG, DS:DSEG
START: MOV AX, DSEG
MOV DS, AX
LEA DX, STRING ;获取要显示的字符串的首地址
MOV AH, 9 ;调用字符串显示功能
INT 21H ;执行
MOV AH, 04CH
INT 21H
CSEG ENDS
END START
E.g.2 从键盘输入一串字符, 在字符串尾插入’$',并显示该字符串.
;定义一个缓冲区接收字符串, 收了之后+个’$'再输出
DATA SEGMENT
BUFSIZE DB 50
INLEN DB ?
CHARS DB 50 DUP(?)
DATA ENDS
;
CODE SEGMENT
ASSUME CS:CODE,DS:DATA
START: MOV DX, DATA
MOV DS, DX
XOR DX, DX
LEA CX, BUFSIZE ;
MOV AH, 0AH
INT 21H ;执行读入
MOV AX, INLEN
LEA BX, CHARS
ADD BX, AX ;BX = CHARS + n = BUFSIZE字符串尾
MOV BYTE, PTR[BX], '$' ;
;Output
XOR DX, DX
LEA DX, CX - DX
MOV AH, 09H
INT 21H
MOV AH, 04CH
INT 21H
HLT
CODE: ENDS
END START
3 - 返回到 DOS - 功能号 04CH
MOV AH, 04CH
INT 21H
4.4 汇编语言设计基础
4.4.1 程序设计概述
一个好的程序不仅应满足设计要求, 实现预先设定的功能并能正常运行, 还应具备以下的一些特性 :
和 .
衡量标准:
- 可理解性, 可维护性 - programme easy to understand, easy commissioning and maintenance
- 高效率 - high execution speed
- 低空间占用率 - small occupied memory space
程序设计的一般步骤 - Methodology
- 通过对实际问题的分析抽象出系统数学模型, 简历系统的模块结构图. Abstract a mathematical model based on the actual problem, and then determine the algorithm
- 确定各程序模块的数据结构及算法.
- 画程序流程图… Draw a block diagram (flow chart)
- 用指令或伪指令为数据和程序代码分配内存单元和寄存器. Allocate memory work units and registers
- 编写源程序并报错, 形成.ASM源程序文件. According to the block diagram program the source program, save as.ASM file
- 通过汇编生成.OBJ目标代码文件, 同时完成静态的语法检查. Programming source codes are compiled, the generation of.OBJ target files
- 链接生成.EXE可执行文件. Link the.OBJ file to the.EXE execution file
- 程序调试和测试. Operation and debugging