1. 实验10的核心价值与设计思路
王爽老师的《汇编语言》实验10之所以成为经典,关键在于它完整呈现了模块化编程思想在底层开发中的实践路径。这个实验不是简单地让我们写三个孤立的功能,而是通过"显示字符串→防溢出除法→数值转换"的递进关系,教会我们如何构建可复用的底层工具链。
我最初学习时犯过一个典型错误:把三个子程序当作独立任务完成,结果在数值显示环节发现前两个模块的接口设计不兼容。比如防溢出除法子程序返回的商和余数存放位置,与数值转换子程序需要的输入格式不匹配。这让我深刻理解了接口设计的重要性——好的子程序不仅要功能正确,还要为后续调用预留合理的参数传递方式。
实验中最精妙的设计在于寄存器传参约定:
- 显示字符串使用DH(行)、DL(列)、CL(颜色)传递显示属性
- 防溢出除法通过AX/DX/CX传递被除数和除数
- 数值转换用AX传递待转换数字,DS:SI指向输出缓冲区
这种一致性让三个子程序可以像乐高积木一样无缝衔接。当我们需要显示一个除法结果时,调用链是这样的:
除法运算 → 数值转换 → 屏幕输出2. 显示字符串子程序深度解析
2.1 显存映射原理
在80x25彩色文本模式下,屏幕内容对应B8000H-B8F9FH的物理内存。每个字符占用2字节:
+-----+-----+ | ASCII码 | 属性字节 | +-----+-----+属性字节的低4位控制前景色,高4位控制背景色。比如CL=02H表示绿色前景。
2.2 精确定位算法
计算指定行列的显存偏移地址是关键步骤:
mov ax, 00A0H ; 每行80字符×2字节=160字节(A0H) mul dh ; 行号×160 → 行起始偏移 mov dh, 0 add ax, dx ; 加上列偏移 add ax, dx ; 列号×2(每个字符占2字节)这个算法比单纯的(dh×160 + dl×2)更高效,因为避免了16位乘法。
2.3 完整代码实现
show_str: push ax push bx push cx push si push es mov ax, 0B800H mov es, ax mov al, 160 mul dh mov bx, ax ; 行偏移 mov al, 2 mul dl add bx, ax ; 行列综合偏移 mov ah, cl ; 保存颜色属性 show_loop: mov cl, [si] jcxz show_end ; 遇到0结束 mov al, cl mov es:[bx], ax ; 写入字符和属性 add bx, 2 inc si jmp show_loop show_end: pop es pop si pop cx pop bx pop ax ret3. 防溢出除法子程序精要
3.1 溢出问题本质
当被除数的高16位(H) ≥ 除数(N)时,直接使用DIV指令会导致商无法存放在AX中而触发除法错误。例如:
被除数:000F4240H (1,000,000) 除数:000AH (10) H=000FH > N=000AH → 溢出3.2 数学公式拆解
王爽给出的公式堪称经典:
X/N = int(H/N)×65536 + [rem(H/N)×65536 + L]/N实际计算分三步:
- 计算H/N得到商Q1和余数R1
- 计算(R1×65536 + L)/N得到商Q2和余数R2
- 最终商=Q1×65536 + Q2,余数=R2
3.3 代码实现技巧
divdw: push bx mov bx, ax ; 保存L到BX mov ax, dx ; AX = H xor dx, dx ; DX = 0 div cx ; H/N → AX=Q1, DX=R1 push ax ; 保存Q1 mov ax, bx ; AX = L div cx ; (R1*65536+L)/N → AX=Q2, DX=R2 mov cx, dx ; 余数存入CX pop dx ; 高16位商来自Q1 pop bx ret这个实现巧妙地用栈暂存中间结果,避免了使用额外内存变量。
4. 数值显示子程序实战
4.1 进制转换算法
将数字转为十进制字符串需要反复除10取余。例如转换12666:
12666 ÷ 10 = 1266...6 1266 ÷ 10 = 126...6 126 ÷ 10 = 12...6 12 ÷ 10 = 1...2 1 ÷ 10 = 0...1逆序读取余数得到"12666"。
4.2 栈的妙用
由于余数读取顺序与显示顺序相反,使用栈结构正合适:
dtoc: push bx push cx push dx mov bx, 10 xor cx, cx ; 计数器清零 push_loop: xor dx, dx ; 被除数高16位置零 div bx ; DX:AX ÷ 10 add dl, '0' ; 余数转ASCII push dx ; 入栈 inc cx test ax, ax ; 商是否为0 jnz push_loop pop_loop: pop dx mov [si], dl inc si loop pop_loop mov byte ptr [si], 0 ; 添加结束符 pop dx pop cx pop bx ret5. 子程序协同工作示例
现在我们组合三个子程序,实现"计算(12666+10000)/10并显示结果":
mov ax, 12666 mov bx, 10000 add ax, bx ; AX=22666 mov cx, 10 xor dx, dx ; 被除数高16位清零 call divdw ; 计算22666/10 → DX:AX=商, CX=余数 mov ax, dx ; 取商高16位(实际为0) call dtoc ; 转换商到字符串 mov ax, 2266 ; 商低16位 call dtoc ; 追加转换 mov dh, 12 ; 第12行 mov dl, 30 ; 第30列 mov cl, 0Ch ; 红底亮黄字 call show_str ; 显示"2266"这个例子展示了如何通过合理的寄存器传参约定,让多个底层子程序像管道(pipe)一样协同工作。在实际开发中,这种模块化思想可以大幅提升代码复用率。