Program Design
May 11th, 2020
1. 程序结构
1.1 分段式程序结构
以一个排序的汇编语言为例:
上面是一个节选的堆栈的定义部分,汇编语言是一个横竖三段式结构:
横三段式是指:[标号:] 指令助记符 指令操作数 [;注释]
标号是用来确定偏移地址,后面一定紧跟冒号
指令助记符就是常见的指令 MOV, ADD 等
指令操作数由指令助记符确定,有零操作数,一个操作数和两个操作数
注释在行尾用分号表示开始,在汇编的时候会自动忽略
竖三段是指:段起始 段内指令 段结束
段起始如上面的:
STACK SEGMENT PARA STACK
段结束如上面的:
STACK ENDS
在段起始位置和段结束位置之间可以编写段内指令
1.1.1 语句格式
指令语句
汇编后生成一个可以供机器执行的机器目标代码
格式:[标号:] 指令助记符 指令操作数 [;注释],如:
START: MOV AX,STACK
MOV DS,AX ;SET SS,SP,DS
伪指令语句
伪指令语句在汇编后不产生对应的目标机器代码,但是在程序中起到指示的作用,只有通过伪指令的组织,指令序列才能正常地放置和执行
格式:[符号名] 伪指令 操作数 [;注释],如:
STACK SEGMENT PARA STACK
STACK ENDS
TABLE_LEN DW 16
符号名有很多种,可以是段名、过程名、变量名、常量名等,可以根据伪指令的需要加上符号名
注释行
用分号开头的一行或多行,常用于说明下面的一段程序的作用,或阐明编程者的思路,以便阅读者理解。
字段对齐
指令的书写应该按照分横向分段的格式,对于有空缺的位置空出
汇编语言程序中的指令语句不允许跨行或拆行,这与高级语言程序不同
如定义一个比较长的数组的时候,实在一行放不下可以分开两行但是两行都必须要有,如:
1.1.2 标号和符号名
指令语句中的标号和伪指令语句中的符号名统称为标识符(identifier)。标号的定义出现在指令语句中,它后面有冒号。符号名(变量名、常量名、段名、过程名)在定义时则没有冒号,且出现在伪指令中。
在命名规则上,标号和符号名相同。标号名和符号名都必须以字母或专用字符(?, @, 一)打头,中间不能有空格或运算符号,其长度会因汇编器的厂家和版本的不同而不同。
为了不与符号名和标号相混淆,指令和伪指令语句中出现的常数,当以A~F打头时,必须在前面加上一个0,否则就会将它当作变量名或常量名而出现错误。
在给标号和符号名命名时,还必须注意与汇编语言中的保留字相区别。保留字主要有:
CPU中的寄存器名
指令助记符
伪指令
表达式中的运算符
指令或伪指令操作数中的属性操作符
在汇编语言中,无论是常数、常量、标号、符号名、指令助记符、伪指令、操作符、参数等,都不区分大小写。
1.1.3 程序中的段和过程
堆栈段和数据段中定义的都是数据,主要是通过 EQU(常量定义) 以及 DB, DW, DD 等实现的。本例中在堆栈段定义的有:
在代码段中:
则告诉了汇编器要将CS设置成CODE1的段首地址,DS设置成DATA1的段首地址,SS设置成STACK1的段首地址,这种ASSUME并没有直接将段寄存器附上初值,在程序中还是要通过显示的指令指定,但在程序代码段的开始还是需要放上这样的一条ASSUME语句。
查阅官方论坛得到的ASSUME的理解:
After an ASSUME is put into effect, the assembler watches for changes to the values of the given registers. ERROR generates an error if the register is used. NOTHING removes register error checking. You can combine different kinds of assumptions in one statement.
尝试修改CS为STACK,SS为CODE,在编译阶段就会发生报错:
尝试修改SS为DATA,DS为STACK,汇编和连接都可以通过:
但是运行会死循环,通过DEBUG发现初始化后唯一的不同:
代码段中的内容由主程序和子过程组成,两者都是统一的过程,都要用
PROC
和ENDP
一对伪指令来指定。过程必须有名字。在主程序的开始阶段的这几条初始化段寄存器的语句是每个程序必不可少的:
程序末尾的返回语句也是不可缺少的:
上面两句命令的结果是使程序执行正常返回到MS-DOS命令提示符,返回值为0。
注意:如果使用 RET 语句代替这两条语句不能正常返回到MS-DOS提示符。
程序最末尾用一个
END MAIN
结束,表示汇编程序的结束,并且告诉编译器执行从MAIN开始,这里的MAIN是一个标识符,可以自己选择。编译器将会忽略后面的所有内容。
1.2 定义程序结构的伪指令
1.2.1 段定义伪指令
段名是用户自定义的,只需要保证一个短的开始SEGMENT和结束ENDS的段名相同即可,并且在段引用指令和ASSUME语句中也要使用相同的段名。
对齐类型是可选字段,表示段在内存中分配时的起始边界设定,可选的参数有:
PAGE(页):表示本段从页的边界开始。一页为256字节,所以段的起始地址(段基地址)的低8位为0。采用PAGE方式对齐时,段与段之间最大的空隙可能为255字节。
PARA(节):表示本段从节的边界开始。一节为16字节,所以段的起始地址的低4位为0。采用此种对齐方式,段内偏移值可以从0开始。
SEGMENT的缺省对位类型就是PARA,所以一般都省略此参数。采用PARA对齐时,段与段之间最大可能空隙是15字节。
WORD(字):表示本段从内存中的偶字节地址开始。段与段之间的空隙最多只有1字节。
BYTE(字节):表示本段从字节地址开始,段与段之间无任何空隙。
当只用BYTE对齐时:
组合类型也是可选字段,用于确定段与段之间的关系。当程序有多个模块(或多个数据段、代码段)以及有特殊的要求时,可通过组合类型来确定各段之间段的组合方式。这些方式有:
NONE:这是缺省选项,表示本段是独立的,不需要与其他段组合。在装人内存时,本段有自己的段基址。不指定组合类型即是NONE。
PUBLIC:在满足定位类型的前提下,连接程序会将本段与其他具有相同段名的段组合成一个大的逻辑段,它们共用同一个段基址。此段基址是最早的那个同名段的段基址。
所有段内的偏移量都要变为相对于新逻辑段的起始地址。采用PUBLIC组合类型,可将不同模块中相同段名的若干小段拼装成-个大的物理段。
COMMON:该段在连接时与其他同名的段有相同的起始地址,所以会产生覆盖。
COMMON连接的段长是各同名段中最长的段的长度。
覆盖的实质是将同一内存块按不同的逻辑方式来组织。
STACK:说明该段为堆栈段的一部分。连接程序在连接时,会把所有同名的具有STACK组合类型的段连接成一个连续段,并将SS初始化成这个连续段的首地址,用段内的最大偏移地址初始化SP。
正确地定义了段的STACK属性后,可以在主程序中省略对SS,SP的初始化。
MEMORY:表示本段在内存中应定位在所有其他连接在一起的段的前面(高地址上)。
如果连接时遇到几个指定了MEMORY的段,则遇到的第一个段被当作MEMORY段,其他段作为COMMON段。
AT表达式:表示本段可以直接定位在内存的某个位置,且以节边界对齐。例如:
S1 SEGMENT PARA AT 0B800H
表示S1段在内存中的物理位置为B800H。
类别名依然是可选字段,必须用单引号(')或双引号(")括起来。
类别名可以是由编程人员指定的任何名字,但它不能与程序中的其他符号名和标号相重。
在程序被连接时,连接程序会将类别名相同的所有段存放在连续的内存区中,但它们仍然是不同的段。
这种组合与组合类型不同,而且当有组合类型时,组合类型先于类别名的组合。
举个例子,画出下面的程序的内存图:
首先可以看到除了STACK1的STACK外没有其他的组合类型,但是大多都有类别名,所以将类别名相同的组织在连续的内存空间中,但因为没有相同的组合类型所以还是属于不同段,所以存放的顺序为:
STACK1
STACK2
DATA1
DATA2
CODE
DATA3(存放于0B8000H)
1.2.2 过程定义伪指令
过程的最后一条语句一般都是RET,用于返回至调用者;对于MAIN而言可能是
MOV AX, 4C00H
和INT 21H
表示返回至MS-DOS指令提示符。每个过程PROC必须有自己的过程名,其他位置可以通过CALL过程名进行跳转,过程名后直接跟PROC,没有冒号
NEAR|FAR 参数
NEAR 属于段内调用,CALL指令执行时只需要向堆栈中压入CALL指令下一条指令的偏移地址;RET指令在汇编后仍然是RET指令,会从堆栈中弹出一个字作为IP值
FAR 属于段间调用,当跳转的地址与当前的IP的偏移量大于16位有符号数范围(-32768~32767)时使用,此时CALL指令会向堆栈中压入CALL指令的下一条指令的段地址以及偏移值;RET在汇编后变成了RETF指令,会从堆栈中先后弹出两个字作为IP值以及CS值
若该参数未定义则默认为NEAR,但如果实际上是FAR,在编译可以通过,连接会失败
1.2.3 定位伪指令 ORG
SEGMENT语句的各种属性会定义段中内存的分配方式,但内存的分配无论是数据还是指令都是从偏移量为0开始往后放置的。
若想要为下一条指令或数据定义指定一个特定的偏移地址,可以使用ORG定位伪指令,参数为偏移量。
样例:
String的逻辑地址首地址为 B800H:2000H,物理地址为 BA000H。
测试:在TABLE_LEN后面加上
ORG 20H
:未加入ORG:
加入ORG:
2. 调试运行
本课程使用的是 MicroSoft 的 MASM v6.1 在 DOSBox v0.74 上进行实验。
2.1 汇编与连接过程
进入 MASM 的 BIN 可执行文件夹,执行汇编指令:
再通过连接指令:
此时可以通过
2.2 文件
|文件后缀|文件类型|生成方式| |ASM|汇编程序文件|自己创建| |LST|清单文件|由宏汇编器生成| |MAP|内存映象文件|连接程序生成|
2.2.1 LST 文件
2.2.2 MAP 文件
就是因为还不知道 STACK 的位置所以地址空出,但是经过连接阶段,内存映象确定后,每个段的起始位置,长度和终止位置已经获得,所以可以反填 LST 中空缺的部分。并且此时确定了程序的入口:0025:0000
2.3 调试
使用debug命令进入调试窗口:
会出现 -
光标,可以输入调试指令:
d -- 显示内存单元内容 display
常用来查看数据段,和DS配合使用
内存的显示分为三列,分别是:
逻辑地址
逻辑地址的表示为
高16位:低16位
中间重叠部分为12bit,也就是3位十六进制
真实地址为:高16位<<4 + 低16位
单元内容
对应的ASCII字符
在实际的编程中,可以通过使得地址1和地址2相等的方式来确定地址上的数据内容,比数格子要方便一些。
e -- 修改内存单元内容 edit
发现数据区的内容不符合我们的预期时,可以使用e进行修改,命令如下:
r -- 显示或修改寄存器内容 register
在调试程序的过程中,如果发现指令结果不对且该结果暂存于寄存器中,则可以先修改寄存器的值得以继续进行调试,并在最终源程序中改正错误。
f -- 填写内存单元(批量修改) fill
t -- 单步跟踪 trace
单步跟踪又称为“单步进入”,即遇到CALL命令时会深入进子程序。t命令的用法为:
p -- 单步执行
单步执行又称“单步通过”,因为其在遇到CALL命令时会一次执行完,不会深入到子程序中。 实际程序调试的情况:
需要进入子程序查看每条指令是否正确 —— t
不需要进入子程序,希望一次执行完 —— p
遇到 INT n 等中断调用,必须用 p 单步通过,因为系统调用是厂家编写,执行过程很长,一旦进入很难回到用户程序
g -- 连续运行 go
u -- 反汇编
用来查看程序段,配合CS使用,使用方法同d指令
a -- 汇编 assemble
用户修改代码段中的指令,可以在源代码的基础上修改也可以直接在某个代码位置处编一段程序。在修改前应该用u命令找到精确的修改的位置,在修改后应用u命令检查修改是否正确。
在程序中还可以输入伪指令,即数据定义,但是插入的数据定义没有标号和变量,只有地址。
n -- 命名文件 name
指定一个文件名,用于后面装入和写入命令使用,格式为:
l -- 装入文件 load
将前面用n命令指定的文件装入到l命令后指定的地址处,装入后文件的长度使用 CX:DX
组成的 32位数表示。
举例:若装入后 CX=0001, DX=A000, DS=2000, 装入指定地址为 100,则装入的文件位于 2000:0100 ~ 3000:A0FF 内存区。
w -- 写回文件 write
将内存中指定的地址处开始的内容写入到前面用n命令指定的文件中。文件的长度由 CX:DX
组成的 32位数定义。
q -- 退出DEBUG quit
退出DEBUG界面回到MS-DOS命令提示符界面。
Appendix
INT
AH
功能
调用参数
返回参数
00
程序终止(同INT 20H)
CS=程序段前缀
01
键盘输入并回显
AL=输入字符
02
显示输出
DL=输出字符
03
异步通迅输入
AL=输入数据
04
异步通迅输出
DL=输出数据
05
打印机输出
DL=输出字符
06
直接控制台I/O
DL=FF(输入)DL=字符(输出)
AL=输入字符
07
键盘输入(无回显)
AL=输入字符
08
键盘输入(无回显)检测Ctrl-Break
AL=输入字符
09
显示字符串
DS:DX=串地址'$'结束字符串
0A
键盘输入到缓冲区
DS:DX=缓冲区首地址(DS:DX)=缓冲区最大字符数
(DS:DX+1)=实际输入的字符数
0B
检验键盘状态
AL=00 有输入AL=FF 无输入
0C
清除输入缓冲区并请求指定的输入功能
AL=输入功能号(1,6,7,8,A)
0D
磁盘复位
清除文件缓冲区
0E
指定当前缺省的磁盘驱动器
DL=驱动器号 0=A,1=B,...
AL=驱动器数
0F
打开文件
DS:DX=FCB首地址
AL=00 文件找到AL=FF 文件未找到
10
关闭文件
DS:DX=FCB首地址
AL=00 目录修改成功AL=FF 目录中未找到文件
11
查找第一个目录项
DS:DX=FCB首地址
AL=00 找到AL=FF 未找到
12
查找下一个目录项
DS:DX=FCB首地址(文件中带有*或?)
AL=00 找到AL=FF 未找到
13
删除文件
DS:DX=FCB首地址
AL=00 删除成功AL=FF 未找到
14
顺序读
DS:DX=FCB首地址
AL=00 读成功 =01 文件结束,记录中无数据 =02 DTA空间不够 =03 文件结束,记录不完整
15
顺序写
DS:DX=FCB首地址
AL=00 写成功 =01 盘满 =02 DTA空间不够
16
建文件
DS:DX=FCB首地址
AL=00 建立成功 =FF 无磁盘空间
17
文件改名
DS:DX=FCB首地址(DS:DX+1)=旧文件名(DS:DX+17)=新文件名
AL=00 成功AL=FF 未成功
19
取当前缺省磁盘驱动器
AL=缺省的驱动器号 0=A,1=B,2=C,...
1A
置DTA地址
DS:DX=DTA地址
1B
取缺省驱动器FAT信息
AL=每簇的扇区数DS:BX=FAT标识字节CX=物理扇区大小DX=缺省驱动器的簇数
1C
取任一驱动器FAT信息
DL=驱动器号
同上
21
随机读
DS:DX=FCB首地址
AL=00 读成功 =01 文件结束 =02 缓冲区溢出 =03 缓冲区不满
22
随机写
DS:DX=FCB首地址
AL=00 写成功 =01 盘满 =02 缓冲区溢出
23
测定文件大小
DS:DX=FCB首地址
AL=00 成功(文件长度填入FCB)AL=FF 未找到
24
设置随机记录号
DS:DX=FCB首地址
25
设置中断向量
DS:DX=中断向量AL=中断类型号
26
建立程序段前缀
DX=新的程序段前缀
27
随机分块读
DS:DX=FCB首地址CX=记录数
AL=00 读成功 =01 文件结束 =02 缓冲区太小,传输结束 =03 缓冲区不满
28
随机分块写
DS:DX=FCB首地址CX=记录数
AL=00 写成功 =01 盘满 =02 缓冲区溢出
29
分析文件名
ES:DI=FCB首地址DS:SI=ASCIIZ串AL=控制分析标志
AL=00 标准文件 =01 多义文件 =02 非法盘符
2A
取日期
CX=年DH:DL=月:日(二进制)
2B
设置日期
CX:DH:DL=年:月:日
AL=00 成功 =FF 无效
2C
取时间
CH:CL=时:分DH:DL=秒:1/100秒
2D
设置时间
CH:CL=时:分DH:DL=秒:1/100秒
AL=00 成功 =FF 无效
2E
置磁盘自动读写标志
AL=00 关闭标志AL=01 打开标志
2F
取磁盘缓冲区的首址
ES:BX=缓冲区首址
30
取DOS版本号
AH=发行号,AL=版本
31
结束并驻留
AL=返回码DX=驻留区大小
33
Ctrl-Break检测
AL=00 取状态 =01 置状态(DL)DL=00 关闭检测 =01 打开检测
DL=00 关闭Ctrl-Break检测 =01 打开Ctrl-Break检测
35
取中断向量
AL=中断类型
ES:BX=中断向量
36
取空闲磁盘空间
DL=驱动器号 0=缺省,1=A,2=B,...
成功:AX=每簇扇区数 BX=有效簇数 CX=每扇区字节数 DX=总簇数失败:AX=FFFF
38
置/取国家信息
DS:DX=信息区首地址
BX=国家码(国际电话前缀码)AX=错误码
39
建立子目录(MKDIR)
DS:DX=ASCIIZ串地址
AX=错误码
3A
删除子目录(RMDIR)
DS:DX=ASCIIZ串地址
AX=错误码
3B
改变当前目录(CHDIR)
DS:DX=ASCIIZ串地址
AX=错误码
3C
建立文件
DS:DX=ASCIIZ串地址CX=文件属性
成功:AX=文件代号错误:AX=错误码
3D
打开文件
DS:DX=ASCIIZ串地址AL=0 读 =1 写 =3 读/写
成功:AX=文件代号错误:AX=错误码
3E
关闭文件
BX=文件代号
失败:AX=错误码
3F
读文件或设备
DS:DX=数据缓冲区地址BX=文件代号CX=读取的字节数
读成功: AX=实际读入的字节数 AX=0 已到文件尾读出错:AX=错误码
40
写文件或设备
DS:DX=数据缓冲区地址BX=文件代号CX=写入的字节数
写成功: AX=实际写入的字节数写出错:AX=错误码
41
删除文件
DS:DX=ASCIIZ串地址
成功:AX=00出错:AX=错误码(2,5)
42
移动文件指针
BX=文件代号CX:DX=位移量AL=移动方式(0:从文件头绝对位移,1:从当前位置相对移动,2:从文件尾绝对位移)
成功:DX:AX=新文件指针位置出错:AX=错误码
43
置/取文件属性
DS:DX=ASCIIZ串地址AL=0 取文件属性AL=1 置文件属性CX=文件属性
成功:CX=文件属性失败:CX=错误码
44
设备文件I/O控制
BX=文件代号AL=0 取状态 =1 置状态DX =2 读数据 =3 写数据 =6 取输入状态 =7 取输出状态
DX=设备信息
45
复制文件代号
BX=文件代号1
成功:AX=文件代号2失败:AX=错误码
46
人工复制文件代号
BX=文件代号1CX=文件代号2
失败:AX=错误码
47
取当前目录路径名
DL=驱动器号DS:SI=ASCIIZ串地址
(DS:SI)=ASCIIZ串失败:AX=出错码
48
分配内存空间
BX=申请内存容量
成功:AX=分配内存首地失败:BX=最大可用内存
49
释放内容空间
ES=内存起始段地址
失败:AX=错误码
4A
调整已分配的存储块
ES=原内存起始地址BX=再申请的容量
失败:BX=最大可用空间 AX=错误码
4B
装配/执行程序
DS:DX=ASCIIZ串地址ES:BX=参数区首地址AL=0 装入执行AL=3 装入不执行
失败:AX=错误码
4C
带返回码结束
AL=返回码
4D
取返回代码
AX=返回代码
4E
查找第一个匹配文件
DS:DX=ASCIIZ串地址CX=属性
AX=出错代码(02,18)
4F
查找下一个匹配文件
DS:DX=ASCIIZ串地址(文件名中带有?或*)
AX=出错代码(18)
54
取盘自动读写标志
AL=当前标志值
56
文件改名
DS:DX=ASCIIZ串(旧)ES:DI=ASCIIZ串(新)
AX=出错码(03,05,17)
57
置/取文件日期和时间
BX=文件代号AL=0 读取AL=1 设置(DX:CX)
DX:CX=日期和时间失败:AX=错误码
58
取/置分配策略码
AL=0 取码AL=1 置码(BX)
成功:AX=策略码失败:AX=错误码
59
取扩充错误码
AX=扩充错误码BH=错误类型BL=建议的操作CH=错误场所
5A
建立临时文件
CX=文件属性DS:DX=ASCIIZ串地址
成功:AX=文件代号失败:AX=错误码
5B
建立新文件
CX=文件属性DS:DX=ASCIIZ串地址
成功:AX=文件代号失败:AX=错误码
5C
控制文件存取
AL=00封锁 =01开启BX=文件代号CX:DX=文件位移SI:DI=文件长度
失败:AX=错误码
62
取程序段前缀
BX=PSP地址
Last updated