声明
本篇文章是在学习王爽老师的汇编语言第三版时写的笔记,并不是很全面。
什么是汇编语言?
说到汇编我们先来说一下机器语言,机器语言是机器指令的集合,机器指令就是一台机器可以正确执行的命令,在计算机中就是一列二进制数字,计算机将其转变为一系列高低电平使计算机的电子器件受到驱动进行运算,但是一列二进制数字难于辨别和记忆,于是汇编语言产生了,他将难于记忆的机器指令转化成与人类语言香接近的汇编指令,使其便于阅读和理解,每一种CPU都拥有自己的汇编指令集。
汇编语言的组成
汇编语言有三类指令组成:
- 汇编指令
- 伪指令:由编译器执行,计算机不执行
- 其他符号:+、-、*、/等,由编译器识别,没有对应的机器码
CPU是如何工作的?
CPU是控制计算机运作的核心部件,但是想让一个计算机工作必须要向他提供指令和数据,指令和数据是存放在存储器中的,也就是我们平时所说的内存。
存储器划分成许多存储单元,每个存储单元都有一个编号,也就是地址,当CPU想要访问数据的时候,它需要先找到这个数据所在的地址,然后再读取数据,同时还需要知道使用什么器件操作。
由此引出CPU进行数据的读写时需要的三个条件:
- 地址信息(通过地址线操作)
- 控制信息(通过控制线操作)
- 数据信息 (通过数据线操作)
地址线
用于确定数据存储地址也就是数据存放在哪个存储单元(存储单元以字节为最小单位),假设一个CPU有N根地址线,那么可以说这个CPU的地址总线的宽度为N,CPU可以寻找的存储单元个数为2的N次方个,内存地址的大小受到地址线的约束。
数据线
用于CPU与其他器件之间的数据传送,8根数据线一次只能传送8位二进制数,也就是一个字节。
控制线
用于CPU对外部器件的控制,控制总线的宽带决定了CPU对其他器件的控制能力。
CPU的组成
CPU是由运算器(信息处理)、控制器(控制器件工作)、寄存器(信息存储)等器件组成,他们之间通过总线相连。
通用寄存器
通用寄存器时用于存放一般性数据的,以8086 CPU为例,8086 CPU所有的寄存器都是16位的,8086 CPU中的通用寄存器有AX、BX、CX、DX。为了兼容上一代CPU中的8位寄存器,这4个寄存器都可以拆成两个8位的寄存器使用,也就是将16位分成两个8位,AX可分为AH和AL,BX可分为BH和BL,CX可分为CH和CL,DX可分为DH和DL。这里的H可以记为High,高位,L可以记为Low,低位。
再来说一下数据宽度,因为寄存器有16位和8位两种,所以数据宽度可以分为字节和字,一个字节由8个bit组成,可以直接使用低位存储,一个字由两个字节,也就是16位,比如一个十六进制3E10,将这个十六进制存放到AX寄存器总,3E就存放在高位AH中,10存放在低位AL中。
内存单元的物理地址
之前说过数据是通过地址线存放在内存单元中的,那么就必须要确定这个内存单元的地址。
基础地址=段地址x16
基础地址+偏移地址=物理地址
所以物理地址=段地址x16+偏移地址,这里的16是十进制的16,也就是十六进制的A。
这里的段地址可以理解为一段一段的内存存储单元,但是段地址并不是固定的,可以认为10000H-100FFH是一个段,也可以将这段地址看成是10000H-1007F和10080H-100FFH组成的段,以10000H-100FFH为例,它的段地址是1000H,偏移地址为FF,这样基础地址就是1000Hx10H=10000H,这样就可以确定基础地址。再加上偏移地址,就可以确定这段内存。
段寄存器
既然可以通过段地址和偏移地址确定物理地址,那么就需要寄存器来存放段地址,8086 CPU中有4个段地址:CS、DS、SS、ES,偏移地址存放在IP寄存器中,这里以CS寄存器为例,可以简单的理解为通过CS(代码段寄存器)、IP(指令指针寄存器)两个寄存器可以确定CPU需要读取的指令的地址,也就是CPU会将CS:IP指向的内容当作指令执行。
操作指令
-
mov指令
mov指令称为传送指令,可以使用这个指令为寄存器赋值,例:mov ax,123
这个语句就类似于C语言中的赋值操作ax=123;
-
jmp指令
jmp指令叫做转移指令,可以用于设置CS、IP中的值(mov指令不能修改CS、IP中的值),例:jmp 2AE3:3
,执行这个指令后,CS的值被修改为2AE3H,IP的值被修改为0003H,CPU将从2AE33H处读取指令。
8086CPU工作过程
(1)从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
(2)IP指向下一条指令
(3)执行读取进指令缓冲器的指令
(4)重复上述三个步骤
字的存储
在上一篇中说到一个16位寄存器可以存放一个字(16位)或者一个字节(8位),当存放一个字节的时候只需要一个内存单元(内存单元是以字节为单位的,8位),而存放一个字需要两个内存单元,这样存放一个字就需要两个连续的内存单元,这个16位的字,高位存放在高地址,低位存放在低地址。
内存地址 | 内存数据 |
---|---|
0 | 20H |
1 | 4EH |
2 | 12H |
3 | 00H |
对于字来说0就是低地址单元,1是高地址单元,则字型数据4E20H的低地址位20存放在0号单元,高地址位4E存放在高地址单元,因为它的起始地址为0,又可以称作0地址字单元。
段地址寄存器
通过前面学过的知识我们可以知道当CPU想要对一个内存单元进行操作时,必须知道它的地址,要知道内存单元的地址就要知道它的段地址和偏移地址,在8086 CPU中,DS寄存器就是用来存放段地址的,执行指令的时候,CPU会自动读取DS中的数据为内存单元的段地址,使用[偏移地址]
来表示偏移地址,假设DS寄存器中此时存放的是1000H,那么mov al,[0]
就表示将10000H(物理地址=段地址x16+偏移地址)地址上存放的数据存到al中。
如果想要修改DS寄存器中的值,那么直接使用mov指令将数字存到DS寄存器中是不行的,只能先将值存到一个寄存器中,再使用mov指令将这个寄存器中的值存到DS中,例:
mov bx,1000H
mov ds,bx
mov ax,[0]
这样就将地址为10000H处的值存放到ax寄存器中了,也就是将下表中的4E20H存放到ax中。(ax是十六位,一个字,两个字节)
内存地址 | 内存数据 |
---|---|
0 | 20H |
1 | 4EH |
2 | 12H |
3 | 00H |
所以一个内存单元地址的确定可以通过段地址DS+[偏移地址]
进行确定。
add和sub指令
顾名思义,add指令就是用来做加法操作的,sub指令就是用来做减法操作的,例如add ax,8
这条指令相当于C语言中的ax = ax + 8
,sub ax,8
相当于C语言中的ax = ax - 8
。
add和sub指令可以操作的对象有以下几种形式,以add指令为例:
add 寄存器,数据 例如:add ax,8
add 寄存器,寄存器 例如:add ax,bx
add 寄存器,内存单元 例如:add ax,[0]
add 内存单元,寄存器 例如:add [0],ax
栈
栈,是一段具有特殊访问方式的存储空间,它的存取规则是先进后出,后进先出,就像是一个上面没有盖的桶,最后放进去的东西只能最先取出来。
那么我们怎么知道在连续的存储空间中,哪一段是栈,哪一段不是栈,回想一下,CPU是根据CS、IP两个寄存器中存放的值判断当前指令存放的位置,根据DS、偏移地址判断数据存放在哪个内存单元,显然,也会有相应的寄存器用来判断哪一段是栈,在8086 CPU中,通过段寄存器SS和寄存器SP就可以确定栈的位置,栈有栈顶和栈底,栈顶的段地址存放在SS中,SP用于存放偏移地址,在任意时刻SS:SP指向栈顶元素,它指向的第一个元素可以理解为栈底,以后每存放一个数据SS:SP就向上提升,而它所指向的就是栈顶。
PUSH和POP指令
有了栈,那么就可以对栈进行存取数据的操作,使用的指令时push和pop指令,push指令用于入栈操作,也就是存数据,pop指令用于出栈,也就是取数据。8086 CPU的入栈、出栈操作都是以字为单位。
假设有如下一段连续的内存单元,此时SS为1000H,SP为000EH,AX为1234H,栈顶为1000EH。
内存地址 | 数据 |
---|---|
1000AH | |
1000BH | |
1000CH | |
1000DH | |
1000EH | |
1000FH |
首先,因为SS为1000H,SP为000EH,所以栈顶是指向1000EH位置的:
SS为1000H,SP为000EH,AX为1234H
内存地址 | 数据 |
---|---|
1000AH | |
1000BH | |
1000CH | |
1000DH | |
1000EH | [SS:SP] |
1000FH |
接着我们执行入栈操作,将AX(1234H)入栈,使用push ax
操作进行压栈。
SS为1000H,SP为000CH,AX为1234H
内存地址 | 数据 |
---|---|
1000AH | |
1000BH | |
1000CH | 34[SS:SP] |
1000DH | 12 |
1000EH | |
1000FH |
此时,新的栈顶就变成了1000CH,接着,我们再将一个立即数5678H压入栈中,使用push 5678
:
SS为1000H,SP为000AH,AX为1234H。
内存地址 | 数据 |
---|---|
1000AH | 78[SS:SP] |
1000BH | 56 |
1000CH | 34 |
1000DH | 12 |
1000EH | |
1000FH |
这是,栈顶就是1000AH,SS为1000H,SP为000AH。
演示完了入栈,我们再进行出栈,使用pop ax
操作,将出栈的数据放入到ax寄存器中:
SS为1000H,SP为000CH,AX为5678H。
内存地址 | 数据 |
---|---|
1000AH | 78 |
1000BH | 56 |
1000CH | 34[SS:SP] |
1000DH | 12 |
1000EH | |
1000FH |
注意,出栈并不意味着之前写入内存单元的数据被删除了,之前写入的数据还是存在的,只不过它不在栈中了。
[BX]的含义
想要完整的描述一个内存单元,需要两个条件:1.内存单元的地址 2.内存单元的长度(类型)。
首先讲一下[0]的含义,[0]表示的内存单元,偏移地址为一个立即数0,段地址默认保存在DS寄存器中(之前的文章中讲到过,通过DS寄存器和偏移地址(ds:[0])就可以确定内存中的一个地址)。
说完[0]再回头来说[bx],[bx]表示的依然是偏移地址,但是它不是一个立即数了,而是保存在一个寄存器中,段地址还是默认保存在DS寄存器中。
loop指令
loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两部操作,第一步:cx = cx - 1 第二步:判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。从这两步我们可以看出loop指令的执行结果受到cx中值的影响,通常情况下,我们使用loop指令来实现循环功能,cx中存放loop指令需要循环的次数。
伪指令
在汇编语言中,包含两种指令,一种是汇编指令,另一种是伪指令,汇编指令是对应着机器码,可以直接被编译为机器指令被CPU执行,而伪指令不会被CPU执行,它是用来让编译器进行相关的编译工作。
(1)segment
segnment和ends是一对成对使用的伪指令,用于定义一个段,segnment用于声明一个段开始,ends用于声明一个段结束,使用格式为:
段名 segnment
段名 ends
比如用codesg为段名声明一个用于存放数据的段:
codesg segnment
codesg ends
一个汇编程序由多个段形成,这些段用来存放代码、数据或者当作栈空间使用,一个有意义的汇编程序至少要有一个段用于存放代码。
(2)end
end指令是一个汇编程序的结束标记,编译器在执行汇编程序的时候如果碰到end就结束对源程序的编译。
(3)assume
assume是假设的意思,它用于假设某一个寄存器和程序中的某一段相关。比如:
assume cs:codesg
就是将代码段codesg和CPU的段寄存器cs联系在一起。
再来讲讲标号,除了汇编指令和伪指令以外,还有标号,一个标号指代一个地址。比如codesg segment,这个codesg最终将被处理为一个段的段地址。
程序分析
接着来分析一段求2的10次方汇编程序
assume cs:code
code segment
mov ax,2
mov cx,9
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
分析:
先来看一下整体框架
assume cs:code表示cs与code代码段有联系
code segment和code ends用于标识一段代码段
end用于表示程序结束
接着分析中间部分
mov ax,2 将2存到ax寄存器中用作初始值
将9存放到cx中用于循环次数计数
s是标号,指代add ax,ax的地址
add ax,ax就是将ax中的值进行自加
loop s就是执行到这一句的时候就就跳转到标号为s的地方,也就是add ax,ax的地址,loop每执行一次cx就减一,只到cx为0就执行结束,loop指令就不再跳转而是接着向下执行
最后的 mov ax,4c00h和 int 21h暂时先不管
这样就将2的10次方的值保存在ax中了。
and和or指令
(1)and指令
and指令:逻辑与,按位进行与运算,运算规则是有0为0
例:
mov al,01100011B
and al,00111011B
与运算之后的结果是00100011B
有了这条指令后,我们就可以将我们想要设置为0的位置设置为0,而其他的位置不用改变
(2)or指令
or指令:逻辑或,按位进行或运算,运算规则是有1为1
例:
mov al,01100011B
and al,00111011B
或运算之后的结果是01111011B
有了这条指令后,我们就可以将我们想要设置为1的位置设置为1,而其他的位置不用改变
内存单元的其他表示方式
可以使用[bx+idata]的方式来表示一个内存单元,他的段地址存放在ds中,偏移地址为bx中的值+idata。
例:
mov ax,[bx+100]
这样ax中存放的地址就是ds*10H+bx+100。
这样的表示方式可以用来表示数组,比如一个存放了三个数字的数组,下标分为为0、1、2,使用ds:bx定位到数组的第一个数,也就是数组下标为0的位置,这样如果想要将下标为1的数字存放到ax中,就可以使用mov ax,[bx+1],想要存放下标为2的数字,只需要mov ax,[bx+2]。
除了使用[bx+idata]之外,还可以使用[bx+si]和[bx+di]来表示内存地址(si和di不能拆成两个8位寄存器使用),也就是使用bx寄存器中存放的值和si或di中的值相加来表示偏移地址。
总结一下内存地址的表示方式:
(1)[idata] 使用一个常量直接表示地址
(2)[bx] 使用一个变量表示内存地址
(3)[bx+idata] 使用一个变量和一个常量表示地址
(4)[bx+si] 使用两个变量表示地址
(5)[bx+si+idata] 使用两个变量和一个常量表示地址
对前几篇的补充
数据宽度
当我们对一个内存单元中的数据进行操作的时候一定要时刻明确以下几点:
- 数据从哪取?
- 取数据的大小?
- 取完的数据存到哪?
(1)数据从哪取?
也就是要能找到存放数据的内存单元的物理地址,前面说到过物理地址=段地址*16+偏移地址,用于表示一个内存单元物理地址的方法有以下五种:
- [idata] 使用一个常量直接表示地址
- [bx] 使用一个变量表示内存地址
- [bx+idata] 使用一个变量和一个常量表示地址
- [bx+si] 使用两个变量表示地址
- [bx+si+idata] 使用两个变量和一个常量表示地址
(2)取数据的大小?
知道怎么确定内存单元的大小之后,下一个问题就是取多大数据,是一个字节,还是一个字,当我们使用mov ax,bx
时,因为ax和bx都是一个字的大小,所以编译器知道我们要取得数据是一个字大小,也就是两个字节,或者当我们使用mov ax,[0]
时,编译器也知道我们取的是一个字大小,因为ax寄存器是一个字大小,但是如果我们仅仅是对一个内存地址进行操作,比如我们希望给内存地址ds:[0]里面存入1,如果是给ax赋值1,那么编译器知道我们是对一个字的数据长度进行操作,那么现在对内存进行操作,就需要自己定义需要操作的数据的长度,以上面那个问题为例,对一个字长度的内存进行操作:mov word ptr [0],1
,这样编译器就知道我们是对一个字长度的数据进行操作,如果是对一个字节长度进行操作就是mov byte ptr [0],1
。
注意:mov ax,bl
是错误的,因为这两个寄存器的熟读宽度不一样。
(3)取完的数据存到哪?
通常,我们想要知道数据存放的位置,是通过ds和偏移地址进行确定,也就是数据从ds+偏移地址处进行取,通常情况下,使用ds和si两个寄存器表示取数据的位置,使用es和di两个寄存器表示数据将要存放的位置,也就是将段地址存放在ds和es,偏移地址存放在si和di。
伪指令
在我们使用伪指令的时候,,比如下面这个例子,假设我们在数据段定义了一段需要用到的数据:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov ax,2
mov cx,9
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
当我们想要运行这个程序的时候,cs:ip是指向code段的,这样就将我们定义的数据当成是汇编指令所对应的机器码,从而将数据变成了汇编指令执行,如果想要避免这种情况的出现,我们就要指定让编译器从哪边开始执行,这就用到了start
伪指令,将上面的程序改成这样:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start: mov ax,2
mov cx,9
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end start
注意:最后的伪指令end,要改成end start,这就是end的另一个用途,告诉编译器程序的入口在什么地方。
div指令
上一篇里面说过当我们对一个内存单元中的数据进行操作的时候一定要时刻明确以下几点:
- 数据从哪取?
- 取数据的大小?
- 取完的数据存到哪?
其实除了这三点外,还有一点就是数据处理,所以在使用汇编语言进行编程的时候最重要的就是要处理好这四点:数据从哪取,取多少,取到的数据怎么处理,处理完放到哪。
接着来说一下除法指令div,在使用出发指令的时候要注意以下问题:
(1)除数:除数有8位和16位两种,存放在一个寄存器或者内存单元中。
(2)被除数:如果除数为8位,被除数为16位,则默认存放在AX中,如果除数为16位,被除数为32位,则高位存放在DX中,低位存放在AX中。
(3)结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数,如果除数为16位,则AX中存储除法操作的商,DX中存储除法操作的余数。
使用1001除100为例:
mov ax,1001
mov bl,100
div bl
首先可以看出这是一个8位的除法,所以程序执行后,al=0AH(商),ah=1(余数)。
验证一下,这里的03E9是十六进制的1001,64是十六进制的100:
我们来看一下ax寄存器的情况:
再以100001除100为例:
mov dx,1
mov ax,86A1H
mov bx,100
div bx
可以看出这是一个16位的除法,100001的十六进制位186A1,可以明显的看出一个寄存器已经存放不了了,这是将高位存放在DX中,低位存放在AX中,也就是将1放在DX中,186A放在AX中,执行后AX中存放上商03E8(1000),DX中存放余数1。
验证:
执行一下,注意AX和DX:
伪指令dd
我们定义一个字节型数据时,用的是db(define byte),定义一个字型数据时,用的是dw(define word),除了这两个以外,还有更大的数据宽度,双字型数据dd用于定义dword(double word),我们可以到内存中验证一下。
assume cs:code,ds:data,ss:stack
data segment
db 1
dw 1
dd 1
data ends
stack segment
dw 0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov,sp,16
mov ax,data
mov ds,ax
mov ax,4c00h
int 21h
code ends
end start
这里我在数据段中声明了db类型的1,dw类型的1和dd类型的1,我们到内存中看一下:
dup指令
dup指令是一个操作符,用于数据的重复。
比如当我想要重复定义20个字节型的1,按照以前的方法我们是这么定义的:
db 1,1,1,1,1,1,1,1,1,1
db 1,1,1,1,1,1,1,1,1,1
可能定义20个也还行,但是如果是200个呢?这时有了dup指令就比较方便了,使用dup指令是这么定义的:
db 200 dup (1)
语法格式:
db 重复的次数 dup (重复的字节型数据)
dw 重复的次数 dup (重复的字型数据)
dd 重复的次数 dup (重复的双字型数据)
我们来实验一下:
这样就直接定义了200个1。
操作符offset
操作符offset的作用取标号的偏移地址,直接举个例子吧:
assume cs:codesg
data segment
db 0
data ends
codesg segment
start: mov ax,offset start
mov ax,4c00h
int 21h
codesg ends
end start
这里取start标号的位置存放到ax中,start是这段程序的开头,所以偏移位置为0,这里也就是将0存放到ax中,看一下内存中的结果:
这里可以看到编译器直接识别到了0.
jmp指令
jmp指令是无条件转移指令,用于实现位移(这里部不分开讲段内远转移和段内近转移),比如:
assume cs:codesg
codesg segment
start: mov ax,0
jmp s
add ax,1
s: inc ax
mov ax,4c00h
int 21h
codesg ends
end start
这里执行到jmp s的时候就会跳过add ax,1这条指令,而跳转到inc ax
(ax = ax + 1相当于在C语言里写的ax++),也就是这个程序执行完ax里面的值是1,编译器里里面看一下:
执行完ax里面的值为1,换个角度看:
这里可以看到,编译器已经帮我们转成了地址,刚好是指向inc ax
那条指令。
那么jmp指令是怎么知道要跳转到哪的呢?
我们先来回忆一下CPU执行程序的过程:
(1)从CS:IP指向的内存单元读取指令,将读取到的指令存放到指令缓冲器。
(2)将IP的值修改为下一条要执行的指令的地址(IP = IP + 所读取指令的长度)。
(3)执行指令,转到第一步重复这个过程。
以上图为例,我们分析一下jmp指令读取和执行过程:
(1)CS=0B74,IP=0003,CS:IP指向指令EB04。
(2)将指令码EB04读取进入指令缓冲器。
(3)IP=IP+所读指令的长度,即IP=0003+2=0005(此处的NOP是空指令可忽略,IP指向006)。
(4)CPU执行指令缓冲器中的指令EB04。
(5)指令执行后,IP指向下一个要执行的位置,也就是0009,CS:IP指向inc ax。
jcxz指令
jcxz是有条件转移指令,j是指jmp,cx是指cx寄存器,z是指zero,jcxz就是当cx等于0的时候跳转,如果不为零就顺序执行,相当于C语言里面的:
if(cx == 0){
jmp 标号;
}
首先,我们向仍然奋战在武汉一线的医护工作人员们致以最诚挚的敬意!同时也祝各位小伙伴新年快乐!
call指令
call指令的作用和jmp指令的作用类似,只是它比jmp指令多做了几个工作,call指令会将当前的IP或者CS和IP压入栈中,然后再进行跳转,也就是说call指令是push ip
和jmp 标号
两个指令的结合。
注:当执行到call指令时,当前的ip中的值是下一条指令的地址,详见之前说过的CPU执行过程。
直接举个例子:
assume cs:codesg,ss:stack
stack segment
db 8 dup (0)
stack ends
codesg segment
start: mov ax,stack
mov ss,ax
mov sp,8
call s
inc ax
s: pop ax
mov ax,4c00h
int 21h
codesg ends
end start
首先声明一个8个字节的栈,然后call s
,先将执行到这条指令时的IP压栈,然后跳转到标号为s处,再将栈里面的一个字型数据出栈,放到ax中:
这是我们写的程序,看一下当我们执行到call s
时,下一条指令所在地址的值是0B75:000B,这时会将IP的值压栈,看一下栈中的情况:
可以看到6和7的位置也就是栈底已经将000B压栈了,下一步就是pop ax
,就是将栈中的数据存放到ax中,此时就是将000B存放到ax中,执行之后看一下ax的变化:
看到ax寄存器中存放的就是000B。
ret指令
ret指令的作用是pop ip
,将栈中的数据出栈到ip寄存器。
通常情况下,ret指令和call指令结合使用,call指令会将ip的值压栈,而ret指令刚好将栈中的数据出栈,再返还给ip寄存器,也就意味着,程序在执行到call指令时,跳转到其他地方执行程序,在程序的结尾加上ret,也就将要执行的指令又回到了call指令下一步。
直接来看程序吧:
assume cs:codesg,ss:stack
stack segment
db 8 dup (0)
stack ends
codesg segment
start: mov ax,stack
mov ss,ax
mov sp,8
call s
inc ax
s: ret
mov ax,4c00h
int 21h
codesg ends
end start
这里是我们的程序:
执行到call指令的时候,将ip压栈,然后跳转到ret指令,这时,ret指令将栈中的数据出栈到ip。
call指令先将000B压栈,也就是指向inc ax
指令的地址,然后跳转到0B75:000C,0B75:000C处的指令是ret,接着执行时,又将000B存放到ip,接着看图:
ip中存放的是000B,也就是又跳转到了inc ax
指令。
mul指令
mul指令是乘法指令,乘法指令和 除法指令一样,分为8位和16位两种:
(1)两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位寄存器或内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位寄存器或者内存字单元中。
(2)结果:如果是8位乘法,结果默认存放在AX中;如果是16位乘法,结果高位默认在AX中存放,低位在AX中存放。
格式如下:
mul reg
mul 内存单元
标志寄存器
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机制,个数和结构都可能不同)具有以下三种作用。
(1)用来存储相关指令的某些执行结果
(2)用来为CPU执行相关指令提供行为依据
(3)用来控制CPU的相关工作方式
这种特殊的寄存器被称为标志寄存器。
- ZF标志位
ZF标志位叫做零标志位,它用于记录相关指令执行后,其结果是否为0,如果结果为零,那么zf=1,如果结果不为0,那么zf=0
比如:
mov ax,1
sub ax,1
执行之后,结果为0,则zf=1。
- PF标志位
PF标志位是奇偶标志位,它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数,如果1的个数为偶数,pf=1,如果为奇数,那么pf=0。
比如:
mov al,1
add al,10
执行之后,结果为00001011B,其中1的个数为3个(奇数),则将PF置零。
- SF标志位
SF标志位是符号标志位,它用于记录相关指令执行后,其结果是否为负,如果结果为负,sf=1,如果结果非负,sf=0。
关于有符号数和无符号数:
对CPU而言,数字是没有正负之分的,都是二进制数据,数字的正负都是认为规定的,如果你把这个数字当成有符号数,那么SF就有意义,如果你把数字当成是无符号数,那么SF标志位则没有意义。
- CF标志位
CF标志位是进位标志位,一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的进位值。
注意:CF位是对于无符号数而言。
例:
mov al,97H
mov al,98H ;执行后:al=FFH,CF=1(有借位)
sub al,al ;执行后:al=0,CF=0(没有借位)
- OF标志位
OF标志位是溢出标志位,溢出是对于有符号数而言,比如8位有符号数,他能表示的范围是-128~127
mov al,98
add al,99
执行完之后的值是197,很明显超出了al寄存器能表示的范围,这里就产生了溢出,那么OF位就置一。
最后
如果你对汇编比较感兴趣可以深入学习一下王爽老师的《汇编语言(第三版)》。