Skip to content

第2讲:除法、分支与循环输出

本周核心:掌握 div 的寄存器约定,学会把 C 语言的 if/if-else/&&/|| 翻译成汇编,并能用循环输出矩形星号和金字塔。


一、除法指令 div

1.1 汇编语言中除法的三种用法

老师 1.asm 中的完整说明:

asm
comment @
汇编语言中的除法一共有3种用法:
(1) 16位/8位: ax/任意的8位寄存器或变量=al..ah
mov ax, 123h
mov bl, 10h
div bl ; ax / bl = al..ah, al=12h, ah=03h
(2) 32位/16位: (dxax)/任意的16位寄存器或变量=ax..dx
mov dx, 12h
mov ax, 3456h
mov bx, 1000h
div bx ; (dx、ax)/bx = 123456h/1000h = ax..dx,
       ; ax=0123h, dx=0456h
(3) 64位/32位: (edxeax)/任意的32位寄存器或变量=eax..edx
mov edx, 0
mov eax, 123456h
mov ebx, 1000h
div ebx ;  (edx、eax)/ebx=0000000000123456h/1000h
        ; eax=123h, edx=456h
;ebx:extended bx, 是一个32位的寄存器
@

总结寄存器约定:

形式被除数除数余数
16位 ÷ 8位AX8位寄存器/变量ALAH
32位 ÷ 16位DX:AX16位寄存器/变量AXDX
64位 ÷ 32位 (.386)EDX:EAX32位寄存器/变量EAXEDX

高频错误:循环除法前忘记清 DX/EDX,导致被除数高位残留垃圾值。

1.2 实战:计算一个数是几位数(8 位除数版本)

思路:反复除以 10,商不为 0 就继续,每除一次位数 +1。这是老师 1.asm 中的例子——用 8 位除法计算 123 的位数:

asm
; 计算123是几位数
code segment
assume cs:code
main:
   mov ax, 123
   mov bl, 10
   mov cx, 0
again:   
   div bl         ; ax / bl = 123 / 10, al=12, ah=3
   add cx, 1
   cmp al, 0
   je done
   mov ah, 0      ; 清空 ah(上次除法的余数),否则下次 div 会出错
   jmp again
done:   
code ends
end main

注意:8 位除法 div bl 的被除数是 AX,商存 AL、余数存 AH。循环中必须 mov ah, 0 清除上次余数,否则下一轮的被除数会包含垃圾值。

1.3 16 位除数版本(老师 2.asm)

asm
; 计算12345是几位数
code segment
assume cs:code
main:
   mov ax, 12345
   mov bx, 10
   mov cx, 0
again:   
   mov dx, 0      ; ★ 每次除法前必须清 dx
   div bx         ; (dx、ax) / bx = 12345 / 10, ax=1234, dx=5
   add cx, 1
   cmp ax, 0
   jne again
   ;je done
   ;jmp again
done:   
code ends
end main

1.4 32 位除数版本(老师 3.asm)

asm
; 计算12345是几位数
.386
code segment use16
assume cs:code
main:
   mov eax, 12345
   mov ebx, 10
   mov cx, 0
again:   
   mov edx, 0     ; ★ 每次除法前必须清 edx
   div ebx        ; (edx、eax) / ebx = 12345 / 10, eax=1234, edx=5
   add cx, 1
   cmp eax, 0
   jne again
   ;je done
   ;jmp again
done:   
code ends
end main

二、条件分支翻译

2.1 基本套路

cmp a, b       ← 比较
jxx label      ← 根据比较结果决定跳不跳

2.2 逻辑与(&&)的翻译(老师 4.asm + 4.c)

C 语言中,&& 的原始写法是嵌套 if:

c
#include <stdio.h>
main()
{
   int a=2, b=-3, c;
   if(a > 0)
   {
      if(b > 0)
         c = 1;
      else
         c = 0;
   }
   else
      c = 0;
   /*
   if(a > 0 && b > 0)
      c = 1;
   else
      c = 0;
    */
}

翻译成汇编时,「&&」意味着前一个条件不满足就直接判 false,不再检查后面(短路):

asm
code segment
assume cs:code
main:
   mov ax, 2       ; a = 2
   mov bx, -3      ; b = -3
   cmp ax, 0
   jg ax_is_positive    ; a > 0 ? 是→继续检查 b
   jmp set_cx_0         ; a ≤ 0 → 直接 c=0(短路!)
ax_is_positive:
   cmp bx, 0
   jg bx_is_positive    ; b > 0 ? 是→c=1
set_cx_0:   
   mov cx, 0
   jmp done
bx_is_positive:
   mov cx, 1
done:   
code ends
end main

流程理解:

  1. 先判断 a > 0,不满足则直接跳到 set_cx_0
  2. 满足才继续判断 b > 0
  3. 两个都满足才执行 bx_is_positive

2.3 逻辑或(||)的翻译(老师 5.asm + 5.c)

C 语言:

c
main()
{
   int a=2, b=-3, c;
   if(a > 0 || b > 0)
      c = 1;
   else
      c = -1;
}

「||」意味着前一个条件满足就直接判 true,不再检查后面

asm
code segment
assume cs:code
main:
   mov ax, 2       ; a = 2
   mov bx, -3      ; b = -3
   cmp ax, 0
   jg set_cx_1          ; a > 0 → 直接 c=1(短路!)
   cmp bx, 0
   jg set_cx_1          ; b > 0 → c=1
   mov cx, -1           ; 两个都不满足 → c = -1
   jmp done
set_cx_1:
   mov cx, 1   
done:   
code ends
end main

总结:&& 用「不满足就跳走」,|| 用「满足就跳走」。这也叫短路求值


三、字符输出与程序退出

汇编没有 printf,一切 I/O 通过 DOS 中断 int 21h 实现。

3.1 输出单个字符

asm
mov ah, 2       ; ah=2 用来指定子函数的编号
mov dl, '*'     ; dl = 要输出的字符(ASCII 码)
int 21h         ; 调用 0x21 号函数集

3.2 输出回车换行

asm
mov ah, 2
mov dl, 0Dh     ; 或 mov dl, 13 —— 回车符 '\r',光标回到行首
int 21h
mov ah, 2
mov dl, 0Ah     ; 或 mov dl, 10 —— 换行符 '\n',光标下移一行
int 21h

Windows/DOS 的换行 = 回车(0Dh) + 换行(0Ah),两者缺一不可。

3.3 程序正常退出

asm
mov ah, 4Ch
mov al, 0
int 21h         ; 相当于 C 语言中的函数调用 exit(0)

四、循环输出实战

4.1 理解 ZF 标志位与 jnz / je

CPU 内部有一个 FL 寄存器(标志寄存器),其中有一位叫 ZF(Zero Flag):

  • 当 add、sub、cmp 等指令的运算结果 = 0 时,ZF 会被置 1,表示「zero is true」
  • 当运算结果 ≠ 0 时,ZF 会被清零,表示「zero is not true」
  • jnz 是当 ZF=0 时才跳转(jump if not zero)
  • je 是当 ZF=1 时才跳转(jump if equal / zero)
asm
mov cx, 5
sub cx, 1      ; cx=4, ZF=0
jnz again      ; ZF=0 所以会发生跳转

4.2 输出 5×3 的星号矩形(老师 6.asm)

用双层循环:外层控制行(bx),内层控制每行星号数(cx),jnz 控制循环。

asm
;用循环输出5*3个星号
code segment
assume cs:code
main:
   mov bx, 3        ; bx=行数
next_row:   
   mov cx, 5        ; cx=每个星号的个数
next_star:   
   mov ah, 2        ; ah=2用来指定子函数的编号
   mov dl, '*'
   int 21h          ; 调用0x21号函数集
   sub cx, 1
   jnz next_star    ; jump if not zero
   ;cpu内部有一个FL寄存器,该寄存器的其中一位叫ZF
   ;当add、sub、cmp等指令使运算结果=0时,ZF会被置1,
   ;表示zero is true,反之ZF会被清零,表示zero is not true
   ;jnz是当ZF=0时才跳转
   ;je是当ZF=1时才跳转
   mov ah, 2
   mov dl, 0Dh      ; 或mov dl, 13; 其中0Dh是回车符,即'\r'
   int 21h
   mov ah, 2
   mov dl, 0Ah      ; 或mov dl, 10; 其中0Ah是换行符,即'\n'
   int 21h
   sub bx, 1
   jnz next_row
   mov ah, 4Ch
   mov al, 0
   int 21h          ; 相当于C语言中的函数调用exit(0)
code ends
end main

运行结果:

*****
*****
*****

4.3 输出三层金字塔(老师 7.asm)

  *
 ***
*****

规律分析(设行号为 bx,从 1 开始,总层数 = 3):

  • 第 bx 行的空格数 = 3 - bx
  • 第 bx 行的星号数 = bx × 2 - 1
asm
comment #
用循环输出一个3层金字塔:
  *
 ***
*****
#

code segment
assume cs:code
main:
   mov bx, 1        ; 行号
next_row:   
   mov cx, bx
   add cx, cx
   sub cx, 1        ; cx=星号的个数
   mov bp, 3        ; 3是金字塔的层数
   sub bp, bx       ; bp=空格的个数
output_1_space:
   cmp bp, 0
   je output_1_star
   mov ah, 2
   mov dl, ' '
   int 21h          ; 输出一个空格
   sub bp, 1
   jmp output_1_space
output_1_star:   
   mov ah, 2
   mov dl, '*'
   int 21h          ; 输出一个星号
   sub cx, 1
   jnz output_1_star ; jnz:jump if not zero
   mov ah, 2
   mov dl, 0Dh
   int 21h          ; 输出一个回车符'\r'
   mov ah, 2
   mov dl, 0Ah
   int 21h          ; 输出一个换行符'\n'
   add bx, 1
   cmp bx, 3
   jle next_row
   mov ah, 4Ch
   mov al, 0
   int 21h          ; 相当于C语言函数调用exit(0), 终止程序的运行
code ends
end main

五、汇编多行注释

asm
comment @
这是多行注释
可以写很多行
@

或:

asm
comment #
这也是多行注释
#

六、本周易错点

  1. div 前忘记清 DX/EDX,或者 8 位除法忘记 mov ah, 0
  2. && 和 || 翻译时短路逻辑写反(该跳时不跳)
  3. 字符输出忘记设置 AH=2
  4. 换行只写 0Ah 漏写 0Dh
  5. 循环条件取反写错(如把 jle 写成 jg