参考教程:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_bilibili
一、寄存器
1、寄存器的作用
(1)在CPU中,运算器进行信息处理,寄存器进行信息存储,控制器控制各种器件进行工作,内部总线连接各种器件,在它们之间进行数据的传送。
(2)对于一个汇编程序员来说,CPU中的主要部件是寄存器,寄存器是CPU中程序员可以用指令读写的部件,程序员通过改变各种寄存器中的内容来实现对CPU的控制。
2、以8086为例介绍其寄存器
(1)8086CPU的所有寄存器都是16位的,可以存放两个字节(16位)的数据。
(2)8086CPU有14个寄存器:
①通用寄存器:AX、BX、CX、DX。
②变址寄存器:SI、DI。
③指针寄存器:SP、BP。
④指令指针寄存器:IP。
⑤段寄存器:CS、SS、DS、ES。
⑥标志寄存器:PSW。
(3)以AX为例,寄存器的逻辑结构如下图所示,一个寄存器可以存放16位数据,意味着数据的取值范围为[0 , 216 - 1],另外可以为每个位进行编号,0为低位,15为高位,若AX中存储的数据为20000D(十进制20000),那么AX中存储的内容应为“0100111000100000”,用二进制表示为0100111000100000B,用十六进制表示为4E20H。
(4)8086CPU的上一代CPU中的寄存器都是8位的,为了保证兼容,使原来基于上代CPU编写的程序稍加修改就可以运行在8086之上,8086CPU的AX、BX、CX、DX这4个寄存器都可分为两个可独立使用的8位寄存器来用,AX可以分为AH和AL,BX可以分为BH和BL,CX可以分为CH和CL,DX可以分为DH和DL。
3、字在寄存器中的存储
(1)字的长度取决于CPU的位数,8086是16位CPU,所以其字长为16bit。
(2)在8086中,一个字(word)可以存在一个16位寄存器中,这个字的高位字节存在这个寄存器的高8位寄存器,这个字的低位字节存在这个寄存器的低8位寄存器。
二、MOV、ADD和SUB指令指令
1、MOV指令
(1)MOV指令的一般格式为“MOV <地址>, <操作数>”,执行的操作为将操作数存储至地址指向的存储空间中。
(2)举例:
汇编指令 | 控制CPU完成的操作 |
MOV AX, 18 | 将18送入寄存器AX |
MOV AH, 78 | 将78送入寄存器AH |
MOV AX, BX | 将寄存器BX中的数据送入寄存器AX |
2、ADD指令
(1)ADD指令的一般格式为“ADD <地址>, <操作数>”,执行的操作为将操作数和地址指向的存储空间中的数据相加,结果存放在地址指向的存储空间(覆盖原来的数据)。
(2)举例:
汇编指令 | 控制CPU完成的操作 |
ADD AX, 8 | 将寄存器AX中的值加上8 |
ADD AX, BX | 将寄存器AX和BX存储的值相加,结果存储在寄存器AX中 |
3、SUB指令
(1)SUB指令的一般格式为“SUB <地址>, <操作数>”,执行的操作为将操作数作为减数,地址指向的存储空间中的数据作为被减数,二者做减法,结果存放在地址指向的存储空间(覆盖原来的数据)。
(2)举例:
汇编指令 | 控制CPU完成的操作 |
SUB AX, 8 | 将寄存器AX中的值减去8 |
SUB AX, BX | 将寄存器AX和BX存储的值相减,结果存储在寄存器AX中 |
三、物理地址与段地址、偏移地址
1、物理地址
(1)所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,这个唯一的地址称为物理地址。
(2)8086有20位地址总线,可传送20位地址,寻址能力为1M,但8086是16位结构的CPU,运算器一次最多可以处理16位的数据,寄存器的最大宽度为16位,在8086内部处理的、传输、暂存的地址也是16位,寻址能力也只有64KB(目标地址也可认为是操作数,需存放在寄存器中,但物理地址的表示需要20位,16位的寄存器显然不够),为了处理在寻址空间上的矛盾,故引入了段地址和偏移地址。
2、段地址和偏移地址
(1)8086用16位的段地址和16位的偏移地址共同表示20位的物理地址,三者的关系为“物理地址 = 段地址 × 16 + 偏移地址”。
(2)8086中通过地址加法器可根据段地址和偏移地址计算出物理地址。
(3)举例:
(4)CPU中有4个专门存放段地址的寄存器,分别是CS-代码段寄存器、DS-数据段寄存器、SS-栈段寄存器、ES-附加段寄存器。
3、内存的分段表示法
(1)在实际硬件中,内存并没有分段,分段完全是CPU为了方便寻址而做出的行为,同一段内存可以有很多种分段方案,但无论怎么分,必须遵循以下两个规律:
①段地址×16必然是16的倍数,所以一个段的起始地址也一定是16的倍数。
②偏移地址为16位,16位地址的寻址能力为64K,所以一个段的长度最大为64K。
(2)同一个物理地址可以有不同的段地址和偏移地址组合,如下所示。
(3)8086中存储单元地址的表示方法:
①内存<段地址>:<偏移地址>单元。
②内存的<段地址>段中的<偏移地址>单元中。
四、Debug的使用(实践环节须知)
1、Debug的操作方法
(1)在调试程序前,首先输入“debug”并按下回车,然后会弹出引导符号“-”。
(2)Debug常用的一些命令:
①用R命令可查看、改变CPU寄存器的内容。
②用D命令查看内存中的内容。
③用E命令可改变内存中的内容。
④用U命令可将内存中的机器指令翻译成汇编指令。
⑤用A命令可以汇编指令的格式在内存中写入机器指令。
⑥用T命令可执行机器指令。
2、R命令的使用
(1)输入“r”,可查看所有寄存器中的内容。
(2)输入“r <寄存器>”并按下回车,可查看目标寄存器中的内容,同时弹出引导符号“:”,这时可输入寄存器要存放的新数值,并按下回车。
3、D命令的使用
(1)输入“d”并按下回车,可查看预设地址内存处的128个字节的内容,继续输入“d”并按下回车,可查看紧挨着往后内存处的128个字节的内容,以此类推。(左边以十六进制数的形式展示,右边以ASCII码的形式展示)
(2)输入“d <段地址>:<偏移地址>”,可查看目标物理地址开始往后128个字节的内容。
(3)输入“d <段地址>:<起始偏移地址> <结尾偏移地址>”,可查看起始物理地址开始往后直到结尾目标地址中存储的内容。
4、E命令的使用
(1)输入“e <段地址>:<偏移地址> <数据1> <数据2> ...”并按下回车,可从目标物理地址开始修改其中的数据,修改后的数据为“<数据1> <数据2> ...”,修改范围取决于数据的长度。
(2)输入“e <段地址>:<偏移地址>”并按下回车,可从目标物理地址开始逐个修改其中的数据,首先会显示当前目标物理地址中的数据,输入修改后的数据后按下空格,则当前物理地址中的数据被修改,并将目标物理地址往后移动两个字节,以此往复,直到输入回车,修改结束。
5、U命令的使用
(1)输入“u <段地址>:<偏移地址>”并按下回车,目标物理地址开始后面存储的内容会以汇编指令的形式解析出来。
(2)内存中存储的内容虽然可以被解析成指令,但这并不意味着它的含义就一定是指令,这取决于用户如何操作。
6、A命令的使用
(1)输入“a <段地址>:<偏移地址>”并按下回车,可以往指定物理地址中输入汇编指令,按下回车后会以机器指令的形式存储在指定物理地址中,然后目标物理地址向后顺延,用户可以继续往下写下一条汇编指令,也可以不做任何输入,直接按下回车以表结束。
(2)内存中存储的内容虽然可以被解析成指令,但这并不意味着它的含义就一定是指令,这取决于用户如何操作。
7、T命令的使用
输入“t”并按下回车,会执行CS:IP处的命令,并且IP寄存器存储的偏移地址相应地会顺延。
8、Q命令的使用
输入“q”并按下回车,会退出debug。
五、CS、IP与JMP指令
1、两个关键寄存器
(1)CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址,其中CS为代码段寄存器,IP为指令指针寄存器。
(2)在8086PC机中,任意时刻,设CS中的内容为M,IP中的内容为N,8086CPU将从内存 Mx16+N单元开始,读取一条指令并执行,同时IP中的内容自增,增量取决于指令的长度。
(3)8086CPU的工作过程可以简要描述如下:
①从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器。(这也意味着CPU将CS:IP指向的内存单元中的内容看作指令)
②IP=IP+所读取指令的长度,从而指向下一条指令。
③执行指令,然后转到步骤①,重复这个过程。
2、指令的读取和执行过程举例
(1)假设8086CPU当前状态如下,CS寄存器中内容的为2000H,IP寄存器中内容的为0000H,内存20000H~20009H处存放着可执行的机器代码。
(2)指令执行过程:
①CS、IP中的内容送入地址加法器(地址加法器完成:物理地址=段地址×16+偏移地址)。
②地址加法器将物理地址20000H送入输入输出控制电路。
③输入输出控制电路将物理地址20000H送上地址总线。
④从内存20000H单元开始存放的机器指令B8 23 01通过数据总线被送入CPU。
⑤输入输出控制电路将机器指令B8 23 01送入指令缓冲器。
⑥读取一条指令后,IP 中的值自动增加,以使 CPU可以读取下一条指令。因当前读入的指令 B82301长度为3个字节,所以IP中的值加3,此时,CS:IP指向内存单元2000:0003。
⑦执行控制器执行指令B8 23 01(即 mov ax,0123H)。
⑧指令B8 23 01被执行后,AX中的内容为0123H。
(3)实际验证:
①使用R指令更改CS寄存器和IP寄存器中的内容,使下一条执行的指令的物理地址为20000H。
②使用A指令从物理地址20000H处开始写入汇编指令,并用U指令查看写入的代码。
③使用T指令执行从物理地址20000H开始的四条汇编指令。
④因篇幅有限,后续建议实际验证的地方无特殊原因都不再详细赘述。
3、JMP指令
(1)8086CPU不提供对CS和IP修改的指令,要想修改其中的内容,需使用JMP指令。
①同时修改CS和IP中的内容,可使用指令“JMP <段地址>:<偏移地址>”,CPU会用指令中给出的段地址修改CS,用指令中给出的偏移地址修改IP。
②若只想修改IP中的内容,可使用指令“JMP <某一合法寄存器>”,CPU会用寄存器中的值修改IP。
(2)如下所示,内存中存有如下汇编指令,CS:IP指向物理地址20000H。
①首先CS:IP此时指向物理地址20000H,所以下一条执行的指令为“MOV AX,6622H”,执行完毕后CS:IP指向物理地址20003H,AX中的内容为6622H。
②此时CS:IP指向物理地址20003H,所以下一条执行的指令为“JMP 1000:3”,执行完毕后CS中的内容被改为1000H,IP中的内容被改为0003H,CS:IP指向物理地址10003H。
③此时CS:IP指向物理地址10003H,所以下一条执行的指令为“MOV AX,0000H”,执行完毕后CS:IP指向物理地址10006H,AX中的内容为0000H。
④此时CS:IP指向物理地址10006H,所以下一条执行的指令为“MOV BX,AX”,执行完毕后CS:IP指向物理地址10008H,BX中的内容为0000H。
⑤此时CS:IP指向物理地址10008H,所以下一条执行的指令为“JMP BX”,执行完毕后IP中的内容被改为0000H,CS:IP指向物理地址10000H。
⑥此时CS:IP指向物理地址10000H,所以下一条执行的指令为“MOV AX,0123H”,执行完毕后CS:IP指向物理地址10003H,AX中的内容为0123H。
⑦至此形成闭环,后续操作从步骤③开始重复进行,无穷无尽。
六、内存中字的存储
1、字在内存中的存储方式
(1)在8086中,一个字(word)可以存在一个16位寄存器中,这个字的高位字节存在这个寄存器的高8位寄存器,这个字的低位字节存在这个寄存器的低8位寄存器。
(2)在8086中,16位的字在内存中需要2个连续字节存储,存储方式为低位字节存在低地址单元,高位字节存在高地址单元。下图所示的是20000D(4E20H)存放0、1两个单元,18D(0012H)存放在2、3两个单元。
2、字单元
(1)字单元由两个地址连续的内存单元组成,存放一个字型数据(16位)。
(2)在一个字单元中,低地址单元存放低位字节,高地址单元存放高位字节.
(3)举例:在起始地址为0的单元中存放的是4E20H,在起始地址为2的单元中存放的是0012H。
①0地址单元中存放的字节型数据是20H。
②0地址字单元中存放的字型数据是4E20H。
③2地址单元中存放的字节型数据是12H。
④2地址字单元中存放的字型数据是0012H。
七、DS寄存器
1、用DS与[address]实现字的传送
(1)CPU要读取一个内存单元的时候,必须先给出这个内存单元的地址,而在8086PC中,内存地址由段地址和偏移地址组成(段地址:偏移地址)。
(2)要想表达出目标的内存地址,可以在DS寄存器中写入目标内存地址的段地址,然后将目标内存地址的偏移地址用“[]”括起来,如下所示。
(3)因为硬件设计的问题,8086CPU不支持将数据直接送入段寄存器,只能先将数据送入通用寄存器,然后再将通过MOV指令将其送入DS寄存器(如“MOV DS, BX”)。
(4)8086CPU可以一次性传送一个字(16位的数据),如下所示。
MOV BX, 1000H ;将数据1000H写入BX寄存器中
MOV DS, BX ;将BX寄存器中的内容1000H写入DS寄存器中
MOV AX, [0] ;将地址1000:0处的字型数据送入AX
MOV [0], CX ;将CX中的16位数据送到1000:0处
(5)举例:内存10000H中存放字为23H,内存10001H中存放字为11H,内存10002H中存放字为22H,内存10003H中存放字为66H。
MOV AX,1000H ;将数据1000H写入AX寄存器中
MOV DS, AX ;将AX寄存器中的内容1000H写入DS寄存器中
MOV AX, [0] ;将地址1000:0处的字型数据送入AX,此时AX中的内容为23H
MOV BX, [2] ;将地址1000:2处的字型数据送入BX,此时BX中的内容为11H
MOV CX, [3] ;将地址1000:3处的字型数据送入CX,此时CX中的内容为66H
ADD BX, [1]
;将地址1000:1处的字型数据与BX中的内容相加,结果存放在BX中,此时BX中的内容为22H
SUB CX, [1]
;将地址1000:1处的字型数据作为减数,CX中的内容作为被减数,做减法,结果存放在CX中,此时CX中的内容为55H
2、DS与数据段
(1)对于8086PC机,可以根据需要将一组内存单元定义为一个段,将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段,将数据段的段地址存放在寄存器DS中。
(2)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器相对应,低地址单元和低8位寄存器相对应。
(3)举例:将123B0H~123BAH的内存单元定义为数据段。
①累加数据段中的前3个单元中的数据。
MOV AX,123BH ;将数据123BH写入AX寄存器中
MOV DS, AX ;将AX寄存器中的内容123BH写入DS寄存器中
MOV AL, 0 ;将一字节数据0H送入AL
ADD AL, [0] ;将地址123B:0处的一字节数据与AL中的内容相加,结果存放在AL中
ADD AL, [1] ;将地址123B:1处的一字节数据与AL中的内容相加,结果存放在AL中
ADD AL, [2] ;将地址123B:2处的一字节数据与AL中的内容相加,结果存放在AL中
②累加数据段中的前3个字型数据。
MOV AX,123BH ;将数据123BH写入AX寄存器中
MOV DS, AX ;将AX寄存器中的内容123BH写入DS寄存器中
MOV AX, 0 ;将一字型数据00H送入AX
ADD AX, [0] ;将地址123B:0处的字型数据与AX中的内容相加,结果存放在AX中
ADD AX, [2] ;将地址123B:2处的字型数据与AX中的内容相加,结果存放在AX中
ADD AX, [4] ;将地址123B:4处的字型数据与AX中的内容相加,结果存放在AX中
3、MOV指令、ADD指令和SUB指令的各种形式总结
(1)MOV指令的各种形式(不全,遇到其它情况可以以此进行推测):
指令形式 | 示例 |
MOV 寄存器, 数据 | MOV AX, 8 将数据8H写入寄存器AX中 |
MOV 寄存器, 寄存器 | MOV AX, BX 将寄存器BX中的内容写入寄存器AX中 |
MOV 寄存器, 内存单元 | MOV AX, [8] 将地址DS:8处的字型数据写入寄存器AX中 |
MOV 内存单元, 寄存器 | MOV [8], AX 将寄存器AX中的字型数据写入地址DS:8处 |
MOV 段寄存器, 寄存器 | MOV DS, BX 将寄存器BX中的内容写入段寄存器DS中 |
(2)ADD指令的各种形式(不全,遇到其它情况可以以此进行推测):
指令形式 | 示例 |
ADD 寄存器, 数据 | ADD AX, 8 将数据8H与寄存器AX中的内容相加,结果存在AX中 |
ADD 寄存器, 寄存器 | ADD AX, BX 将寄存器AX与BX中的内容相加,结果存在AX中 |
ADD 寄存器, 内存单元 | ADD AX, [8] 将地址DS:8处的字型数据与寄存器AX中的内容相加,结果存在AX中 |
ADD 内存单元, 寄存器 | ADD [8], AX 将寄存器AX中的内容与地址DS:8处的字形数据相加,结果存在地址DS:8处 |
(3)SUB指令的各种形式(不全,遇到其它情况可以以此进行推测):
指令形式 | 示例 |
SUB 寄存器, 数据 | SUB AX, 8 将寄存器AX中的内容与数据8H相减,结果存在AX中 |
SUB 寄存器, 寄存器 | SUB AX, BX 将寄存器AX与BX中的内容相减,结果存在AX中 |
SUB 寄存器, 内存单元 | SUB AX, [8] 将寄存器AX中的内容与地址DS:8处的字型数据相减,结果存在AX中 |
SUB 内存单元, 寄存器 | SUB [8], AX 将地址DS:8处的字型数据与寄存器AX中的内容相减,结果存在地址DS:8处 |
八、栈及栈的操作实现
1、栈结构
(1)栈是一种只能在一端进行插入或删除操作的数据结构。
(2)栈有两个基本的操作——入栈和出栈,栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。
①入栈:将一个新的元素放到栈顶。
②出栈:从栈顶取出一个元素。
(3)栈的操作规则:LIFO(Last In First out,后进先出)。
(4)现今的CPU中都有栈的设计,8086CPU提供相关的指令,支持用栈的方式访问内存空间,也就是说,基于8086CPU的编程,可以将一段内存当作栈来使用。
2、CPU提供的栈机制
(1)PUSH和POP指令:
①入栈指令PUSH:该指令只有一个操作数(操作数不能是立即数,也就是类似00H这样的数据),它会将操作数指向的内存单元中的数据或者寄存器中的数据压入栈中。
②出栈指令POP:该指令只有一个操作数(操作数不能是立即数,也就是类似00H这样的数据),它会将栈顶元素弹出至操作数指向的内存单元或者寄存器中。
(2)8086CPU中,有两个与栈相关的寄存器:
①栈段寄存器SS:存放栈顶的段地址。
②栈顶指针寄存器SP:存放栈顶的偏移地址。
③任意时刻,SS:SP都指向栈顶元素的存储单元。(这也意味着SP中的内容会随入栈/出栈指令的使用而变化)
(3)PUSH指令和POP指令的执行过程:
①PUSH <寄存器/内存单元的地址>:SP中的内容自减2,并将寄存器或指定内存单元中的内容送入SS:SP指向的存储单元。
②POP <寄存器/内存单元的地址>:将SS:SP指向的存储单元中的数据送入寄存器或指定内存单元中,SP中的内容自增2。
(4)举例:(箭头表示SS:SP指向的位置,未画出表示指向10010H)
3、操作栈的注意事项
(1)当栈满的时候再使用PUSH指令入栈,将发生栈顶超界问题,这是非常危险的。
(2)当栈空的时候再使用POP指令出栈,将发生栈顶超界问题,这是非常危险的。
(3)8086CPU不保证对栈的操作不会超界,只知道栈顶在何处,不知道程序安排的栈空间有多大,程序员在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界,防止出栈时栈空了仍然继续出栈而导致的超界。