x86汇编基础

很久之前看王爽老师的《汇编语言》写的笔记,可能不太准确,先做个存档吧。

1. 基础知识

汇编指令是机器指令的助记符,同机器指令一一对应。

每一种CPU都有自己的汇编指令集。

CPU可以直接使用的信息在存储器中存放。

指令和数据没有任何区别,都是二进制信息。

存储单元从零开始顺序编号。

存储器的存储单元可以存储1B,即8个二进制位。微机存储器的容量是以字节为最小单位来计算的。

1B=8b, 1KB=1024B, 1MB=1024KB, 1GB=1024MB, 1TB=1024GB

地址总线:

  • CPU是通过地址总线来指定存储单元的
  • 地址总线上能传送多少个不同的信息,CPU就可以对多少个存储单元进行寻址
  • 一个CPU有N根地址线,则可以说这个CPU的地址总线的宽度为N,这样的CPU最多可以寻找2的N次方个内存单元(B)

数据总线:

  • CPU与内存或其他器件之间的数据传送是通过数据总线来进行的
  • 数据总线的宽度决定了CPU和外界的数据传送速度,8根数据总线一次可传送一个8位二进制数据(即1B)

控制总线:

  • CPU对外部器件的控制是通过控制总线来进行的
  • 有多少根控制总线就意味着CPU提供了对外部器件的多少种控制
  • 控制总线的宽度决定了CPU对外部器件的控制能力

2. 寄存器

内部总线实现CPU内部各个器件之间的连接,外部总线实现CPU和主板上其他器件的联系。

2.1 通用寄存器

AX, BX, CX, DX

以AX为例,数据18,二进制表示10010,小端序

AX
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0
8~15 AH 0~7 AL

AH和AL可以看成是一个字型数据的高8位和低8位,也可以看成是两个独立的字节型数据。

2.2 几条汇编指令

汇编指令不区分大小写。

原AX=0000H,BX=0000H

程序段中的指令 指令执行后AX中的数据 指令执行后BX中的数据
mov ax, 4E20H 4E20H 0000H
add ax, 1406H 6226H 0000H
mov bx, 2000H 6226H 2000H
add ax, bx 8226H 2000H
mov bx, ax 8226H 8226H
add ax, bx 044CH 8226H

2.3 物理地址

1
物理地址=段地址×16+偏移地址=基础地址+偏移地址

CPU可以用不同的段地址和偏移地址形成同一个物理地址。

偏移地址16位,变化范围0~FFFFH,仅用偏移地址来寻址最多可寻$2^{16}B=2^{6}KB=64KB$个内存单元。

2.4 CS 和 IP

CS为代码段寄存器(存放段地址),IP为指令指针寄存器(存放偏移地址)。

同时修改CS、IP的内容:jmp 段地址:偏移地址

1
jmp 3:0B16	执行后:CS=0003H,IP=0B16H,CPU将从00030H+0B16H=00B46H处读取指令

仅修改IP的内容:jmp 某一合法寄存器

1
2
jmp ax	执行指令前:ax=1000H,CS=2000H,IP=0003H
执行指令后:ax=1000H,CS=2000H,IP=1000H

8086CPU工作过程:

  1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
  2. IP指向下一条指令
  3. 执行指令(转到1,重复)

3. 内存访问

字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。

起始地址为N的字单元简称为N地址字单元。比如一个字单元由2、3两个内存单元组成,则这个字单元的起始地址为2。

3.1 DS 和 [address]

DS为数据段寄存器(存放段地址),[address]表示一个内存单元(存放偏移地址)。

3.2 mov指令(add、sub指令同)

  1. 将数据直接送入寄存器:mov 寄存器,数据
  2. 将一个寄存器中的内容送人另一个寄存器:mov 寄存器,寄存器

  3. 将一个内存单元中的内容送入一个寄存器中:mov 寄存器,内存单元地址

1
2
3
4
将10000H(1000:0)中的数据读到al中:
mov bx, 1000H
mov ds, bx
mov al, [0]
  1. mov 内存单元,寄存器
  2. mov 段寄存器,寄存器

8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds,1000H这条指令是非法的,只好用一个寄存器来进行中转,即先将1000H送入一个通用寄存器,再将通用寄存器的内容送入ds。

1
2
3
4
将al中的数据送入内存单元10000H中:
mov bx, 1000H
mov ds, bx
mov [0], al

3.3 CPU提供的栈机制

1
2
push ax 表示将寄存器ax中的数据送入栈中
pop ax 表示从栈顶取出数据送入ax

8086CPU的入栈和出栈操作都是以字为单位进行的。

CPU如何知道栈顶位置?SS:SP,任意时刻,SS:SP指向栈顶元素。

push ax由以下两步完成:

  1. SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
  2. 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶

pop ax由以下两步完成:

  1. 将SS:SP指向的内存单元处的数据送入ax中
  2. SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

3.4 push指令(pop指令同)

  1. 将一个寄存器中的数据入栈:push 寄存器
  2. 将一个段寄存器中的数据入栈:push 段寄存器
  3. 将一个内存字单元处的字入栈:push 内存单元

指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从DS中取得。

1
2
3
4
mov ax,1000H
mov ds,ax
push [0] ;将1000:0处的字压入栈
pop [2] ;出栈的数据送入1000:2处

将10000H~1000FH这段空间当作栈,初始状态栈是空的,将ax, bx, DS中的数据入栈。

1
2
3
4
5
6
mov ax,1000H
mov ss,ax ;设置栈的段地址
mov sp,0010H ;设置栈的偏移地址,因栈为空,所以sp=000FH+0002H=0010H
push ax
push bx
push ds

push、pop等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为0~FFFFH

一个栈段的容量最大为64KB。

在10000H处写入字型数据2266H

1
2
3
4
5
6
7
8
9
10
11
12
方法一
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax

方法二
mov ax,1000H
mov ss,ax
mov sp,2
mov ax,2266H
push ax

3.5 段的综述

对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问。

对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码中的指令。

对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来用。

CPU将内存中的某段内容当作代码,是因CS:IP指向了那里;CPU将某段内存当作栈,是因为SS:SP指向了那里。

4. 第一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:codesg

codesg segment

mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax

mov ax,4c00H
int 21H

codesg ends

end

在汇编语言程序中包含两种指令:伪指令、汇编指令。汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。伪指令没有对应的机器指令,最终不被CPU执行。伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。

4.1 伪指令

4.1.1 XXX segment …… XXX ends

segment和ends的功能是定义一个段,segment说明一个段开始,ends说明一个段结束。

一个有意义的汇编程序中至少要有一个段,这个段用来存放代码。

4.1.2 end

end是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束对源程序的编译。

4.1.3 assume

这条伪指令的含义为“假设”,它假设某一段寄存器和程序中的某一个用segment…ends定义的段相关联。通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。

4.2 汇编指令

4.3 标号

一个标号指代了一个地址,比如“codesg”。codesg在segment前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。

4.4 程序的结构

编程运算$2^3$

  1. 定义一个段,名称为abc
1
2
abc segment
abc ends
  1. 在这个段中写入汇编指令,来实现我们的任务
1
2
3
4
5
abc segment
mov ax,2
add ax,ax
add ax,ax
abc ends
  1. 指出程序要在何处结束
1
2
3
4
5
6
abc segment
mov ax,2
add ax,ax
add ax,ax
abc ends
end
  1. abc被当作代码段来用,所以应该将abc和cs联系起来
1
2
3
4
5
6
7
assume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax
abc ends
end
  1. 程序返回
1
2
3
4
5
6
7
8
9
assume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00H
int 21H
abc ends
end
  1. 语法错误和逻辑错误

4.5 编译

源程序文件.asm->目标文件.obj

4.6 连接

目标文件.obj->可执行文件.exe

5. [bx]和loop指令

[bx]同样表示一个内存单元,它的偏移地址在bx中。

“()”中的元素可以有3种类型:①寄存器名;②段寄存器名;③内存单元的物理地址。

(ax)表示ax中的内容,(20000H)表示内存20000H单元的内容,((ds)16+(bx))表示ds中的内容为ADR1,bx中的内容为ADR2,内存ADR1\16+ADR2单元的内容,即内存ADR1:ADR2单元的内容。

约定idata表示常量。

1
2
mov ax,[idata]
mov ax,idata
1
2
mov ax,[bx]		;(ax)=((ds)*16+(bx))
mov [bx],ax ;((ds)*16+(bx))=(ax)
1
inc bx			;inc bx的含义是bx中的内容加1

5.1 loop指令

loop指令的格式:loop 标号

CPU执行loop指令的时候要进行两步操作:

  1. (cx)=(cx)-1
  2. 判断cx中的值,不为零则转至标号处执行程序,为零则向下执行

编程运算$2^{12}$

1
2
3
4
5
6
7
8
9
10
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

cx和loop指令相配合实现循环功能:①在cx中存放循环次数;②loop指令中的标号所标识地址要在前面;③要循环执行的程序段,要写在标号和loop指令的中间。

在汇编源程序,数据不能以字母开头,所以要在前面加0。A000H在汇编源程序中要写为0A000H

1
2
3
4
mov al,[0]		;(al)=0,将常量0送入al中(mov al,0含义相同)
mov al,ds:[0] ;(al)=((ds)*16+0),将内存单元中的数据送入al中
mov al,[bx] ;(al)=((ds)*16+(bx)),将内存单元中的数据送入al中
mov al,ds:[bx] ;与mov al,[bx]含义相同

(1)在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用”[]“来表示存储单元,如果在”[]“里用一个常量idata直接给出内存单元的偏移地址,就要在”[]“的前面显式地给出段地址所在的寄存器。比如mov al,ds:[0]

如果没有在”[]“的前面显式地给出段地址所在的段寄存器,那么编译器会把指令中的[idata]解释为data,比如mov al,[0]

(2)如果在”[]“里用寄存器,比如bx,间接给出内存单元的偏移地址,则段地址默认在ds中。当然也可以显式地给出段地址所在的段寄存器。

5.2 一段安全的空间

我们需要直接向一段内存中写入内容,这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作很可能引发错误。DOS方式下,一般情况,0:200~0:2ff空间中没有系统或其他程序的数据或代码。

5.3 loop和[bx]的联合应用

在循环中,源始单元ffff:X和目标单元0020:X的偏移地址X是变量,可以用bx来存放。

5.4 段前缀

6. 包含多个段的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov bx,0
mov ax,0

mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21h
code ends
end

dw即define word,定义字型数据。

程序在运行的时候CS中存放代码段的段地址,所以可以从CS中得到它们的段地址。dw定义的数据处于代码段的最开始,所以偏移地址为0,这8个数据就在代码段的偏移0、2、4、6、8、A、C、E处。程序运行时,它们的地址就是cs:0, cs:2, cs:4, cs:6, cs:8, cs:a, cs:c, cs:e。

end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。end 标号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start: mov bx,0 ;程序的第一条指令
mov ax,0

mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21h
code ends
end start

6.1 在代码段中使用栈

程序运行时,定义的数据存放在cs:0~cs:F单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。(将cs:10~cs:2F的内存空间当作栈来用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
;用dw定义16个字型数据,在程序加载后,将取得16个字的内存空间存放这16个数据
;在后面的程序中将这段空间当作栈来使用
start: mov ax,cs
mov ss,ax
mov sp,30h ;将设置栈顶ss:sp指向cs:30

mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s ;将代码段0~15单元中的8个字型数据依次入栈

mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0 ;出栈8个字型数据到代码段0~15单元中

mov ax,4c00h
int 21h
codesg ends
end start ;指明程序的入口在start处

6.2 将数据、代码、栈放入不同的段

在前面的内容中,我们在程序中用到了数据和栈,将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。这样做会产生两个问题:

  1. 把它们放到一个段中使程序显得混乱
  2. 前面数据中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。但如果数据、栈、代码需要的空间超过64KB,就不能放在一个段中(8086模式中一个段的容量不能大于64KB)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack ;将名为stack段的段地址送入ax
mov ss,ax
mov sp,20h
;设置栈顶ss:sp指向stack:20(在整个程序中是cs:30,但在栈段是从0:10~0:2F,所以是stack:20)
mov ax,data
mov ds,ax ;ds指向data段
mov bx,0 ;ds:bx指向data段中的第一个单元

mov cx,8
s: push [bx]
add bx,2
loop s ;将data段中的0~15单元中的8个字型数据依次入栈

mov cx,8
s0: pop [bx]
add bx,2
loop s0 ;依次出栈8个字型数据到data段的0~15单元中

mov ax,4c00h
int 21h
code ends
end start

7. 更灵活的定位内存地址的方法

7.1 and和or指令

1
2
3
4
5
6
7
8
9
and指令:逻辑与指令,按位进行与运算(1and1=1,1and0=0,0and0=0)
mov al,01100011B
and al,00111011B
执行后:al=00100011B

or指令:逻辑或指令,按位进行或运算(1or1=1,1or0=1,0or0=0)
mov al,01100011B
or al,00111011B
执行后:al=01111011B

7.2 ASCII码

1
a->61h,b->62h

7.3 以字符形式给出的数据

在汇编程序中,用’’的方式指明数据是以字符的形式给出的,编译器将它们转化为相对应的ASCII码。

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code,ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov al,'a'
mov bl,'b'
mov ax,4c00h
int 21h
code ends
end start

7.4 大小写转换的问题

在codesg中填写代码,将tadasg中的第一个字符串转化为大写,第二个字符串转化为小写。

方法一:小写字母的ASCII码值比大写字母的ASCII码值大20H。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,[bx]
如果(al)>61H,则为小写字母的ASCII码,则:sub al,20h
mov [bx],al
inc bx
loop s
codesg ends
end start

方法二:大写字母的第6位全为0,小写字母的第6位全为1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax ;设置ds指向datasg段
mov bx,0 ;设置(bx)=0,ds:bx指向'BaSiC'的第一个字母
mov cx,5
s: mov al,[bx] ;将ASCII码从ds:bx所指向的单元中取出
and al,11011111B;将al中的ASCII码的第6位置0,变为大写字母
mov [bx],al
inc bx
loop s

mov bx,5 ;设置(bx)=5,ds:bx指向'iNfOrMaTiOn'的第一个字母
mov cx,11
s0: mov al,[bx]
or al,00100000B
mov [bx],al
inc bx
loop s0

mov ax,4c00h
int 21h
codesg ends
end start

7.5 [bx+idata]

[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata

mov ax,[bx+200]->(ax)=((ds)*16+(bx)+200)

简化7.4方法二(但这个一定要两个字符串长度相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
    mov ax,datasg
mov ds,ax
mov bx,0

mov cx,5
s: mov al,[bx] ;定位第一个字符串中的字符
and al,11011111b
mov [bx],al
mov al,[5+bx] ;定位第二个字符串中的字符
or al,00100000b
mov [5+bx],al
inc bx
loop s

[bx]=0[bx],[5+bx]=5[bx]

7.6 SI 和 DI

si 和 di不能够分成两个8位寄存器来使用。

用ds:si指向要复制的源始字符串,用ds:di指向复制的目的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!'
db '................'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov di,16 ;复制到它后面的数据区,后面数据区的偏移地址为16

mov cx,8 ;一次复制2个字节,一共循环8次
s: mov ax,[si]
mov [di],ax
add si,2
add di,2
loop s

mov ax,4c00h
int 21h
codesg ends
end start

利用[bx(si或di)+idata]的方式使程序变简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov cx,8
s: mov ax,0[si]
mov 16[si],ax
add si,2
loop s

mov ax,4c00h
int 21h
codesg ends
end start

7.7 [bx+si]和[bx+di]

[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si),[bx+di]同。

mov ax,[bx+si]->(ax)=((ds)*16+(bx)+(si))

mov ax,[bx+si]=mov ax,[bx][si]

7.8 [bx+si+idata]和[bx+di+idata]

[bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata,[bx+di+idata]同。

mov ax,[bx+si+idata]->(ax)=((ds)*16+(bx)+(si)+idata)

1
2
3
4
  mov ax,[bx+si+idata]
= mov ax,idata[bx][si]
= mov ax,[bx].idata[si]
= mov ax,[bx][si].idata

7.9 不同寻址方式的灵活应用

编程,将datasg段中每个单词首字母改为大写字母(用bx定位每行的起始地址,用3定位要修改的列,用[bx+idata]对目标单元进行寻址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:codesg,ds:datasg
datasg segment
db '1..file.........' ;16个字节
db '2..edit.........'
db '3..search.......'
db '4..view.........'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s: mov al,[bx+3]
and al,11011111b
mov [bx+3],al
add bx,16
loop s

mov ax,4c00h
int 21h
codesg ends
end start

编程,将datasg段中每个单词改为大写字母(用bx定位每行的起始地址,用si定位要修改的列,用[bx+si]方式对目标单元进行寻址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
assume cs:codesg,ds:datasg
datasg segment
db 'ibm.............'
db 'dec.............'
db 'dos.............'
db 'vax.............'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: mov dx,cx ;将外层循环的cx值保存在dx中
mov si,0

mov cx,3 ;cx设置为内层循环的次数
s: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s

add bx,16
mov cx,dx ;用dx中存放的外层循环的计数值恢复cx
loop s0

mov ax,4c00h
int 21h
codesg ends
end start

如果dx也被用了呢?所有寄存器都被用了呢?可以使用内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
assume cs:codesg,ds:datasg
datasg segment
db 'ibm.............'
db 'dec.............'
db 'dos.............'
db 'vax.............'
dw 0 ;定义一个字,用来暂存cx
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: mov ds:[40h],cx ;将外层循环的cx值保存在datasg:40h单元中
mov si,0

mov cx,3 ;cx设置为内层循环的次数
s: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s

add bx,16
mov cx,ds:[40h] ;用datasg:40h单元中的值恢复cx
loop s0

mov ax,4c00h
int 21h
codesg ends
end start

如果需要保存多个数据,需要记住哪个数据暂存在哪个单元中,这样程序容易混乱。一般来说,在需要暂存数据的时候,我们都应该使用栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
assume cs:codesg,ds:datasg,ss:stacksg
datasg segment
db 'ibm.............'
db 'dec.............'
db 'dos.............'
db 'vax.............'
datasg ends
stacksg segment
dw 0,0,0,0,0,0,0,0
stacksg ends
codesg segment
start: mov ax,stacksg
mov ss,ax
mov sp,16
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: push cx ;将外层循环的cx值压栈
mov si,0

mov cx,3 ;cx设置为内层循环的次数
s: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s

add bx,16
pop cx ;从栈顶弹出原cx的值恢复cx
loop s0

mov ax,4c00h
int 21h
codesg ends
end start

8. 数据处理

reg表示一个寄存器,sreg表示一个段寄存器。

reg有ax, bx, cx, dx, ah, al, bh, bl, ch, cl, dh, dl, sp, bp, si, di

sreg有ds, ss, cs, es

8.1 bx, si, di和bp

  1. 在8086CPU中又有这4个寄存器可以在“[]”中进行内存单元寻址
  2. 在“[]”中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si, bx和di, bp和si, bp和di
1
2
3
mov ax,[bx]
mov ax,[bx+si]
mov ax,[bp+di]
  1. 只要在“[]”中使用寄存器bp,而指令中没有显性地给出段地址,段地址就默认在ss中
1
2
mov ax,[bp]				(ax)=((ss)*16+(bp))
mov ax,[bp+si+idata] (ax)=((ss)*16+(bp)+(si)+idata)

8.2 数据的位置

指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口。

汇编指令 指令执行前数据的位置
mov bx,[0] 内存,ds:0单元
mov bx,ax CPU内部,ax寄存器
mov bx,1 CPU内部,指令缓冲器

8.2.1 数据位置的表达

  1. 立即数(idata)
  2. 寄存器
  3. 段地址(SA)和偏移地址(EA)

8.2.2 寻址方式

8.3 数据的长度

8086CPU可以处理两种尺寸的数据,byte和word。

  1. 通过寄存器名指明要处理的数据的尺寸
  2. 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte

例如下面的指令中,用word ptr指明了指令访问的内存单元是一个字单元

1
2
mov word ptr ds:[0],1
inc word ptr [bx]

下面的指令中,用byte ptr指明了指令访问的内存单元是一个字节单元

1
2
mov byte ptr ds:[0],1
add byte ptr [bx],2
  1. 其他方法

有些指令默认了访问的是字单元还是字节单元,比如push [1000H]就不用指明访问的是字单元还是字节单元。因为push指令只进行字操作。

8.4 div指令

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

  1. 除数:有8位和16位两种,在一个reg或内存单元中。
  2. 被除数:默认放在AX 或 DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。
  3. 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
1
2
3
4
5
6
7
8
9
10
div reg
div 内存单元

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)的余数

编程,利用除法指令计算100001/100

被除数100001大于65535,不能用ax寄存器存放,所以只能用dx和ax两个寄存器联合存放100001,也就是说要进行16位的除法。除数100小于255,可以在一个8位寄存器中存放,但是因为被除数是32位的,除数应为16位,所以要用一个16位寄存器来存放除数100。

因为要分别为dx和ax赋100001的高16位值和低16位值,所以应先将100001表示为16进制形式:186A1H

1
2
3
4
mov dx,1
mov ax,86a1h ;(dx)*10000H+(ax)=100001h
mov bx,100
div bx

程序执行后,(ax)=03E8H(即1000),(dx)=1(余数为1)

8.5 伪指令dd

dd(double word)双字型数据

用div计算data段中第一个数据除以第二个数据后的结果,商存在第三个数据的存储单元中。

1
2
3
4
5
6
7
8
9
10
11
12
13
data segment
dd 100001
dw 100
dw 0
data ends
code segment
mov ax,data
mov ds,ax
mov ax,ds:[0] ;ds:[0]字单元中的低16位存储在ax中
mov dx,ds:[2] ;ds:[2]字单元中的高16位存储在dx中
div word ptr ds:[4] ;用dx:ax中的32位数据除以ds:[4]字单元中的数据
mov ds:[6],ax ;将商存储在ds:[6]字单元中
code ends

8.6 dup

dup是一个操作符,在汇编语言中同db, dw, dd等一样,也是由编译器识别处理的符号。它是和db, dw, dd等数据定义伪指令配合使用,用来进行数据的重复。

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

dup使用格式

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

9. 转移指令的原理

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

8086CPU的转移行为有以下几类

  • 只修改IP时,称为段内转移,比如:jmp ax
  • 同时修改CS和IP时,称为段间转移,比如:jmp 1000:0

由于转移指令对IP的修改范围不同,段内转移又分为短转移和近转移

  • 短转移IP的修改范围为-128~127
  • 近转移IP的修改范围为-32768~32767

8086CPU的转移指令分为以下几类

  • 无条件转移指令(jmp)
  • 条件转移指令
  • 循环指令(loop)
  • 过程
  • 中断

9.1 操作符offset

操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。

1
2
3
4
5
6
assume cs:codesg
codesg segment
start: mov ax,offset start ;相当于mov ax,0(指令长度为3字节)
s: mov ax,offset s ;相当于mov ax,3
codesg ends
end start

程序在运行中将s处的一条指令复制到s0处

1
2
3
4
5
6
7
8
9
10
11
assume cd: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
end s

9.2 jmp指令

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

jmp指令要给出两种信息:

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

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

1
jmp short 标号(转到标号处执行指令):(IP)=(IP)+8位位移

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

CPU在执行jmp指令的时候并不需要转移的目的地址。

1
2
3
4
5
6
7
8
assume cs:codesg
codesg segment
start: mov ax,0
jmp short s
add ax,1
s: inc ax
codesg ends
end start
1
jmp near ptr 标号:(IP)=(IP)+16位位移

这种格式的jmp指令实现的是段内近转移,它对IP的修改范围为-32768~32767,向前转移时最多可以越过32768个字节,向后转移最多可以越过32767个字节。

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

1
jmp far ptr 标号:(CS)=标号所在段的段地址;(IP)=标号在段中的偏移地址

这种格式的jmp指令实现的是段间转移,又称远转移。far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。

1
2
3
4
5
6
7
8
9
10
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

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

1
jmp 16位reg:(IP)=(16位reg)

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

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

  1. jmp word ptr 内存单元地址(段内转移)

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

1
2
3
mov ax,0123h
mov ds:[0],ax
jmp word ptr ds:[0]

执行后,(IP)=0123H

  1. jmp dword ptr 内存单元地址(段间转移)

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

(CS)=(内存单元地址+2),(IP)=(内存单元地址)

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

9.3 jcxz指令

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

1
2
jcxz 标号(如果(cx)=0,转移到标号处执行):当(cx)=0时,(IP)=(IP)+8位位移
if((cx)==0) jmp short 标号;

9.4 loop指令

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

1
2
3
loop 标号((cx)=(cx)-1,如果(cx)≠0,转移到标号处执行):如果(cx)≠0,(IP)=(IP)+8位位移
(cx)--;
if((cx)!=0) jmp short 标号;

10. call和ret指令

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

10.1 ret和retf

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

10.1.1 ret指令

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

  1. (IP)=((SS)*16+(SP))
  2. (SP)=(SP)+2

CPU执行ret指令时相当于进行:

1
pop IP

10.1.2 retf指令

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

  1. (IP)=((SS)*16+(SP))
  2. (SP)=(SP)+2
  3. (CS)=((SS)*16+(SP))
  4. (SP)=(SP)+2

CPU执行retf指令时相当于进行:

1
2
pop IP
pop CS

10.2 call指令

CPU执行call指令时,进行2步操作:

  1. 将当前的IP或CS和IP压入栈中
  2. 转移

call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。

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

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

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

  1. (sp)=(sp)-2
    ((ss)*16+(sp))=(IP)
  2. (IP)=(IP)+16位位移

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

1
2
push IP
jmp near ptr 标号

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

1
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 标号“时,相当于进行:

1
2
3
push CS
push IP
jmp far ptr 标号

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

1
call 16位reg

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

1
2
3
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(IP)=(16位reg)

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

1
2
push IP
jmp 16位reg

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

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

  1. call word ptr 内存单元地址

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

1
2
push IP
jmp word ptr 内存单元地址
1
2
3
4
mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0]

执行后,(IP)=0123h,(sp)=0EH

  1. call dword ptr 内存单元地址

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

1
2
3
push CS
push IP
jmp dword ptr 内存单元地址
1
2
3
4
5
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

10.3 call和ret的配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cd:code
code segment
start: mov ax,1
mov cx,3
call s
mov bx,ax ;(bx)=8
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end start

10.4 mul指令

mul指令是乘法指令,使用mul做乘法时要注意两点:

  1. 两个相乘的数:要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认放在AX中,另一个放在16位reg或内存字单元中
  2. 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中存放
1
2
3
4
5
6
7
8
9
mul reg
mul 内存单元

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
mov al,100
mov bl,10
mul bl

结果:(ax)=1000(03E8H)

计算100*10000。(100小于255,但10000大于255,所以必须做16位乘法)

1
2
3
mov ax,100
mov bx,10000
mul bx

结果:(ax)=4240h,(dx)=000FH(F4240H=1000000)

11. 标志寄存器

标志寄存器作用:

  1. 用来存储相关指令的某些执行结果
  2. 用来为CPU执行相关指令提供行为依据
  3. 用来控制CPU的相关工作方式

flag和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
OF DF IF TF SF ZF AF PF CF

8086中flag寄存器只有标注的这些位有特殊的含义,其它8086CPU没有使用。

11.1 ZF标志

flag的第6位是ZF,零标志位。它记录相关指令执行后,其结果是否为0。如果为0,ZF=1;否则ZF=0。

1
2
mov ax,1
sub ax,1

执行后,(ax)=0,ZF=1

11.2 PF标志

flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。如果1的个数为偶数,PF=1;否则PF=0。

1
2
mov al,1
add al,10

执行后,结果为11=00001011B,其中有3个1,PF=0

11.3 SF标志

flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果是否为负。如果结果为负,SF=1;否则SF=0。

1
2
mov al,10000001B		;al=-127
add al,1

结果:(al)=10000010B,SF=1

11.4 CF标志

flag的第0位是CF,进位标志位。一般情况下,在进行无符号数运算时,它记录了运算结果的最高有效位向更高位的进位值,或从更高位借位。

7 6 5 4 3 2 1 0
0 0 0 1 1 1 0 0
假想的更高位 最高有效位

当两数相加时,有可能产生从最高有效位向更高位的进位。CPU在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。8086CPU就用flag的CF位来记录这个进位值。

1
2
mov al,98H
add al,al ;98h+98h=130h=0001 0011 0000B

执行后,(al)=30H,CF=1,CF记录了从最高有效位向更高位的进位值。

11.5 OF标志

flag的第11位是OF,溢出标志位。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF=1;否则OF=0。

对于8位的有符号数据,机器所能表示的范围就是-128~127;对于16位的有符号数据,机器所能表示的范围是-32768~32767。

1
2
mov al,0F0H		;0f0h=240=-16	240-128*2=-16
add al,78H ;78h=120=-8

执行后,对于无符号运算,0F0H+78H=168H=0001 0110 1000B,CF=1;对于有符号数,-16-8=-24,OF=0。

11.6 adc指令

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

1
adc 操作对象1,操作对象2

操作对象1=操作对象1+操作对象2+CF

1
2
3
4
mov ax,2		;(ax)=2
mov bx,1 ;(bx)=1
sub bx,ax ;(bx)=-1=FFFFH CF=1
adc ax,1 ;(ax)=(ax)+1+CF=4

11.7 sbb指令

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

1
sbb 操作对象1,操作对象2

操作对象1=操作对象1-操作对象2-CF

1
2
3
4
mov bx,1000H		;(bx)=1000h
mov ax,003EH ;(ax)=003eh
sub bx,2000H ;(bx)=1000h-2000h=F000H CF=1
sbb ax,0020H ;(ax)=(ax)-20h-1=1D

11.8 cmp指令

cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。

1
cmp 操作对象1,操作对象2

计算操作对象1-操作对象2但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。

1
2
3
mov ax,8		;(ax)=8
mov bx,3 ;(bx)=3
cmp ax,bx ;(ax)-(bx)=5=0101b

指令执行后,零标志位ZF=0,奇偶标志位PF=1,符号标志位SF=0,进位标志位CF=0,溢出标志位OF=0

如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正;反之亦然。

11.9 检测比较结果的条件转移指令

除了jcxz指令外,CPU还提供了其他条件转移指令,大多数条件转移指令都检测标志寄存器的相关标志位,根据检测结果来决定是否修改IP。这些条件转移指令通常和cmp相配合使用,类似call和ret配合。

因为cmp指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种,即根据无符号数的比较结果进行转移的条件转移指令(它们检测ZF、CF的值)和根据有符号数的比较结果进行转移的条件转移指令(SF、OF、ZF)。

常用的根据无符号数的比较结果进行转移的条件转移指令

指令 全称 含义 检测的相关标志位
je jump equal 等于则转移 zf=1
jne jump not equal 不等于则转移 zf=0
jb jump below 低于则转移 cf=1
jnb jmp not below 不低于则转移 cf=0
ja jump above 高于则转移 cf=0且zf=0
jna jump not above 不高于则转移 cf=1或zf=1
jz jump zero 零则转移 zf=1
jnz jump not zero 非零则转移 zf=0

编程统计data段中数值为8的字节的个数,用ax保存统计结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
mov ax,data
mov ds,ax
mov bx,0 ;ds:bx指向第一个字节
mov ax,0 ;初始化累加器
mov cx,8
s: cmp byte ptr [bx],8 ;和8进行比较
jne next ;如果不相等则转到next,继续循环
inc ax ;如果相等将计数加1
next: inc bx
loop s ;程序执行后(ax)=3
code ends

11.10 DF标志和串传送指令

flag的第10位是DF,方向标志位。在串处理指令中,控制每次操作后si、di的增减。

df=0 每次操作后si、di递增;

df=1 每次操作后si、di递减。

11.10.1 串传送指令

1
movsb

执行movsb指令相当于进行下面操作:

  1. ((es)16+(di))=((ds)\16+(si))

  2. 如果df=0则 (si)=(si)+1;(di)=(di)+1

    如果df=1则 (si)=(si)-1;(di)=(di)-1

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

1
movsw

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

movsb和movsw进行的是串传送操作的一个步骤,一般来说,movsb和movsw都和rep配合使用

1
rep movsb

用汇编语法来描述就是

1
2
s:	movsb
loop s

rep的作用是根据cx的值,重复执行后面的串传送指令。由于每执行一次movsb指令si和di都会递增或递减后一个或前一个单元,则rep movsb就可以循环实现(cx)个字符的传送。

由于df位决定着串传送指令执行后si和di改变的方向,所以CPU应该提供相应的指令来对df位进行设置,从而使人能够决定传送的方向。

8086CPU提供下面两条指令对df位进行设置

1
2
cld指令:将标志寄存器的df位置0
std指令:将标志寄存器的df位置1

编程,用串传送指令,将data段中的第一个字符串复制到它后面的空间中。

1
2
3
4
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends

①传送的原始位置:ds:si data:0

②传送的目的位置:es:di data:10h

③传送的长度:cx (cx)=16

④传送的方向:df df=1

1
2
3
4
5
6
7
8
mov ax,data
mov ds,ax
mov si,0 ;ds:si指向data:0
mov es,ax
mov di,16 ;es:di指向data:0010
mov cx,16
cld ;df=0,正向传送
rep movsb

11.11 pushf和popf

pushf的功能是将标志寄存器的值压栈,popf是从栈中弹出数据,送入标志寄存器中。

12. 内中断

任何一个通用的CPU,可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。中断的意思是CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。

对于8086CPU,当内部有下面的情况发生的时候将产生相应的中断信息:

  1. 除法错误,比如执行div指令产生的除法溢出 0
  2. 单步执行 1
  3. 执行into指令 4
  4. 执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码

12.1 中断处理程序

CPU收到中断信息后,应该转去执行该中断信息的处理程序。中断信息中包含有标识中断源的类型码,根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表在0000:0000~0000:03FF的1024个单元存放着。在中断向量表中,一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。

12.2 中断过程

用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动完成的。CPU硬件完成这个工作的过程被称为中断过程。

中断过程:

  1. (从中断信息中)取得中断类型码N
  2. 标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中) pushf
  3. 设置标志寄存器的第8位TF和第9位IF为0 TF=0,IF=0
  4. CS的内容入栈 push CS
  5. IP的内容入栈 push IP
  6. 从内存地址为中断类型码4 和中断类型码\4+2 的两个字单元中读取中断处理程序的入口地址设置IP和CS (IP)=(N*4),(CS)=(N*4+2)

12.3 中断处理程序和iret指令

中断处理程序的编写步骤:

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

iret指令的功能用汇编语法描述:

1
2
3
pop IP
pop CS
popf

iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序。

12.4 单步中断

CPU在执行完一条指令后,如果检测到标志寄存器的TF=1,则产生单步中断,引发中断过程。

13. int指令

int n也是内中断的一种。

13.1 BIOS中断例程

int 10h中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的子程序。

(ah)=2表示调用第10h号中断例程的2号子程序,功能为设置光标位置。

(ah)=9表示调用第10h号中断例程的9号子程序,功能为在光标位置显示字符,可以提供要显示的字符、颜色属性、页号、字符重复个数作为参数。

bl中的颜色属性的格式如下:

7 6 5 4 3 2 1 0
BL R G B I R G B
闪烁 4~6背景 高亮 0~2前景

编程,在屏幕的5行12列显示3个红底高亮闪烁绿色的’a’。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code
code segment
mov ah,2 ;置光标
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,12 ;dl中放列号
int 10h

mov ah,9 ;在光标位置显示字符
mov al,'a' ;字符
mov bl,11001010b ;颜色属性
mov bh,0 ;第0页
mov cx,3 ;字符重复个数
int 10h

mov ax,4c00h
int 21h
code ends
end

13.2 DOS中断例程

int 21h中断例程是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。

前面一直使用的是int 21h中断例程的4ch号功能,即程序返回功能

1
2
3
mov ah,4ch		;程序返回
mov al,0 ;返回值
int 21h

(ah)=9表示调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数。

编程,在屏幕的5行12列显示字符串“Welcome to masm!”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume cs:code,ds:data
data segment
db 'Welcome to masm!'
data ends
code segment
start: mov ah,2 ;置光标
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,12 ;dl中放列号
int 10h

mov ax,data
mov ds,ax
mov dx,0 ;ds:dx指向字符串的首地址data:0
mov ah,9
int 21h

mov ax,4c00h
int 21h
code ends
end start

14. 端口

在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下三种芯片

  1. 各种接口卡(网卡、显卡)上的接口芯片,它们控制接口卡进行工作
  2. 主板上的接口芯片,CPU通过它们对部分外设进行访问
  3. 其他芯片,用来存储相关的系统信息,或进行相关的出入输出处理

从CPU的角度,将寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。每一个端口在地址空间中都有一个地址。

CPU可以直接读写以下3个地方的数据:

  1. CPU内部的寄存器
  2. 内存单元
  3. 端口

14.1 端口的读写

因为端口所在的芯片和CPU通过总线相连,所以端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口,端口地址的范围为0~65535

端口的读写指令只有两条:in和out,分别用于从端口读取数据和往端口写入数据。

访问内存:

1
mov ax,ds:[8]				;假设执行前(ds)=0

①CPU通过地址线将地址信息8发出

②CPU通过控制线发出内存读命令,选中存储器芯片并通知它将要从中读取数据

③存储器将8号单元中的数据通过数据线送入CPU

访问端口:

1
in al,60h					;从60h号端口读入一个字节

①CPU通过地址线将地址信息60h发出

②CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它将要从中读取数据

③端口所在的芯片将60h端口中的数据通过数据线送入CPU

注:在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。访问8位端口时用al,访问16位时用ax。

对0~255以内的端口进行读写时:

1
2
in al,20h				;从20h端口读入一个字节
out 20h,al ;往20h端口写入一个字节

对256~65535的端口进行读写时,端口号放在dx中:

1
2
3
mov dx,3f8h				;将端口号3f8h送入dx
in al,dx ;从3f8h端口读入一个字节
out dx,al ;往3f8h端口写入一个字节

14.2 shl和shr指令

shl和shr是逻辑移位指令。

shl是逻辑左移指令,它的功能为:

  • 将一个寄存器或内存单元中的数据向左移位
  • 将最后移出的一位写入CF中
  • 最低位用0补充
1
2
mov al,01001000b
shl al,1 ;将al中的数据左移一位

执行后(al)=10010000b,CF=0

shr是逻辑右移指令:

  • 将一个寄存器或内存单元中的数据向右移位
  • 将最后移出的一位写入CF中
  • 最高位用0补充
1
2
mov al,10000001b
shr al,1

执行后(al)=01000000b,CF=1

14.3 CMOS RAM芯片

CMOS特征:

  1. 包含一个实时钟和一个有128个存储单元的RAM存储器
  2. 该芯片靠电池供电,关机后其内部的实时钟仍可正常工作,RAM中的信息不丢失
  3. 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。
  4. 芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口读写CMOS
  5. 70h为地址端口,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS单元中读取的数据,或要写入到其中的数据。CPU对CMOS的读写分两步进行,读CMOS的2号单元:①将2送入端口70h;②从端口71h读出2号单元的内容

在CMOS中,存放这当前的时间:年、月、日、时、分、秒。这6个信息的长度都为1个字节。存放单元为:秒:0 分:2 时:4 日:7 月:8 年:9

这些数据以BCD码的方式存放。BCD码是以4位二进制数表示十进制数码的编码方法。数值26,用BCD码表示为:0010 0110

1个字节表示2个BCD码,高4位的BCD码表示十位,低4位的BCD码表示个位。

编程,在屏幕中间显示当前的月份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:code
code segment
start: mov al,8
out 70h,al ;从8号单元读出当前月份的BCD码
in al,71h ;从数据端口71h中取得指定单元中的数据

mov ah,al ;al中为从CMOS的8号单元中读出的数据
mov cl,4
shr ah,cl ;ah中为月份的十位数码值
and al,00001111b ;al中为月份的个位数码值

add ah,30h
add al,30h ;显示对应的ASCII码字符

mov bx,0b800h
mov es,bx
mov byte ptr es:[160*12+40*2],ah ;显示月份的十位数码
mov byte ptr es:[160*12+40*2+2],al ;显示月份的个位数码

mov ax,4c00h
int 21h
code ends
end start

15. 外中断

外设输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关芯片送到外设。

在PC系统中,外中断源一共有以下两类:

  1. 可屏蔽中断

可屏蔽中断是CPU可以不响应的外中断。CPU是否响应要看标志寄存器的IF位。如果IF=1,CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,不响应可屏蔽中断。

8086CPU提供设置IF的指令如下:

1
2
sti		;设置IF=1
cli ;设置IF=0
  1. 不可屏蔽中断

不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:

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

②CS、IP入栈

③(IP)=8,(CS)=(0ah)