汇编语言学习在学习语言的过程中,一些等效的操作,一种语法的效率会比另一种语法高效很多,但程序员很难直观感受到其中的原因。例如C++中,数组通过指针访问要比通过数组索引下标访问的效率高。如果之前没接触过相关的知识,第一反应很难说出原因,但是如果打印一下反汇编代码,可以很清晰的看出,通过指针访问数组的操作指令数要比通过数组下标访问少很多。
这也道出了学习汇编语言的原因,掌握汇编语言,可以便于程序员清晰的了解到语言的某个语法糖或者新特性的底层实现,万变不离其宗。另外就是在一些高性能优化的场景,需要对特定硬件的指令集进行优化加速,同样需要汇编语言。
由于本人此前对汇编语言知之甚少,此文以王爽版《汇编语言》为学习资料,进行各个章节的学习,并进行重点知识总结,虽然该书中用到的8086 CPU比较久远,指令集比较简单,但也正因如此,适合入门学习,也便于以后扩展到更新的CPU上。此外,王爽版本的《汇编语言》也是众多技术博主推荐的入门教材,是国内汇编语言教材为数不多的经典之作。
课本中涉及到的实验部分,需要使用DOS Box和debug工具,在WIN10系统上这个工具默认不存在了,具体环境配置和DOSBox安装可以参考:https://blog.csdn.net/plus_re/article/details/60761467
第一章 基础知识主要介绍了计算机、CPU相关的基本知识,包括存储器,指令、数据和总线等,较为基础,感兴趣的可以迅速浏览一遍。
小结汇编指令是机器指令的助记符,同机器指令一一对应;每一种CPU都有自己的汇编指令集;CPU可以直接使用的信息在存储器中存放;在存储器中的指令和数据没有任何区别,都是二进制信息;存储单元从0开始顺序编号;一个存储单元可以存储8个bit, 即8位二进制数;1Byte = 8bit , 1KB = 1024B, 1MB = 1024KB, 1GB = 1024MB每个CPU芯片都有许多管脚,这些管脚与总线相连。也可以说,这些管脚引出总线。一个CPU可以引出3中总线的宽度标志了这个CPU的不同方面的性能:地址总线宽度决定了CPU寻址能力;数据总线宽度决定了CPU与其他器件进行数据传输时的一次数据传输量;控制总线的宽度决定了CPU对系统中其他器件的控制能力。第二章 寄存器8086CPU有4个通用16位寄存器,AX、BX、CX、DX,其中每个寄存器都可以独立当做两个8位寄存器来使用,
AX可分为AH和AL;BX可分为BH和BL;CX可分为CH和CL;DX可分为DH和DL
几条汇编指令:
在进行数据传送或者运算时,要注意指令的两个操作对象的位数应该是一致的,例如:
mov ax, bx
mov bx, cx
mov ax, 18H
mov al, 18H
add ax, bx
add ax, 20000
以上指令都是正确的。
而:
mov ax, bl (在8位寄存器和16位寄存器之间传送数据)
mov bh, ax (在16位寄存器和8位寄存器之间传送数据)
mov al, 20000 (8位寄存器最大可存储值为255的数据)
mov al, 100H (将高于8位的数据加到8位寄存器中) (一个字节为两个16进制位表示)
以上都是错误的指令,错误原因是指令的两个操作对象位数不一致。
8086是16位的CPU,16位结构描述了一个CPU具有以下几个方面的结构特性:运算器一次最多可以处理16位数据;寄存器的最大宽度是16位;寄存器与运算器之间的通路为16位。8086CPU给出物理地址的方法8086 cpu有20位地址总线,可以传送20位地址,有1MB的寻址能力。但是CPU是16位结构,因此一次只能处理或者传输16位地址。8086CPU采用一种内部使用两个16位地址合成一个20位物理地址。相关部件的逻辑结构如下图:
地址加法器采用 ` 物理地址 = 段地址x16 + 偏移地址`的方式合成物理地址。例如想要访问地址123C8H的内存单元,地址加法器的工作流程如下:
其本质原因是CPU是16位结构,无法存储20位结构的地址,因此需要两个16位,并约定特定的地址计算方式,生成20位的地址。
段的概念上文中有提到“段地址”,这个名称中包含了”段“的概念,这可能会使人产生误导,认为内存被划分成一个一个的段,每个段有一个段地址。但其实内存并没有分段,段的划分来自于CPU,8086CPU使用 ` 物理地址 = 段地址x16 + 偏移地址`的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。
段寄存器以及CS和IP8086CPU有四个段寄存器:CS、DS、SS和ES。
CS和IP是最关键的两个寄存器,他们指示了要读取指令的地方。CS为代码段寄存器,IP为指令指针寄存器,在8086PC机上,任意时刻,设CS寄存器中的内容为M,IP中的内容为N,8086CPU将从内存Mx16 + N单元开始,读取一条指令并执行。
简言之,8086机中,任意时刻,CPU将CS:IP指向的内容当做指令执行。
这里便于理解,举例说明读取和执行指令的流程,如不理解,更详细的流程可以参考书籍第二章2.10小结。
8086CPU的工作流程可以简单描述如下:
从CS:IP指向的内存读取指令,读取的指令进入到指令缓冲器;IP = IP + 所读取指令的长度,从而指向下一条指令;执行指令。转到步骤1,重复这个过程。修改CS、IP指令8086CPU的大部分寄存器的值,都可以通过mov指令来改变,mov指令称为传送指令。但是mov指令不能用于设置CS和IP的值,原因很简单,因为8086CPU没有提供这样的功能。能改变CS和IP内容的指令成为转移指令。这里介绍一个最简单的:jmp指令。
如果想同时修改CS和IP的值,可用形如“jmp段地址:偏移地址”的指令完成,如:
jmp 2AE3:3, 执行后:CS = 2AE3H, IP=0003H, CPU将从2AE33H处读取指令。
如果只想修改IP寄存器的内容,可以使用形如“jmp 某一合法寄存器”的指令完成, 如:
jmp ax, 指令执行前: ax = 1000H, CS = 2000H, IP = 0003H
指令执行后: ax = 1000H, CS = 2000H, IP = 1000H
“jmp 某一合法寄存器”的指令功能为:用寄存器的值修改IP。jmp ax 在含义上等价于 mov IP, ax。
第三章 寄存器(内存访问)DS和[address]8086CPU中有一个DS寄存器,通常存放要访问的数据的段地址,比如我们要读取10000H单元的内容,可以用如下程序进行:
mov bx 1000H
mov ds bx
mov al [0]
上面3条指令将10000H(1000:0)中的数据读取到al中。
前面提到过,mov指令可完成两种传送:
将数据直接送入寄存器;将一个寄存器中的内容送入另一个寄存器。也可以使用mov指令将一个内存单元中的内容送入到寄存器。此时需要指明内存单元的地址,具体格式为: mov 寄存器名, 内存单元地址。
“[···]” 表示一个内存单元, “[···]” 中的0 表示内存单元的偏移地址。这一点和c语言的数组索引下标一致。只有偏移地址无法定位到一个内存单元,定位内存单元还需要段地址。8086CPU自动取DS寄存器中的数据作为内存单元的段地址。
再来看下如何用mov指令从10000H中读取数据。10000H用段地址和偏移地址表示为1000:0, 我们将段地址1000H放入DS,然后用mov al,[0] 完成传送。
需要说明的是,在上述代码中,我们使用mov bx 1000H和mov ds bx的方式,其中使用到bx进行中转将1000H送入到寄存器DS,那么我们是否可以使用类似mov ds 1000H的方式直接将1000H送入DS。但现实并非如此,8086CPU不支持直接将数据送入到段寄存器的操作,DS是一个段寄存器,所以mov ds 1000H这条指令非法,不支持这个操作的原因源于硬件设计。
另外:
mov bx 1000H mov ds bx mov [0] al
这几条指令可以实现将al寄存器中的值传送到1000H的内存单元中。
小结字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中;
用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中;[address]表示一个偏移地址为address的内存单元;在内存与寄存器之间传动字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应;mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。可以根据自己的推测在debug中实验指令的新格式。CPU提供的栈机制8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如push ax 表示将寄存器ax中的数据送入栈中,pop ax表示从栈顶取出数据送入ax。8086CPU的入栈和出栈操作都是以字为单位进行的。
CPU是如何知道栈顶位置的? 不禁让我们想起另外一个讨论过的问题, 就是CPU如何知道当前要执行的指令所在的位置? 我们知道答案,那就是CS、IP中存放着段地址和偏移地址。同样地, 也有相应的寄存器存放栈顶地址。在8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中,任意时刻,SS:SP 指向栈顶元素。push和pop指令执行时,CPU从SS和SP中得到栈顶的地址。
从图中可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
我们要十分清楚的是,push和pop指令与mov指令不同,CPU的mov指令只需要一步操作,就是传送,而push和pop需要两部操作。执行push的时候,cpu两步操作为:先改变SP,再向SS:SP处传送;执行pop的时候两步操作为:先读取SS:SP处数据,再改变SP。
栈的综述
8086提供栈操作的机制方案如下:
在SS、SP中存放栈顶的段地址和偏移地址;
提供入栈和出栈的指令,他们根据SS:SP指示的地址,按照栈的方式访问内存数据。
push指令的执行步骤:1)SP=SP-2; 2)向SS:SP指向的字单元中传送数据。
pop指令的执行步骤: 1)从SS:SP指向的字单元中读取数据;2) SP = SP + 2。
任意时刻,SS:SP指向栈顶元素;
8086CPU只记录栈顶,栈空间大小需要我们自己管理。
第四章 第一个程序主要讲解源程序,编译,链接等相关知识,以及如何使用debug跟踪编译好的可执行文件。
第五章 [BX]和LOOP指令[bx]与内存单元的描述[bx]表示一个内存单元时,它的偏移地址在bx中,比如下边的指令:
mov ax [bx]
表示将一个内存单元的内容送入ax,这个内存单元的长度是2字节,存放一个字,偏移地址在bx中,段地址在ds中。
mov al [bx]
表示将一个内存单元的内容送入al,这个内存单元的长度是1字节,存放一个字节,偏移地址在bx中,段地址在ds中。
在汇编源程序中,数据不能以字母开头,所以要在前面加0。例如,9138h在汇编源程序中可以直接写成“9138h”,而A000h在汇编源程序中要写成“0A000h”。
第六章 包含多个段的程序6.3 将数据、代码和栈放入不同的段用多个段来存取数据、代码和栈,和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,或者通过定义数据来取得栈空间。通过以下一段程序来详细说明。
下面对程序6.4做出说明。
定义多个段的方法这点从程序可以明显看出,定义一个段的方法和之前所讲的定义代码段的方法没有区别,只是不同的段要有不同的段名。
对段地址的引用 访问一个段中的数据需要通过地址,而地址分为两部分,即段地址和偏移地址。如何指明要访问段中数据的段地址呢? 在程序中,段名就相当于一个标号,它代表了段地址。所以指令“move ax, data”的含义是将名称为“data”的段的段地址送入ax。一个段中的数据的段地址可由段名代表,偏移地址要看它在段中的位置了。程序中“data”段中的数据“0abch”的地址就是: data:6 。要将他送入到bx,就要用到以下代码:
mov ax, data
mov ds, ax
mov bx, ds:[6]
我们不能使用下面的指令:
mov ds, data
move bx, ds:[6]
其中“mov ds, data”是错误的,因为8086CPU不允许将一个数值直接送入段寄存器。程序中对段名的引用,如指令“mov ds, data”中的data,将被编译器处理为一个表示段地址的数值。
“代码段”、“数据段”、“栈段”完全是我们的安排 现在我们以一个具体的程序再次讨论一下所谓的“代码段”、“数据段”、“栈段”。在汇编程序中,可以定义许多的段,比如程序6.4中定义了三个段,“code”,“data”和“stack”。我们可以分别安排他们存放代码、数据和栈。那么我们如何让CPU按照我们的安排执行这个程序呢? 来看看源程序对3个段所做的处理。
我们在源程序中将用来存放数据的段命名为“data”, 放代码的段叫“code”, 用作栈空间的段命名“stack”,如此命名之后,CPU是否就会去执行“code”段中的内容,处理“data”段中的数据,然后将”stack“当做栈了呢?** 当然不是,我们这样命名只是为了使程序便于阅读,类似命名的段还有很多,CPU并不知道他们。
我们在源程序中用伪指令“assume cs:code, ds:data, ss:stack”将cs,ds和ss分别和code、data、stack段相连。这样之后,CPU是都就会将cs指向code,ds指向data,ss指向stack,从未按照我们的意图去处理这些段呢? 当然也不是,要知道assume是伪指令 ,有编译器执行,也是仅在源程序中存在的信息,CPU并不知道它们。
若要CPU按照我们的安排行事,需要用机器指令控制它,源程序中汇编指令是CPU要执行的内容。CPU如何知道去执行它们? 我们在源程序最后用“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,可执行文件中的程序被加载入内存之后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令。标号“start”在“code”段中,这样CPU就将code段中的内容当做指令来执行了。我们在code段中使用指令:mov ax, stack
mov ss, ax
mov sp, 20h
设置ss指向stack,设置ss指向stack:20, CPU执行这些指令后,将把stack当做栈空间来用,CPU若要访问data段中的数据,可用ds指向data段,用其他寄存器(如bx)来存放data段中数据的偏移地址。
总之,CPU如何处理我们定义段中的内容,是当做指令执行,当做数据访问,还是当做栈空间,完全是靠程序中具体的汇编指令,和汇编指令对CS:IP, SS:SP,DS等寄存器的设置来决定的。
完全可以将程序6.4改写成下面的样子实现,相同的功能。
第七章 更灵活的定位内存地址的方法7.5 [bx+idata]前面我们使用[bx]的方式来指明一个内存单元,还可以用一种更为灵活的方式:[bx + idata]表示一个内存单元,他的偏移地址为(bx) + idata(bx中的数值加上idata)
我们看下指令 mov ax, [bx+200]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2个字节(字单元),存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。
数学化描述为:(ax)=((ds)*16 + (bx) + 200)
该指令通常也可以写成如下格式:
mov ax, [200+bx]
mov ax, 200[bx]
mov ax, [bx].200
7.6 用[bx+idata]的方式进行数组的处理以下代码中,在codesg中填写代码,在datasg中定义的第一个字符串转换为大写,第二个字符串转化为小写。
现在我们使用[bx+idata]的方式,可以简化上边的程序。datasg中的两个字符串可以看做两个字符数组,一个从0地址开始存放,另一个从5开始存放。那么我们可以使用[0+bx]和[5+bx]的方式在一个循环中定位两个字符串中的字符。改进程序如下:
mov ax, datasg
mov ds, ax
mov bx, 0
mov cs, 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
对应的高级语言,比如C语言描述大致如下:
可以对比看一下相似之处。这种访问内存单元的机制为高级语言实现数组提供了便利。
解释一下代码中的与和或操作,在本书的7.4小节中也有介绍。
7.7 SI和DIsi和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用,下面的3组指令实现了相同的功能:
mov bx, 0 mov ax, bx
mov si, 0 mov ax, [si]
mov di, 0
mov ax, [di]
7.8 [bx+si]和[bx+di]在前面我们使用过[bx(si 或者 di)]和 [bx(si 或 di) + idata]的方式指明一个内存单元,我们还可以使用更灵活的方式: [bx+si] 和 [bx+di]。
以[bx+si]为例讲解。[bx+si]表示一个内存单元,偏移地址为(bx)+(si),即bx中的数值加上si中的数值。
指令 mov ax, [bx+si]含义如下:
将一个内存单元的内容送入ax,这个内存单元的长度为2个字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中。
数学化描述:(ax) = ((ds)*16 + (bx) + (si))
指令也可以写成如下格式:
mov ax, [bx] [si]
同理扩展 7.9小节主要讲[bx+si+idata]和[bx+di+idata]两种定位内存单元的格式,这里不再赘述。
7.10 不同寻址方式的灵活应用如果我们对比一下几种定位内存地址的方法(可称为寻址方式),可以发现:
[idata]用一个常量表示地址,可用于直接定位一个内存单元;[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;[bx+si]用两个变量表示地址;[bx+si+idata]用两个变量和一个常量表示地址。可以看到,从[idata]一直到[bx+si+idata],我们可以用更加灵活的方式来定位一个内存单元的地址。这使我们可以从更加结构化的角度去看待要处理的数据。
相关编程技巧可以看这一小节的课后问题7.6。
第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和sp这一小节介绍了这四种寄存器用“[…]”中进行内存单元的寻址时,各种组合什么情况下是正确得使用。
8.2 机器指令处理的数据在什么地方大部分机器指令是进行数据的处理,处理大致分为三类:读取、写入、运算。在机器指令这一层来说,并不关心数值的值是多少,而是关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行钱,所处理的数据可以在3个地方:CPU内部,内存,端口。如下表:
机器码汇编指令指令执行前数据的位置8E1E0000mov bx, [0]内存,ds:0单元89C3mov bx, axCPU内部,ax寄存器BB0100mov bx, 1CPU内部,指令缓冲器8.3 汇编语言中数据位置的表达汇编语言中用3个概念表达数据的位置
立即数(idata) 直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称为:立即数(idata),在汇编指令中直接给出:
mov ax, 1
add bx, 2000
or bx, 00010000b
mov al, ‘a’
寄存器 指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
例如:
mov ax, bx
mov ds, ax
push bx
mov ds:[0], bx
段地址(SA)和偏移地址(EA) 指令要处理的数据放在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
存在段地址的寄存器可以是默认的,比如:
mov ax, [0]
mov ax, [di]
mov ax, [bx+8]
mov ax, [bx+si]
mov ax, [bx+si+8]
等指令,段地址默认在ds中。
mov ax, [bp]
mov ax, [bp+8]
mov ax, [bp+si]
mov ax, [bp+si+8]
等,段地址默认在ss中。
存放段地址的寄存器也可以显性给出,如:
mov ax, ds:[bp] 含义:(ax)=((ds)*16+(bp))
mov ax, es:[bx] 含义:(ax)=((es)*16+(bx))
mov ax, ss:[bx+si] 含义:(ax)=((ss)*16+(bx)+(si))
mov ax, cs:[bx+si+8] 含义:(ax)=((cs)*16+(bx)+(si)+8)
8.4 寻址方式8086CPU有多种寻址方式,总结如下表:
8.5 指令要处理的数据有多长8086CPU中可以处理两种尺寸的数据,byte和word。在机器指令中要指明指令进行的是字操作还是字节操作。对于这个问题,汇编语言有以下处理方法:
通过寄存器名指明要处理的数据的尺寸。下面指令中,寄存器指明指令进行的是字操作。
mov ax, 1
mov bx, ds:[0]
mov ds, ax
下面指令中,寄存器指明了指令进行的是字节操作
mov al, 1
mov al, bl
mov al, ds:[0]
在没有寄存器名存在的情况下,可以用操作符 X ptr指明内存单元长度,X在汇编指令中可以为word或者byte。例如下面指令用 word ptr指明指令访问的内存单元是一个字单元。
mov word ptr ds:[0], 1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx], 2
下面指令中,用byte ptr 指明指令访问的内存单元时一个字节单元
mov byte ptr ds:[0], 1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx], 2
其他方法有些指令默认了访问的是字单元还是字节单元,比如,push [1000H]就不用指明访问的是字单元还是字节单元,因为push 指令只进行字操作。
8.7 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存储除法操作的余数。使用格式如下:
div reg
div 内存单元
这里以除法指令计算100001 / 100为例说明:
首先分析,被除数100001大于65535, 不能用ax寄存器存放,所以只能用dx和ax两个寄存器联合存放100001,也就是说进行16位除法。除数100小于255,可以使用一个8位寄存器存放,但是因为被除数是32位的,除数应该为16位,所以要用一个16位寄存器来存放除数100.
因为要分别为dx和ax赋100001的高16位值和低16位值,所以应先将100001表示为16进制形式:186A1H。程序如下:
mov dx, 1
mov ax, 86A1H ;(dx)*10000H + (ax) = 100001
mov bx, 100
div bx
程序执行后,(ax)=03E8H(即 1000),(dx)=1(余数为1)。
第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在汇编语言中是由编译器处理的符号,功能时取得标号的偏移地址。例如以下程序:
assume cs:codesg
codesg segment
start:mov ax, offset start ;相当于mov ax, 0
s:mov ax, offset s ;相当于mov ax, 3
codesg ends
end start
上面的程序中,offset操作符取得了标号start和s的偏移地址0和3,所以指令mov ax, offset start相当于相当于mov ax, 0, 因为start是代码段中的标号,他所标记的指令是代码段中的第一条指令,偏移地址是0;
mov ax, offset s相当于mov ax, 3,因为s是代码段中的标号,它标记的指令是代码段中的第二条指令,第一条指令长度为3个字节,则偏移地址为3。
9.2 jmp指令jmp指令为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。
jmp指令要给出两种信息:
转移的目的地址转移的距离(段间转移、段内短转移,段内近转移)9.3 依据位移进行转移的jmp指令jmp short 标号(转到标号处执行指令)
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127,也就是说,它向前转移时最多越过128字节,向后转移最多越过127字节。指令中的short说明指令进行的是短位移,标号则是指明了要转移的目的地,转移指令结束后,CS:IP应该指向标号处的指令。
9.3小节详细介绍了jmp机器指令中不包含要转移的目的地址的原因:
9.4 转移的目的地之在指令中的jmp指令前面讲的jmp指令,对应机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。
“jmp far ptr 标号”实现的是段间转移,又称远转移,功能如下:
(CS)=标号所在段的段地址;(IP)=标号在段中的偏移地址。
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。
9.5 转移地址在寄存器中的jmp指令指令格式: jmp 16位reg
功能:(IP)=(16位reg)
9.6 转移地址在内存中的jmp指令转移地址在内存中的jmp指令有两种格式:
jmp word ptr 内存地址单元(段内转移)功能:从内存单元地址处开始存放这一个字,是转移的目的偏移地址。
内存单元地址可用寻址方式的任一格式给出。比如:
mov ax, 0123H
mov ds[0], ax
jmp word ptr ds:[0]
执行后,(IP)=0123H。
又比如,下面的指令:
mov ax,0123H
mov [bx], ax
jmp word ptr [bx]
执行后,(IP)=0123H。
jmp dword ptr 内存单元地址(段间转移)功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
(CS)=(内存单元地址+2)
(IP)=(内存单元地址)
内存单元地址可用寻址方式的任一格式给出。
比如下面的指令:
mov ax, 0123H
mov ds:[0] ax
mov word ptr ds:[2], 0
jmp dwoed ptr ds:[0]
执行后,(CS)=0, (IP)=0123H, CS:IP指向0000:0123
9.7 jcxz指令jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:-128~237。
指令格式:jcxz 标号(如果(cx)=0, 转移到标号处执行。)
操作:当(cs)=0时,(IP)=(IP)+8位位移;
8位位移=标号处的地址 - jcxz指令后的第一个字节地址;
8位位移的范围是-128~237,用补码表示;
8位位移由编程序在编译时算出。
当(cx)!= 0时,什么也不做,程序向下执行。
我们从jcxz功能中可以看出, “jcxz 标号” 的功能相当于:
if((cx)==0) jmp short 标号;
9.8 loop指令loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:-128~127。
指令格式: loop 标号((cx)=(cx)-1, 如果(cx)!=0, 转移到标号处执行)
操作:
(cx)=(cx)-1;如果(cx)!=0,(IP)=(IP)+8位位移;8位位移=标号处的地址 - loop指令后的第一个字节地址;
8位位移的范围是-128~237,用补码表示;
8位位移由编程序在编译时算出。
当(cx)== 0时,什么也不做,程序向下执行。
我们从loop功能中可以看出, “loop标号” 的功能相当于:
(cx)–;
if((cx)!=0) jmp short 标号;
9.9 根据位移进行转移的意义使用位移而不是固定的地址,这使得程序的通用性更强,在程序装载在不同的位置都能够正确执行。
第10章 CALL和RET指令call和ret指令都是转移指令,他们都修改IP,或者同时修改CS和IP。
10.1 ret 和 retfret 指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
CPU执行ret指令时,进行下面的两步操作:
(IP)=((ss)*16+(sp))(sp)=(sp)+2CPU执行retf指令时,进行下面4步操作:
(IP)=((ss)*16+(sp))(sp)=(sp)+2(CS)=((ss)*16+(sp))(sp)=(sp)+2可以看出,如果用汇编语法解释ret和retf指令,则CPU执行ret指令时,相当于进行:
pop IP
执行retf指令时,相当于进行:
pop IP
pop CS
10.2 call指令CPU执行call指令时,进行两步操作:
将当前的IP或者CS和IP压入栈中;转移。call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令相同,下面主要讲解转移目的地址不同时,call指令的应用格式。
10.3 依据位移进行转移的call指令call 标号(将当前IP压栈后,转移到标号处执行指令)
CPU执行此种格式的call指令时,进行如下操作:
(sp)=(sp)-2 ((ss)*16+(sp))=(IP)
(IP)=(IP)+16位位移16位位移=标号处的地址 - call指令后的第一个字节的地址
16位位移的范围是-32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
从上面的描述中,可以看出,如果使用汇编语法来解释此种格式的call指令,则:
CPU执行“call 标号”时,相当于执行:
push IP
jmp near ptr 标号
10.4 转移的目的地址在指令中的call指令前面讲的call指令,其对应的机器指令并没有转移的目的地址,而是相对于当前IP的转移位移。
“call far ptr 标号” 实现的是段间转移。
CPU执行此种格式的call指令时,进行如下操作:
(sp)=(sp)-2 ((ss)*16+(sp))=(CS)
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(CS)=标号所在段的段地址 (IP)=标号在断中的偏移地址
从上面的描述中可以看出,如果我们用汇编语法来解释call指令,则:
CPU执行“call far ptr 标号”时,相当于进行:
push CS
push IP
jmp far ptr 标号
10.5 转移地址在寄存器中的call指令指令格式 : call 16为reg
功能:
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(IP)=(16位reg)
用汇编语法解释此种格式的call指令,CPU执行 “call 16位reg”时,相当于:
push IP
jmp 16位reg
10.6 转移地址在内存中的call指令转移地址在内存中的call指令有两种格式。
(1) call word ptr 内存单元地址
用汇编语法来解释此种格式的call 指令,则:
push IP
jmp word ptr 内存单元地址
举例说明:
mov sp, 10h
mov ax, 0123h
mov ds:[0], ax
call word ptr ds:[0]
执行后,(IP)=0123H, (sp)=0EH
(2) call dword ptr 内存单元地址
用汇编语法来解释此种格式的call 指令,则:
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
10.7 call和ret的配合使用此小结可以查看书中具体代码示例,跟着思路走一遍,看看能否得到相关的过程和结果,如果有哪个点对不上,则需要对应复习前边的章节。
10.8 mul指令mul是乘法指令,使用mul做乘法时注意一下两点:
两个相乘的数:要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或者内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位reg或者内存单元中。结果: 如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位放在AX中。格式如下:
mul reg
mul 内存单元
内存单元可以用不同的寻址方式给出,比如:
mul byte ptr ds:[0]
含义:(al)=(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*10100和10小于255,可以做8位乘法,程序如下:
mov al, 100
mov bl, 10
mul bl
结果:(ax)=1000(16进制表示为03E8H)
计算100* 10000100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax, 100
mov bx, 10
mul bx
结果:(ax)=4240H, (dx)=000FH (F4240H = 1000000)
10.9 模块化程序设计从上面可以看到,call和ret指令共同支持了汇编语言编程中的模块化设计。利用call和ret指令,我们可以用简洁的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。
10.10 参数和结果传递的问题略
10.11 批量数据的传递对于数量较少的参数,可以使用寄存器来存放,但寄存器的数量终究是有限的,不可能简单地使用寄存器来存放多个需要传递的数据。对于返回值也有同样的问题。
这种时候,我们可以将批量数量的数据存放在内存中,然后将他们的内存空间首地址存放在寄存器中,传递给需要的子程序。对于具有批量数据的放回结果,也可使用相同的方法。
看下面一个例子,设计子程序,功能是将一个全是字母的字符串转换为大写,具体操作将在注释中说明。
这个子程序需要知道两件事,字符串的内容和字符串的长度。因为字符串中字母很多,所以不便将字符串中所有字母直接传给子程序。但可以将字符串所在内存的首地址放在寄存器中传递给子程序。因为子程序要用到循环,而循环的次数恰好就是字符串的长度。
assume cs:code
data segment
db 'conversation'
data ends
code segment
start:mov ax, data
mov ds, ax
mov si, 0 ;ds:si指向字符串(批量数据)所在空间的首地址
mov cx, 12 ;cx存放字符串的长度,也就是循环次数
call capital
mov ax, 4c00h
int 21h
capital:and byte ptr [si], 11011111b ;将ds:si指向的字符转换为大写字母
inc si
loop capital
ret
code ends
end start
除了用寄存器传递参数外,还可以使用栈来传递参数。
10.12 寄存器冲突的问题在主程序和子程序都是用相同寄存器的情况下,很可能会造成寄存器使用上的冲突,进而导致程序出错,如何解决呢?
解决这个问题的简洁方法是,在子程序的开始将子程序中所用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容。
以后,我们编写子程序的标准框架如下:
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)
第12章 内中断任何一款通用的CPU,比如8086,都具备一种能力,可以再执行完当前正在执行的指令之后,检测从CPU外部发过来的或者内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理,这种特殊信息我们称之为:中断信息。中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
注意,这里所说的中断信息,是为了便于理解而采用的一种逻辑上的说法,它是对几个具有先后顺序的硬件操作所产生的的事件的统一描述。“中断信息”是要求CPU马上进行某种处理,并向所要进行的该种处理提供了必备的参数的通知信息。
中断信息可以来自CPU内部和外部,本章重点讨论来自于CPU内部的中断信息。
12.1 内中断的产生当CPU内部发生什么事情时,将产生需要马上处理的中断信息呢? 对于8086CPU 下列情况将产生中断信息。
除法错误,比如执行div指令产生的除法溢出;单步执行;执行into指令;执行int指令。8086CPU用称为中断类型码的数据来标识中断信息的来源。中断类型码为一个字节型数据,可以表示256种中断信息的来源。以后,我们将产生中断的事件,即中断信息的来源,称之为中断源。上述四种中断源,在8086CPU的中断类型码如下:
除法错误:0单步执行:1执行into指令:4执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。12.2 中断处理程序CPU收到中断信息后,对中断信息进行处理,如何处理可由我们编程决定。我们编写的用来处理中断信息的程序称为中断处理程序。
CPU在收到中断信息后,应该转去执行中断信息的处理程序。我们知道,若要8086CPU去执行某处的程序,就要将CS:IP指向它的入口。可见首要问题是,CPU收到中断信息后如何根据中断信息确定其处理程序的入口。
12.3 中断向量表CPU使用8位的中断码通过中断向量表找到对应的中断处理程序的入口地址。中断向量表就是中断向量的列表,中断向量指的就是中断处理程序的入口地址。
12.4 中断过程CPU收到中断信息后,要对中断信息进行处理,首先引发中断过程。硬件在完成中断过程后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理函数。
有一个问题需要考虑,CPU在执行完中断处理程序后需要返回原来的执行点继续执行下面的指令。所以在中断过程中,在设置CS:IP前,还要将原来的CS和IP的值保存起来,在执行call指令调用子程序的时候有同样的问题,子程序执行后还要返回到原来的执行点继续执行。所以call指令先保存当前的CS和IP的值,然后再设置CS和IP。
以下为8086CPU收到中断信息后,所引发的中断过程:
(从中断信息中)取得中断类型码;标志寄存器的值入栈(因为中断过程中要改变标志寄存器的值,所以先将其保存在栈中);设置标志寄存器的第8位TF和第9位IF的值为0(这一步的目的后边将介绍);CS的内容入栈;IP的内容入栈;从内存地址为中断类型码*4 和中断类型码 * 4 + 2 的两个字单元中读取中断处理程序的入口地址设置为CS和IP。更为简洁的描述中断过程如下:
取得中断类型码N;pushfTF=0, IF=0push CSpush IP(IP)=(N*4), (CS)=(N * 4 + 2)在最后一步完成后,CPU将开始执行有程序员编写的中断处理程序。
12.5 中断处理程序和iret指令中断处理程序的编写方法和子程序的比较相似。步骤如下:
保存用到的寄存器;处理中断;恢复用到的寄存器;用iret指令返回iret的功能用汇编语法描述为:
pop IP
pop CS
popf
iret通常和硬件自动完成的中断过程配合使用。可以看出,在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP, 而iret的出栈顺序是IP、CS、标志寄存器,刚好和其对应,实现了用执行中断处理程序前的CPU现场恢复标志寄存器和CS、IP的工作。iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序。
第13章 int指令本章讲解另一种重要的内中断,由int指令引发的中断。
13.1 int指令int指令的格式为: int n, n为中断类型码,他的功能是引发中断过程。
CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下。
取中断类型码n;标志寄存器入栈, IF=0, TF=0;CS, IP入栈;(IP)=(n*4),(CS)=(n * 4 + 2)从此出转去执行n号中断的中断处理程序。
13.2 编写供应用程序调用的中断例程中断编写、安装的流程实例,看书具体学习。
13.3 对int、iret和栈的深入理解问题:用7ch中断例程完成loop指令的功能。
loop s的执行需要两个信息,循环次数和s的位移。所以在7ch中断例程完成loop指令的功能,也需要这两个信息作为参数。我们用cs存放循环次数,bx存放位移。
应用举例:在屏幕中间显示80个‘ !’。
assume cs:code
code segment
start:mov ax, 0b800h
mov es, ax
mov di, 160*12
mov bx, offset s - offset se ;设置从标号se到标号s的转移位移
mov cs, 80
s:mov byte ptr es:[di], '!'
add di, 2
int 7ch ;如果(cs)!= 0,转移到标号s出
se:nop
mov ax, 4c00h
int 21h
code ends
end start
上面的程序中,用int 7ch调用7ch中断例程进行转移,用bx传递转移的位移。
分析,为了模拟loop指令,7ch中断例程应具备以下功能。
dec cx;如果(cx) != 0, 转移标号s处执行,否则向下执行。下面分析7ch中断例程如何实现目的地之的转移
转到标号s处显然需要设置(CS) = 标号s处的段地址,(IP)=标号s的偏移地址那么,中断例程如何得到s的段地址和偏移地址呢?int 7ch引发中断后,进入中断例程,在中断过程中,当前的标志寄存器、CS和IP都要入栈,此时CS和IP的内容分别是调用程序的段地址(可以认为是s的段地址)和int 7ch后一条指令的偏移地址(即标号se的偏移地址)。
所以在中断例程中,可以从栈里取得标号s的段地址和标号se的偏移地址,而用标号se的偏移地址加上bx中存放的转移位移就可以得到标号s的偏移地址。
现在知道可从栈中直接和间接地获取标号s的段地址和偏移地址,那么如何用他们设置CS和IP呢?可以利用iret指令,我们将栈中的se的偏移地址加上bx中的转移位移,则栈中的se的偏移地址就变成了s的偏移地址,在使用iret指令,用栈中的内容设置CS、IP,从而实现转移到标号s处。
7ch中断例程如下。
lp: push bp
mov bp, sp
dec cx
jcxz lpret
add [bp+2], bx
lpret: pop bp
iret
因为要访问栈,使用了bp,在程序开始处将bp入栈保存,结束时出栈恢复。当要修改栈中se的偏移地址的时候,栈中的情况为:栈顶处是bp原来的数值,下面是se的偏移地址,再下面是s的段地址,在下面是标志寄存器的值。而此时,bp中为栈顶的偏移地址,所以((ss)*16 + (bp) + 2)处为se的偏移地址,将它加上bx中的转移位移就变成了s的偏移地址。最后用iret出栈返回,CS:IP即从标号s处开始执行指令。
如果(cx)=0, 则不需要修改栈中se的偏移地址,直接返回即可。CPU将从标号se出向下执行指令。
第14章 端口14.3 shl和shr指令shl和shr是逻辑移位指令,shl是逻辑左移指令,他的功能是:
将一个寄存器或者内存单元中的数据向左移位;将最后移出的一位写入CF中;最低位用0补充。执行后,(al)=10010000b, CF=0。
可以看出,将x逻辑左移一位,相当于执行X=X*2。
shr则为逻辑右移指令,它与shl所进行的操作刚好相反。
将一个寄存器或者内存单元中的数据向右位移;将最后移出的一位写入CF中;最高位用0补充。指令:
mov al, 10000001b
shr al, 1
执行后(al)=01000000b, CF=1。
第15章 外中断略
参考《汇编语言》 王爽文档信息本文作者:JianZheng本文链接:https://zhengjian526.github.io/left-handed_knife//2023/03/20/Assembly-language/版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)