Skip to content

Lecture 4. 汇编语言 Part3:算术、逻辑、位、转移指令 ​

汇编指令可以分很多种类。有二进制算术指令、十进制算术指令、逻辑指令、数据传输指令、栈指令、控制传输指令、字符串指令、指针指令、IO 指令、前缀指令、系统指令、浮点指令…

注意:在这份笔记表格的「示例」列中,为了简洁,有些操作数用 A/B/X 等来表示,也有的用含义来表示。但是这并不准确,应当说明是寄存器、内存还是立即数等。更精确的表示还要带上位数等信息。

可以参考 斯坦福大学的手册,内含 80836 指令集详情。然而这门课学的主要是 8086,它们之间存在一定差异。(80836 是 1985 年的,8086 是 1978 的,80836 更强大)

二进制算术指令 ​

指令示例含义备注
addadd A, B整数加法
adcadc A, B带进位的整数加法,即 add A, B+CF
subsub A, B减法
cmpcmp A, B计算 A-B 的值,结果反映给标志寄存器不修改 A
incinc A加一不影响 CF
decdec A减一不影响 CF
divdiv X无符号除法有坑,见下文
idividiv X有符号除法有坑,见下文
mulmul X无符号乘法有坑,见下文
imulimul X有符号乘法有坑,见下文

DIV 与 IDIV 的坑 ​

这两条指令的操作数只有除数,而被除数是隐含的,由除数的类型决定。

特别注意,当除数是字(16)字节的时候,被除数是 32 位的 DX:AX。也就是说,如果 DX 没有赋零,使用字作为除数,可能会导致意想不到的错误。因此,进行字的除法前,一定要把 DX 清零。

规模Dividend 被除数Divisor 除数Quotient 商Remainder 余数
byteAXr/m8ALAH
wordDX:AXr/m16AXDX
dwordEDX:EAXr/m32EAXEDX

MUL 与 IMUL 的坑 ​

像刚刚一样,被乘数是隐含的,由乘数的类型决定。

特别注意,两个 16 位相乘,产生运算结果是 32 位的。低 16 位保存在 AX 里面,高 16 位溢出到 DX 里面。因此,执行字的乘法,可能会污染 DX 寄存器!

规模被乘数乘数积
byteALr/m8AX
wordAXr/m16DX:AX
dwordEAXr/m32EDX:EAX

IMUL 指令比较特殊,它可以拥有两个操作数或者三个操作数。不过这比较复杂,就不研究了。

十进制算术指令 ​

课件上说这些东西了解即可

分析这些指令,一定要记得:AL AH 是 8 字节的,而最小分析单位(nibble)是四字节的。不要把 AL AH 跟 nibble 弄混。

指令时钟周期示例含义备注
AAA4AAA加法后调整
AAD19AAD除法前调整
AAM17AAM乘法后调整
AAS4AAS减法后调整
DAA4DAA加法后调整
DAS4DAS减法后调整

分析:AAA(ASCII Adjust for Addition) ​

众所周知,字符表示的数字,ASCII 值比数字大 0x30。

例一 ​

如,'5' 的 ASCII 是 53,'9' 的 ASCII 是 57。设 AL 是 53(0011 0101),BL 是 57(0011 1001),执行:

asm
add al, bl

此时 AL 就会变成 110,即 0110 1110。两个 0x30 合在一起,组成了高四位的 0110。若只看低四位,1110,等于 14,恰好就是 5+9 的值。

再执行:

asm
aaa

此时的 AL 就会变成 0000 0100(清零高四位;若低四位的值在 10~15 之间或 AF 已经为 1,就把低四位加上 6,AF 与 CF 位设 1,AH 自增)。按照 BCD 的解码规则来分析现在的 AL,就是 0 4。由于进位标志被设置了,所以高位其实是 1,即表示 14。

加上 6 是因为,十六进制逢十六进一,而十进制是逢十进一,而四位二进制相当于十六进制,因此值少了 6。所以加上 6 结果就对了。避开 A~F 共计 6 个无效数。

例二 ​

asm
mov al, '5'
add al, '7'  ; 此时 al 是 0110 1100 = 110,不能直接当作 BCD,需要调整
aaa          ; 清零高四位,低四位在 A~F 之间
             ; 于是低四位加上 6,AL 变成 0010
             ; 并 AH+=1,AF=CF=1,AL = 0000 0010,BCD 解析为 0 2
             ; 算上进位,即表示 12 = 5+7

分析:AAS(Ascii Adjust for Subtraction) ​

与 aaa 几乎一样。区别是:

  • aaa 低四位 A~F 之间则加上 6,aas 是减去 6
  • aaa 低四位 A~F 之间则让 ah 加上 1,aas 让 ah 减去 1。

分析:AAD(Ascii Adjust for Division) ​

这是六条指令当中,唯一一个要在运算指令 之前 执行的指令。

用于除法运算之前,把未打包的 BCD 码调整为打包的 BCD。

asm
mov ax, 0703h  ; 未压缩的 BCD,ah = 07h, al = 03h,表示十进制 73
mov bl, 02h    ; 除数
aad            ; BCD 调整为二进制,al = 70+3 = 73 = 49h, ah = 0
div bl         ; 由于操作单位是字节,al 保存商(al=24h),ah 保存余数(ah=1)

由于 al 和 ah 都是 8421 码,最大是 9,所以调整后,最大是 99,在 127 范围内,不会爆。

分析:AAM(Ascii Adjust for Multiplication) ​

两个未打包的 BCD 相乘( 范围内),结果必定在 范围内。使用 AAM 调整后,AH=AL/10,存储十进制的十位数,AL=AL%10,存储个位数。

AAM 只能在两个未打包的 BCD 相乘后执行。

asm
mov al, 07h  ; AL = 07h
mov bl, 09h  ; BL = 09h
mul bl       ; AX = 003fh = 63 = 7*9
aam          ; AH = 6,AL = 3

应用:输出一百以内的整数 ​

刚才提到过,字符表示的数字,ASCII 值比数字大 0x30。因此:

  • 想要把 半个字节 的未压缩 BCD 转换成一个对应的字符,只需要 或上 0x30
  • 想要把 一个字节 的压缩 BCD 转换成两个对应的字符,只需要 或上 0x3030。
asm
mov al, 5
mov bh, 9
mul bh        ; AX = 45
aam           ; AH = 4, AL = 5

or ax, 3030h  ; 转化成字符

mov dl, ah    ; dl 是 21h 的参数,先输出高位
mov dh, al    ; 由于 al 的值会随时改变,先备份到 dh 里面
mov ah, 02h   ; 设置 21h 中断到输出 dl 的模式
int 21h       ; 输出高位:4
mov dl, dh    ; 把 dl 的值赋成刚刚备份的低位
int 21h       ; 输出低位:5

逻辑指令 ​

指令示例含义备注
ANDAND A, B按位与可以用来掩码(mask)
OROR A, B按位或常用于 BCD 转 ASCII
XORXOR A, B异或
BTBT Base, Offset指定地址的值赋给 CF
BTCBTC Base, Offset指定地址位的值赋给 CF,该位取反
BTRBTR Base, Offset指定地址位的值赋给 CF,该位设零
BTSBTS Base, Offset指定地址位的值赋给 CF,该位设一
BSFBSF A, BB 最低的 1 是第几位,赋给 A影响 ZF
BSRBSR A, BB 最高的 1 是第几位,赋给 A影响 ZF

注意:课件里关于 BSF 和 BSR 的描述好像错了。跟网络资源很不一样。而且,这俩好像是 80386 而不是 8086 的。

移位指令 ​

指令示例含义备注
SHLSHL r/m, 1/CL首位给 CF,尾位置零
SHRSHR r/m, 1/CL尾位给 CF,首位置零
SALSAL r/m, 1/CL算术,首位给 CF,尾位维持不变
SARSAR r/m, 1/CL算术,尾位给 CF,首位维持不变
ROLROL r/m, 1/CL循环左移,最高位同时给 CF 和最低位(w)
RORROR r/m, 1/CL循环右移,最低位同时给 CF 和最高位(w)
RCLRCL r/m, 1/CL循环左移,把 CF 视作额外最高位(w+1)
RCRRCR r/m, 1/CL循环右移,把 CF 视作额外最高位(w+1)
SHLDSHLD A, B, cnt双精度左移,缺位由 B 的高位填充,B 不变
SHRDSHRD A, B, cnt双精度右移,缺位由 B 的低位填充,B 不变

SHLD 和 SHRD 好像 8086 不支持。

其他几个移位指令的操作数,好像只能是 1,或者寄存器 CL。到 80836 上可以是立即数。

标志位指令 ​

指令示例含义备注
CLCCLC进位标志置零
STCSTC进位标志设一
CMCCMC进位标志取反
CLDCLD方向标志置零
STDSTD方向标志设一
CLICLIIF 置零,关闭中断
STISTIIF 设一,打开中断

分支控制转移 ​

指令指针(IP)可以视作当前执行第几行代码,执行指令后自动递增。

对于分支指令,就修改 IP;而对于调用指令,相当于把当前 IP 压入栈中,然后再修改 IP,然后再弹栈。

汇编代码中,一个字符串后面跟上冒号,表示一个标签(label)。比如:haha: 就是一个标签。标签可以清晰的表示代码片段结构和作用,转移指令当中也可以指定跳转到哪一个标签。

条件跳转 conditional jump ​

条件转移指令通常都是接在 cmp 指令之后。所有的 J 都是「Jump if」的意思,后面的字母代表条件的英文首字母缩写。

条件转移的操作数是单字节的,所以 不能跳太远,范围是向前 128 到向后 127(-128~127)。

指令含义flag反指令反含义反 flag
JAAboveZF=0 and CF=0JNANot AboveZF=1 or CF=1
JAEAbove or EqualCF=0JNAENot Above or EqualCF=1
JBBelowCF=1JNBNot BelowCF=0
JBEBelow or EqualZF=1 or CF=1JNBENot Below or EqualZF=0 and CF=0
JCCarryCF=1JNCNot CarryCF=0
JCXZCX is ZeroCX=0---
JEEqualZF=1JNENot EqualZF=0
JGGreaterZF=0 and SF=OFJNGNot GreaterZF=1 or SF!=OF
JGEGreater or EqualSF = OFJNGENot Greater or EqualSF != OF
JLLessSF != OFJNLNot LessSF = OF
JLELess or EqualZF=1 or SF!=OFJNLENot Less or EqualZF=0 and SF=OF
JOOverflowOF=1JNONo OverflowOF=0
JPParityPF=1JNPNot ParityPF=0
JPEParity is EvenPF=1JPOParity is OddPF=0
JSSignedSF=1JNSNot SignedSF=0
JZZeroZF=1JNZNot ZeroZF=0

其中:

  • JA,JB 系列比较的是无符号数,JG,JL 系列比较的是有符号数
  • 如果不支持 JB 和 JNB,可以用 JC 表示 A < B,用 JNC 表示 A <= B,这些其实是等价的
  • JG,JL 系列的 flag 要求不是很好理解。课件上 JG 的条件是 ZF=0 and SF=0,JGE 的条件是 SF=0,JL 和 JLE 的条件明显写错了。

可移植性 ​

汇编语言不像 C,Java 那样具有可移植性。因为,许多微处理器,只有基于 C 和 Z 的 jump。

这门课学习的汇编是 x86 的一个子集。

无条件跳转 unconditional jump ​

JMP 指令,啥都不管,直接跳过去。可以跳的远一些(范围 -32768~32767),甚至可以把目标地址存进寄存器再跳到寄存器去(jmp ax)。

循环 ​

主要有三种:

  • LOOP,相当于 do {} while ((cx!=0) && (--cx||true));
  • LOOPZ,相当于 do {} while ((cx!=0 && ZF=1) && (--cx||true));
  • LOOPNZ,相当于 do {} while ((cx!=0 && ZF=0) && (--cx||true));

时钟周期与延迟计算 ​

短延迟(一层循环) ​

对于如下程序

asm
    mov cx, N  ; 消耗 4 时钟周期,这里 N 是字面量,但是取值还未确定
tag:
    nop        ; 消耗 3 时钟周期
    nop        ; 消耗 3 时钟周期
    loop tag   ; 消耗 17 个时钟周期,退出循环时仅 5 个时钟周期

那么,循环体总共消耗 3+3+17 = 23 个时钟周期。

这个程序总周期消耗是:。

假设现在有一台机器,时钟频率是 100MHz(每个时钟周期 秒),想让他睡 25 微秒( 秒),该如何取 的值?

首先容易计算:25 微秒是 2500 个时钟周期。也就是说需要计算: 取什么值,可以让时钟周期数 ?

刚才那个式子移项可以得到:$$N = \frac{C_T - 4 + 12}{23} = 109$$,即需要 mov cx, 109,睡 25 微秒。

长延迟(两层甚至更多层循环) ​

考虑如下程序

asm
    mov bx, 30000  ; 4 个周期
lp1:               ; 外层循环开始
    mov cx, 30000  ; 4 个周期
lp2:               ; 内层循环开始
    nop            ; 3 个周期
    loop lp2       ; 17/5 个周期;内层循环结束
    dec bx         ; 2 个周期;cx 被占用,这里只能用 dec 配合 jnz 了
    jnz lp1        ; 16/4 个周期;外层循环结束

对于内层循环,。

对于总循环,。

有一台实验机,标 300MHz,跑了 15s。

然而,跑 15s 说明频率高达 1.2GHz,与标注的 300MHz 不符。这说明,这台机器每秒执行四条指令。这个现象可能是由于多核。

打印数字 ​

方案一:取模 ​

在「十进制算术指令」一节当中,提到了输出 100 以内整数的方法。这里再补充一个输出 16 位无符号整数即 范围内整数的方法。

这种方法的缺陷是,如果不到 10000,打印出来会带前导零。

代码实现一:用减法实现取模(源自课件) ​

asm
.MODEL medium
.STACK
.DATA
.CODE
.STARTUP
    mov ax, 12345
    call Print     ; 调用 Print 过程,打印 ax
.EXIT


Print:
    push bx        ; 过程开始,把所有的寄存器都存栈备份
    push cx
    push dx
    mov dx, 10000
    mov cx, 5
NXTCH:
    push cx
    push ax
    push dx

    ; save (num / pw10) into stack
    mov cx, dx     ; 此时 cx 就是 pw10
    mov dx, 0h
    div cx         ; ax /= pw10
    push ax        ; 存栈 (num / pw10)

    ; 求出 num 该减去多少
    mov bl, al     ; bl 存首位
    mov bh, 0h
    mov dx, 0h
    mov ax, cx     ; cx 此时还是 pw10
    mul bx         ; 让 ax 变成 x0000 的形式
    mov bx, ax     ; 把 ax 赋给 bx,此时 bx 就是该减去多少

    ; pop (num / pw10) into dx, putchar(dx)
    pop dx         ; 此时 dx 是刚刚的 ax(对应 Line26)
    or dl,  30h    ; 转 ASCII
    mov ah, 02h
    int 021h

    ; pw10 /= 10
    pop ax         ; 此时 ax 是 NXTCH 开始时的 dx,即 pw10(对应 Line20)
    mov dx, 0h
    mov cx, 10
    div cx
    mov dx, ax     ; 现在 dx 保存了新的 pw10,可以给下一轮循环用

    ; 用减法实现 ax %= old_pw10
    pop ax         ; 此时 ax 就是一开始的 num(对应 Line19)
    sub ax, bx     ; 删掉 ax 的最高位
    pop cx         ; 此时 cx 就是一开始的 cx(对应 Line18),即恢复原值,循环变量值不能乱
    loop NXTCH     ; 循环

    pop dx         ; 过程结束,恢复栈当中的备份
    pop cx
    pop bx
    ret


END

代码实现二:从寄存器中直接提取余数(源自期中考试) ​

asm
    .MODEL medium
    .STACK
    .DATA
count   byte    5
ten     word    10
    .CODE
    .STARTUP
    mov ax, 12345
    call Print
    .EXIT
Print:
    push bx        ; 存栈
    push cx
    push dx
    mov bx, 10000
again:
    mov dx, 0h     ; 除法前清空 dx
    div bx         ; dx:ax /= bx
    or al, 030h    ; 数字转 ASCII
    push dx        ; 保存余数
    mov dl, al     ; 传参给 21h 中断
    mov ah, 02h    ; 输出字符模式
    int 021h
    mov dx, 0h     ; 除法前清空 dx
    mov ax, bx     ; bx /= 10
    div ten
    mov bx, ax
    pop ax
    dec count
    jnz again
    pop dx         ; 恢复
    pop cx
    pop bx
    ret            ; 必须记得返回,否则会出问题
    END

方案二:使用栈 ​

代码实现一:带前导零(根据课件伪代码) ​

asm
    .MODEL medium
    .STACK
    .DATA
ten word 10
    .CODE
    .STARTUP
    mov ax, 0
    call Print
    .EXIT
Print:
    push bx
    push cx
    push dx
    mov cx, 5
again:
    mov dx, 0
    div ten
    push dx
    loop again
    mov cx, 5
again2:
    pop dx
    or dx, 30h
    mov ah, 02h
    int 021h
    loop again2
    pop dx
    pop cx
    pop bx
    ret
    END

代码实现二:不带前导零(修改一下) ​

弹栈的时候特判一下,就不带前导零了。

但是也要特判,如果要输出的本来就是零,怎么办。

asm
    .MODEL medium
    .STACK
    .DATA
ten word 10
    .CODE
    .STARTUP
    mov ax, 2345
    call Print
    .EXIT
Print:
    push bx
    push cx
    push dx
    cmp ax, 0
    jz iszero    ; 特判,如果要输出的是零
    mov cx, 5
again:
    mov dx, 0
    div ten
    push dx
    loop again
    mov cx, 5
again2:
    pop dx
    cmp dx, 0
    je ignore    ; 忽略前导零
    or dx, 30h
    mov ah, 02h
    int 021h
ignore:
    loop again2
    jmp notzero
iszero:
    mov ah, 02h
    mov dl, '0'
    int 021h
notzero:
    pop dx
    pop cx
    pop bx
    ret
    END