操作指令

有些指令属于双操作数指令,双操作数指令必须满足:

  1. 两个操作数的长度必须匹配
  2. 两个操作数不能同时为内存单元

mov

数据传送指令。把一个字节、字或双字的操作数从源位置传送到目的位置,源操作数的内容不变,类似于复制操作

  1. mov 属于双操作数指令,两个操作数的长度必须匹配
    正确:mov ax, bx(AX 为 16 位,BX 为 16 位)
    错误:mov ax, bh(AX 为 16 位,BH 为 8 位)
    正确:mov al, 'A'(AL 为 8 位,'A' 会以字节 61h 的形式存进 AL)
    正确:mov ax, 'A'(AX 为 16 位,'A' 会以字 0061h 的形式存进 AX)
    错误:mov ah, 258(AH 为 8 位,十进制 258 超出了 8 位二进制)

  2. mov 属于双操作数指令,两个操作数不能同时为内存单元
    正确:mov ax, [bx](AX 为寄存器,[BX] 为偏移地址,段地址默认在 DS 中)
    错误:mov [ax], [bx]([AX] 和 [BX] 都是偏移地址,段地址默认在 DS 中)

  3. mov 指令的目的操作数不能是 CS 或 IP,因为 CS:IP 指向当前要执行的指令所在的地址

  4. mov 指令中的操作数,如果是十六进制常数,且非数字开头,需要在前面加 0
    正确:mov ax, 0Dh
    错误:mov ax, Dh

  5. mov 指令需要在指令中说明内存单元的类型,以便于操作数的长度匹配
    正确:mov [bx], ax(由于 AX 为 16 位,因此 [BX] 必定指向字单元)
    错误:mov [bx], 0(由于常数 0 没有类型,因此 [BX] 没有说明是字节单元还是字单元)
    正确:mov byte ptr [bx], 0byteptr 说明是字节操作,写一个字节单元)
    正确:mov word ptr [bx], 0wordptr 说明是字操作,写一个字单元)

  6. mov 指令不能直接将符号地址、段寄存器、立即数送往段寄存器,只能通过寄存器得到
    正确:mov ax, dataseg mov ds, ax(先将 dataseg 送往 AX,再由 AX 送往 DS)
    错误:mov ds, dataseg(不能直接将符号地址送往段寄存器)
    错误:mov ds, es(不能直接将段寄存器送往段寄存器)
    错误:mov ds, 1234(不能直接将立即数送往段寄存器)
    错误:mov cs, ax指令虽然合法,但是代码段寄存器不能赋值

  • 示例,假设 TABLE 是一个 16 位的变量,使用 mov 传送变量
    正确:mov bx, table(将变量 TABLE 的值送往 BX)
    错误:mov bl, table(BL 为 8 位,TABLE 为 16 位,操作数长度不一致)
    错误:mov [bx], table([BX] 和 TABLE 都是内存单元)
    正确:mov bx, offest table(将变量 TABLE 的偏移地址送往 BX)
    错误:mov bl, offest table(不论变量的类型如何,偏移地址总是 16 位,操作数长度不一致)

movsb

串字节传送指令。将 DS:SI 指向的内存单元中的字节送入 ES:DI 中,然后根据标志寄存器 DF 位的值对 SI 和 DI 进行加一或减一

  • 执行 movsb 指令相当于:
((es) * 16 + (di)) = ((ds) * 16 + (si))

如果 DF = 0,则:
(si) = (si) + 1
(di) = (di) + 1

如果 DF = 1,则:
(si) = (si) - 1
(di) = (di) - 1

movsw

串字传送指令。将 DS:SI 指向的内存单元中的字送入 ES:DI 中,然后根据标志寄存器 DF 位的值对 SI 和 DI 进行加二或减二

  • 执行 movsw 指令相当于:
((es) * 16 + (di)) = ((ds) * 16 + (si))

如果 DF = 0,则:
(si) = (si) + 2
(di) = (di) + 2

如果 DF = 1,则:
(si) = (si) - 2
(di) = (di) - 2

push 和 pop

进栈和出栈指令。push 实现压入操作,将数据压入栈中;pop 实现弹出操作,将数据从栈上弹出到寄存器

栈操作以字节为单位

指令用法操作
pushpush axSP = SP - 2,将寄存器 AX 中的数据送入栈中
poppop ax从栈顶取出数据送入 AX 寄存器,SP = SP + 2
  • 例如,执行如下代码:
mov ax, 0123h
push ax
mov bx, 2266h
push bx
mov cx, 1122h
push cx
pop ax
pop bx
pop cx

x86汇编_栈1.png

  • pushpop 操作可以在寄存器和内存之间传送数据

    1. push/pop 寄存器
      push ax
      pop bx
    2. push/pop 段寄存器
      push ds
      pop es
  • pushpop 操作还可以在内存和内存之间传送数据

    1. push/pop 内存单元(可以只给出偏移地址,段地址在 DS 中)
      push [0]
      pop [2]

pushf 和 popf

标志寄存器进栈和出栈指令。pushf 实现将标志寄存器 FLAG 的值入栈;popf 实现将栈中的数据从栈上弹出,送入标志寄存器 FLAG 中

栈操作以字节为单位

  • pushfpopf 为直接访问标志寄存器提供了一种方法

  • 例如:

mov ax, 0
push ax
popf
mov ax, 0FFF0h
add ax, 0010h
pushf
pop ax
and al, 11000101b
and ah, 00001000b

x86汇编_pushf和popf1.png

如上图,执行程序后 AX = 0045h,过程如下:

  1. 执行到 popf 这一条指令时,将栈中的 AX = 0 弹出到 FLAG 寄存器,即:FLAG = 0
    此时 ZF = 0,PF = 0,SF = 0,CF = 0,OF = 0,DF = 0
  2. add ax, 0010h 时,计算结果为 0(ZF),有偶数个 1(PF),存在进位(CF)
    此时 ZF = 1,PF = 1,SF = 0,CF = 1,OF = 0,DF = 0
   1111111111110000
+  0000000000010000
   ----------------
 1 0000000000000000
  1. pushf 时,将 FLAG 寄存器的值 0000000001000111b = 0047h 入栈,并通过 pop ax 弹出到 AX 中,AX = 0000000001000111b = 0047h
  2. 执行 and al, 11000101band ah, 00001000b 计算后,AX = 0000000001000101b = 0045h
     11000101
and  01000111
     --------
     01000101

     00001000
and  00000000
     --------
     00000000

xchg

交换指令。实现将两个操作数互换位置,寄存器和内存变量之间的类型必须相同,可以是一个字节、一个字或双字

注意:xchg 也是双操作数指令,须遵循双操作数指令的要求

  • 示例:
    正确:xchg ax, bx(AX 与 BX 长度相同,交换 AX 与 BX 中的值)
    正确:xchg ax, [bx](AX 为 16 位,所以 [BX] 也指向字单元)
    正确:xchg ax, var(AX 为 16 位,所以 var 必须为字变量)
    错误:xchg ax, 5操作数不能为立即数
    错误:xchg [bx], var(两个操作数不能同时为内存单元)
    错误:xchg ax, bh(两个操作数长度不一致)

rep

重复指令。根据 CX 的值,重复执行后面的串传送指令

  • 例如:rep movsb 等价于:
s:
	movsb
	loop s

串字节传送指令 movsb 配合 rep 指令使用,每执行一次 movsb 指令 SI 和 DI 都会递增或递减(根据标志寄存器 DF 位的值),因此 rep movsb 可以循环实现 CX 个字符的传送

  • 同理,rep movsw 等价于:
s:
	movsw
	loop s

lea

取偏移地址指令。与 mov 指令类似,也有数据传送的功能,但 lea 主要是用来将一个内存地址直接赋给目的操作数,而 mov 主要是将数值赋值给目的操作数

即:lea 传的是地址,mov 传的是值

  • lea 指令与 mov 指令的区别:
mov eax, [ebx+8]   ; 把内存地址为 ebx+8 处存放的数据赋给 eax
lea eax, [ebx+8]   ; 把 ebx+8 这个值 (内存地址) 直接赋给 eax, 而不是把 ebx+8 处的内存地址里的数据赋给 eax

总而言之:

  1. lea 指令的功能是取偏移地址,例如:lea ax, [1000H],作用是将源操作数 [1000H] 的偏移地址 1000H 送至 ax
  2. mov 指令的功能是传送数据,例如:mov ax, [1000H],作用是将 1000H 作为偏移地址,寻址找到内存单元,将该内存单元中的数据送至 ax
  • 指令 lea eax, [ebx+8] 等价于:
add ebx, 8
mov eax, ebx

虽然这两段指令的功能等价,但是直接用 lea 指令比用 mov 指令来实现 lea 指令的机器码要短,更简洁

  • 指令 lea ax, table 等价于:
mov ax, offset table
  • 但也有时候 lea 指令不能直接使用 mov 代替

例如:lea ax, [si+6] 不能直接替换成 mov ax, si+6
但可以替换成:

mov ax, si
add ax, 6

enter 和 leave

函数开始时的保存指令和函数返回前的清理指令。通常,enter 指令会在函数的开始执行,用于保存函数调用之前的栈帧,leave 指令会在函数末尾执行(紧跟在 ret 指令之前),用于恢复函数调用前的栈帧

  • enter 指令等价于:
push ebp
mov ebp, esp

enter 指令一般用于函数的开始,保存函数调用之前的栈帧信息

  • leave 指令等价于:
mov esp, ebp  ; 恢复栈指针
pop ebp       ; 恢复基址指针

在函数调用前,ebp 通常被用作栈帧基址指针,而在函数返回前,通过 leave 指令来还原栈帧,leaveret 配合共同完成了子函数的返回

  • 以一般函数调用为例:
main:
	...
	call fun
	...


fun:
	push ebp
	mov ebp, esp
	...
	leave
	retn

循环指令

loop

循环指令。loop 指令的格式为 loop 标号,通常用 CX 来存放循环次数

CPU 在执行 loop 标号 时,机器码中不包含转移的目的地址,包含的是转移的位移

  • 执行 loop 指令时,要进行两步:

    1. (CX) = (CX) - 1
    2. 判断 CX 中的值,不为 0 则转至标号处执行,为 0 就向下执行(循环结束)
  • 例如,实现计算 $2 ^ {12}$:

assume cs:code

code segment
	mov ax, 2
	
	mov cx, 11
s:	add ax, ax
	loop s
	
	mov ax, 4c00h
	int 21h
code ends
end
  • 除此之外,还有其他循环指令:
    1. 先将 CX 减 1
    2. 然后根据测试条件决定是否转移
指令循环条件含义
loopCX ≠ 0循环
loopzloopeZF = 1 且 CX ≠ 0当为 0 时循环
loopnzloopneZF = 0 且 CX ≠ 0当不为 0 时循环

转移指令

可以修改 IP,或者可以同时修改 CS 和 IP 的指令统称为转移指令。即:转移指令就是可以控制 CPU 执行内存中某处代码的指令

  • 8086CPU 的转移行为分为两类:

    1. 段内转移:只修改 IP 的值,例如 jmp ax
    2. 段间转移:同时修改 CS 和 IP 的值,例如 jmp 1000:0
  • 根据转移指令对 IP 的修改范围不同可分为两类:

    1. 段内短转移:IP 的修改范围为 -128 ~ 127
    2. 段内近转移:IP 的修改范围为 -32768 ~ 32767
  • 8086CPU 的转移指令可分为五类:

    1. 无条件转移指令(例如:jmp
    2. 条件转移指令(例如:jzjnz
    3. 循环指令(例如:loop
    4. 过程
    5. 中断

根据位移进行转移的指令,机器码中不包含目的地址,包含的是转移的位移:
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号

例如:loop s 对应的机器码可能是 E2 FCjmp short s 对应的机器码可能是 EB 03FC03 都是位移,而不是真实的地址

这样的好处在于:代码装在内存的不同位置都可以正确执行,如果包含的是目的地址,就会对偏移地址有严格的限制,一旦指令不在目的地址处就会发生错误


无条件转移指令

jmp 为无条件转移指令,可以只修改 IP,也可以同时修改 CS 和 IP

jmp 指令要给出两种信息:

  1. 转移的目的地址
  2. 转移的距离(段间转移、段内短转移、段内近转移)

jmp short 标号

段内短转移,IP 修改范围为 -128 ~ 127,即:向前转移最多可越过 128 字节,向后转移最多可越过 127 字节

CPU 在执行 jmp short 标号 时,机器码中不包含转移的目的地址,包含的是转移的位移

  • jmp short 标号 的功能为:(IP) = (IP) + 8 位位移

  • jmp 指令中 short 指明段内短转移,标号指明要转移的目的地,转移指令结束后,CS:IP 指向标号处的指令

  • 例如:

assume cs:codesg

codesg segment
	start:  mov ax, 0
			jmp short s
			add ax, 1
		s:	inc ax
codesg ends
end start

执行后 AX = 1,因为执行 jmp short s 后越过了 add ax, 1 直接执行 inc ax


jmp near ptr 标号

段内近转移,IP 修改范围为 -32768 ~ 32767,即:向前转移最多可越过 32768 字节,向后转移最多可越过 32767 字节

CPU 在执行 jmp near ptr 标号 时,机器码中不包含转移的目的地址,包含的是转移的位移

  • jmp near ptr 标号 的功能为:(IP) = (IP) + 16 位位移

  • jmp 指令中 near ptr 指明段内近转移,标号指明要转移的目的地,转移指令结束后,CS:IP 指向标号处的指令


jmp far ptr 标号

段间转移(远转移)

(CS) = 标号所在段的段地址
(IP) = 标号在段中的偏移地址

CPU 在执行 jmp far ptr 标号 时,机器码中包含转移的目的地址

  • jmp 指令中 far ptr 指明指令用标号的段地址和偏移地址修改 CS 和 IP

  • 例如:

assume cs:codesg

codesg segment
	start:  mov ax, 0
			mov bx, 0
			jmp far ptr s
			db 256 dup (0)
		s:	add ax, 1
			inc ax
codesg ends
end start

jmp 十六位寄存器

这种 jmp 指令可仅修改 IP 的值

  • jmp 十六位寄存器 的功能为:mov IP, 十六位寄存器

  • 例如:

mov ax, 1000h
jmp ax
; 执行后,CS:IP 指向 CS:1000

jmp word ptr 内存单元

段内间接转移,从内存单元地址处开始的一个字,是转移的目的偏移地址

内存单元地址可以用寻址方式的任一格式给出

mov ax, 0123h
mov ds:[0], ax
jmp word ptr ds:[0]
; 执行后 (IP) = 0123h

mov ax, 4567h
mov [bx], ax
jmp word ptr [bx]
; 执行后 (IP) = 4567h
  • 假设 BX = 2000h,DS = 4000h,(42000h) = 6050h,(44000h) = 8090h,table 的偏移地址为 2000h
jmp bx                ; 寄存器寻址,ip = bx
jmp word ptr [bx]     ; 寄存器间接寻址,ip = [ds:bx]
jmp word ptr table    ; 直接寻址,ip = [ds:table]
jmp table[bx]         ; 寄存器相对寻址,ip = [ds:(table + bx)]

第一条指令执行后:IP = BX = 2000h
第二条指令执行后:IP = (DS:2000h) = (42000h) = 6050h
第三条指令执行后:IP = (DS:2000h) = (42000h) = 6050h
第四条指令执行后:IP = (DS:4000h) = (44000h) = 8090h


jmp dword ptr 内存单元

段间间接转移,从内存单元地址处开始的两个字,高地址的一个字是转移的目的段地址,低地址的一个字是转移的目的偏移地址

内存单元地址可以用寻址方式的任一格式给出

mov ax, 0123h
mov ds:[0], ax
mov word ptr ds:[2], 0
jmp dword ptr ds:[0]
; 执行后,(CS) = 0,(IP) = 0123h,CS:IP 指向 0000:0123

mov ax, 4567h
mov [bx], ax
mov word ptr [bx + 2], 0
jmp dword ptr [bx]
; 执行后,(CS) = 0,(IP) = 4567h,CS:IP 指向 0000:4567
  • 假设 BX = 2000h,DS = 4000h,(42000h) = 6050h,(42002h) = 1234h
jmp dword ptr [bx]

执行指令后:
IP = (DS:2000h) = (40000h + 2000h) = (42000h) = 6050h
CS = (42002h) = 1234h


条件转移指令

所有的有条件转移指令都是短转移,对 IP 修改的范围为:-128 ~ 127,在机器码中包含的是转移的位移,而不是目的地址

指令跳转条件含义
jzjeZF = 1结果为 0 则转移(结果相等则转移)
jnzjneZF = 0结果不为 0 则转移(结果不相等则转移)
jsSF = 1结果为负则转移
jnsSF = 0结果为正则转移
joOF = 1结果溢出则转移
jnoOF = 0结果不溢出则转移
jpjpePF = 1奇偶位为 1 则转移(偶数个 1 则转移)
jnpjpoPF = 0奇偶位为 0 则转移(奇数个 1 则转移)
jcxzCX = 0CX 寄存器为 0 则转移
jcjbjnaeCF = 1【无符号数】进位位为 1 则转移(低于则转移)(不高于等于则转移)
jncjnbjaeCF = 0【无符号数】进位位为 0 则转移(不低于则转移)(高于等于则转移)
jbejnaCF = 1 or ZF = 1【无符号数】低于等于则转移(不高于则转移)
jnbejaCF = 0 and ZF = 0【无符号数】不低于等于则转移(高于则转移)
jljngeSF ≠ OF【有符号数】小于则转移(不大于等于则转移)
jnljgeSF = OF【有符号数】不小于则转移(大于等于则转移)
jlejngSF ≠ OF or ZF = 1【有符号数】小于等于则转移(不大于则转移)
jnlejgSF = OF and ZF = 0【有符号数】不小于等于则转移(大于则转移)

call 和 ret

call 和 ret 都是转移指令,他们都修改 IP 或都同时修改 CS 和 IP。这两个指令常被共同用来实现子程序的设计

call 指令不能实现短转移。与 jmp 不同的是,call 指令会先向堆栈保存返回地址,再实现程序的转移

  • 主程序通过 call 指令启动子程序,call 指令执行时,会将下一条指令的地址压入堆栈保存(执行完子程序后会回到该地址继续往下执行,即:ret 回下一条指令的地址),再把子程序的入口地址送入 IP(CS)寄存器,以便实现转移

  • CPU 执行 call 指令时,分为两步:

    1. 将当前的 IP 或 CS 和 IP 压入栈中,即:
      (SP) = (SP) - 2
      ((SS) * 16 + (SP)) = (IP)
      或者:
      (SP) = (SP) - 2
      ((SS) * 16 + (SP)) = (CS)
      (SP) = (SP) - 2
      ((SS) * 16 + (SP)) = (IP)
    2. 转移
      (IP) = XXXX
      或者:
      (CS) = XXXX
      (IP) = XXXX

call 标号

将当前 IP 压栈后,转到标号处执行指令

  • 操作流程:
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (ip)

(ip) = (ip) + 16 位位移
  • CPU 在执行 call 标号 时,相当于执行:
push IP
jmp near ptr 标号
  • 例如,执行如下代码:
assume cs:codesg

codesg segment
start:
        mov ax, 0
        call s
        inc ax
    s:  pop ax
    
        mov ax, 4c00h
        int 21h
codesg ends
end start

在 Debug 中查看代码:

x86汇编_call1.png

在 Debug 中运行:

x86汇编_call2.png

如上图,执行程序后 AX = 6,过程如下:

  1. 执行到 call s 这一条指令时,IP 指向下一条指令 inc ax 的地址,即:IP = 6
  2. 此时开始执行 call s 指令,将 IP = 6 入栈,同时 IP 指向 s 所在的位置 pop ax,即:IP = 7
  3. pop ax 时,将栈上的 6 弹出到 AX,所以 AX = 6

call far ptr 标号

实现段间转移,会同时将当前 CS 和 IP 压栈后,转到标号处执行指令

  • 操作流程:
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (cs)
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (ip)

(cs) = 标号所在段的段地址
(ip) = 标号在段中的偏移地址
  • CPU 在执行 call far ptr 标号 时,相当于执行:
push CS
push IP
jmp far ptr 标号
  • 例如,执行如下代码:
assume cs:codesg

codesg segment
start:
		mov ax, 0
		call far ptr s
		inc ax
    s:  pop ax
		add ax, ax
		pop bx
		add ax, bx
		
		mov ax, 4c00h
		int 21h
codesg ends
end start

在 Debug 中查看代码:

x86汇编_call3.png

在 Debug 中运行:

x86汇编_call4.png

如上图,执行程序后 AX = 077Ch,过程如下:

  1. 执行到 call far ptr s 这一条指令时,IP 指向下一条指令 inc ax 的地址,即:IP = 8
  2. 此时开始执行 call far ptr s 指令,将 CS = 076Ch 和 IP = 8 依次入栈,同时 IP 指向 s 所在的位置 pop ax,即:IP = 9
  3. pop ax 时,将栈上的 8 弹出到 AX,所以 AX = 8,执行 add ax, ax 后,AX = 16,即:10h
  4. pop bx 时,将栈上的 076Ch 弹出到 BX,所以 BX = 076Ch,执行 add ax, bx 后,AX = 077Ch

call 十六位寄存器

将十六位寄存器中的值作为 IP 的值

  • 操作流程:
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (ip)

(ip) = 十六位寄存器
  • CPU 在执行 call 十六位寄存器 时,相当于执行:
push IP
jmp 十六位寄存器
  • 例如,执行如下代码:
assume cs:codesg

codesg segment
start:
		mov ax, 6
		call ax
		inc ax
	    mov bp, sp
		add ax, [bp]
		
		mov ax, 4c00h
		int 21h
codesg ends
end start

在 Debug 中查看代码:

x86汇编_call5.png

在 Debug 中运行:

x86汇编_call6.png

如上图,执行程序后 AX = Bh,过程如下:

  1. 执行到 call ax 这一条指令时,IP 指向下一条指令 inc ax 的地址,即:IP = 5
  2. 此时开始执行 call ax 指令,将 IP = 5 入栈,同时 IP 指向 ax 所在的位置 mov bp, sp,即:IP = 6
  3. mov bp, sp 时,将栈顶指针 SP 的值送往 BP
  4. add ax, [bp] 时,由于 BP = SP 指向栈顶元素,所以 [bp] 就是栈上的第一个元素的值 5,即将栈上的 5 与 AX 相加,所以 AX = 11,即:Bh

call word ptr 内存单元

  • CPU 在执行 call word ptr 内存单元 时,相当于执行:
push IP
jmp word ptr 内存单元
  • 例如:
mov sp, 10h
mov ax, 0123h
mov ds:[0], ax
call word ptr ds:[0]
; 执行后,(IP) = 0123h,(SP) = 0Eh

call dword ptr 内存单元

  • CPU 在执行 call word ptr 内存单元 时,相当于执行:
push CS
push IP
jmp dword ptr 内存单元
  • 例如:
mov sp, 10h
mov ax, 0123h
mov ds:[0], ax
mov word ptr ds:[2], 0
call dword ptr ds:[0]
; 执行后,(CS) = 0,(IP) = 0123h,(SP) = 0Ch

ret 和 retf

ret 指令用栈中的数据,修改 IP 的内容,实现近转移
retf 指令用栈中的数据,修改 CS 和 IP 的内容,实现远转移

  1. CPU 在执行 ret 时,操作流程:
(IP) = ((SS) * 16 + (SP))
(SP) = (SP) + 2

; 相当于执行:
; pop IP


; 若执行的是 ret n 则操作流程为:
(IP) = ((SS) * 16 + (SP))
(SP) = (SP) + 2
(SP) = (SP) + n

; 相当于执行:
; pop IP
; add SP, n
  • 示例:
assume cs:code

stack segment
	db 16 dup (0)
stack ends

code segment
		mov ax, 4c00h
		int 21h
		
start:  mov ax, stack
		mov ss, ax
		mov sp, 16
		mov ax, 0
	    push ax
		mov bx, 0
		ret
code ends
end start

执行 ret 指令后,(IP) = 0,CS:IP 指向代码段的第一条指令

  1. CPU 在执行 retf 时,操作流程:
(IP) = ((SS) * 16 + (SP))
(SP) = (SP) + 2
(CS) = ((SS) * 16 + (SP))
(SP) = (SP) + 2

; 相当于执行:
; pop IP
; pop CS
  • 示例:
assume cs:code

stack segment
	db 16 dup (0)
stack ends

code segment
		mov ax, 4c00h
		int 21h
		
start:  mov ax, stack
		mov ss, ax
		mov sp, 16
		mov ax, 0
	    push cs
	    push ax
		mov bx, 0
		retf
code ends
end start

执行 retf 指令后,(IP) = 0,CS:IP 指向代码段的第一条指令


call 和 ret 配合实现子程序

  • 示例:
assume cs:code

code segment
	start:  mov ax, 1
			mov cx, 3
			call s
			mov bx, ax
			mov ax, 4c00h
			int 21h

		s:  add ax, ax
			loop s
			ret
code ends
end start

如上图,执行程序后 BX = 8,过程如下:

  1. 执行到 call s 这一条指令时,IP 指向下一条指令 mov bx, ax 的地址
  2. 此时开始执行 call ax 指令,将指令 mov bx, ax 的地址入栈,同时 IP 指向 s 所在的位置 add ax, ax,并执行 add ax, ax
  3. loop s 时,由于 CX = 3,因此会执行三次 add ax, ax 指令,当 loop s 结束后,AX = 8
  4. ret 时,将栈上的指令 mov bx, ax 的地址弹出到 IP 寄存器,转而执行指令 mov bx, ax,因此 BX = 8

根据上述分析,示例中的程序从标号 sret 这一段其实就是实现计算 $2 ^ {N}$ 的功能,因此可以通过 callret 实现子程序,类似于 C 语言中的函数

只在要使用的时候通过 call 去调用,调用完后再通过 ret 回到 call 指令的下一条语句继续执行,从而实现子程序的调用

  • 大致框架如下:
assume cs:code

code segment
	main: 
		···
		call sub1   ; 调用子程序 sub1
		···
			 
		mov ax, 4c00h
		int 21h


	sub1: 
		···
		call sub2   ; 调用子程序 sub2
		···
		ret   ; 返回到 main 中 call sub1 的下一条指令


	sub2:
		···
		ret   ; 返回到 sub1 中 call sub2 的下一条指令

code ends
end main

子程序的参数和结果传递

子程序一般都要根据提供的参数处理一定的事务,因此传递参数、传递返回值很有必要

使用寄存器来存储参数和结果是最常用的。调用者将参数送入寄存器并从寄存器中取出返回值,子程序从寄存器中取出参数并将返回值送入寄存器

  • 示例:计算 data 段中第一组 word 数据的 3 次方,并将结果保存在后面一组 dword 单元中
assume cs:code, ds:data

data segment
		dw 1, 2, 3, 4, 5, 6, 7, 8
		dd 0, 0, 0, 0, 0, 0, 0, 0
data ends

code segment
	main: 
		mov ax, data
		mov ds, ax
		mov si, 0   ; ds:si 指向第一组 word 单元
		mov di, 16   ; ds:di 指向后面一组 dword 单元
		
		mov cx, 8
	s:	mov bx, [si]
		call sub1
		mov [di], ax   ; 存储结果的低 16 位
		mov [di] . 2, dx   ; 存储结果的高 16 位
		add si, 2   ; ds:si 指向下一个 word 单元
		add di, 4   ; ds:di 指向下一个 dword 单元
		loop s

		mov ax, 4c00h
		int 21h


	; 子程序:实现 n * n * n
	sub1:
		mov ax, bx
		mul bx
		mul bx
		ret

code ends
end main

在上述示例中,子程序只有一个参数,存放在 BX 中。如果有两个参数,那么可以选择两个寄存器来存放,但如果有更多参数,使用寄存器存放参数就不方便了

因此,也可以将参数存放在内存中,然后将内存单元的首地址存放在寄存器中,传递给子程序。对于批量的返回值,也可以用同样的方法

  • 示例:将一个全是字母的字符串转化为大写
assume cs:code, ds:data

data segment
		db 'conversation'
data ends

code segment
	main: 
		mov ax, data
		mov ds, ax
		mov si, 0   ; ds:si 指向 'conversation' 第一个字节
		
		mov cx, 12   ; 字符串的长度
		call sub1

		mov ax, 4c00h
		int 21h


	; 子程序:将全是字母的字符串转换为大写
	sub1:
		and byte ptr [si], 11011111b   ; 将字母转换为大写
		inc si
		loop sub1
		ret

code ends
end main

除此之外,也可以用栈来传递参数

  • 示例:假设 a = 3,b = 1,计算 $(a - b) ^ 3$
assume cs:code, ss:stack

stack segment
		db 0, 0, 0, 0, 0, 0, 0, 0
		db 0, 0, 0, 0, 0, 0, 0, 0
stack ends

code segment
	main: 
		mov ax, stack
		mov ss, ax
		mov sp, 16   ; ss:sp 指向栈底
		
		mov ax, 1
		push ax
		mov ax, 3
		push ax
		call sub1

		mov ax, 4c00h
		int 21h

	; 子程序:实现 (a - b) 的三次方
	sub1:
		push bp
		mov bp, sp
		mov ax, [bp + 4]   ; 将栈中 a 的值送入 ax
		sub ax, [bp + 6]   ; ax 减去栈中 b 的值
		mov bp, ax
		mul bp
		mul bp
		pop bp
		ret 4

code ends
end main

分析程序执行时栈中的情况:

x86汇编_子程序的参数和结果传递1.png


子程序和主程序寄存器冲突问题

在子程序中使用的寄存器,很可能在主程序中也会使用,如果按照以前的方法来编写代码,就可能造成子程序和主程序发生寄存器冲突

  • 首先以一个错误的例子来说明

例如:在前面“将一个全是字母的字符串转化为大写”这个例子的基础上,变为“将一个全是字母,并且以 0 结尾的字符串转化为大写”

区别在于:这样可以通过 0 来判断是否已经处理完整个字符串,而不需要提前知道字符串的具体长度

assume cs:code, ds:data

data segment
		db 'ctfa', 0
		db 'ctfb', 0
		db 'ctfc', 0
		db 'ctfd', 0
data ends

code segment
	main: 
		mov ax, data
		mov ds, ax
		mov bx, 0
		
		mov cx, 4   ; 循环处理四组字符串
	s:	mov si, bx
		call sub1
		add bx, 5   ; 每处理完一组字符串,bx 后移 5,指向下一组字符串的首字母
		loop s

		mov ax, 4c00h
		int 21h


	; 子程序:将字符串转换为大写,以 0 结束
	sub1:
		mov cl, [si]   ; CX 的低位用来记录每组字符串的 byte 型数据
		mov ch, 0   ; CX 的高位补 0,即:将每组字符串的 byte 型数据凑足 word 存入 CX
		jcxz ok   ; 如果 CX 为 0 则说明一组字符串已经处理完,结束本次子程序
		and byte ptr [si], 11011111b   ; 将字母转换为大写
		inc si
		jmp short sub1
	
	ok:	ret

code ends
end main

在这个程序中,主程序使用 CX 作为 loop 循环次数的计数器,子程序使用 CX 存放字符串的数据,并通过 jcxz 判断 CX 是否为 0 作为结束条件,导致主程序与子程序 CX 寄存器发生冲突

如果在编写主程序时就得考虑到所有的子程序中使用了哪些寄存器,显然不现实

因此,可以在子程序的开头将子程序中用到的所有寄存器内容提前保存到栈中,等子程序结束时再从栈中恢复

  • 基于栈保存子程序寄存器的思想,改正上面的示例:
assume cs:code, ds:data

data segment
		db 'ctfa', 0
		db 'ctfb', 0
		db 'ctfc', 0
		db 'ctfd', 0
data ends

code segment
	main: 
		mov ax, data
		mov ds, ax
		mov bx, 0
		
		mov cx, 4   ; 循环处理四组字符串
	s:	mov si, bx
		call sub1
		add bx, 5   ; 每处理完一组字符串,bx 后移 5,指向下一组字符串的首字母
		loop s

		mov ax, 4c00h
		int 21h


	; 子程序:将字符串转换为大写,以 0 结束
	sub1:
		push cx   ; 提前将子程序中用到的寄存器保存在栈上
		push si

	sub1_start:
		mov cl, [si]   ; CX 的低位用来记录每组字符串的 byte 型数据
		mov ch, 0   ; CX 的高位补 0,即:将每组字符串的 byte 型数据凑足 word 存入 CX
		jcxz ok   ; 如果 CX 为 0 则说明一组字符串已经处理完,结束本次子程序
		and byte ptr [si], 11011111b   ; 将字母转换为大写
		inc si
		jmp short sub1_start
	
	ok:	
		pop si   ; 注意入栈和出栈的顺序
		pop cx
		ret

code ends
end main

运算指令

and 和 or

逻辑与、逻辑或指令,按位进行计算

用法:
and ax, bx
or ax, bx

  • 例如:
mov al, 01100011b
and al, 00111011b
; 执行后 al = 00100011b

mov al, 01100011b
or al, 00111011b
; 执行后 al = 01111011b
  • and 指令可以将操作对象的对应位设为 0,其他位不变
and al, 11111110b   ; 将 al 的第 0 位设为 0
and al, 10111111b   ; 将 al 的第 6 位设为 0
and al, 01111111b   ; 将 al 的第 7 位设为 0
  • or 指令可以将操作对象的对应位设为 1,其他位不变
and al, 00000001b   ; 将 al 的第 0 位设为 1
and al, 01000000b   ; 将 al 的第 6 位设为 1
and al, 10000000b   ; 将 al 的第 7 位设为 1

xor

异或指令。在两个操作数的对应位之间进行按位逻辑异或操作,并将结果存放在目标操作数中。如果两个位的值相同(同为 0 或同为 1),则结果位等于 0;否则结果位等于 1

用法:
xor 寄存器 寄存器
xor 寄存器 立即数

  • 例如:

    mov al, 01100011b
    xor al, 00111011b
    ; 执行后 al = 01011000b
  • xor 指令还常用于寄存器的清零操作:

xor ax, ax

以上代码将 AX 寄存器清零,等价于:

mov ax, 0

问:为什么不用 mov ax, 0 来清零,要使用 xor ax, ax 呢?
因为他们虽然作用相同,但是 xor 指令比 mov 指令的机器码要短,实现效率更高


div

除法指令。需注意除数的长度,有 8 位和 16 位两种

用法:
div 寄存器
div 内存单元

  • 使用 div 指令做除法时,注意:
    1. 除数:有 8 位和 16 位两种
      存放在一个寄存器或内存单元中
    2. 被除数:默认在 AX(被除数 16 位)或 AX 和 DX(被除数 32 位)中
      如果除数为 8 位,则被除数为 16 位,在 AX 中
      如果除数为 16 位,则被除数为 32 位,在 DX 和 AX 中,DX 存放高 16 位,AX 存放低 16 位
    3. 结果:默认在 AL 和 AH(除数 8 位)或 AX 和 DX(除数 16 位)中
      如果除数为 8 位,则 AL 存放商,AH 存放余数
      如果除数为 16 位,则 AX 存放商,DX 存放余数
被除数被除数位置除数除数位置商位置余数位置
16 位AX8 位8 位寄存器或内存字节单元ALAH
32 位DX(高 16 位)
AX(低 16 位)
16 位16 位寄存器或内存字节单元AXDX
指令含义
div byte ptr ds:[0](al) = (ax) / ((ds) * 16 + 0) 的商
(ah) = (ax) / ((ds) * 16 + 0) 的余数
div word ptr es:[0](ax) = [(dx) * 10000h + (ax)] / ((es) * 16 + 0) 的商
(dx) = [(dx) * 10000h + (ax)] / ((es) * 16 + 0) 的余数
div byte ptr [bx + si + 8](al) = (ax) / ((ds) * 16 + (bx) + (si) + 8) 的商
(ah) = (ax) / ((ds) * 16 + (bx) + (si) + 8) 的余数
div word ptr [bx + si + 8](ax) = [(dx) * 10000h + (ax)] / ((ds) * 16 + (bx) + (si) + 8) 的商
(dx) = [(dx) * 10000h + (ax)] / ((ds) * 16 + (bx) + (si) + 8) 的余数
  • 例如:计算 1001 / 100
    1. 被除数 1001 < 65535,未超过 16 位,可以单独存放在 AX 中
    2. 除数 100 < 255,没有超过 8 位,但是由于被除数是 16 位,因此除数应该为 8 位
    3. 计算完成后,AL 用来存放商,AH 用来存放余数
mov ax, 1001   ; 存放被除数 1001
mov bl, 100   ; 存放除数 100
div bl

x86汇编_运算指令-div2.png

  • 例如:计算 100001 / 100
    1. 被除数 100001 > 65535,超过了 16 位,因此不能单独用 AX 存放,需要同时使用 DX 和 AX 来存放 100001,也就是说被除数为 32 位,需要进行 16 位的除法
    2. 除数 100 < 255,没有超过 8 位,但是由于被除数是 32 位,因此除数应该为 16 位,不能用 8 位寄存器存放 100,而要使用 16 位寄存器存放
    3. 计算完成后,AX 用来存放商,DX 用来存放余数
mov dx, 1   ; 存放被除数 100001 的高十六位,即:0000 0000 0000 0001b = 1h
mov ax, 86A1h   ; 存放被除数 100001 的低十六位,即:1000 0110 1010 0001b = 86A1h

; (dx) * 10000h + (ax) = 100001

mov bx, 100   ; 存放除数 100
div bx

x86汇编_运算指令-div1.png


mul

乘法指令。需注意两个乘数的长度,有 8 位和 16 位两种

用法:
mul 寄存器
mul 内存单元

  • 使用 mul 指令做乘法时,注意:
    1. 乘数:有 8 位和 16 位两种,要么都是 8 位,要么都是 16 位
      如果是 8 位,一个默认在 AL 中,另一个在 8 位寄存器或字节单元中
      如果是 16 位,一个默认在 AX 中,另一个在 16 位寄存器或字节单元中
    2. 结果
      如果 8 位乘法,则存放在 AX
      如果 16 位乘法,则高 16 位存放在 DX,低 16 位存放在 AX
乘数1乘数1 位置乘数2乘数2 位置结果位置
8 位AL8 位8 位寄存器或内存字节单元AX
16 位AX16 位16 位寄存器或内存字节单元DX(高 16 位)
AX(低 16 位)
指令含义
mul byte ptr ds:[0](ax) = (al) * ((ds) * 16 + 0)
mul word ptr [bx + si + 8](ax) = (ax) * ((ds) * 16 + (bx) + (si) + 8) 的低 16 位
(dx) = (ax) * ((ds) * 16 + (bx) + (si) + 8) 的高 16 位
  • 例如:计算 100 * 10
    1. 乘数 100 和 10 都小于 255,可以做 8 位乘法,因此一个在寄存器 AL 中,另一个假设在 8 位寄存器 BL 中
mov al, 100   ; 存放乘数 100
mov bl, 10   ; 存放乘数 10
mul bl

x86汇编_运算指令-mul1.png

  • 例如:计算 100 * 10000
    1. 乘数 100 < 255,但 10000 > 255,所以必须做 16 位乘法,因此一个在寄存器 AX 中,另一个假设在 16 位寄存器 BX 中
mov ax, 100   ; 存放乘数 100
mov bx, 10000   ; 存放乘数 10000
mul bx

x86汇编_运算指令-mul2.png


adc

带进位加法指令。利用了 FLAG 寄存器中 CF 位上记录的进位值

用法:
adc ax, bx

  • 指令 adc ax, bx 相当于执行:
(ax) = (ax) + (bx) + CF
  1. 例如:
mov ax, 2
mov bx, 1
sub bx, ax   ; 最高位借 1,所以 CF = 1
adc ax, 1   ; ax = 2 + 1 + 1 = 4
  1. 例如:
mov ax, 1
add ax, ax   ; 最高位无借位和进位,CF = 0
adc ax, 3   ; ax = 2 + 3 + 0 = 5
  1. 例如:
mov al, 98h
add al, al   ; 最高位进 1,所以 CF = 1,al 丢掉进位后为 00110000b = 30h
adc al, 3   ; al = 30h + 3 + 1 = 34h

可以看出 adc 指令与 add 指令的区别就在于 adc 会加上 CF 中的值

但是 CPU 提供这条指令有什么作用呢?

  • 例如:计算 0198h + 0183h
   01 98
+  01 83
   -----
   03 1B

可见加法是可以分为两步进行的:
低位相加
高位相加再加上低位产生的进位值

也就是说 add ax, bx 等价于:

add al, bl
adc ah, bh

由于寄存器的长度限制,例如计算:1EF000h + 201000h 时,两个操作数以及结果都超过了 16 位,add 指令是无法实现求和的

此时,就可以利用 adc 指令配合 add 指令,将一个加法分为两步,即可解决这个问题

  • 例如:计算 1EF000h + 201000h,并将结果的高位放在 AX 中,低位放在 BX 中
mov ax, 001Eh   ; 存放第一个操作数的高位
mov bx, 0F000h   ; 存放第一个操作数的低位

add bx, 1000h   ; 使用 add 指令将两个操作数的低位相加
adc ax, 0020h   ; 使用 adc 指令将两个操作数的高位相加

另外,由于 adc 指令也可能产生进位值,也会对 CF 位进行设置

这样一来,就可以对任意大的数据进行加法运算

  • 例如:计算 1EF0001000h + 2010001EF0h,并将结果的最高 16 位放在 AX 中,次高 16 位放在 BX 中,低 16 位放在 CX 中
mov ax, 001Eh   ; 存放第一个操作数的最高 16 位
mov bx, 0F000h   ; 存放第一个操作数的次高 16 位
mov cx, 1000h   ; 存放第一个操作数的低 16 位

add cx, 1EF0h   ; 使用 add 指令将两个操作数的低 16 位相加
add bx, 1000h   ; 使用 adc 指令将两个操作数的次高 16 位相加
adc ax, 0020h   ; 使用 adc 指令将两个操作数的最高 16 位相加
  • 例如:编程实现一个子程序,用来对两个 128 位的数据进行相加
add128:
		push ax
		push cx
		push si
		push di

		sub ax, ax   ; 将 CF 设置为 0

		mov cx, 8   ; 循环 8 次,每次处理 16 位
	s:	mov	ax, [si]   ; ds:si 指向第一个数的内存单元
		adc ax, [di]   ; ds:di 指向第二个数的内存单元
		mov [si], ax   ; 将结果存放在第一个数的内存单元中
		inc si
		inc si
		inc di
		inc di
		loop s

		pop di
		pop si
		pop cx
		pop ax
		ret

注意:上面的例子中,inc 指令和 loop 指令不影响 CF 位

如果将两个 inc si 和两个 inc di 写成 add si, 2add di, 2 可能会造成 add 指令改变 CF 位导致计算错误


sbb

带借位减法指令。利用了 FLAG 寄存器中 CF 位上记录的借位值

用法:
sbb ax, bx

  • 指令 sbb ax, bx 相当于执行:
(ax) = (ax) - (bx) - CF
  • 与 adc 指令类似,sbb 指令也可以对任意大的数据进行减法运算

  • 例如:计算 003E1000h - 00202000h,结果的高位放在 AX 中,低位放在 BX 中

mov ax, 003Eh   ; 存放第一个操作数的高位
mov bx, 1000h   ; 存放第一个操作数的低位

sub bx, 2000h   ; 使用 sub 指令将两个操作数的低位相减
sbb ax, 0020h   ; 使用 sbb 指令将两个操作数的高位相减

cmp

比较指令。功能相当于减法指令,但是不保存结果,会对标志寄存器产生影响,其他相关指令可以通过识别这些标志寄存器位来进行相关操作

用法:
cmp ax, bx

  • 指令 cmp ax, bx 相当于执行:
(ax) - (bx) 但结果不在 ax 中保存
  • 例如:
mov ax, 52
cmp ax, ax
; 执行后,ax = 52,zf = 1,pf = 1,sf = 0,cf = 0,of = 0

mov ax, 8
mov bx, 3
cmp ax, bx
; 执行后,ax = 8,zf = 0,pf = 1,sf = 0,cf = 0,of = 0

如果 ax = bx,则 ax - bx = 0,ZF = 1
如果 ax ≠ bx,则 ax - bx ≠ 0,ZF = 0
如果 ax < bx,则 ax - bx < 0,将产生借位,CF = 1,ZF = 0
如果 ax > bx,则 ax - bx > 0,不必借位,CF = 0,ZF = 0

注意:
SF 位记录结果是否为负,但执行 cmp ax, bx 后 SF = 1 并不能说明 ax < bx

  • 例如:两个有符号数进行减法运算
ah = 22h
bh = 0A0h
sub ah, bh

$[22h]_补$ = 00100010
$[-0A0h]_补$ = 01100000

  00100010
+ 01100000
  --------
  10000010

这里 10000010b = 82h,即 22h - 0A0h = 34 - (-96) = 130 = 82h(注意:82h 是 -126 的补码)
计算结果为负,所以 SF = 1
但明显 34 > -96

因此,单独根据 SF 来判断结果是否为负显然不对,原因在于计算过程中可能会发生溢出

执行 cmp ax, bx 后,若要说明 ax < bx,需结合 SF 和 OF 来判断:

  1. SF = 1,OF = 0
    ① OF = 0 说明没有溢出,计算结果的正负 = 真正结果的正负
    ② SF = 1 说明计算的结果为负
    ③ 所以,真正的结果为负,ax < bx
  2. SF = 1,OF = 1
    ① OF = 1 说明溢出,计算结果的正负 ≠ 真正结果的正负
    ② SF = 1 说明计算的结果为负
    ③ 所以,由于溢出导致了计算的结果为负,则真正的结果为正,ax > bx
  3. SF = 0,OF = 1
    ① OF = 1 说明溢出,计算结果的正负 ≠ 真正结果的正负
    ② SF = 0 说明计算的结果为非负,有溢出说明计算的结果非 0,因此计算的结果为正
    ③ 所以,由于溢出导致了计算的结果为正,则真正的结果为负,ax < bx
  4. SF = 0,OF = 0
    ① OF = 0 说明没有溢出,计算结果的正负 = 真正结果的正负
    ② SF = 0 说明计算的结果为非负
    ③ 所以,真正的结果为非负,ax ≥ bx