☰
汇编语言与接口技术四段经典代码:从大数乘到贪吃蛇的实战避坑指南
2026/10/20 9:42:31 网站建设 项目流程

简介:这是一份面向北理工《汇编语言与接口技术》课程的汇编实践资料包,涵盖大数相乘、Windows界面计算器、文本文件比较与贪吃蛇游戏四个独立/分组任务,可满足课程设计或上机作业的完整参考需求。压缩包共241个文件(608KB),以asm汇编源码、exe可执行程序、obj目标文件为主,另含rc界面资源、bmp图标位图、docx作业说明及VC工程文件,便于直接编译运行与对照学习。已有534人学习浏览,适合计算机专业学生巩固汇编语法、接口调用及Windows编程能力。通过四份完整代码,可重点掌握十进制大整数乘法算法、浮点运算与三角函数实现、文件差异行号定位,以及图形界面游戏开发思路,配合工程调试记录,能有效提高课程实践效率。

1. 某高校汇编语言与接口技术课程资料里的四段代码,为什么值得当模板研究

很多同学把这门课的资料当成“过考工具”,跑通就扔。但你要是把《汇编语言与接口技术》的典型作业题翻出来看,最常被单独拎出来讲的就是大数相乘、计算器、文本比较、贪吃蛇这四个程序。这四段代码听名字分散,实际上正好覆盖了十六位实模式汇编最关键的四个能力点:多字节数据的存取与进位、键盘中断与接口时序、串操作指令、显存直接访问和实时更新。

我后来做过一段时间嵌入式方向,回头看这四个题,发现它们比课本后面那些“把两个数相加显示”的习题实用得多。大数相乘逼你设计数据表示,计算器逼你处理异步按键和运算符优先级,文本比较逼你用对 CMPSB 和 ES 段,贪吃蛇逼你把状态机和定时器钩子搭在一起。适合三类人:准备这门课验收的学生、想把 DOS 汇编做扎实的从业者、以及需要用底层思路排查问题的人。

这套题看着简单,翻车点一点不少。下面我按题目拆开讲,每个模块给最小可复现的骨架代码,再单独列四个最常见的坑。

2. 大数相乘与计算器:从 BCD 编码到键盘中断的完整链路

把这两个题放在一起讲,是因为它们本质是一条链路:大数相乘解决“数怎么存、怎么进位”,计算器解决“按键怎么进系统、结果怎么显示”。在这门课里,几乎所有最终要显示十进制结果的大作业,都能复用这套链路。

2.1 为什么大数相乘要绕开 16 位寄存器:BCD 与多字节进位

8086 里 MUL 指令最常用的是 MUL BX 这种十六位乘十六位形式,结果放在 DX:AX 这对寄存器组里。也就是说,单条 MUL 最多只能装下 32 位乘积,超过这个范围就必须把大数拆成多段,分别相乘再按权重合并。课程资料里说的“大数相乘”,不是写一个 MUL 就完事,而是要求你设计一种能突破寄存器位宽限制的数据结构。

常见的数据结构有两种:ASCII 字符串和压缩 BCD。ASCII 字符串每一位十进制数占一个字节,显示方便,但运算时要先减 30H 再处理进位;压缩 BCD 每个字节放两位十进制,存储省一半,运算时依赖 DAA 和 DAS 指令调整。课程作业里我一般建议用压缩 BCD 做加法和计算器,用“基 10000”分段做乘法,这是这条链路里最不容易翻车的组合。

存储方式每字节容量加法进位显示难度乘法适用性
ASCII 字符串1 位十进制手动减 30H 后判进位直接显示适合竖式乘法
压缩 BCD2 位十进制DAA 调整需拆高低位再转 ASCII仅适合加法,乘法慎用
基 10000 分段每字 0~9999除以 10000 取商进位每段补零后输出最适合大数乘法

“基 10000”的意思是把大数拆成若干个十六位字,每个字存 0 到 9999 之间的一段。两个这样的字相乘,最大是 9999×9999,约等于 99980001,再加进位也不超过 32 位,正好能让 DX:AX 接住。乘完后用 DX:AX 除以 10000,余数留在当前位,商加到下一位。这个过程里不能用 DAA,因为 MUL 产生的是二进制结果,DAA 只会按 BCD 的规则调整,两种进位模型混在一起结果必然错。

2.2 计算器模块:从 INT 16H 读键到表达式求值

计算器在这门课里通常做成“数字键 + 四则运算符 + 回车执行”的结构。软件层面第一步是读键盘,最常用的入口是 INT 16H:AH=0 是阻塞读,返回时 AL 是 ASCII 码,AH 是扫描码。数字和运算符判断用 AL,功能键判断用 AH,这样分区清楚,不会被扩展键干扰。

如果课程实验要求用 8255 并行接口外接矩阵键盘,那 INT 16H 就用不上了,得直接读端口。这时必须自己处理去抖:第一次检测到某行某列变低后,延时 10 到 20 毫秒再读一次,两次结果一致才确认按键有效。这个去抖延时不能太长,否则会挡掉定时器中断的响应;也不能太短,否则机械开关抖动会带来一次误触发。常见的阈值是 15ms 左右,这个值是物理开关的参数,可以直接写在注释里。

表达式求值部分,最简单的方案只处理“数 运算符 数 等号”的输入:用一个变量存当前累加值,一个变量存上次运算符,输入新数字时做 num = num × 10 + digit。等号按下时先检查除数是否为零,再执行一次运算。如果资料里要求支持连续运算,比如 12+34-5=,就需要引入一个很小的运算符栈:遇到 + 和 - 优先级相同就先算左边的,遇到 * 或 / 则要判断是否等待下一次运算。栈空间可以用 BP 做基址,PUSH 保存操作数,POP 恢复,十六位寄存器够用,不需要上内存堆。

2.3 可复现骨架:多字节 BCD 加法与按键循环

先给大数运算里最常用的多字节 BCD 加法。这段代码在计算器里用来做大数累加,也可以作为大数相乘显示前的辅助转换:

; 多字节 BCD 加法: 结果写回 NUM1 ; LEN 为字节数, 每字节存两位十进制, 低地址存放低位 ADD_BCD: MOV SI, 0 CLC ; 清进位, 避免首次 ADC 带入脏数据 add_loop: MOV AL, NUM1[SI] ADC AL, NUM2[SI] ; 带进位相加, AL = NUM1[SI] + NUM2[SI] + CF DAA ; 加法后十进制调整, 校正非法 BCD 结果 MOV NUM1[SI], AL INC SI CMP SI, LEN JB add_loop JC OVERFLOW ; 最高字节仍有进位, 说明超过表示范围 RET

这段代码的关键是顺序:先 CLC 清进位,再逐字节用 ADC 做带进位加法,最后用 DAA 调整。DAA 会检查 AL 的低四位和高四位是否超过 9,以及辅助进位标志 AF,把结果修正成合法的压缩 BCD。参数 LEN 取 10 时,能表示 20 位十进制数;如果两个数相加后最高字节还有进位,JC 就会跳到溢出处理。显示前需要把每字节拆成高、低两个半字节,各自加上 30H 转成 ASCII,这个拆字过程不要用 DIV,用 AND 和移位更省时间。

按键循环的骨架也很好写:

; 读取键盘并回显, 遇到回车结束 READ_KEYS: MOV AH, 0 INT 16H ; 阻塞读键; 返回时 AL = ASCII, AH = 扫描码 CMP AL, 0DH ; 0DH 是回车键 JE KEY_DONE MOV DL, AL MOV AH, 02H INT 21H ; 把当前字符送到光标位置 JMP READ_KEYS KEY_DONE: RET

这里要用 INT 21H AH=02H 做单字符显示,而不是 INT 21H AH=09H,后者要求字符串以 $ 结尾,单字符回显用 02H 最直接。INT 16H AH=0 是阻塞调用,如果主程序还挂了定时器中断,按键等待太久会拖慢后面的移动逻辑,这时可以改成 AH=1 查询,有键返回 ZF=0,没键则继续做其他事。

3. 文本比较与贪吃蛇:一个吃串操作指令,一个吃坐标状态机

这两个题放在一起看很有意思:文本比较练的是“串操作 + 段寄存器”,贪吃蛇练的是“坐标状态 + 显存刷新”。一个是纯数据比较,一个是图形状态机,但它们都依赖数据段布局设计,写好了能共用同一套缓冲区。

3.1 文本比较的三种实现:为什么 REPE CMPSB 不一定快

文本比较最常见的实现是逐字节循环:MOV AL, [SI],CMP AL, [DI],不等就跳出。这种写法最直观,也最容易调试。第二种是 REPE CMPSB,一条带重复前缀的串比较指令,CPU 每比较一个字节就自动修改 SI、DI,并且递减 CX;只要发现不相等,指令立即停止。第三种是按行比较,从两个文本里分别截出以回车换行结尾的一行,逐行比,适合比较配置文件或日志。

很多人在 REPE CMPSB 上翻车,是因为 CMPSB 默认使用 DS:SI 和 ES:DI 两个段:源串在 DS 段,目标串在 ES 段。如果 ES 没有正确设置,比较的内存就是错乱的。常见做法是在程序开头用两句:

PUSH DS POP ES ; 让 ES = DS, 两个串都在同一数据段

还有一个隐藏坑是方向标志。CLD 保证 SI、DI 在比较过程中递增;如果某个子程序里用过 STD 忘了恢复,REPE CMPSB 会反过来从高地址往低地址比较,结果基本稳定失败。所以在串操作前显式写一次 CLD,成本很低,但能消除一类“看起来毫无规律”的错误。

另一个边界条件是长度。两个长度不同的字符串,内容再像也一定不相等;而 REPE CMPSB 只负责比较指定长度,长度本身需要你自己先判断。以 $ 结尾的字符串,如果尾部带有 0DH、0AH,肉眼看不出来,程序却会认为不同。所以我一般会先做规格化,把尾部回车、换行、$ 都截掉,再进入长度判断和 CMPSB。下面是能直接跑通的骨架:

; STR1 与 STR2 均以 '$' 结尾, 比较结果: 相等 AL=0, 不等 AL=1 MOV DI, OFFSET STR1 CALL STR_LEN ; 返回长度在 AX MOV BX, AX ; 暂存 STR1 长度 MOV DI, OFFSET STR2 CALL STR_LEN CMP AX, BX JNE NOT_EQUAL ; 长度不同, 直接判不等 MOV CX, AX ; 长度相同, 比较 CX 个字节 MOV SI, OFFSET STR1 MOV DI, OFFSET STR2 CLD REPE CMPSB ; 一致时继续比较, 直到 CX=0 或 ZF=0 JE EQUAL NOT_EQUAL: MOV AL, 1 JMP DONE EQUAL: MOV AL, 0 DONE: RET

这里的 STR_LEN 子程序以 DI 为入口参数,扫描到 $ 停止,返回长度在 AX。要注意 CALL 会破坏 DI,所以在 REPE CMPSB 前必须重新给 SI、DI 赋值,不能依赖之前的残留地址。参数方面,两个串都限定在 80 字节以内,这是这门课最常用的大小;如果比较的是从文件里读出的长文本,建议把 CX 的上限提高,并在 REPE 之后检查 CX 是否为 0——只有 CX=0 且 ZF=1 才说明全部字节都相等。

3.2 贪吃蛇的核心:文本模式显存、方向队列与碰撞判定

贪吃蛇在十六位实模式下的经典实现是文本模式 80×25,往 B800:0000 这个显存段直接写数据。显存里每个字符占两个字节:第一个字节是 ASCII 码,第二个字节是属性。蛇身习惯用 0DBH 实心方块字符,属性用 07H(黑底白字)或 4FH(红底白字)。写显存时偏移量是 (行 × 80 + 列) × 2,这个公式要背熟,因为后面所有绘制都靠它。

速度控制不能靠空转延时循环。课程要求的通常做法是挂钩 INT 1CH 定时器中断,频率是每秒约 18.2 次。我一般维护两个字节:SPEED_CNT 计数,SPEED_LMT 阈值。每次定时器中断只把 SPEED_CNT 加 1,当计数达到阈值时,才在主循环里执行一次蛇的移动并清零计数。初始阈值设 18,蛇约一秒走一格;吃到食物后阈值减 1,蛇自然加速。中断例程里不要做坐标更新,只做计数,否则中断和主循环同时改数组,很容易出现同一帧移动两次的瞬移问题。

方向处理也存在同类问题。一次移动过程中,玩家可能连续按下“上”和“左”,如果直接用当前方向变量 DIR,蛇头会在同一帧里被消费两次。解决办法是加一个“待处理方向”暂存变量:主循环每次移动前只从待处理方向里取一个新方向,并且要检查新旧方向不能相反——比如当前向右,新方向是左,就直接丢弃。蛇头撞自己往往不是因为边界判定错了,而是没做这个反向过滤。

碰撞判定的顺序也很关键:先按新方向计算蛇头的新坐标,再依次判撞墙、撞食物、撞自己。如果先把蛇整体移动了再判断,吃到食物时蛇尾已经被覆盖,增长逻辑就会错位。撞墙条件是 X<0 或 X>79 或 Y<0 或 Y>24;判断蛇身碰撞需要从蛇头后第一个节点开始遍历整个蛇身坐标数组,复杂度不高,蛇长 64 时完全够用。

移动核心代码可以这样写:

; 蛇节点占 2 字节: 低字节是列 X, 高字节是行 Y ; 因此 MOV AX, SNAKE[...] 后 AL = X, AH = Y MOVE_SNAKE: MOV BX, LEN DEC BX ; 指向蛇尾索引 SHL BX, 1 ; 索引转字节偏移 move_loop: MOV AX, SNAKE[BX-2] ; 取前一节坐标 MOV SNAKE[BX], AX ; 当前节覆盖后一节 SUB BX, 2 CMP BX, 0 JA move_loop ; BX > 0 继续; BX=0 时停止 MOV AX, [HEAD] ; 旧的蛇头坐标 CMP DIR, 1 JNE CHECK_DOWN SUB AH, 1 ; 上移: 行号减 1 JMP HEAD_DONE CHECK_DOWN: CMP DIR, 2 JNE CHECK_LEFT ADD AH, 1 ; 下移: 行号加 1 JMP HEAD_DONE CHECK_LEFT: CMP DIR, 3 JNE IS_RIGHT SUB AL, 1 ; 左移: 列号减 1 JMP HEAD_DONE IS_RIGHT: ADD AL, 1 ; 右移: 列号加 1 HEAD_DONE: MOV SNAKE[0], AX MOV [HEAD], AX RET

为什么必须倒着挪?如果从蛇头正着往后复制,前面节点的新坐标会覆盖后面节点还没用到的旧坐标,整条蛇会在一帧内乱掉。倒着挪则保证每个节点在被覆盖之前,它的坐标已经复制到了后一个位置。参数 LEN 是当前蛇身长度,SNAKE 数组按 [X, Y] 顺序存放,SNAKE[0] 是蛇头。吃到食物时不要删尾,直接把 LEN 加 1,再在蛇尾位置保留旧坐标即可。

3.3 数据段布局模板:让四个题目共用一套缓冲区

四个题目如果能共用一份数据段,程序结构会清爽很多。下面是我常用的布局:

DATA_SEG SEGMENT NUM1 DB 10 DUP(0) ; 20位十进制数, 压缩 BCD NUM2 DB 10 DUP(0) STR1 DB 80 DUP('$') ; 文本比较源串 STR2 DB 80 DUP('$') ; 文本比较目标串 SNAKE_X DB 64 DUP(0) ; 蛇身列坐标 SNAKE_Y DB 64 DUP(0) ; 蛇身行坐标 SNAKE_LEN DB 4 ; 初始长度 DIR DB 1 ; 1上 2下 3左 4右 PEND_DIR DB 0 ; 待处理方向, 为 0 表示无新方向 FOOD_X DB 40 FOOD_Y DB 12 SPEED_CNT DB 0 ; 定时器计数 SPEED_LMT DB 18 ; 移动阈值 DATA_SEG ENDS

把蛇身 X、Y 拆成两个一维数组,而不是一个二维数组,是因为碰撞检测时可以用同一个下标同时取 X 和 Y,避免二维索引乘法的开销。SNAKE_LEN 初始值 4,对应一条四个节点的蛇;数组上限 64 对 80×25 的屏幕来说足够展示,实际要做得更完整,可以改成 256。SPEED_LMT 初始 18,也就是约一秒一步,这个值在测试时改成 2 或 3,能迅速观察碰撞逻辑是否正确,验收前再调回舒适节奏。

4. 汇编与接口技术实战避坑:四个高频翻车点和现场排查

这一章写给正在调试的人。四个题目各有各的脾气,但翻车点高度集中,按下面的顺序排查能省很多时间。

4.1 退出程序后死机或键盘失灵

现象:程序里挂了自写的 INT 1CH 定时器中断,第一次运行正常,退出到 DOS 后再次运行,发现屏幕花了或者直接死机;在调试器里单步到中断例程附近也会卡住。

原因:自定义中断向量没有在退出前恢复。程序结束后,INT 1CH 指向的内存地址已经不存在了,定时器一触发,CPU 就跳到一个悬空地址,行为完全随机。

解决:启动时用 INT 21H AH=35H 保存原中断向量,退出前用 AH=25H 恢复。代码顺序不能反:

; 保存原定时器中断向量 MOV AX, 351CH INT 21H ; 返回 ES:BX = 原中断向量 MOV WORD PTR OLD_1C, BX MOV WORD PTR OLD_1C+2, ES ; 安装自定义中断例程 CLI MOV AX, 251CH MOV DX, OFFSET MY_1C INT 21H STI ; 退出前恢复原向量 MOV AX, 251CH MOV DX, WORD PTR OLD_1C MOV DS, WORD PTR OLD_1C+2 INT 21H

恢复时必须先把 OLD_1C 里的内容取到 DX,再修改 DS。如果先 MOV DS 再访问 OLD_1C,DS 已经指向了别的段,取出来的值就是错的。

4.2 大数相乘低位对、高位错

现象:两个多字节数相乘,结果低几位完全正确,高几位错得离谱;尝试用 DAA 调整,越调越乱。

原因:把 MUL 产生的二进制结果直接当成 BCD 来处理。二进制里每个字节的权重是 16 的幂,压缩 BCD 里每个字节的权重是 100 的幂,进位模型不同,DAA 只能修正加法的 BCD 结果,对乘法无效。

解决:大数相乘先按二进制多字节方式算乘积,或者用基 10000 分段计算,每段乘法后把 DX:AX 除以 10000,余数作为当前段,商加到下一段。显示之前单独做一次二进制到十进制转换,不要在乘法中间插 DAA。

4.3 贪吃蛇一按方向就瞬移或者闪屏

现象:蛇不是一格一格移动,按一次方向键后连跳好几格;或者屏幕闪烁厉害,蛇越长越明显。

原因:移动逻辑放在了定时器中断里,或者主循环等键盘时阻塞太久,INT 1CH 多次触发导致坐标被连续更新。闪屏则是写显存没有配合刷新时序,字符写到一半时屏幕扫描就过去了。

解决:把 INT 1CH 例程精简成只做计数器加一,坐标更新放主循环;键盘读取改用非阻塞查询。写显存前检测 3DAH 端口状态寄存器,等垂直回扫位为 1 时再写入,能显著减少闪烁。

4.4 文本比较永远返回不等

现象:两串肉眼完全相同的文本,比较结果始终是“不相同”;有时把 $ 去掉又正常,规律不稳定。

原因:第一嫌疑是 ES 没设为数据段,CMPSB 比较了错误内存;第二嫌疑是字符串尾部藏着 0DH 和 0AH,两串一个有换行一个没有,长度不同导致返回不等。

解决:先在程序开头执行 PUSH DS / POP ES。然后对字符串做规格化,把尾部的 0DH、0AH、$ 全部截掉,再比较长度。调试顺序建议是:先看长度,再看尾部字符,最后检查 ES,按这个顺序能定位九成以上的文本比较问题。

5. 把这份资料用成自己的:三个改造方向和一组验证方法

5.1 三个改造方向

如果只想过验收,跑通四段代码就够了。想真正吸收,我建议做三个小改造。

第一,把显示逻辑抽成公共子程序。一个函数负责把 BCD 数转成十进制字符串,另一个函数负责在光标处打印。大数相乘、计算器、文本比较都要输出,公用一套显示例程,代码量能砍掉三分之一,被问“模块化”时也拿得出手。

第二,给计算器加连续表达式支持。比如输入 12+34-5= 能正确算出 41。需要引入一个很小的运算符栈:遇到加减就先结算左边,遇到乘除要判断优先级。这个改造把“接口层”和“逻辑层”彻底分开,按键处理只负责收集字符,求值逻辑独立成子程序,答辩时很好讲。

第三,给贪吃蛇加暂停键。按 P 键置暂停标志,暂停时不更新坐标但仍在读键盘。这个改造会让你专门处理“恢复后蛇方向乱跑”的问题,解法是把方向更新放在移动函数入口统一消费,而不是在按键中断里立刻修改 DIR。

5.2 一组提效验证方法

验证阶段我习惯用边界用例加对拍。对拍就是手算几组结果和程序输出对比,下面这几组是必跑的:

测试用例期望结果关注点什么
0 × 123450初始进位是否清干净,乘零分支
9999 × 999999980001基 10000 分段乘法的进位
空串 vs 空串相等长度 0 时 REPE CMPSB 是否直接跳过
"abc" vs "abcd"不等长度不同是否在比较前被拦截
蛇头从右边界出界游戏结束碰撞判定是否覆盖 X>79
快速两次反向按键蛇不能反向撞自己方向过滤是否生效

跑这些用例时不要只盯着屏幕结果,断点要看 CS:IP 是否跳进预期的子程序。有一次我调文本比较屏幕输出一直对,断点一看程序根本没走比较流程,是显示函数把旧结果缓存了,这种问题只有单步才看得到。

最后说一个血泪经验:我以前做贪吃蛇,总觉得反向撞自己是定时器太玄学,后来发现是方向变量在一次移动里被改了两次,蛇头先向上又向左,实际判定时已经越界。解决方式就是文档里那个待处理方向变量,用“每次移动只消费一个方向”来约束状态。这类汇编题目的坑大多是状态耦合,不是指令记不住。把一个程序拆成键盘读取、坐标更新、显存刷新三块,用标志位隔离,比凭感觉改代码靠谱得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询