汇编语言基础学习

前言

记录汇编语言的基础知识。

寄存器

通用寄存器

又叫作数据寄存器,用于存放一般性的数据,例如操作数和运算结果,通常有四个:

  • 64位:RAX、RBX、RCX、RDX
  • 32位:EAX、EBX、ECX、EDX
  • 16位:AX、BX、CX、DX

其中四个通用寄存器的一般作用如下

  • AX:累加寄存器,常用于运算;
  • BX:基址寄存器,常用于地址索引;
  • CX:计数寄存器,常用于计数;
  • DX:数据寄存器,常用于数据传递;

变址寄存器

主要用于存放存储单元在段内的偏移量,有两个:

  • 64位:RSI、RDI
  • 32位:ESI、EDI
  • 16位:SI、DI

SI(Source Index):源变址寄存器,可用来存放相对于DS段的源变址指针;

DI(Destination Index):目的变址寄存器,可用来存放相对于ES段的目的变址指针;

SI、DI不能够分成两个8位寄存器来使用;

指针寄存器

主要用于存放堆栈内存储单元的偏移量 ,用它们可实现多种存储器操作数的寻址方式,有两个:

  • 64位:RBP、RSP

  • 32位:EBP、ESP

  • 16位:BP、SP

BP为基指针(BasePointer)寄存器,用它可直接存取堆栈中的数据;

SP为堆栈指针(StackPointer)寄存器,用它只可访问栈顶,存储栈顶的偏移地址,SS:SP指向栈顶;

段寄存器

  • 段寄存器是根据内存分段的管理模式而设置的;

  • 内存单元的物理地址由段寄存器的值和一个偏移量值组合而成的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址;

段寄存器包括:

  • CS代码段寄存器(CodeSegmentRegister),其值为代码段的地址

  • DS数据段寄存器(DataSegmentRegister),其值为数据段的地址

  • SS堆栈段寄存器(StackSegmentRegister),其值为堆栈段的段地址

  • ES附加段寄存器(ExtraSegmentRegister),其值为附加数据段的地址

  • FS附加段寄存器(ExtraSegmentRegister),其值为附加数据段的地址

  • GS附加段寄存器(ExtraSegmentRegister),其值为附加数据段的地址

指令指针寄存器

存放下次将要执行的指令在代码段的偏移量,配合CS寄存器一起使用CS:IP,只有一个:

  • 64位:RIP
  • 32位:EIP
  • 16位:IP

标志寄存器(flag)

  • 标志寄存器的作用:

    1. 用来存储相关指令的某些执行结果;
    2. 用来为CPU执行相关指令提供行为依据;
    3. 用来控制CPU的相关工作方式;
  • flag寄存器按位起作用,每一位都有专门的含义

ZF标志位

  • 第6位,零标志位;

  • 它记录相关指令执行后,其结果是否为0:

    • 如果结果为0,那么zf=1
    • 如果结果不为0,那么zf=0
  • 例如:

    1
    2
    3
    mov ax,1
    sub ax,1
    ; 执行后结果为0,zf=1

PF标志位

  • 第2位,奇偶标志位;

  • 它记录相关指令执行后,其结果的所有bit位中1的个数是否位偶数:

    • 如果1的个数为偶数,pf=1;
    • 如果为奇数,那么pf=0;
  • 例如:

    1
    2
    3
    mov ax,1
    add al,10
    ; 执行后,结果为00001011B,其中有3个1,则pf=0

SF标志位

  • 第7位,符号标志位;

  • 它记录相关指令后,其结果是否为负:

    • 如果结果为负,sf=1;
    • 如果不为负,sf=0;
  • 计算机通常用补码来表示有符号数据。计算机中的一个数据可以看做是有符号数,也可以看做成无符号数。

    比如:
    00000001B,可以看做无符号数1,或有符号数+1;
    10000001B,可以看做无符号数129,也可以看作有符号数-127;
    不管我们如何看待,CPU在执行add等指令的时候,就已经包含了两种含义,也将得到同一种信息来记录的两种结果。关键在于程序需要哪一种结果;

  • 例如:

    1
    2
    3
    4
    5
    6
    mov al,10000001B
    add al,1
    ; 执行后,结果为10000010B,sf=1,表示:如果指令进行的是有符号数运算,那么结果为负;
    mov al,100000001B
    add al,011111111B
    ; 执行后,结果为0,sf=0,表示:如果指令进行的是有符号数运算,那么结果为非负;
  • 某些指令将影响寄存器中的多个标记位,这些被影响的标记位比较全面地记录了指令的执行结果,为相关的处理提供了所需的依据。比如指令sub al,al执行后,ZF、PF、SF等标志位都要受到影响,它们分别为:1,1,0;

CF标志位

  • 第0位,进位标志位;

  • 一般情况下,在进行无符号运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位;

  • 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    ; 进位
    mov al,98H
    add al,al ; 执行后:(al)=30H,CF=1,CF记录了从最高有效位向更高位的进位值
    add al,al ; 执行后:(al)=60H,CF=0
    ; 借位
    mov al,97H
    sub al,98H ; 执行后(al)=FFH,CF=1,CF记录了向更高位的借位值
    sub al,al ; 执行后(al)=0,CF=0,CF记录了向更高位借位值

OF标志位

  • 第11位,溢出标志位;

  • 一般情况下,OF记录了有符号数运算的结果是否发生了溢出:

    • 如果发生溢出,OF=1;
    • 如果没有,OF=0;
  • 例如:

    1
    2
    3
    4
    mov al,F0H
    add al,88H
    ; 执行后:CF=1,OF=0.对于无符号运算,F0H+78H有进位,CF=1;
    ; 对于有符号数运算,F0H+78H不发生溢出,OF=0

AF标志位

  • 第四位,辅助标志位;
  • 反应运算结果低四位产生进位或错位的情况;

DF标志位

  • 第10位,方向标志位;
  • 在串处理指令中,控制每次操作后si,di的增减:
    • df=0,每次操作后si、di递增;
    • df=1,每次操作后si、di递减;

TF标志位

  • 第8位,单步标志位;
  • 用于程序跟踪调试:
    • 当TF=1,CPU进入单步方式;

IF标志位

  • 第9位,中断允许位
    • 当IF=1时,CPU为开中断;
    • 当IF=0时,CPU为关中断;

寻址方式

立即寻址

操作数包含在指令中,它作为指令的一部分,跟在操作码后存放在代码段;

1
2
3
mov ax,idata
# example
mov ax,1234h

寄存器寻址

指令所要的操作数已存储在某寄存器中,直接对寄存器进行操作;

1
2
3
mov 寄存器1,寄存器2
# example
mov si,ax

直接寻址

操作数在存储器中,指令直接包含有操作数的有效地址;

由于操作数一般存放在数据段,所以操作数的地址由DS加上指令中给出的16位偏移得到;

1
2
3
4
5
mov 寄存器,[idata]
EA = idata # 偏移地址
SA = (ds) # 段地址
# example
mov ax,[1234h] # 假设DS内容是50000H,地址为51234H字存储单元中的内容时6789H,那么在执行“MOV AX, [1234H]”后寄存器AX的内容是6789H

寄存器间接寻址

操作数在存储器中,操作数有效地址在SI、DI、BX、BP这四个寄存器之一中;

在一般情况下:

  • 如果有效地址在SI、DI和BX中,则以DS段寄存器的内容为段值;
  • 如果有效地址在BP中,则以SS段寄存器的内容为段值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[bx]
EA = (bx)
SA = (ds)
[si]
EA = (si)
SA = (ds)
[di]
EA = (di)
SA = (ds)
[bp]
EA = (bp)
SA = (ss)
# example
MOV ax,[si] #自动引用DS作为段寄存器,假设(DS)= 50000H,(SI)= 1234H,那么存储的物理存储单元地址是51234H,再假设该字存储单元的内容是6789H,那么在执行该指令后,(AX)= 6789H

寄存器相对寻址

操作数在存储器中,操作数的有效地址是一个基址寄存器(BX、BP)或变址寄存器的(SI、DI)内容加上指令中给定的8位或16位偏移量之和;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[bx+idata]
EA = (bx)+idata
SA = (ds)
[si+idata]
EA = (si)+idata
SA = (ds)
[di+idata]
EA = (di)+idata
SA = (ds)
[bp+idata]
EA = (bp)+idata
SA = (ss)
# example
MOV ax, [di+1223h] #自动引用DS作为段寄存器,假设(DS)= 50000H,(DI)= 3678H,那么,存储的物理存储单元地址是5489BH,再假设该字存储单元的内容是55AAH,那么在执行该指令后,(AX)= 55AAH

基址变址寻址

操作数在存储器中,操作数的有效地址由基址寄存器之一的内容与变址寄存器之一的内容相加得到;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[bx+si]
EA = (bx+si)
SA = (ds)
[bx+di]
EA = (bx+di)
SA = (ds)
[bp+si]
EA = (bp+si)
SA = (ss)
[bp+di]
EA = (bp+di)
SA = (ss)
# example
MOV ax,[bp+di] # 自动引用SS作为段寄存器,假设(SS)= 50000H,(BP)= 1223H, (DI)= 54H,那么,存储的物理存储单元地址是51277H,再假设改字存储单元的内容是168H,那么在执行该指令后,(AX)= 168H

相对基址变址寻址

操作数在存储器中,操作数的有效地址由基址寄存器之一的内容与变址寄存器之一的内容及指令中给定的偏移量相加得到;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[bx+si+idata]
EA = (bx+si)+idata
SA = (ds)
[bx+di+idata]
EA = (bx+di)+idata
SA = (ds)
[bp+si+idata]
EA = (bp+si)+idata
SA = (ss)
[bp+di+idata]
EA = (bp+di)+idata
SA = (ss)
# example
MOV ax, [bx+di-2] # 自动引用DS作为段寄存器,假设,(DS)= 50000H,(BX)= 1223H, (DI)= 54H,那么,存取的物理存储单元地址是51275H,再设该字存储单元的内容是7654H,那么在执行该指令后,(AX)= 7654H
# 以下四种表示方法均是等价的
MOV AX,[BX+DI+1234H]
MOV AX,1234H[BX+DI]
MOV AX,1234H[BX][DI]
MOV AX,1234H[DI][BX]

指令

CPU指令执行过程

  1. 从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲区;
  2. (IP) = (IP)+所读取指令的长度(8位地址,因为范围是-128~127),从而指向下一条指令;
  3. 执行指令;
  4. 转到步骤1,重复这个过程;

mov指令

MOV指令可以在CPU内或CPU和存储器之间传送字或字节;

它传送的信息可以从

  • 寄存器到寄存器,mov ax,bx
  • 立即数到寄存器,mov ax,8
  • 立即数到存储单元,mov [0],8
  • 从存储单元到寄存器,mov ax,[0]
  • 从寄存器到存储单元,mov [0],ax
  • 从寄存器或存储单元到除CS外的段寄存器 (注意立即数不能直接送段寄存器),mov ds,axmov ds,[0]
  • 从段寄存器到寄存器或存储单元,mov ax,dsmov [0],ds

注意

  • MOV指令中的源操作数绝对不能是立即数和代码段CS寄存器;
  • MOV指令中绝对不允许在两个存储单元之间直接传送数据;
  • MOV指令中绝对不允许在两个段寄存器之间直接传送数据;
  • MOV指令不会影响标志位;

add指令

用于实现数据加法运算,操作和要求与mov相同;

sub指令

用于实现数据减法运算,操作和要求与mov相同;

div指令

div是除法指令,使用div做除法的时候应注意以下问题:

  1. 除数:有8位和16位两种,在一个寄存器或者内存中。

  2. 被除数:默认放在AX或(DX和AX)中,如果除数为8位,被除数为16位,被除数默认在AX中存放,如果除数为16位,被除数为32位,被除数则在(DX和AX)中存放,DX存放高16位,AX存放低16位。

  3. 结果:如果除数是8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数是16位,则AX存储除法操作的商,DX存储除法操作的余数。

    被除数 除数 余数
    AX reg/mem8 AL AH
    DX:AX reg/mem16 AX DX
    EDX:EAX reg/mem32 EAX EDX
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ; 8位无符号除法
    mov ax, 0083h ; 被除数
    mov bl, 2 ; 除数
    div bl ; AL = 41h, AH = Olh
    ; 16位无符号除法
    mov dx, 0 ; 清除被除数高16位
    mov ax, 8003h ; 被除数的低16位
    mov ex, 100h ; 除数
    div ex ; AX = 0080h, DX = 0003h

mul指令

  • mul 是乘法指令;

  • 两个相乘的数:两个相乘的数,要么都是 8 位,要么都是 16 位;

    • 如果是 8 位,一个默认放在 AL 中,另一个放在 8 位寄存器或内存字节单元中;
    • 如果是 16 位,一个默认在 AX 中,另一个放在 16 位寄存器或内存字单元中;
  • 结果:如果是 8 位乘法,结果默认放在 AX 中;如果是 16 位乘法,结果高位默认在 DX 中存放,低位在 AX 中存放;

  • 格式:

    1
    2
    mul reg
    mul 内存单元
  • 内存单元可以用不同的寻址方式给出

    1
    2
    3
    4
    5
    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:

      100 和 10 小于 255,可以做 8 位乘法;

      1
      2
      3
      4
      mov al,100
      mov bl,10
      mul bl
      ; 结果(ax)=1000(03E8H)
    • 100*10000

      100 小于 255,10000 大于 255,要做 16 位乘法;

      1
      2
      3
      4
      mov ax,100
      mov bx,10000
      mul bx
      ; 结果 (ax)=4240H,(dx)=000FH,(1000000=F4240H)

pushpop指令

push指令和pop指令用于实现寄存器/内存单元和栈空间之间传送数据,push用于数据入栈,pop用于数据出栈;

具体指令为

1
2
3
4
5
6
7
8
9
push	寄存器/段寄存器/内存空间
pop 寄存器/段寄存器/内存空间
# example
push ax
push ds
push [0]
pop bx
pop es
pop [2]
  • push指令执行步骤:
    1. SP=SP-2;
    2. 向SS:SP指向的字单元送入数据;
  • pop指令执行步骤:
    1. 从SS:SP指向的字单元中读取数据;
    2. SP=SP+2;

补充栈知识:

  • 从高地址向低地址存数据,即入栈时,栈顶从高地址向低地址增长;
  • 当栈为空时,SP指向栈的最底部单元的上一个单元(高地址在底部);
  • 栈顶地址通过SS:SP获取;
  • 栈越界包括PUSH越界和POP越界;

loop指令

作用是循环执行某段指令;

格式如下

1
2
3
4
     mov cx, 循环的次数 # 当遇到Loop标号时 cx就代表循环的次数
标号: 循环执行的程序代码
......
Loop 标号

loop指令执行流程:

  1. cx=cx-1
  2. 判断cx的值:
    1. 如果不为零,就执行标号处的代码,然后执行第一步;
    2. 如果为零, 执行loop后面的代码;

loop的实现其实就是判断cx>0然后jump到标号所在地址;

1
2
3
4
5
6
7
8
9
10
    mov ax, ffffh 
mov ds, ax # 数据段段地址
mov dx, 0h
mov bx, 0h # 清空dx和bx 用来接收数据
mov cx, 3h # 设置循环次数为3
s: mov al, [bx] # 循环取ds:[bx]中的数据, 赋值给al
mov ah, 0h # 清空ah中的数据, 用来累加的时候进位
add dx, ax # dx = ax + dx
add bx, 1 # bx = bx + 1
loop s # 循环s标记下的代码

and指令

逻辑与指令,按位进行逻辑与运算;

1
2
mov al,01100011B
and al,00111011B

结果为al=00100011B

or指令

逻辑或指令,按位进行逻辑或运算;

1
2
mov al,01100011B
or al,00111011B

结果为al=01111011B

db/dw/dd指令

伪指令,由编译器识别处理;

db定义字节型数据,define byte

dw定义字型数据,define word

dd定义双字型数据,define double word

1
2
3
4
5
6
; example
data segment
db 1
dw 1
dd 1
data ends

在data段定义了3个数据:

第一个数据为01H,在data:0,占1个字节;

第二个数据为0001H,在data:1,占1个字,即2个字节;

第三个数据为00000001H,在data:3,占2个字,即4个字节;

dup指令

伪指令,与dd,dw,db配合使用,用来重复定义数据,由编译器识别处理;

1
2
3
db 3 dup (0)	; 定义了三个字节,它们的值都是0,相当于:db 0,0,0
db 3 dup (0,1,2) ; 定义了九个字节,它们的值分别是0,1,2,0,1,2,0,1,2,相当于:db 0,1,2,0,1,2,0,1,2
db 3 dup ('abc','ABC') ; 定义了18个字节,它们的值是'abcABCabcABCabcABC',相当于:db 'abcABCabcABCabcABC'

dup使用格式如下:

1
2
3
db 重复的次数 dup (重复的字节型数据)
dw 重复的次数 dup (重复的字型数据)
dd 重复的次数 dup (重复的双字型数据)

offset指令

伪指令,由编译器识别处理;

用于返回数据标号的偏移地址([bx,bp,si,di]),这个偏移地址按字节计算,表示的是该数据标号距离数据段起始地址的距离;

1
2
3
4
5
6
7
8
9
10
11
12
; 该程序在运行中将s处的一条指令复制到s0处
assume cs:codesg
codesg segment
s: mov ax,bx ;(mov ax,bx 的机器码占两个字节)
mov si,offset s
mov di,offset s0
mov ax,cs:[si]
mov cs:[di],ax
s0: nop ;(nop的机器码占一个字节)
nop
codesg ends
ends

jmp指令

依据位移进行转移的jmp指令

无条件转移,可以只修改IP,也可以同时修改CS和IP;

jmp指令要给出两种信息:

  • 转移的目的地址
  • 转移的距离(段间转移、段内短转移,段内近转移)
段内短转移

格式:jmp short 标号(转到标号处执行指令)

这种格式的 jmp 指令实现的是段内短转移,它对IP的修改范围为 -128~127,也就是说,它向前转移时可以最多越过128个字节,向后转移可以最多越过127个字节;

short符号说明指令进行的是短转移;

标号是指代码段中的标号,指明了指令要转移的目的地址,指令转移结束后,CS:IP应该指向标号处的指令;

1
2
3
4
5
6
7
8
9
10
assume cs:codesg
codesg segment
start:mov ax,10h
jmp short s
mov ax,0h
s:inc ax
mov ax,4c00h
int 21h
codesg ends
end

程序执行后,ax的值为11h,因为执行jmp short s后,直接越过mov ax,0hIP指向了标号s处的inc ax

实际上,jmp short 标号的功能为:(IP)=(IP)+8位位移

  • 8位位移=标号后处的地址-jmp指令后第一个字节的地址;

  • short指明此处的位移是8位位移;

  • 8位位移的范围为-128~127,用补码表示;

  • 8位位移由编译程序在编译时算出,如下图;

    机器码EB03中的03即上述提到的8位位移;

段内近转移

格式:jmp near ptr 标号(转到标号处执行指令),与段内短转移用法基本相同;

功能为:(IP)=(IP)+16位位移,对IP的修改范围为-32768~32767;

  • 16位位移=标号后处的地址-jmp指令后第一个字节的地址;
  • near ptr指明此处的位移是16位位移,进行的是段内近转移;
  • 16位位移的范围为-32768~32767,用补码表示;
  • 16位位移由编译程序在编译时算出;

转移的目的地址在指令中的jmp指令

前面的两个jmp指令对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。

段间转移

格式:jmp far ptr 标号(转到标号处执行指令),实现段间转移,又称为远转移;

功能:(CS)=标号所在段的段地址;(IP)=标号所在段中的偏移地址;
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP;

机器码EA0B012B0B中的0B012B0B即CS、IP的内容;

转移地址在寄存器中的jmp指令

格式:jmp 16位reg

功能:(IP)=(16位reg)

1
2
3
mov ax,2233H
jmp ax
; ax中的值会覆盖ip

转移地址在内存中的jmp指令

段内转移

格式:jmp word ptr 内存单元地址

功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址,内存地址的表示需要段地址和偏移地址;

1
2
3
4
mov ax,2233h
mov ds:[0],ax
jmp word ptr ds:[0] ; IP=ds:[0]的字型数据
; 执行之后IP=2233H
段间转移

格式:jmp dword ptr 内存单元地址

功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的地址,低地址处是转移的目的偏移地址

cs=内存单元地址+2
ip=内存单元地址

1
2
3
4
5
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0] ; ip=ds:[0]的字型数据,cs=ds:[2]的字型数据
; 执行之后,cs=0,ip=0123h cs:ip指向0000:0123

jcxz指令

有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址;

对 IP 的修改范围都为: -128~127;

格式:jcxz 标号 (如果 (cx)=0 ,转移到标号处执行)

操作: 当 (cx)=0时 ,(IP)=(IP)+8位位移

  • 8位位移 = 标号处的地址 - jmp指令后的第一个字节地址;

  • 8位位移的范围-128~127,用补码表示;

  • 8位位移是编译程序时在编译时算出的;

(cx)≠0 时 ,什么也不做(程序向下执行)

jcxz 标号 的功能相当于:if((cx)=0) jmp short 标号

ret/retf指令

调用栈中的数据;

  • ret指令用栈中的数据,修改IP的内容,从而实现近转移;

    • CPU执行ret指令时,进行下面两步操作:

      1. (IP)=((SS)*16+(SP))
      2. (SP)=(SP)+2
    • 相当于执行

      pop ip

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

  • retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;

    • CPU执行retf指令时,进行下面四步操作:

      1. (IP)=((SS)*16+(SP))
      2. (SP)=(SP)+2
      3. (CS)=((SS)*16+(SP))
      4. (SP)=(SP)+2
    • 相当于执行

      pop ip

      pop cs

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

call指令

  • CPU执行call指令时,进行两步操作:
    1. 将当前的IP或CS和IP压入栈中;
    2. 转移;
  • call指令与ret/retf配合使用实现压栈和出栈;
  • call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同;

依据位移进行转移的call指令

  • 格式:call 标号(将当前的IP压栈后,转到标号处执行指令)

  • CPU执行此种格式的call指令时,进行如下的操作:

    1. (sp)=(sp)-2

      ((ss)*16+(sp))=(IP)

    2. (IP)=(IP)+16位位移

      • 16位位移 = 标号处的地址 - call指令后的第一个字节地址;

      • 16位位移的范围-32768~32767,用补码表示;

      • 16位位移是编译程序时在编译时算出的;

  • call执行call 标号时,相当于进行:

    push IP

    jmp near ptr 标号

转移目的地址在指令中的call指令

  • 格式:call far ptr 标号,实现的是段间转移.

  • CPU执行此种格式的call指令时,进行如下的操作:

    1. (sp)=(sp)-2

      ((ss)*16+(sp))=(CS)

      (sp)=(sp)-2

      ((ss)*16+(sp))=(IP)

    2. (cs)=标号所在段的段地址

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

  • CPU执行call far ptr 标号时,相当于进行:

    push CS

    push IP

    jmp far ptr 标号

转移地址在寄存器中的call指令

  • 格式:call 16位reg

  • CPU执行此种格式的call指令时,进行如下的操作:

    (sp)=(sp)-2

    ((ss)*16+(sp))=(IP)

    (IP)=(16位reg)

  • CPU执行call 16位reg时,相当于:

    push IP

    jmp 16 位reg

转移地址在内存中的call指令

转移地址在内存中的call指令有两种格式;

call word ptr 内存单元地址
  • CPU执行时,相当于:

    push IP

    jmp word ptr 内存单元地址

call dword ptr 内存单元地址
  • CPU执行时,相当于:

    push CS

    push IP

    jmp word ptr 内存单元地址

adc指令

带进位加法指令,它利用了CF位上记录的进位值;

指令格式: adc 操作对象1,操作对象2

功能:操作对象1 = 操作对象1 + 操作对象2 + CF

例如:指令adc ax,bx实现的功能是(ax)=(ax)+(bx)+CF

1
2
3
4
5
mov ax,3
mov bx,1
sub bx,ax
adc ax,1
; 执行后,(ax)=4。adc执行时,相当于计算: (ax)+1+CF = 2+1+1 = 4。

sbb指令

带借位减法指令,它利用了CF位上记录的借位值;

指令格式:sbb 操作对象1,操作对象2

功能:操作对象1=操作对象1-操作对象2-CF

例如,指令sbb ax,bx实现的功能是(ax)=(ax)-(bx)-CF

sbb指令执行后,将对CF进行设置;

1
2
3
4
5
; 计算003E1000H-00202000H,结果放在ax,bx中
mov bx,1000H
mov ax,003EH
sub bx,2000H
sbb ax,0020H

cmp指令

cmp是比较指令,cmp的功能相当于减法指令(sub)。它不保存结果,只是影响相应的标志位,其他的指令通过识别这些被影响的标志位来得知比较结果;

指令格式: cmp 操作对象1,操作对象2

功能:计算操作对象1-操作对象2,但不保存结果,只是根据结果修改相应的标志位;

条件转移指令

此处的条件转移指令根据无符号数的比较结果进行转移;

指令 含义 检测的相关标志位
je 等于则转移 zf=1
jne 不等于则转移 zf=0
jb 低于则转移 cf=1
jnb 不低于则转移 cf=0
ja 高于则转移 cf=0 & zf=0
jna 不高于则转移 cf=1 | zf=1

j——jump

e——equal

b——below

a——above

n——not

movsb指令

格式:movsb

功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减;

表示操作如下

1
2
3
4
5
6
7
((es)*16+(di)) = ((ds)*16+(si))
if df = 0
(si) = (si) + 1
(di) = (di) + 1
if df = 1
(si) = (si) - 1
(di) = (di) - 1

movsw指令

格式:movsb

功能:将ds:si指向的内存单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2;

表示操作如下

1
2
3
4
5
6
7
mov es:[di],word ptr ds:[si]
if df = 0
add si,2
add di,2
if df = 1
sub si,2
sub di,2

rep指令

按照计数寄存器CX中指定的次数重复执行字符串指令;

1
2
3
4
rep movsb
; 相当于
s: movsb
loop s

cld/std指令

cld:将标志寄存器的df位置0;

std:将标志寄存器的df位置1;

pushfpopf指令

pushf:将标志寄存器的值压栈;

popf:从栈中弹出数据送入标志寄存器;

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

iret指令

中断返回,中断服务程序的最后一条指令;

iret指令将推入堆栈的段地址和偏移地址弹出,使程序返回到原来发生中断的地方,其作用是从中断中恢复中断前的状态;

iret指令的功能用汇编语言描述为

1
2
3
pop IP
pop CS
popf

int指令

引发中断,调用“中断例程”(interrupt routine);

格式:int n,n为中断类型码;

指令执行过程如下:

  1. 取中断类型码n;

  2. 标志寄存器入栈pushfIF=0,TF=0(重置中断标志位);

  3. CS、IP入栈;

  4. 查中断向量表, (IP)=(n*4)(CS)=(n*4+2)

    从此处转去执行n号中断的中断处理程序;

in/out指令

CPU对外设的操作通过专门的端口读写指令来完成;

读端口用in指令;

写端口用out指令;

例如:

1
2
3
4
5
6
7
8
IN AL,21H		; 表示从21H端口读取一字节数据到AL
IN AX,21H ; 表示从端口地址21H读取1字节数据到AL,从端口地址22H读取1字节到AH
MOV DX,379H
IN AL,DX ; 从端口379H读取1字节到AL
OUT 21H,AL ; 将AL的值写入21H端口
OUT 21H,AX ; 将AX的值写入端口地址21H开始的连续两个字节。(port[21H]=AL,port[22h]=AH)
MOV DX,378H
OUT DX,AX ; 将AH和AL分别写入端口379H和378H

shl/shr指令

shl是逻辑左移指令;

功能为:

  1. 将一个寄存器或内存单元中的数据向左移位;
  2. 将最后移出的一位写入CF中;
  3. 最低位用0补充;

例如:

1
2
3
mov al,01001000b
shl al,1 ; 将al中数据左移一位
; 执行后(al)=10010000b,CF=0

注意:如果移动位数大于1时,必须将移动位数放在cl中;

例如:

1
2
3
4
mov al,00110001b
mov cl,3
shl al,cl
; 执行后(al)=10001000b,因为最后移出的一位是1,所以CF=1

shr是逻辑右移指令,和shl所进行的操作刚好相反;

字/字节操作判断

  1. 通过寄存器名判断

    ax表示字操作;

    al表示字节操作;

  2. 通过操作符X ptr判断

    X在汇编指令中可以是wordbyte

    1
    2
    mov word ptr ds:[0],1
    add byte ptr [bx],2
  3. 默认访问

    有些指令默认访问字单元或者字节单元;

    比如push和pop只进行字操作;

中断

中断信息:任何一个通用 CPU 都具备一种能力, 可以在执行完当前正在执行的指令之后, 检测到从 CPU 外部发送过来的或者内部产生的一种特殊信息, 并且可以立即对所接受到的信息进行处理. 这种特殊的信息称为:中断信息。中断意味着 CPU 不再继续向下执行, 而是转去处理这个特殊的信息.

内中断

  1. CPU 内部产生的中断称为内中断

  2. 中断类型码

    中断类型码是中断来源信息的编码,在8086CPU中使用一个字节的长度来编码中断源;

    对于 8086CPU, 有四种中断信息,类型码表示如下;

    中断原因 中断类型码
    除法错误 0
    单步执行 1
    执行 into 指令 4
    执行 int 指令 n(int n,n为字节型立即数)
  3. 中断处理程序

    CPU 在收到中断信息之后,需要对中断进行处理,中断处理程序就是用来处理对应中断的程序,CPU 在收到中断信息之后, 就会转去执行对应的中断处理程序,中断处理程序由程序员编写;

    中断处理程序的常规步骤:

    1. 保存用到的寄存器;
    2. 处理中断;
    3. 恢复用到的寄存器;
    4. 用 iret 指令返回;
  4. 中断向量

    中断程序的入口地址;

  5. 中断向量表

    • 记录中断向量的列表;
    • 中断向量表在内存中保存, 存放着 256 个中断源所对应的中断处理程序的入口地址;
    • CPU 根据中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址;
    • 对于 8086PC 机,中断向量表存放在0000:0000~0000:03FF所在的内存中,每个表项占用两个字节的大小,高地址字存放段地址,低地址字存放偏移地址;
  6. 中断过程

    CPU进行中断处理

    1. 从中断信息中获取中断类型码;
    2. 标志寄存器的值入栈;
    3. 设置标志寄存器的第8位TF和第9位IF的值为0;
    4. CS的内容入栈;
    5. IP的内容入栈;
    6. 从内存地址为中断类型码*4和中断类型码*4+2的两个单元中读取中断处理程序的入口地址放入IP和CS中;

    更简洁的描述中断过程如下:

    1. 取得中断码N;
    2. pushf
    3. TF=0,IF=0
    4. push CS
    5. Push IP
    6. (IP)=(N*4),CS=(N*4+2)

外中断

  • 由外部设备发生的事件引发的中断;

  • 外中断分为两大类:不可屏蔽中断、可屏蔽中断;

    • 不可屏蔽中断
      • 不可屏蔽中断是CPU必须响应的中断;
      • 当CPU检测不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程;
      • 对8086CPU不可屏蔽中断的中断类型码固定为2;
    • 可屏蔽中断
      • 可屏蔽中断是CPU可以不响应的外中断;
      • CPU是否响应可屏蔽中断,要看标志寄存位IF位的设置;
      • 当CPU检测到可屏蔽中断信息:
        • 如果IF = 1,CPU在执行完当前指令后响应中断,引发中断过程;
        • 如果IF = 0,不响应可屏蔽中断;
  • 几乎所有由外设引发的中断,都是可屏蔽中断,如键盘输入、打印机请求;

  • 不可屏蔽中断在系统中有必须处理的紧急请求情况发生时来通知CPU中断信息,比如机器突然掉电;

  • 外中断处理过程如下

    • 可屏蔽中断引发的中断处理过程:

      1. 取中断类型码n;

        • 可屏蔽中断信息来自CPU外部,中断类型码是通过数据总线送入CPU;
        • 对比内中断:中断类型码是在CPU内部产生;
      2. 标志寄存器入栈,IF=0 ,F=0

        • 将IF置为0的原因:进入中断处理程序后,禁止其他可屏蔽中断;
        • 如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置为1;
      3. CS,IP入栈;

      4. (IP)=(n*4),CS=(n*4+2)

      5. 由此转去执行中断处理程序;

    • 不可屏蔽中断的中断过程:

      1. 中断值固定为2,不必取中断码

      2. 标志寄存器入栈,IF= 0,TF = 0

      3. CS,IP入栈

      4. (IP) = (8),CS = (0AH)

直接定址表

标号

在汇编代码中,可以用标号表示该段代码的内存地址;

标号格式为标号名:冒号 例如start:

1
2
start:mov ax,0
mov bx,ax

数据标号

  • 普通的标号只能表示内存地址;

  • 数据标号既可以表示内存地址,也可以表示内存单元的长度;

  • 使用数据标号可以以更加简洁的方式访问内存中的数据;

  • 数据标号的格式为:标号名,没有冒号;

  • 例如:使用数据标号,实现数值的累加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    assume cs:code
    code segment
    a db 1,2,3,4,5,6,7,8
    b dw 0

    start:mov si,0
    mov cx,8
    s:mov al,a[si]
    mov ah,0
    add b,ax
    inc si
    loop s

    mov ax,4c00h
    int 21h
    code ends
    end start
    • 在上面的代码中,a、b为数据标号;
    • 可以使用类似a[si]的方式获取标号a处偏移量为si的值的数据;
    • 注意数据标号的类型,例如上面的a是字节型数据单元,不能使用字型单元的mov指令;
  • 在其他段使用数据标号

    • 普通的标号只能在代码段中使用,例如标号s:出现在数据段中会报错;

    • 一般情况下会将数据段和代码段分开,并且在数据段中保存数据,在代码段中保存代码,普通标号无法满足访问数据的需要;

    • 使用数据标号可以解决标号无法在其它段中的问题;

      例如

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      assume cs:code,ds:data
      data segment
      a db 1,2,3,4,5
      b dw 0
      data ends

      code segment
      start: mov ax,data
      mov ds,ax
      mov si,0
      mov cx,5
      s: mov al,a[si]
      mov ah,0
      add b,ax
      inc si
      loop s
      mov ax,4c00h
      int 21h
      code ends
      end start
      • 如果想要在代码段中使用标号访问数据段,需要使用伪指令assume将段寄存器和标号所在段关联;
      • assume的关联是为了让编译器可以识别标号所在段的段地址在哪个寄存器中,实际代码中还需要将段寄存器指向对应的段;
      • 比如:上面的代码中,想要使用数据标号a、b,用assume伪指令关联ds和data;
      • 但assume只是将关联告诉了编译器,实际的代码段中还需要用mov ax,data和mov ds,ax指令来将ds指向data;
  • 使用数据标号储存两个数据标号的段地址和偏移地址

    1
    2
    3
    4
    5
    data segment
    a db 1,2,3,4,5
    b dw 0
    c dd a,b
    data ends

    相当于:

    1
    2
    3
    4
    5
    data segment
    a db 1,2,3,4,5
    b dw 0
    c dw offset a,seg a,offset b,seg b
    data ends

    seg功能为取得某一标号的段地址;

直接定址表

  • 在编程时常会遇到一些比如用给出的数据通过计算来获得结果的问题;

  • 例如:将一个数转换成十六进制显示;

    • 当一次需要转换的数字很多时,需要多次进行重复的计算转换;
    • 为了简化操作,可以将问题转换为用给出的数据作为查表的依据,通过查表获得结果;
    • 具体操作是:
      • 建一张数据和结果映射表;
      • 用数据为条件,计算结果在表中的位置也就获取了结果;
    • 例如:将0~F对应的ascii码放入一个数据段中,根据所给数值的大小来计算该数值对应的ascii在数据段中的地址;这样就不用每次都做将数值转换成ascii的操作;
  • 这种可以依据数据,直接计算出所要找的元素的位置的表称为直接定址表;

参考:《汇编语言》-王爽