王爽汇编实验十:子程序、div溢出与数值显示全程解析

1. 实验目标与三个子程序任务拆解

汇编语言学到实验十,基本就进入了一个分水岭:前面的章节一直在教你怎么用指令、怎么访问内存、怎么写分支和循环,但程序稍微一长,代码就乱成一锅粥。这个实验的核心就一个字——结构化。用子程序把重复的逻辑封装起来,让主程序变得简洁、可读、可复用。

王爽教材的实验十,要求编写三个子程序:显示字符串、解决除法溢出问题、数值显示。三个任务难度是递进的,第一个偏“套路”,第二个偏“数学”,第三个偏“综合”。把它们吃透,你对call/ret这对跳转指令的理解,以及对栈的运用能力,会上一个台阶。

先说下我的环境:Windows 下用 masm 5.0 编译连接,debug 调试;如果你用 DOSBox 加 masm,或者用其他汇编环境,逻辑完全一样,只是编译命令略有差异。下文给的都是完整可运行的思路,目的不是让你抄代码,而是让你搞清楚每一步为什么这么写。

提示:本文覆盖三个子程序的实现细节、背后的数学原理、调用约定设计,以及我实际调试时踩过的坑。建议你手里备着教材,边看边对照。

2. 子程序的调用机制与参数传递约定

2.1 call 和 ret 的底层行为

很多初学者写子程序最大的障碍,是搞不懂call和ret到底做了什么。其实就两句话:

  • call先把返回地址(也就是 call 指令下一条指令的地址)压入栈中,然后跳转到子程序的标号处执行。
  • ret从栈中弹出这个地址,跳转回去。

因为栈是“后进先出”的,所以支持嵌套调用:A 调用 B,B 调用 C,返回时先回 B,再回 A。这个机制是所有高级语言函数调用的底层基础。

这里有个隐含问题:既然call和ret依赖于栈顶,那么子程序内部用栈时,必须保证返回前栈顶恢复到入栈时的位置。很多初学者在子程序里用push保存寄存器,却在某些分支路径上忘记pop,导致ret读到错误的返回地址,程序直接飞掉。这是实验十最常见的崩溃原因之一。

2.2 寄存器传递参数的约定

这个实验三个子程序都需要主程序传参数进去。8086 汇编没有高级语言的“形式参数”机制,约定俗成的做法是用寄存器传参。教材也是这么设计的:

  • show_str:用dh传行号(0~24),dl传列号(0~79),cl传颜色,ds:si指向字符串首地址。
  • divdw:用ax传被除数低 16 位,dx传高 16 位,cx传除数,返回时ax存商低 16 位,dx存商高 16 位,cx存余数。
  • dtoc:用ax传要转换的数值,ds:si指向存放字符的首地址,转换结果以 0 结尾。

这些约定不是硬性规定,但一定要在子程序开头用注释写清楚,否则过两周你自己都忘了哪个寄存器里存的什么。我见过很多同学调试时盯着寄存器窗口发呆,就是因为传参约定没记牢。

2.3 哪些寄存器需要保护

子程序内部通常会用到一些寄存器,而这些寄存器的值对主程序可能很重要。所以要做到“不影响调用者”:凡是在子程序里被修改、而调用者可能还在用的寄存器,都要在子程序开头push保存,返回前pop恢复。

这个实验里,我的原则是:除了专门用来返回结果的寄存器(以及显式传参的寄存器),其他的全保存。比如show_str里会用ax、bx、cx、dx、si,其中部分本来就是入口参数,但像cx、si这类如果确定不需要保留,可以不恢复;为了安全起见,我通常统一保存。代价是多几条指令,但调试时能少掉一大半的麻烦。

3. 任务一:显示字符串,避开“回车换行”的坑

3.1 显存映射与光标位置计算

show_str的功能:在屏幕指定位置用指定颜色显示一个以 0 结尾的字符串。这是三个任务中最简单的一个,但涉及一个关键知识点——彩色文本模式下显存地址的计算。

显存从B8000H开始,每个字符占用 2 字节:第一个字节是字符的 ASCII 码,第二个字节是颜色属性。80 列 x 25 行,所以第dh行、第dl列的字符偏移量是:

偏移量 = (dh * 80 + dl) * 2

我们要用es:di指向显存目标位置,把字符和颜色逐个写入。汇编里乘法不直观,所以一般用移位和加法:

  • dh * 80:因为 80 = 64 + 16,可以mov al, 0A0h,mul dh(注意mul的操作数不能直接是内存/寄存器吗?可以,mul dh表示ax = al * dh),但要小心mul的结果是 16 位的,放在ax里。
  • 再加上dl,再乘以 2,就是最终偏移量。

有个更稳的做法:用bx保存行偏移,si初始化为 0,在循环里用mov al, [si]读字符,mov es:[bx+si*2]写显存。这样不用每次循环都重新计算偏移量,效率高也不会算错。

3.2 字符串结束判断与循环退出条件

子程序没有显式传入字符串长度,所以必须以 0 结尾来检测结束。这也是教材设计成“以 0 结尾”的原因。

注意一点:判断 0 时一定要用jcxz或者cmp al, 0+je,不能在循环里把al为 0 时的字符也写进显存。很多同学写成先复制、后判断,结果 0 也被写到屏幕上。

正确的结构是先取字符、判断是否为 0、非 0 才写入,然后loop或jmp继续。如果用loop,要先把循环次数和地址都提前算好;如果不知道长度,用jmp $形式死循环加判断退出更自然。

3.3 颜色属性快速记忆

cl传颜色属性,例如02H是黑底绿字、07H是黑底白字、44H红底红字等。属性字节的低 4 位是前景色,高 4 位是背景色。数字可以心算:0 黑、1 蓝、2 绿、3 青、4 红、5 品红、6 棕、7 白。

我调试时习惯用07H(黑白),因为白色字在黑色终端上最清晰。等逻辑全通了,再换彩色的验证显示效果。

3.4 一个容易忽略的细节:es 的初始化

show_str里要把es指向0B800H。但注意:mov es, 0B800H是合法的,因为 0B800H 是立即数,而mov es, ax也可以。不要写成mov es, 0B8000H——显存段地址是B800H,偏移从 0 开始,物理地址才拼成B8000H。这个“段地址+偏移”的概念不牢,后面写显存类程序会经常出 bug。

我给出核心代码框架,标号注释写得很清楚,你可以对照自己的实现检查:

show_str: push ax push bx push cx push dx push si push di mov ax, 0B800H mov es, ax mov al, 160 ; 每行字节数 80*2 mul dh ; 16位乘法,ax = 行号 * 160 mov bx, ax ; bx 存行偏移 mov al, 2 mul dl ; ax = 列号 * 2 add bx, ax ; bx = 最终偏移 mov di, 0 ; 字符串索引(源地址从 ds:si 取) mov si, 0 ; 或者直接用 si 做源 show_str_loop: mov al, ds:[si] ; 取字符 cmp al, 0 je show_str_ok mov es:[bx+di*2], al ; 写字符 mov es:[bx+di*2+1], cl ; 写颜色 inc si inc di jmp show_str_loop show_str_ok: pop di pop si pop dx pop cx pop bx pop ax ret

注意es:[bx+di*2+1]这种寻址方式在 8086 里是不允许的!8086 的基址变址寻址不支持“基址+变址*比例+常量”这种形式,只有 386 以上才支持比例因子。所以上面的框架代码只是示意——真实写法要么直接用es:[di]写字符再加2,要么用两个寄存器分别维护。实在要用 386 扩展寻址,就得换addr32,但实验环境一般还是 8086,别在课堂上这么写。

具体的正确的循环体写法,我在下面给出。

3.5 正确写法:用两个指针分别走源和目标

我的建议是分开维护源索引si和目标偏移di,每次循环源索引加 1、目标偏移加 2。这样既简单又能避开 8086 寻址限制:

show_str_loop: mov al, ds:[si] cmp al, 0 je show_str_ok mov es:[bx+di], al ; 写入字符 mov es:[bx+di+1], cl ; 写入颜色 inc si add di, 2 jmp show_str_loop

这里es:[bx+di]是合法的基址加变址寻址。加 1 写属性不要写成es:[bx+di+1]之外的形式,因为 8086 支持 8 位位移量,+1没问题,但不支持*2。

调试这个函数时,我建议先用dh=12, dl=30这样的中间位置测试。如果字符串只显示了一半,大概率是循环退出条件写错;如果连字符都没有,先查es是不是B800H。

4. 任务二:解决除法溢出,核心是数学推导

4.1 div 溢出发生的条件

8086 的div指令(无符号除法)格式是:被除数默认在dx:ax(32 位),除数是操作数,商放在ax,余数放在dx。如果商太大,超过ax的 16 位范围(大于 65535),就会触发除法溢出中断,程序直接终止。

教材给的数据是:1000000 / 10。1000000十进制 =0F4240H,所以被除数高 16 位dx = 000FH,低 16 位ax = 4240H。如果直接div cx(cx = 10),商是100000,超过 16 位最大 65535,必溢出。

这个实验的意义就在于此:div指令本身处理不了“商超过 16 位”的情况,但我们可以把除法拆成多次 16 位除法,每步的商都控制在 16 位以内。

4.2 公式拆解:高位数除法 + 低位修正

教材给出的推导思路(大家一般称为“溢出除法公式”)是这样的:

设被除数为X = X_H * 65536 + X_L,除数为N(16 位)。我们想求X / N。

第一步,用高 16 位去除:

X_H / N = Q_H 余 R_H

这里Q_H是 16 位的商高位,R_H是余数(必然小于 N,16 位能装下)。

第二步,把余数并入低位:

原商的低位部分应该是:

(X_L + R_H * 65536) / N

注意现在被除数是(R_H 放在 dx,X_L 放在 ax)组成的 32 位数,正好可以直接用一次div。商就是最终结果的低 16 位,余数就是整个除法的最终余数。

所以完整结果:

  • 商 =Q_H * 65536 + Q_L
  • 余数 =R

4.3 用实例算一遍:1000000 / 10

用具体数据走一遍:

  • 被除数高 16 位dx = 000FH,低 16 位ax = 4240H。
  • mov cx, 10,先mov ax, dx把高位移到ax,xor dx, dx,然后div cx。这条除法是0000F4240H?不对——要先把ax = dx(高 16 位),dx = 0,所以第一步除的是0 000FH / 10,商ax = 0001H,余数dx = 0005H。
  • 此时ax = 1(这是高 16 位商,最终结果的商高位),dx = 5。
  • 把dx(5)和原来的低 16 位4240H组成 32 位被除数:dx = 0005H,ax = 4240H。再div cx。
  • 第二步除法:0005 4240H = 344064,除以 10,商ax = 8613H(十进制 34406),余数dx = 0004H。
  • 最终商 = 高 16 位0001H放dx,低 16 位8613H放ax,即0001 8613H = 100000,余数 4。验证:1000000 / 10 = 100000 余 0?等等,我算错了,1000000 / 10 整除,余数应该是 0。

问题出在哪?第二步的 342? 重新算:x = 1000000,N = 10。第一步X_H = dx = 0F H = 15,15 / 10商 1 余 5。这一步ax = 1, dx = 5。第二步把被除数变成dx = 5, ax = 4240H,也就是5 * 65536 + 16960 = 327680 + 16960 = 344640。344640 / 10 = 34464,余数 0。34464 十六进制是86A0H。那为什么我刚才算出8613H?因为0x4240 = 16960,不是 16960 吗?对,344640 / 10 = 34464,34464 的十六进制是86A0H,不是8613H。这里我不能再犯计算错误,所以代码注释里最好直接用div指令,别手工算错。

但这里有一个关键点:dx:ax第二步的除法中,dx = 0005H, ax = 4240H,商一定小于 65536 吗?不一定!极端情况下可能接近 65535 以上。教材的场景里,因为第一次除法的余数R_H < N,所以R_H * 65536 + X_L最大也就是(N-1) * 65536 + 65535,除以 N 最大 = 65535 + 65535/N,可能略大于 65535 导致溢出。所以第二步也可能溢出吗?

测试一下:N = 65535,X_H = 65534,X_L = 65535。第一步65534 / 65535 = 0 余 65534,第二步被除数 = 65534 * 65536 + 65535 = 4294901761? 这除以 65535 接近 65536 可能溢出。所以公式并非绝对安全?教材的 divdw 例子里给的公式其实要求第二步被除数也满足商 < 65536?让我细想。

其实书上的公式是这样的:divdw用三次div而不是两次?回忆王爽《汇编语言》实验十的 divdw 方案,是:

x / n, x 为 32 位,n 为 16 位 结果:商 32 位,余数 16 位 公式: int(H/N) * 65536 + [ (rem(H/N) * 65536 + L) / N ]

其中int为整数商,rem为余数,[ ... ]里的除法还是一步 32 位除以 16 位,商低 16 位可能出现溢出吗? 书中给的思路是:由于rem(H/N) < N,所以rem(H/N)*65536 + L的最大值是(N-1)*65536 + 65535,当 N 足够大时,除以 N 可能超过 65535。但书中此处的隐含条件不是 N 任意大,而是我们希望最终商高 16 位存 dx,且此算法只是“利用两次除法”,如果第二步溢出说明原始商本来超过 32 位?也不对,16 位除数除 32 位数,商最多 32 位,不会超过 32 位。

再算一下极限:N = 65535,rem(H/N) <= 65534,则分子的上限约 4294901760,除以 65535 = 65536.? 计算一下:65535 * 65536 = 4294901760,加 65535 = 4294967295? 实际上最大值 = (N-1)65536+65535 = N65536 -1 = 4294967295,除以 N = 65536 - 1/N = 65535.999。这小于 65536,所以商低 16 位最大 65535,不会溢出!我搞错了,(N-1)*65536 + 65535 = 65536*N - 1,除以 N 略小于 65536,商为 65535(余 N-1),不会溢出。所以第二步的商严格小于 65536,安全。之前忘了这个代数恒等式,这里算清楚很关键。

也就是说,这个公式是严谨的:因为rem * 65536 + low最大是N*65536-1,所以除以 N 的商最大是 65535,余数最大 65534(或 N-1)?反正商 < 65536,绝不会溢出。这样两次div就是安全的。把这个数学关系写进博客里,理解记忆会深刻很多。

4.4 divdw 子程序的标准写法

根据上面的思路,代码可以这样组织:

divdw: push bx push di ; 保存会用到的寄存器 mov bx, ax ; bx 暂存低 16 位 mov ax, dx ; 高 16 位移到 ax mov dx, 0 ; 第一步被除数高位置 0 div cx ; ax = int(H/N), dx = rem(H/N) push ax ; 保存商高位 QH mov ax, bx ; 恢复低 16 位 ; 注意 dx 已经是第一步的余数,正好作为第二步被除数的高 16 位 div cx ; 32 位被除数 dx:ax 除以 cx mov cx, dx ; 余数放入 cx pop dx ; 商高位放入 dx,剩下 ax 是商低位 pop di pop bx ret

这里几个细节要说清楚:

  • 第一步div cx之前,必须保证被除数dx:ax是H本身,也就是dx = 0, ax = X_H。因为X_H本身是 16 位,被除数就是高 16 位那个数自己。
  • 第二步的div指令会自动使用dx:ax作为被除数。如果第一步结束后没有修改dx,那么dx正好就是第一步的余数,拿mov ax, bx恢复低 16 位后,dx:ax正好是rem*65536 + low,直接div cx就行。
  • 返回约定是dx存商高位、ax存商低位、cx存余数,这也是教材指定的。
  • 我代码里用push ax暂存商高位,是因为第二步除法必须用ax恢复低 16 位;等第二步完事,再从栈里把商高弹回dx。这个“栈里转一圈”的技巧非常典型,理解栈的用途就看这里。

4.5 验证与自查

写完divdw后,建议跑三个用例:

  • 1000000 / 10,期望商100000,余数0。
  • 1000000 / 100,期望商10000,余数0。
  • 0FFFFH / 1,期望商65535,余数0。这个用例能验证第一步商高位为0、第二步商低位直接等于原始低 16 位的情形成立。

如果第三个用例显示结果不对,多半是第二步之前dx被污染了。这种边界用例比正常数据更能暴露问题。

5. 任务三:数值显示,综合考验栈与循环

5.1 转换思路:除 10 取余,从低位到高位

dtoc的功能是把一个 16 位无符号数转换成十进制字符串。十进制转换的标准思路是“除 10 取余”:每次用 10 去除当前数,得到的余数加上30H就是对应数字字符的 ASCII 码,商继续循环,直到商为 0。

问题是:第一次取到的余数是最低位,比如1234第一次余数是4,我们想存成'1','2','3','4','0'这样的顺序。如果在内存里从前往后写,顺序就反了。所以教材的提示是:把每次得到的余数依次压栈,全部转换完后再依次弹出并写入目的字符串。栈的后进先出特性天然解决了这个问题。

这也是实验设计的高明之处——不是让你写一个复杂的反转逻辑,而是借助栈这个现成的“倒序缓冲”。

5.2 循环嵌套与寄存器分配的冲突

dtoc里有两个循环:外层循环处理每一位(每次除 10),内层循环把栈里的字符弹出并写入ds:si指向的缓冲区。两个循环都用cx,必须小心冲突。

我的建议是:外层循环不用loop,用jmp加判断;内层循环记录栈里压了多少个字符,用一个计数变量(比如bx或bp)来管理,然后用loop或者继续jmp。

具体流程:

dtoc: push ax push bx push cx push dx push si push di mov bx, 0 ; bx 记录压栈字符个数 mov di, 0 ; di 记录写入目标字符串的偏移 dtoc_loop: mov dx, 0 ; 被除数高 16 位清零 mov cx, 10 div cx ; ax = 商, dx = 余数 add dx, 30H ; 转 ASCII push dx ; 压栈保存 inc bx cmp ax, 0 jne dtoc_loop ; 商不为 0 继续 dtoc_pop: pop dx mov ds:[si+di], dl ; 弹出字符写入目标 inc di dec bx jnz dtoc_pop mov byte ptr ds:[si+di], 0 ; 补结尾 0 pop di pop si pop dx pop cx pop bx pop ax ret

有几个易错点:

  1. div cx前dx必须清零,否则被除数会带着残留的高位,结果完全不对。
  2. add dx, 30H是用dl转 ASCII,但push dx会把整个dx压进去,不干净。更稳的是mov dl, xxx后push dx,或者先push dx再处理,确保多余高 8 位不影响后续。我建议压栈前把dh清一下:mov dh, 0。
  3. 弹栈写字符时,ds:[si+di]这种寻址也是合法的,si+di是基址+变址。mov ds:[si+di], dl没问题,但注意这条指令不能写成mov ds:[si+di], dh之外的形式而忘记只传dl。
  4. 最后一定要在字符串末尾写上0,否则show_str会读到垃圾内存,显示出一堆乱码。

5.3 主程序串联三个子程序

三个子程序平时可以单独测,最终要合在同一个程序里跑。建议主程序流程:

main: ; 显示字符串验证 mov dh, 12 mov dl, 30 mov cl, 07H mov si, offset msg_hello call show_str ; 除法溢出验证 mov ax, 4240H mov dx, 000FH mov cx, 10 call divdw ; 此时 dx = 商高位, ax = 商低位, cx = 余数 ; 可以再调用 dtoc 把结果显示出来(注意要组装成 32 位,或先用 16 位版本) ; 数值显示验证 mov ax, 12666 mov si, offset buf_num call dtoc mov dh, 13 mov dl, 30 mov cl, 07H mov si, offset buf_num ; 显示转换结果 call show_str mov ax, 4C00H int 21H

注意:show_str要求ds:si指向字符串,而dtoc也是借助ds:si写结果,两者可以串联。建议测试时先只测前两个,确认没问题后再把结果显示出来,否则容易出现“子程序本身对了但显示函数有问题”的排查混乱。

5.4 显示除法结果的 32 位数难点

教材要求的 divdw 返回 32 位商,但 dtoc 接收的是 16 位ax。如果想完整显示商,就需要额外处理:把dx作为高 16 位、ax作为低 16 位,再用“除 10 取余”的 32 位除法循环输出。这在后续扩展里是个很好的练习,但实验本身一般只要求数值小于 65535 时的显示,所以我建议分两步:先用验证数据确认 divdw 结果存在寄存器里是对的,再单独用 16 位数测 dtoc 显示。等两个都稳了,再去打通 32 位显示。

我后来自己扩展过一个版本,每次除 10 时直接调用 divdw(x=当前值,N=10),余数就是一位,比单独处理 32 位除法简单得多。有兴趣的可以试试:循环里始终维护 32 位被除数,每次调用 divdw,把余数cx压栈,商继续当被除数。这样 divdw 就成了一个通用的“32 位除以 10”工具,代码也很顺。

6. 常见问题与调试技巧实录

6.1 编译连接与运行环境的坑

写汇编最典型的报错是error A2006: undefined symbol,我遇到过最多的原因是:

  • 标号名打错(show_str和showstr是两个不同标号)。
  • 大小写不一致,masm 默认不区分大小写,但某些版本配置不同。
  • 数据段里定义的变量名和子程序标号重复。

连接时如果报LINK : fatal error L1101,通常是end后还留了多余代码,或者没有指定入口。检查一下程序的end标号是否正确。

运行阶段如果程序一跑就死机或无响应,没有别的招,就是用 debug 逐行单步。重点看call进子程序前寄存器值是否符合预期,ret回来后ip是否正常。

6.2 寄存器污染导致的“灵异现象”

我调试divdw时遇到一次:单独测试子程序没问题,但放到完整程序里结果就错。后来才发现是show_str里我用cx传颜色,而divdw返回余数也放在cx,主程序调用顺序没理清,导致颜色被改掉。

解决办法是明确约定:每个子程序入口前,主程序负责把所有入口参数按约定填好;调用返回后,如果要继续使用之前的寄存器,先保存。子程序内部随意用寄存器没关系,但返回时必须保证除了出口参数外,其他可能被调用者使用的寄存器恢复原样。

这看起来像是“各管各的”,但真正写汇编的人都知道,约定就是纪律。没有纪律,程序越小越容易乱。

6.3 显存里出现乱码的排查方法

显示乱码通常有三个来源:

  1. 字符串没以0结尾,越界读到显存或者别的数据。
  2. 颜色属性写成字节还是字的问题,导致显存错位。
  3. 写入位置算错,把字符串写到屏幕边缘,看起来像没显示。

排查时可以先用debug查看显存对应地址的内容,比如目标在0B800H + 偏移,用d命令看有没有期望的 ASCII 码。如果能看见字符数据,但屏幕没显示,多半是颜色属性为00H——黑底黑字,肉眼完全看不出来。我踩过这个坑,当时还以为是显存地址算错了,查了半小时。

6.4 快速验证:写一个小型的“单元测试”

汇编没有现成的单测框架,但你可以自己写一个简单的“断言语段”:每次子程序返回后,比较关键寄存器值,如果不对就直接退出并显示错误标记。比如:

cmp cx, 0 jne test_failed ; 继续其他断言

当然,实验阶段更多是用 debug 手动看寄存器。我的建议是:每完成一个子程序,立刻用边界数据测试,不要等到三个全写完再统一调试。三个 bug 同时出现时,你根本分不清是谁的问题。

6.5 几个让我印象深刻的小坑

  • mul和div都是隐式使用ax/dx的,写之前一定要确认这两个寄存器里没有你还需要的值。
  • push的次数必须和pop的次数严格相等。我见过有人子程序里条件分支只pop了一半,返回地址被当数据弹出,程序跳到一个垃圾地址。
  • offset在数据段内取标号偏移没问题,但如果你把字符串定义在代码段里,ds不一定指向它。字符串数据一定要放在data segment,并正确初始化ds。

7. 调试实录:一个典型的 bug 修复全过程

分享一个我最近带别人做实验十时遇到的实际问题。他的divdw在输入4240H(低)和0000H(高)除以0AH时正常,但换到1000000 / 10就崩溃。我单步跟进去看,发现他第一步除法前忘了把dx清零,而是直接用了ax里的值当高 16 位,导致真正计算的是0F4240H / 10而不是0FH / 10。这就是“除法溢出”没被解决但以为自己解决了。

修好后,他第二步又出了问题:他把第一步的商和余数顺序搞反了,写的代码是先把商存起来,再拿余数去当低 16 位的一部分——逻辑完全反了。后来我让他把每一步用笔在纸上算一遍,把寄存器值写出来,再对着代码查,才找到错在哪。

这种错误在汇编实验里极其常见。原因就是div指令的隐式操作数太多,不像高级语言一个变量名写清楚就完了。所以我建议你调试时,在关键位置插入几条mov指令把寄存器值搬运到显存或者直接暂停观察,不要只盯着 debug 寄存器窗口。

如果你是新手,我强烈建议把这一步当成一个“用纸笔模拟 CPU 执行”的练习。拿实验十的数据,手动算一遍每条指令执行后寄存器(ax、dx、cx、sp、ip)和栈顶的值,写满一张纸。等你推完一遍,call/ret和div就再也不会成为障碍了。

8. 三个子程序的综合对比与进一步扩展

对这三个子程序做一个横向对比,可以帮助你理解它们各自的难点和彼此之间的依赖关系:

子程序核心知识点主要难点经典坑
show_str显存映射、字符与颜色属性地址计算忘记写颜色属性导致黑底黑字
divdwdiv 溢出机制、高地位拆分数学公式推导忘记清零 dx 导致溢出未解决
dtoc除 10 取余、栈后进先出倒序问题忘记补结尾 0 导致乱码

实验十的巧妙之处在于三个子程序环环相扣:dtoc转换出的字符串可以用show_str显示,divdw的 32 位商又可能需要dtoc来转成十进制字符串。实际工作中,“格式化输出”和“大数除法”就是这么组合使用的。

扩展方向上,你可以尝试:

  • 把show_str改为支持透明背景、闪烁效果,甚至做一个简单的文本滚动。
  • 把divdw推广为 32 位除以 32 位的通用除法,思路是把 64 位被除数拆成更多段多次div。
  • 把dtoc改成支持有符号数、16 进制或二进制的转换,输出前自己控制样式。

这些扩展都不难,但每一个都能把你对 8086 的寄存器、内存和栈的理解再压实一层。很多人问学 8086 汇编有没有用,我的答案是:它虽然指令集老,但“子程序调用+栈管理+内存布局+异常处理”这套思想,是理解现代处理器和操作系统的基础。你能把实验十完整做对,再看高级语言的函数调用栈、递归和系统调用,都会有“原来如此”的通透感。

最后给一个实用建议:不要在代码里把三个子程序全都写完再调试。先写show_str,用固定字符串验证;再写divdw,用两个边界用例验证;最后写dtoc,把它和前面两个串联起来。每次只改一个变量,出了问题立刻能定位。这个“最小可验证单元”的习惯,做实验和写真实项目都适用。

相关新闻

YOLO人数统计与追踪实战:从检测到越线计数的避坑指南

YOLO人数统计与追踪实战:从检测到越线计数的避坑指南

简介&#xff1a;这份资源面向深度学习入门者、计算机视觉方向的学生以及需要完成毕业设计或课程设计的开发者&#xff0c;提供了一套基于YOLO的实时人数统计与追踪完整方案&#xff0c;可用于智能视频监控、人流分析等场景。压缩包共5个文件&#xff0c;约32.8MB&#xff0c;包…

2026/10/20 23:57:05 阅读更多 →
双座舰载机技术拆解:从气动布局到模型仿真的完整指南

双座舰载机技术拆解:从气动布局到模型仿真的完整指南

1. 双座舰载机项目拆解&#xff1a;从概念到模型的完整技术路径前阵子军工圈和模型圈都在传一个消息&#xff0c;说某款双座隐身舰载机成了焦点机型&#xff0c;有人把它称作“舰载杀手”&#xff0c;也有人更关注它背后体现的航空技术跨度。我没法去评论它的作战定位&#xff…

2026/10/20 23:57:05 阅读更多 →
拉曼光谱检测器选型核心逻辑:CCD与InGaAs的权衡及避坑指南

拉曼光谱检测器选型核心逻辑:CCD与InGaAs的权衡及避坑指南

做拉曼光谱的人&#xff0c;十有八九都绕不开同一个烦恼&#xff1a;激光器选了又选&#xff0c;光路调了又调&#xff0c;结果谱图还是一团噪声&#xff0c;换了激发波长也没救回来&#xff0c;最后才意识到问题出在检测器上。拉曼光谱本身是一种“信号极弱”的光谱技术&#…

2026/10/20 23:57:05 阅读更多 →

最新新闻

51单片机贪吃蛇实战:12864驱动、EEPROM掉电保存与嵌入式分层设计

51单片机贪吃蛇实战:12864驱动、EEPROM掉电保存与嵌入式分层设计

简介&#xff1a;本资源是一套面向大学本科课程设计的51单片机贪吃蛇游戏完整开发方案&#xff0c;适用于单片机原理与应用、嵌入式系统实践等课程学习与项目实训。资源包含Proteus仿真工程&#xff08;.pdsprj&#xff09;、Keil C51源码&#xff08;.c/.a51&#xff09;、编译…

2026/10/21 3:32:25 阅读更多 →
图书推荐系统实战:Python协同过滤从原理到代码避坑指南

图书推荐系统实战:Python协同过滤从原理到代码避坑指南

我记得第一次带学生跑这个图书推荐系统项目时&#xff0c;好几个人卡在了同一个地方&#xff1a;代码能跑通&#xff0c;但推荐结果全是乱的。后来查了半天&#xff0c;问题出在一个被注释掉的函数上。这个项目在中文互联网上流传了很多版本&#xff0c;绝大多数人拿到的都是简…

2026/10/21 3:32:25 阅读更多 →
空间注意力模块实战指南:原理、代码与调参经验

空间注意力模块实战指南:原理、代码与调参经验

做视觉任务这些年&#xff0c;大家都会遇到一个很实际的需求&#xff1a;网络深了、宽了&#xff0c;但该学的东西没学会。SpatialAttention空间注意力模块&#xff0c;是我调过的结构里性价比最高的一个——代码就几十行、参数量几乎可以忽略&#xff0c;却能明显改变网络的关…

2026/10/21 3:32:25 阅读更多 →
awesome-shizuku vs Dhizuku:两大Android权限提升工具终极对比指南

awesome-shizuku vs Dhizuku:两大Android权限提升工具终极对比指南

awesome-shizuku vs Dhizuku&#xff1a;两大Android权限提升工具终极对比指南 在Android开发的世界中&#xff0c;awesome-shizuku和Dhizuku是两个备受关注的高权限工具&#xff0c;它们都致力于让普通应用获得系统级权限&#xff0c;从而突破Android系统的限制。无论你是普通…

2026/10/21 3:32:25 阅读更多 →
CANN内测实战:从环境初始化到模型推理的完整指南

CANN内测实战:从环境初始化到模型推理的完整指南

前阵子看到某社区的“内测招募&#xff0c;免费体验CANN开发环境”活动&#xff0c;我第一时间就报名了。说实话&#xff0c;当时打动我的不只是“免费”两个字&#xff0c;而是想看看这个面向昇腾AI处理器的异构计算架构&#xff0c;在过去一年里到底进化到了什么程度。CANN这…

2026/10/21 3:32:25 阅读更多 →
Redis单线程还是多线程?从4.0到7.2的线程模型演进真相

Redis单线程还是多线程?从4.0到7.2的线程模型演进真相

1. 这个问题为什么值得花十分钟认真搞懂“Redis 到底是单线程还是多线程&#xff1f;”——这句提问在技术群、面试现场、甚至线上课程评论区里&#xff0c;几乎每周都会高频出现。它表面看是个“是非题”&#xff0c;但背后牵扯的是你对 Redis 性能本质、架构演进逻辑、以及高…

2026/10/21 3:31:24 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →