很久之前看王爽老师的《汇编语言》写的笔记,可能不太准确,先做个存档吧。
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 | jmp ax 执行指令前:ax=1000H,CS=2000H,IP=0003H |
8086CPU工作过程:
- 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
- IP指向下一条指令
- 执行指令(转到1,重复)
3. 内存访问
字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
起始地址为N的字单元简称为N地址字单元。比如一个字单元由2、3两个内存单元组成,则这个字单元的起始地址为2。
3.1 DS 和 [address]
DS为数据段寄存器(存放段地址),[address]表示一个内存单元(存放偏移地址)。
3.2 mov指令(add、sub指令同)
- 将数据直接送入寄存器:mov 寄存器,数据
将一个寄存器中的内容送人另一个寄存器:mov 寄存器,寄存器
将一个内存单元中的内容送入一个寄存器中:mov 寄存器,内存单元地址
1 | 将10000H(1000:0)中的数据读到al中: |
- mov 内存单元,寄存器
- mov 段寄存器,寄存器
8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds,1000H
这条指令是非法的,只好用一个寄存器来进行中转,即先将1000H送入一个通用寄存器,再将通用寄存器的内容送入ds。
1 | 将al中的数据送入内存单元10000H中: |
3.3 CPU提供的栈机制
1 | push ax 表示将寄存器ax中的数据送入栈中 |
8086CPU的入栈和出栈操作都是以字为单位进行的。
CPU如何知道栈顶位置?SS:SP,任意时刻,SS:SP指向栈顶元素。
push ax由以下两步完成:
- SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
- 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶
pop ax由以下两步完成:
- 将SS:SP指向的内存单元处的数据送入ax中
- SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
3.4 push指令(pop指令同)
- 将一个寄存器中的数据入栈:push 寄存器
- 将一个段寄存器中的数据入栈:push 段寄存器
- 将一个内存字单元处的字入栈:push 内存单元
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从DS中取得。
1 | mov ax,1000H |
将10000H~1000FH这段空间当作栈,初始状态栈是空的,将ax, bx, DS中的数据入栈。
1 | mov ax,1000H |
push、pop等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为0~FFFFH
一个栈段的容量最大为64KB。
在10000H处写入字型数据2266H
1 | 方法一 |
3.5 段的综述
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问。
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码中的指令。
对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来用。
CPU将内存中的某段内容当作代码,是因CS:IP指向了那里;CPU将某段内存当作栈,是因为SS:SP指向了那里。
4. 第一个程序
1 | assume cs:codesg |
在汇编语言程序中包含两种指令:伪指令、汇编指令。汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终为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$
- 定义一个段,名称为abc
1 | abc segment |
- 在这个段中写入汇编指令,来实现我们的任务
1 | abc segment |
- 指出程序要在何处结束
1 | abc segment |
- abc被当作代码段来用,所以应该将abc和cs联系起来
1 | assume cs:abc |
- 程序返回
1 | assume cs:abc |
- 语法错误和逻辑错误
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 | mov ax,[idata] |
1 | mov ax,[bx] ;(ax)=((ds)*16+(bx)) |
1 | inc bx ;inc bx的含义是bx中的内容加1 |
5.1 loop指令
loop指令的格式:loop 标号
CPU执行loop指令的时候要进行两步操作:
- (cx)=(cx)-1
- 判断cx中的值,不为零则转至标号处执行程序,为零则向下执行
编程运算$2^{12}$
1 | assume cs:code |
cx和loop指令相配合实现循环功能:①在cx中存放循环次数;②loop指令中的标号所标识地址要在前面;③要循环执行的程序段,要写在标号和loop指令的中间。
在汇编源程序,数据不能以字母开头,所以要在前面加0。A000H
在汇编源程序中要写为0A000H
。
1 | mov al,[0] ;(al)=0,将常量0送入al中(mov al,0含义相同) |
(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 | assume cs:code |
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 | assume cs:code |
6.1 在代码段中使用栈
程序运行时,定义的数据存放在cs:0~cs:F单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。(将cs:10~cs:2F的内存空间当作栈来用)
1 | assume cs:codesg |
6.2 将数据、代码、栈放入不同的段
在前面的内容中,我们在程序中用到了数据和栈,将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。这样做会产生两个问题:
- 把它们放到一个段中使程序显得混乱
- 前面数据中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。但如果数据、栈、代码需要的空间超过64KB,就不能放在一个段中(8086模式中一个段的容量不能大于64KB)
1 | assume cs:code,ds:data,ss:stack |
7. 更灵活的定位内存地址的方法
7.1 and和or指令
1 | and指令:逻辑与指令,按位进行与运算(1and1=1,1and0=0,0and0=0) |
7.2 ASCII码
1 | a->61h,b->62h |
7.3 以字符形式给出的数据
在汇编程序中,用’’的方式指明数据是以字符的形式给出的,编译器将它们转化为相对应的ASCII码。
1 | assume cs:code,ds:data |
7.4 大小写转换的问题
在codesg中填写代码,将tadasg中的第一个字符串转化为大写,第二个字符串转化为小写。
方法一:小写字母的ASCII码值比大写字母的ASCII码值大20H。
1 | assume cs:codesg,ds:datasg |
方法二:大写字母的第6位全为0,小写字母的第6位全为1。
1 | assume cs:codesg,ds:datasg |
7.5 [bx+idata]
[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata
mov ax,[bx+200]->(ax)=((ds)*16+(bx)+200)
简化7.4方法二(但这个一定要两个字符串长度相同)
1 | mov ax,datasg |
[bx]=0[bx],[5+bx]=5[bx]
7.6 SI 和 DI
si 和 di不能够分成两个8位寄存器来使用。
用ds:si指向要复制的源始字符串,用ds:di指向复制的目的空间。
1 | assume cs:codesg,ds:datasg |
利用[bx(si或di)+idata]的方式使程序变简洁:
1 | codesg segment |
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 | mov ax,[bx+si+idata] |
7.9 不同寻址方式的灵活应用
编程,将datasg段中每个单词首字母改为大写字母(用bx定位每行的起始地址,用3定位要修改的列,用[bx+idata]对目标单元进行寻址)
1 | assume cs:codesg,ds:datasg |
编程,将datasg段中每个单词改为大写字母(用bx定位每行的起始地址,用si定位要修改的列,用[bx+si]方式对目标单元进行寻址)
1 | assume cs:codesg,ds:datasg |
如果dx也被用了呢?所有寄存器都被用了呢?可以使用内存。
1 | assume cs:codesg,ds:datasg |
如果需要保存多个数据,需要记住哪个数据暂存在哪个单元中,这样程序容易混乱。一般来说,在需要暂存数据的时候,我们都应该使用栈。
1 | assume cs:codesg,ds:datasg,ss:stacksg |
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
- 在8086CPU中又有这4个寄存器可以在“[]”中进行内存单元寻址
- 在“[]”中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si, bx和di, bp和si, bp和di
1 | mov ax,[bx] |
- 只要在“[]”中使用寄存器bp,而指令中没有显性地给出段地址,段地址就默认在ss中
1 | mov ax,[bp] (ax)=((ss)*16+(bp)) |
8.2 数据的位置
指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口。
汇编指令 | 指令执行前数据的位置 |
---|---|
mov bx,[0] | 内存,ds:0单元 |
mov bx,ax | CPU内部,ax寄存器 |
mov bx,1 | CPU内部,指令缓冲器 |
8.2.1 数据位置的表达
- 立即数(idata)
- 寄存器
- 段地址(SA)和偏移地址(EA)
8.2.2 寻址方式
8.3 数据的长度
8086CPU可以处理两种尺寸的数据,byte和word。
- 通过寄存器名指明要处理的数据的尺寸
- 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte
例如下面的指令中,用word ptr指明了指令访问的内存单元是一个字单元
1 | mov word ptr ds:[0],1 |
下面的指令中,用byte ptr指明了指令访问的内存单元是一个字节单元
1 | mov byte ptr ds:[0],1 |
- 其他方法
有些指令默认了访问的是字单元还是字节单元,比如push [1000H]就不用指明访问的是字单元还是字节单元。因为push指令只进行字操作。
8.4 div指令
div是除法指令,使用div做除法时应注意以下问题
- 除数:有8位和16位两种,在一个reg或内存单元中。
- 被除数:默认放在AX 或 DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。
- 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
1 | div reg |
编程,利用除法指令计算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 | mov dx,1 |
程序执行后,(ax)=03E8H(即1000),(dx)=1(余数为1)
8.5 伪指令dd
dd(double word)双字型数据
用div计算data段中第一个数据除以第二个数据后的结果,商存在第三个数据的存储单元中。
1 | data segment |
8.6 dup
dup是一个操作符,在汇编语言中同db, dw, dd等一样,也是由编译器识别处理的符号。它是和db, dw, dd等数据定义伪指令配合使用,用来进行数据的重复。
1 | db 3 dup (0) ;定义了3个字节,它们的值都是0,相当于db 0,0,0 |
dup使用格式
1 | db 重复的次数 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 | assume cs:codesg |
程序在运行中将s处的一条指令复制到s0处
1 | assume cd:codesg |
9.2 jmp指令
jmp为无条件转移指令,可以指修改IP,也可以同时修改CS和IP。
jmp指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移、段内近转移)
9.2.1 依据位移进行转移的jmp指令
1 | jmp short 标号(转到标号处执行指令):(IP)=(IP)+8位位移 |
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127,向前转移时最多可以越过128个字节,向后转移最多可以越过127个字节。
CPU在执行jmp指令的时候并不需要转移的目的地址。
1 | assume cs:codesg |
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 | assume cs:codesg |
9.2.3 转移地址在寄存器中的 jmp指令
1 | jmp 16位reg:(IP)=(16位reg) |
9.2.4 转移地址在内存中的jmp指令
转移地址在内存中的jmp指令有两种格式:
- jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
1 | mov ax,0123h |
执行后,(IP)=0123H
- jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
(CS)=(内存单元地址+2),(IP)=(内存单元地址)
1 | mov ax,0123h |
执行后,(CS)=0,(IP)=0123h,CS:IP指向0000:0123
9.3 jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127
1 | jcxz 标号(如果(cx)=0,转移到标号处执行):当(cx)=0时,(IP)=(IP)+8位位移 |
9.4 loop指令
loop指令是循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127
1 | loop 标号((cx)=(cx)-1,如果(cx)≠0,转移到标号处执行):如果(cx)≠0,(IP)=(IP)+8位位移 |
10. call和ret指令
call和ret指令都是转移指令,他们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计。
10.1 ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
10.1.1 ret指令
CPU执行ret指令时,进行下面2步操作:
- (IP)=((SS)*16+(SP))
- (SP)=(SP)+2
CPU执行ret指令时相当于进行:
1 | pop IP |
10.1.2 retf指令
CPU执行retf指令时,进行下面4步操作:
- (IP)=((SS)*16+(SP))
- (SP)=(SP)+2
- (CS)=((SS)*16+(SP))
- (SP)=(SP)+2
CPU执行retf指令时相当于进行:
1 | pop IP |
10.2 call指令
CPU执行call指令时,进行2步操作:
- 将当前的IP或CS和IP压入栈中
- 转移
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。
10.2.1 依据位移进行转移的call指令
1 | call 标号(将当前的IP压栈后,转到标号处执行指令) |
CPU执行此种格式的call指令时,进行如下操作:
- (sp)=(sp)-2
((ss)*16+(sp))=(IP) - (IP)=(IP)+16位位移
CPU执行“call 标号“时,相当于进行:
1 | push IP |
10.2.2 转移的目的地址在指令中的call指令
1 | call far ptr 标号 |
实现的是段间转移。
CPU执行此种格式的call指令时,进行如下操作:
(sp)=(sp)-2
((ss)*16+(sp))=(CS)
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(CS)=标号所在段的段地址
(IP)=标号在段中的偏移地址
CPU执行“call far ptr 标号“时,相当于进行:
1 | push CS |
10.2.3 转移地址在寄存器中的call指令
1 | call 16位reg |
CPU执行此种格式的call指令时,进行如下操作:
1 | (sp)=(sp)-2 |
CPU执行“call 16位reg“时,相当于进行:
1 | push IP |
10.2.4 转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式:
- call word ptr 内存单元地址
CPU执行“call word ptr 内存单元地址“时,相当于进行:
1 | push IP |
1 | mov sp,10h |
执行后,(IP)=0123h,(sp)=0EH
- call dword ptr 内存单元地址
CPU执行“call dword ptr 内存单元地址“时,相当于进行:
1 | push CS |
1 | mov sp,10h |
执行后,(CS)=0,(IP)=0123H,(sp)=0CH
10.3 call和ret的配合使用
1 | assume cd:code |
10.4 mul指令
mul指令是乘法指令,使用mul做乘法时要注意两点:
- 两个相乘的数:要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认放在AX中,另一个放在16位reg或内存字单元中
- 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中存放
1 | mul reg |
计算100*10。(100和10小于255,可以做8位乘法)
1 | mov al,100 |
结果:(ax)=1000(03E8H)
计算100*10000。(100小于255,但10000大于255,所以必须做16位乘法)
1 | mov ax,100 |
结果:(ax)=4240h,(dx)=000FH(F4240H=1000000)
11. 标志寄存器
标志寄存器作用:
- 用来存储相关指令的某些执行结果
- 用来为CPU执行相关指令提供行为依据
- 用来控制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 | mov ax,1 |
执行后,(ax)=0,ZF=1
11.2 PF标志
flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。如果1的个数为偶数,PF=1;否则PF=0。
1 | mov al,1 |
执行后,结果为11=00001011B,其中有3个1,PF=0
11.3 SF标志
flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果是否为负。如果结果为负,SF=1;否则SF=0。
1 | mov al,10000001B ;al=-127 |
结果:(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 | mov al,98H |
执行后,(al)=30H,CF=1,CF记录了从最高有效位向更高位的进位值。
11.5 OF标志
flag的第11位是OF,溢出标志位。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF=1;否则OF=0。
对于8位的有符号数据,机器所能表示的范围就是-128~127;对于16位的有符号数据,机器所能表示的范围是-32768~32767。
1 | mov al,0F0H ;0f0h=240=-16 240-128*2=-16 |
执行后,对于无符号运算,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 | mov ax,2 ;(ax)=2 |
11.7 sbb指令
sbb是带借位减法指令,它利用了CF位上记录的借位值。
1 | sbb 操作对象1,操作对象2 |
操作对象1=操作对象1-操作对象2-CF
1 | mov bx,1000H ;(bx)=1000h |
11.8 cmp指令
cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。
1 | cmp 操作对象1,操作对象2 |
计算操作对象1-操作对象2但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。
1 | mov ax,8 ;(ax)=8 |
指令执行后,零标志位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 | data segment |
11.10 DF标志和串传送指令
flag的第10位是DF,方向标志位。在串处理指令中,控制每次操作后si、di的增减。
df=0 每次操作后si、di递增;
df=1 每次操作后si、di递减。
11.10.1 串传送指令
1 | movsb |
执行movsb指令相当于进行下面操作:
((es)16+(di))=((ds)\16+(si))
如果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 | s: movsb |
rep的作用是根据cx的值,重复执行后面的串传送指令。由于每执行一次movsb指令si和di都会递增或递减后一个或前一个单元,则rep movsb就可以循环实现(cx)个字符的传送。
由于df位决定着串传送指令执行后si和di改变的方向,所以CPU应该提供相应的指令来对df位进行设置,从而使人能够决定传送的方向。
8086CPU提供下面两条指令对df位进行设置
1 | cld指令:将标志寄存器的df位置0 |
编程,用串传送指令,将data段中的第一个字符串复制到它后面的空间中。
1 | data segment |
①传送的原始位置:ds:si data:0
②传送的目的位置:es:di data:10h
③传送的长度:cx (cx)=16
④传送的方向:df df=1
1 | mov ax,data |
11.11 pushf和popf
pushf的功能是将标志寄存器的值压栈,popf是从栈中弹出数据,送入标志寄存器中。
12. 内中断
任何一个通用的CPU,可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。中断的意思是CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
对于8086CPU,当内部有下面的情况发生的时候将产生相应的中断信息:
- 除法错误,比如执行div指令产生的除法溢出 0
- 单步执行 1
- 执行into指令 4
- 执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码
12.1 中断处理程序
CPU收到中断信息后,应该转去执行该中断信息的处理程序。中断信息中包含有标识中断源的类型码,根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表在0000:0000~0000:03FF的1024个单元存放着。在中断向量表中,一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。
12.2 中断过程
用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动完成的。CPU硬件完成这个工作的过程被称为中断过程。
中断过程:
- (从中断信息中)取得中断类型码N
- 标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中) pushf
- 设置标志寄存器的第8位TF和第9位IF为0 TF=0,IF=0
- CS的内容入栈 push CS
- IP的内容入栈 push IP
- 从内存地址为中断类型码4 和中断类型码\4+2 的两个字单元中读取中断处理程序的入口地址设置IP和CS (IP)=(N*4),(CS)=(N*4+2)
12.3 中断处理程序和iret指令
中断处理程序的编写步骤:
- 保存用到的寄存器
- 处理中断
- 恢复用到的寄存器
- 用iret指令返回
iret指令的功能用汇编语法描述:
1 | pop IP |
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 | assume cs:code |
13.2 DOS中断例程
int 21h中断例程是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。
前面一直使用的是int 21h中断例程的4ch号功能,即程序返回功能
1 | mov ah,4ch ;程序返回 |
(ah)=9表示调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数。
编程,在屏幕的5行12列显示字符串“Welcome to masm!”
1 | assume cs:code,ds:data |
14. 端口
在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下三种芯片
- 各种接口卡(网卡、显卡)上的接口芯片,它们控制接口卡进行工作
- 主板上的接口芯片,CPU通过它们对部分外设进行访问
- 其他芯片,用来存储相关的系统信息,或进行相关的出入输出处理
从CPU的角度,将寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。每一个端口在地址空间中都有一个地址。
CPU可以直接读写以下3个地方的数据:
- CPU内部的寄存器
- 内存单元
- 端口
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 | in al,20h ;从20h端口读入一个字节 |
对256~65535的端口进行读写时,端口号放在dx中:
1 | mov dx,3f8h ;将端口号3f8h送入dx |
14.2 shl和shr指令
shl和shr是逻辑移位指令。
shl是逻辑左移指令,它的功能为:
- 将一个寄存器或内存单元中的数据向左移位
- 将最后移出的一位写入CF中
- 最低位用0补充
1 | mov al,01001000b |
执行后(al)=10010000b,CF=0
shr是逻辑右移指令:
- 将一个寄存器或内存单元中的数据向右移位
- 将最后移出的一位写入CF中
- 最高位用0补充
1 | mov al,10000001b |
执行后(al)=01000000b,CF=1
14.3 CMOS RAM芯片
CMOS特征:
- 包含一个实时钟和一个有128个存储单元的RAM存储器
- 该芯片靠电池供电,关机后其内部的实时钟仍可正常工作,RAM中的信息不丢失
- 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。
- 芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口读写CMOS
- 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 | assume cs:code |
15. 外中断
外设输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关芯片送到外设。
在PC系统中,外中断源一共有以下两类:
- 可屏蔽中断
可屏蔽中断是CPU可以不响应的外中断。CPU是否响应要看标志寄存器的IF位。如果IF=1,CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,不响应可屏蔽中断。
8086CPU提供设置IF的指令如下:
1 | sti ;设置IF=1 |
- 不可屏蔽中断
不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:
①标志寄存器入栈,IF=0,TF=0
②CS、IP入栈
③(IP)=8,(CS)=(0ah)