ICS 与 8086 复习 09:MASM 程序结构与分支循环编程
汇编编程题通常同时考两件事:程序能否正确表达算法,以及程序是否具备正确的汇编、装入和运行条件。只写出一串计算指令,未必已经构成一个完整程序。本篇围绕 MASM 的 DOS 16 位程序,复习程序骨架、伪操作、条件分支和循环。
先分清汇编时和运行时
源文件到执行文件的基本过程是:
源代码 .ASM → MASM 汇编 → 目标文件 .OBJ
目标文件及其他模块 → LINK 连接 → 执行文件 .EXE
MASM 负责识别符号、计算表达式、检查指令格式并生成目标代码;LINK 负责组合模块、解析相应引用并形成执行文件。CPU 运行的是生成后的机器指令。
SEGMENT、ENDS、ASSUME、DB、DW、EQU、END 等是给汇编器看的伪操作,不要把它们画进程序运行时的指令流程。
例如:
COUNT EQU 4
ARRAY DW 10,20,30,40
COUNT 是符号常量,不为自己分配内存;ARRAY 分配四个字,共 8 字节,并设置初值。DW ? 只预留相应存储空间,不能把问号理解为已经填入零。
程序骨架中的三个关键动作
以下是简化段格式的基本骨架:
.model small
.stack 100h
.data
VALUE DW 1234h
.code
start:
mov ax,@data
mov ds,ax
; 在这里放算法
mov ax,4c00h
int 21h
end start
第一,.MODEL SMALL 等声明组织程序的段和存储模型。第二,执行 MOV AX,@DATA 与 MOV DS,AX 才真正初始化 DS。第三,INT 21H 的 4CH 功能结束这个 DOS 程序,END START 则表示源文件结束并指定入口。
ASSUME DS:DATA 仅告诉汇编器如何理解段寄存器与逻辑段的关系,不能代替装入 DS 的机器指令;同样,END 不能代替程序退出代码。这两组区别常出现在改错题中。
完整段格式与简化段格式应分别掌握。前者使用成对的 SEGMENT/ENDS;后者先声明 .MODEL,再用 .DATA、.CODE 等切换,不必机械地给每一段补上 ENDS。
数据定义题怎样逐项计算
看一个内存布局例子:
A DB 3 DUP(1,2)
B DW 1234h,-1
A 展开为 01 02 01 02 01 02,占 6 字节。B 占 4 字节,按小端顺序保存为 34 12 FF FF。数组的元素数与总字节数必须分开回答。
课件所用老版本 MASM 中,TYPE 常用于获得元素字节数,LENGTH 的语义与 DUP 定义相关。例如 ARRAY DW 100 DUP(?),TYPE 为 2、LENGTH 为 100、SIZE 为 200;不能把后续版本的 LENGTHOF 或其他汇编器的规则直接代入所有题目。
OFFSET ARRAY 得到偏移地址,MOV AX,ARRAY 则读取 ARRAY 处的字;WORD PTR [BX] 声明这次访问按字处理,不会把原变量永久改成另一种类型。
用一道最大值题检查分支逻辑
要求在三个带符号字 X、Y、Z 中找最大值。可让 AX 始终保存“目前见过的最大值”:
mov ax,X
cmp ax,Y
jge check_z
mov ax,Y
check_z:
cmp ax,Z
jge save_max
mov ax,Z
save_max:
mov MAXVAL,ax
假设 X=-5、Y=12、Z=7,逐步跟踪如下。
- AX 初值为 -5。
- 比较 AX 与 Y:-5 小于 12,不跳转,AX 更新为 12。
- 比较 AX 与 Z:12 大于等于 7,跳到保存结果。
- MAXVAL 最后为 12。
关键不是记住 JGE 的英文,而是先确定题目是有符号比较。若把 JGE 换成 JAE,FFFBH 会被当成无符号数 65531,结果就会改变。
分支指令应紧接着消费自己需要的标志。若在 CMP 与 Jxx 之间加入会改写标志的 ADD、SUB 等指令,跳转判断便可能不再对应原比较。
循环写成初始化、循环体和控制条件
以下片段累加四个无符号字,假设它们的总和能够装入 AX:
xor ax,ax
lea si,ARRAY
mov cx,4
sum_loop:
add ax,[si]
add si,2
loop sum_loop
若 ARRAY 为 1、2、3、4,四轮之后 AX 依次成为 1、3、6、10;SI 每轮加 2,因为每个元素占一个字。
LOOP 将 CX 减 1,若结果非零则跳转。它不依赖 ZF,也不改写算术标志。因此,“LOOP 与 DEC CX/JNZ 在所有方面完全一样”是不正确的:二者的循环控制效果相近,但对标志的影响不同。
还有一个边界:进入循环体前若 CX 已为 0,执行到 LOOP 后会减成 FFFFH,而不是立即结束。需要允许空数组时,应在进入循环体前使用 JCXZ 等方式检查。
多重循环和字符转换的常见失分处
冒泡排序对 n 个元素通常做 n-1 轮;第 k 轮只比较尚未就位的部分。使用 CX 同时控制内外循环时,必须保存外层计数,并在每次进入内层时重新初始化内层状态。数组为字类型时,访问相邻元素的位移差为 2。
十六进制输入题则可维护如下不变量:BX 始终保存已经接收的合法数字所构成的数。读到下一位 d 后,执行 BX=BX×16+d。例如输入 1A3F,BX 依次为 0001H、001AH、01A3H、1A3FH。
必须先确认字符合法并转换为 0—15,再移动和更新 BX。若先移位、后发现输入非法却重新读字符,旧结果已经被破坏。A—F 的 ASCII 范围是 41H—46H;小写 a—f 转大写可在确认字母范围后清除位 5,而不能随意做 OR。若限定结果保存在16位BX中,还应限制有效输入位数或检测溢出,不能让超过四位的十六进制数默默丢失高位。
显示十六进制数字时,0—9 加 30H,10—15 加 37H。CL 常用于多位移位计数,若循环又用 CX,就必须保存恢复 CX 或另选循环计数方案。
数组题先检查边界,再选择控制方式
有序数组插入题适合从末尾向前移动。若要把 32 插入 3,15,37,49,先把 49 后移,再把 37 后移,遇到 15 时停止,最后把 32 写入空位。若反方向覆盖原数组,尚未搬走的数据可能已经丢失。还需保证数组末尾预留一个元素的容量;算法正确不等于存储空间自动增加。
课件介绍了在正数数组前放置负数哨兵的方法。它依赖“数组元素均为正数”以及有符号比较,不能把哨兵直接复制到任意无符号数组中。若题目未保证该前提,使用显式首地址或元素个数检查更容易说明边界正确性。
折半查找题则先确认有序、确定下标单位,再计算元素地址。逻辑下标为 i 的字元素,对应字节位移通常是 2*i;课件示例把数组首字用作元素个数,真正的数据从下一个字开始,这个表头不能参与普通元素比较。查找失败和成功的返回约定也要写清,例如 CF 表示是否找到,而不能靠一个未说明含义的寄存器值让调用者猜测。
逻辑尺和跳跃表属于同一类控制思想:先把条件或选择值转成位信息、表下标,再确定分支。16 位近跳转地址表每项通常占一个字,所以第 k 项的字节位移是 2*k。遍历位图时若题目只要求第一个置位,应说明从低位还是高位扫描;若有多个 1,扫描方向会改变选中的分支。
三道自测
题 1。 ASSUME DS:DATA 之后,可否直接认为 DS 已指向 DATA?
答: 不可以。ASSUME 是汇编时声明,运行时仍需执行正确的段寄存器初始化。
题 2。 A DW 5 DUP(?) 占多少字节?遍历时每个元素跨多远?
答: 占 10 字节,每个元素跨 2 字节。若 CX 表示元素数,应装入 5,而非 10。
题 3。 已经用 MOV CX,4 控制循环,循环体里再执行 MOV CL,4,有什么问题?
答: 它会改写 CX 的低字节,从而破坏循环计数。可保存恢复 CX,或用独立的寄存器管理外层计数。
对应课件
《汇编语言 第4章.pptx》第3—7页:程序建立过程与伪操作;第14—26页:完整段与简化段格式;第27—31、40—48页:数据定义、类型及符号常量。《汇编语言 第5章.ppt》第12—20页:循环与数制转换;第21—25页:有序数组插入;第29—35页:循环实验与冒泡排序;第36—50页:分支、查找及跳跃表;第51—52页:带符号最大值题。本文重写了示例程序,并修正十六进制输入处理中的字符范围和更新顺序。
讨论这篇文章
关于《ICS 与 8086 复习 09:MASM 程序结构与分支循环编程》的补充、不同意见或延伸话题,都可以留在这里。
滚动到这里时自动载入留言。