尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

8086汇编实现多位十进制数相加:BCD码、DAA与进位链

8086汇编实现多位十进制数相加:BCD码、DAA与进位链 简介这份 PDF 实验报告围绕微机原理中的两个多位十进制数相加实验展开面向正在学习汇编语言、微机原理课程或需要完成验证性实验的学生与自学者。报告从实验目的、内容、步骤、结果到程序设计与实现均有记录可帮助读者理解 ASCII 码存储、ADD/ADC 加法指令以及 DAA、AAA 调整指令的配合方式。资源包为单一 PDF 文档压缩后约 9KB轻量便于查阅和打印。其中收录 WJSY0.ASM、WJSY1.ASM、WJSY2.ASM 三个程序清单分别处理低位在前、高位在前以及显示加数、被加数和 471932805675249 格式的需求并涉及 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 修正后CF25 183DH低 4 位 D 943H012 3446H无46H09 811H低 4 位产生进位AF 117H0第三行值得盯一眼。11H 的低 4 位是 1肉眼看着完全正常但 9 加 8 在低 4 位内部产生了进位AF 已经被置 1DAA 照样要补 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 43HCF 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 字节高地址1234567878H56H34H12H8765432222H43H65H87H写成数据定义就是下面这样注释里标清楚每个字节代表的十进制片段改数值时不容易填反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 位 9AL ← AL 06HAF 1CF 1 或 AL 高 4 位 9AL ← AL 60HCF 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每个字节位都是 7822、5643 这样刚刚过百的组合循环体会连续四次把 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 改 8RESULT 开 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]把当前字节取进 ALADC AL, [DI]把对方字节和 CF 一起加进来DAA把结果修正成合法 BCD 并给出新的 CFMOV [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 看不到任何输出因为程序只写了内存没做显示这是正常的。要看结果得进 DEBUGDEBUG 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 参数怎么改位数、结果缓冲区与显示前导零变量当前值含义修改后的连带影响LEN4加数字节数改 8 即 16 位十进制循环自动跟着变RESULT5 DUP(0)结果缓冲区必须 ≥ LEN 1少一个字节就丢最高进位NUM1 / NUM2各 4 字节加数本身位数变了要同步补齐缺位补 0SI / 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 或偏 66DAA 漏用或位置不对现象根因定位方法每一位数字都比手算大 6ADD 后根本没写 DAA-u反汇编看 ADC 后面跟的是什么只有高位字节偏 6DAA 被挪到了 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 当移位计数用顺手就把循环计数清零或改成 4LOOP 指令读到 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 数相加结果最多 N1 字节因为两个 99…9 相加必然产生最高进位。缓冲区开 N1 字节循环仍然只跑 N 轮第 N1 字节由循环外的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] 起 CX1 字节最高字节为最终进位 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% 看一遍等宽对齐有没有散这一步能挡掉大部分本地看着好好的、导出来是乱的问题。本文还有配套的精品资源点击获取
返回列表