简介:这份 PDF 实验报告围绕微机原理中的两个多位十进制数相加实验展开,面向正在学习汇编语言、微机原理课程或需要完成验证性实验的学生与自学者。报告从实验目的、内容、步骤、结果到程序设计与实现均有记录,可帮助读者理解 ASCII 码存储、ADD/ADC 加法指令以及 DAA、AAA 调整指令的配合方式。资源包为单一 PDF 文档,压缩后约 9KB,轻量便于查阅和打印。其中收录 WJSY0.ASM、WJSY1.ASM、WJSY2.ASM 三个程序清单,分别处理低位在前、高位在前以及显示加数、被加数和 47193+28056=75249 格式的需求,并涉及 DEBUG 调试流程与结果还原显示方法。目前已有 1096 人学习,适合用作课程实验参考、程序对照和排错思路整理。
1. 两个多位十进制数相加到底难在哪:从 BCD 码和进位链说起
很多人看到实验一这个题目会觉得没什么可写的,两个十进制数相加,写一条 ADD 指令不就完了。真正在 emu8086 或 MASM 里跑一遍才会发现,8086 只认二进制加法,把 25H 和 18H 直接相加得到的是 3DH,这个字节里的值根本不是十进制的 43。要让机器按十进制规则进位,每一次字节加法后面都必须跟上一条 DAA 做十进制调整,同时还得把上一字节产生的进位通过 ADC 带到下一字节,串成一条跨字节的进位链。
加数一旦超过一个字节,问题就从"会不会用 DAA"变成"进位链有没有断"。字节序排反了,结果整体倒过来;结果缓冲区少开一个字节,最高位进位被悄悄丢掉;DAA 和 ADC 之间插进一条会改写 AF 的指令,低半字节的修正就失效。这些错误在屏幕上往往只表现为结果偏 6 或者少一位,看不出根因在哪。
这篇沿着 BCD 存储约定、DAA 调整规则、8086 汇编循环实现、DEBUG 内存验证、实验报告成文这条线往下走,给的是能直接编译运行的代码和能对着内存逐字节排查的路径。正在做微机原理与接口技术实验、被"结果差一点点"折磨过的人,可以照着改自己的数据段和循环体。
2. 多位十进制数相加的底层模型:BCD 存储、DAA 与进位传递
2.1 25H 加 18H 为什么得不到 43:二进制加法与 BCD 修正的差异
8086 的 ADD 和 ADC 只认二进制权值。一个字节内部,高 4 位和低 4 位各自按 8-4-2-1 加权,低 4 位相加一旦超过 9,硬件就按二进制规则向高 4 位进 1,而这一位在高 4 位里代表 16,比十进制需要的 10 多出 6。BCD 码(8421 码)用一个字节装两位十进制数,这"多进的 6"必须靠软件补回来,否则每一位数字都会落在 A 到 F 之间,无法解释成十进制。
DAA 就是干这件事的:它检查刚才那次加法的低 4 位是否超过 9,或者辅助进位标志 AF 是否被置位,满足任一条就给低 4 位补 6;随后再检查高 4 位是否超过 9,或者进位标志 CF 是否被置位,满足任一条就给高 4 位补 6,并把 CF 置 1。三个典型例子放在一张表里更清楚:
| 十进制算式 | 直接相加结果 | 触发条件 | DAA 修正后 | CF |
|---|---|---|---|---|
| 25 + 18 | 3DH | 低 4 位 D > 9 | 43H | 0 |
| 12 + 34 | 46H | 无 | 46H | 0 |
| 9 + 8 | 11H | 低 4 位产生进位,AF = 1 | 17H | 0 |
第三行值得盯一眼。11H 的低 4 位是 1,肉眼看着完全正常,但 9 加 8 在低 4 位内部产生了进位,AF 已经被置 1,DAA 照样要补 6 才能得到 17H。只盯着数字看而不看 AF,是新手最容易踩的坑。
; 对比:不加 DAA 与加 DAA 的结果差异 MOV AL, 25H ADD AL, 18H ; AL = 3DH,低半字节是 D,不是合法的十进制数字 MOV BL, AL ; BL = 3DH,直接当 BCD 用会错 MOV AL, 25H ADD AL, 18H DAA ; AL = 43H,CF = 0,结果可直接当两位十进制数用逻辑说明:DAA 没有显式操作数,它只修正 AL,因此必须紧跟在 ADD 或 ADC 后面。参数说明:它隐式读 AF 和 CF,隐式写 AF 和 CF,其中 CF 就是多字节加法里向上传递的进位来源。DAA 和 INC、DEC 之间不能随便调换顺序,INC 会改写 AF,一旦 AF 被覆盖,DAA 就判断不出低 4 位是否溢出。
2.2 多字节十进制数的存储约定:低位字节放在低地址
多位十进制数在内存里怎么摆,实验指导书写法各不相同,但循环体依赖这个约定,所以必须先定死。常见做法是低位字节放低地址,和手算时从个位开始加的顺序一致,循环指针递增就能自然从低位走到高位。以 12345678 和 87654322 为例:
| 字节序号 | 第 0 字节(低地址) | 第 1 字节 | 第 2 字节 | 第 3 字节(高地址) |
|---|---|---|---|---|
| 12345678 | 78H | 56H | 34H | 12H |
| 87654322 | 22H | 43H | 65H | 87H |
写成数据定义就是下面这样,注释里标清楚每个字节代表的十进制片段,改数值时不容易填反:
NUM1 DB 78H, 56H, 34H, 12H ; 十进制 12345678,低位在前 NUM2 DB 22H, 43H, 65H, 87H ; 十进制 87654322,低位在前如果反过来把高位放低地址,循环体本身不用改,但所有指针的初值和显示顺序都要跟着调,稍不留神就会出现"结果整体倒过来"的现象。我一般会在数据段注释里直接写出十进制原值,评审看实验报告时也能一眼核对。
2.3 DAA 的调整规则与 CF、AF 在多字节进位链里的角色
把 DAA 的判定写全,循环体才敢放心写。它分两步,第一步处理低半字节,第二步处理高半字节,第二步的判定会把第一步补 6 产生的进位也算进去:
| 判定条件 | 调整动作 | 结果标志 |
|---|---|---|
| AF = 1 或 AL 低 4 位 > 9 | AL ← AL + 06H | AF = 1 |
| CF = 1 或 AL 高 4 位 > 9 | AL ← AL + 60H | CF = 1 |
关键点在于,第二步触发时把 CF 置 1,这个 CF 正好可以喂给下一字节的 ADC。也就是说,一条ADC AL, [DI]加一条DAA,就构成了一个自带进位输入和进位输出的"十进制加法单元"。多字节加法不过是把这个单元在 SI、DI、BX 三个指针上重复 LEN 次,每次循环结束后 CF 自动变成下一轮的进位输入。
注意:整个循环体里,除了 ADC 和 DAA,其他指令必须选不影响 CF 的。MOV、LEA、INC、DEC 都不动 CF,可以放心用;ADD、SUB、AND、OR 会动 CF,绝对不能插进进位链中间。
再回头看 12345678 + 87654322 这个例子,四个字节相加的结果恰好是 100000000,每个字节位都是 78+22、56+43 这样"刚刚过百"的组合,循环体会连续四次把 CF 置 1,最高字节再单独收一次进位。用它做测试数据,进位链上任何一环断了都会立刻暴露。
3. 8086 汇编实现:两个多位十进制数相加的完整可运行代码
3.1 数据段定义:加数、结果缓冲区与长度变量
数据段承担三件事:放两个加数、放结果、记录字节数。结果缓冲区必须比加数多一字节,用来承接最高位可能出现的进位,这个字节少了,后面无论怎么调循环都没用。
DATA SEGMENT NUM1 DB 78H, 56H, 34H, 12H ; 加数1:十进制 12345678 NUM2 DB 22H, 43H, 65H, 87H ; 加数2:十进制 87654322 LEN DW 4 ; 每个加数的字节数,4 字节 = 8 位十进制 RESULT DB 5 DUP(0) ; 结果:LEN + 1 字节,最后一字节存最高进位 DATA ENDS参数说明:LEN 用 DW 定义而不是 EQU 常量,是为了方便在循环里用MOV CX, LEN装载;RESULT 的 5 个字节初值全为 0,方便在 DEBUG 里区分"没写进去"和"写进去的是 0"。改成 16 位十进制时,NUM1 和 NUM2 各扩到 8 字节,LEN 改 8,RESULT 开 9 字节,其余代码一行不用动。
3.2 主循环:ADC 与 DAA 拼出的逐字节进位链
代码段是整篇的核心。段寄存器先在入口处初始化,然后清 CF,再进入循环。清 CF 那一步很容易被漏掉,因为第一次执行 ADC 时会把 CF 带进来,如果 CF 恰好是 1,整个结果就多了一。
CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX ; 段寄存器先初始化,否则取到的是 PSP 区域的数据 MOV CX, LEN ; 循环次数 = 加数字节数 LEA SI, NUM1 LEA DI, NUM2 LEA BX, RESULT CLC ; 第一次用 ADC,先把 CF 清零 ADD_LOOP: MOV AL, [SI] ADC AL, [DI] ; 带进位加,进位来自上一字节 DAA 产生的 CF DAA ; 十进制调整,同时产生新的 CF 传给下一轮 MOV [BX], AL INC SI ; INC 不影响 CF,进位链不会被破坏 INC DI INC BX LOOP ADD_LOOP MOV AL, 0 ADC AL, 0 ; 把最后一轮遗留的 CF 收进最高字节 MOV [BX], AL MOV AH, 4CH INT 21H CODE ENDS END START逻辑说明:MOV AL, [SI]把当前字节取进 AL,ADC AL, [DI]把对方字节和 CF 一起加进来,DAA把结果修正成合法 BCD 并给出新的 CF,MOV [BX], AL落盘。三条指针自增放在后面,是因为 INC 不改 CF,可以安全夹在进位链中间。循环结束时 CF 里还留着最后一次 DAA 的进位,MOV AL, 0加ADC AL, 0就是把它单独取出来写进结果最高字节。
参数说明:SI、DI 分别指向两个加数,BX 指向结果;三者步长都是 1,因为 BCD 是按字节存储而不是按位存储。如果实验要求用同一个缓冲区做结果,把 BX 指向 NUM1 也可以,但要注意覆盖顺序,必须在读取完当前字节之后才能写入。
3.3 汇编、链接与在 DEBUG 里验证结果的命令序列
实验环境通常有三套,命令各不相同,认准自己机器上装的是哪一套:
# MASM 5.x MASM ADDBCD.ASM; LINK ADDBCD.OBJ; ADDBCD.EXE # MASM 6.11 ML /c /Zm ADDBCD.ASM LINK16 /3 ADDBCD.OBJ; # TASM TASM ADDBCD.ASM TLINK ADDBCD.OBJ链接完成后直接运行 EXE 看不到任何输出,因为程序只写了内存没做显示,这是正常的。要看结果得进 DEBUG:
DEBUG ADDBCD.EXE -u # 反汇编,确认代码段入口地址 -t # 单步执行 -d DS:0 # 查看数据段内容 -g # 全速运行到程序结束提示:EXE 刚载入 DEBUG 时,DS 指向的是 PSP 而不是数据段。必须先单步执行过
MOV DS, AX那一条,再-d DS:0,看到的才是 NUM1 的原始字节。用-r看寄存器时,重点盯 CF、ZF 和 CX 三个值。
在 DEBUG 里逐字节核对的方法很朴素:先-d DS:0,对照 NUM1 和 NUM2 的原始字节确认没读错位置,再单步到DAA之后,用-r看 AL 和 CF,看每一轮的 AL 是不是合法 BCD(每个半字节不超过 9)。四轮走完,DS:0 偏移 12 处应该出现00 00 00 00 01,最后那个 01 就是百万位上的进位。
3.4 参数怎么改:位数、结果缓冲区与显示前导零
| 变量 | 当前值 | 含义 | 修改后的连带影响 |
|---|---|---|---|
| LEN | 4 | 加数字节数 | 改 8 即 16 位十进制,循环自动跟着变 |
| RESULT | 5 DUP(0) | 结果缓冲区 | 必须 ≥ LEN + 1,少一个字节就丢最高进位 |
| NUM1 / NUM2 | 各 4 字节 | 加数本身 | 位数变了要同步补齐,缺位补 0 |
| SI / DI / BX 步长 | 1 | 按字节推进 | BCD 按字节存,不要改成 2 |
如果实验要求把结果显示到屏幕上,还得补一段 BCD 转 ASCII 的代码。每个字节拆成高、低两个半字节,各自加 30H 送 INT 21H 的 02H 功能输出。这里有两个坑:一是 INT 21H 返回后 AL 会被改写成功能号,所以 AL 必须先 PUSH 保存;二是 CX 被拿去做移位计数时会覆盖循环计数,要 PUSH/POP 保护。结果最高位通常只有 0 或 1,直接逐位输出会打出一串前导零,可以在输出前先找到第一个非零半字节再开始打印。
4. 结果对不上:5 类典型错误与排错路径
4.1 结果整体偏 6 或偏 66:DAA 漏用或位置不对
| 现象 | 根因 | 定位方法 |
|---|---|---|
| 每一位数字都比手算大 6 | ADD 后根本没写 DAA | -u反汇编,看 ADC 后面跟的是什么 |
| 只有高位字节偏 6 | DAA 被挪到了 INC 之后 | 检查 DAA 与 ADC 之间是否夹了改 AF 的指令 |
DAA 的位置不是风格问题。INC 会改写 AF,一旦INC SI插在ADC和DAA中间,DAA 读到的就是被覆盖过的 AF,低半字节该补的 6 补不上,高半字节的判定也会跟着错位。排错时不要凭记忆看源码,直接在 DEBUG 里-u把机器码反汇编出来,指令顺序一目了然。
4.2 只加了最低字节就结束:CX 被别的指令覆盖
现象很典型:结果里只有第 0 字节是正确值,后面全是初值 0。原因几乎都是 CX 在循环体内被别的用途借走了。最常见的是写显示代码时为了做SHR AL, CL把 CL 当移位计数用,顺手就把循环计数清零或改成 4,LOOP 指令读到 CX 变成 0,下一轮直接退出。
PUSH CX ; 借用 CL 做移位计数前先压栈 MOV CL, 4 SHR AL, CL POP CX ; 用完立刻恢复循环计数逻辑说明:PUSH/POP 成对出现,保证进出循环体时 CX 的值不变。另一种更省事的做法是用固定次数移位SHR AL, 1连写四次,彻底不动 CL。参数说明:如果循环体里确实需要额外的循环变量,优先用 DI、SI 之外的寄存器,例如 BP,别去动 CX。
4.3 最高位进位丢失:结果缓冲区少开一个字节
把 RESULT 定义成4 DUP(0)而 LEN 也是 4 的时候,循环结束后那句MOV [BX], AL会写到 RESULT 后面的变量上去。这个错误在屏幕上不一定看得出来——结果低 4 字节可能是对的,但它悄悄改掉了数据段里别的数据,一般是 LEN 本身或者相邻的加数。DEBUG 下-d DS:0会看到某个不该变的字节变成了 00 或 01。
排查顺序是先数清结果需要几个字节:两个 N 字节 BCD 数相加,结果最多 N+1 字节,因为两个 99…9 相加必然产生最高进位。缓冲区开 N+1 字节,循环仍然只跑 N 轮,第 N+1 字节由循环外的ADC AL, 0单独填写。
4.4 屏幕输出乱码:BCD 字节直接当 ASCII 送出去
把MOV DL, [BX]之后直接INT 21H是另一个高频错误。DL 里装的是 BCD 字节 43H,功能 02H 会把它当 ASCII 码输出,屏幕上出现的是字母 C,不是数字 43。必须先把字节拆成两个半字节,每个加上 30H 才变成可显示的 '4' 和 '3'。
MOV AL, [SI] PUSH AX ; INT 21H 返回后 AL 会被改写,先保存 MOV CL, 4 SHR AL, CL ; 取高 4 位 ADD AL, 30H MOV DL, AL MOV AH, 02H INT 21H POP AX AND AL, 0FH ; 取低 4 位 ADD AL, 30H MOV DL, AL MOV AH, 02H INT 21H逻辑说明:PUSH AX 那一步不能省,DOS 的 02H 功能返回后 AL 通常变成 02H,不保存的话第二个半字节就会输出成乱码。参数说明:30H 是字符 '0' 的 ASCII 码,半字节 0 到 9 加了之后正好落在 '0' 到 '9' 区间;如果半字节本来是 A 到 F,说明前面的 DAA 没生效,得回到 4.1 去查。
4.5 每次运行结果都不一样:段寄存器没初始化
代码里写了ASSUME DS:DATA不等于运行时会自动填 DS。ASSUME 只是告诉汇编器按什么段去解析符号,真正的段值必须在入口处用MOV AX, DATA和MOV DS, AX两条指令装进去。少了这一步,LEA SI, NUM1算出来的是偏移量,而 DS 恰好指向 PSP,程序读到的就是 PSP 里的命令行长数据。
现象是每次运行的结果都不同,甚至同一次运行在 DEBUG 下单步和全速跑出来也不一样,因为 PSP 里的内容依赖启动参数。排查方法很简单,程序第一条指令下面直接看有没有MOV AX, DATA / MOV DS, AX这两行,ES 和 SS 如果也要用,同样处理。
5. 从 4 字节到任意位数:子程序化与实验报告成文
5.1 把加法循环封成 BCD_ADD 子程序
实验做顺之后,很自然会想把它复用。把循环体抽成子程序,用寄存器传参,位数就完全由调用方决定:
; 入口:SI = 加数1首地址,DI = 加数2首地址,BX = 结果首地址,CX = 字节数 ; 出口:结果写入 [BX] 起 CX+1 字节,最高字节为最终进位 BCD_ADD PROC CLC BCD_LOOP: MOV AL, [SI] ADC AL, [DI] DAA MOV [BX], AL INC SI INC DI INC BX LOOP BCD_LOOP MOV AL, 0 ADC AL, 0 MOV [BX], AL RET BCD_ADD ENDP逻辑说明:过程内部不再初始化 DS,段寄存器由调用方准备好,这样同一个子程序可以处理不同数据段里的加数。参数说明:CX 必须由调用方设为加数字节数,返回时 CX 已归零,如果不希望调用方丢失 CX,可以在过程入口 PUSH CX、出口 POP CX。要注意过程内的 CALL/RET 会压栈,但栈操作不改 CF,进位链在子程序范围内是完整的。
5.2 三层自检:手工验算、DEBUG 断点与边界数据
自检不要只跑一组数据。第一层是手工验算,把 12345678 + 87654322 = 100000000 写在纸上,逐字节列出中间结果和进位,和 DEBUG 里-r显示的 CF 序列一一对照。第二层是 DEBUG 断点,在DAA后面用-t单步,每轮记下 AL 和 CF,确认 AL 的每个半字节都不超过 9。第三层是边界数据,至少覆盖三组:两数全为 00、两数全为 99(必须产生最高进位)、两数位数不同(高位补 0 参与运算)。
边界数据里最容易出问题的是全 9 那一组。4 字节全 99 即十进制 99999999,两数相加是 199999998,结果需要 5 个字节,最高字节是 01,后面依次是 98 99 99 99。如果结果缓冲区只开了 4 字节,这一组必然崩,而前面几组简单数据完全看不出来。
5.3 实验报告成文:代码块、内存截图与 markdown 转 PDF 的排版要点
实验报告最终要交成 PDF,排版上有几个具体检查点。代码块一定要标语言,asm或nasm都行,不标语言的话导出时不会启用等宽字体,汇编的对齐会全乱。代码行宽度控制在 80 列以内,DAA这类指令加上注释很容易超宽,转 PDF 时右侧会被裁掉,评审看到的是半条指令。
内存截图建议用-d DS:0的输出,把加数、结果、进位字节一次性框进去,不要只截一行。截图和手工验算表并排放,左边是 BCD 原始字节,右边是十进制值,核对时不用来回换算。表格如果列数超过 5 列,转成纵向表更稳,PDF 阅读器里不会因为横向溢出而丢列。
用 VS Code 的 markdown 插件导出时,先确认代码块和表格在预览里没有横向滚动条,再导 PDF。导出后在 PDF 阅读器里翻到代码页,把字号放大到 150% 看一遍等宽对齐有没有散,这一步能挡掉大部分"本地看着好好的、导出来是乱的"问题。
本文还有配套的精品资源,点击获取