《汇编语言》学习笔记-0
汇编语言
注:本文档为“《汇编语言(第3版) 》王爽著”阅读过程中记的笔记。
参考视频:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_bilibili
环境搭建
搭建步骤
目的:搭建8086 CPU的汇编程序仿真运行环境。在DOS环境下执行汇编程序。
搭建步骤:
(1)下载安装包
下载地址:简单/AssemblyLanguageTest (gitee.com)
(2)安装DOSBox
点击DOSBox0.74-2-win32-installer.exe安装。
(2)拷贝命令文件,后面挂接的目录。
(3)启动DOSBox,挂接工作目录
debug使用
Debug是一个调试工具,使用它可以查看CPU各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
debug的一些命令:
-
R:查看、改变CPU寄存器的内容;
R - 查看CPU寄存器的内容
R 寄存器名 - 改变寄存器的内容
-
D:查看内存中的内容
D - 列出预设地址内存处的128个字节的内容
D 段地址:偏移地址 - 列出内存中指定地址处的内容
D 段地址:偏移地址 结尾地址 - 列出内存中指定地址范围内的内容
-
E:改写内存中的内容,可以写入数据、指令,可以通过字符串形式;
E 段地址:偏移地址 数据1 数据2 ...
-
U:将内存中的机器指令翻译成汇编指令
U 地址 - 查看代码
-
T:执行一条机器指令,CS:IP指向的内存单元处的指令
-
A:以汇编指令格式在内存中写入一条机器指令
查看寄存器和内存:
R查看,并改变寄存器的值(AX改为123H):
查看内存:
修改内存,使用数字:
修改内存,使用字符串:
查看内存中原有的机器码所对应的汇编指令:
执行一条或多条指令,直接使用T,执行CS:IP指向的指令:
以汇编的形式在内存中写入机器指令:
1 基础知识
1.1 机器语言
机器语言是机器指令的集合,机器指令就是一台机器可以正常执行的命令,也就是一列二进制数字。计算机将其转换为一列高低电平,以使计算机的电子器件受到驱动,进行运算。
每一种微处理器都有自己的机器指令集,也就是机器语言。
1.2 汇编语言
为了解决机器语言难以辨别和记忆,产生了汇编语言。
汇编语言的主体就是汇编指令,汇编指令是机器指令便于记忆的书写格式。
汇编指令转换为机器指令需要使用编译器,汇编语言写出源程序,再使用汇编编译器将其翻译为机器码,由计算机最终执行。
汇编语言主要由以下三类指令组成:
- 汇编指令:机器指令的助记符,有对应的机器码。
- 伪指令:没有对应的机器码,由编译器执行,计算机并不执行。
- 其它符号:比如+、-、*、/,由编译器识别,没有对应的机器码。
1.3 存储器
CPU是计算机核心部件,控制整个计算机的运作并进行运算。CPU工作需要提供指令和数据。指令和数据在存储器(内存)中存放,需要了解CPU如何从内存中读取信息,以及如何向内存中写入信息。
1.4 指令和数据
指令和数据是在应用的概念上。CPU在工作的时候把有的信息看作指令,有的信息看作数据,为同样的信息赋予不同的含义。
1.5 存储单元
存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号。
微机存储器的容量是以字节为最小单位的,对于一个拥有128个存储单元的存储器,它的容量是128个字节。
对于大容量的存储器可使用以下单位计算容量:
1KB = 1024B 1MB = 1024KB 1GB = 1024MB 1TB = 1024GB
1.5.1 存储器分类
从读写属性可分为两类:随机存储器(RAM)和只读存储器(ROM)。随机存储器可读可写,必须带电存储,掉电内容丢失;只读存储器只能读不能写,掉电内容不丢失。
随机存储器:存放程序和数据。
装有BIOS(Basic Input/Output System)的ROM:可通过它利用硬件设备进行最基本的输入输出。
接口卡上的RAM:比如显卡上的RAM,称为显存。
1.5.2 内存地址空间
内存地址空间的大小受到CPU地址总线宽度的限制。比如一个CPU的地址总线位宽为10,可寻址1024个存储单元,这1024个存储单元构成了这个CPU的内存地址空间。
CPU操作不同的存储器的时候,把它们总的看作一个由若干存储单元组成的逻辑存储器,这个逻辑存储器也就是内存地址空间。
图中所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址空间。CPU在这段空间中读写数据,实际就是在对应的物理存储器中读写数据。
1.6 CPU对存储器的访问
CPU从内存中读写数据,需要先指定存储单元的地址,还需要指明操作类型,是读出数据,还是写入数据。总结进行下面3类信息的交互:
- 存储单元的地址(地址信息);
- 器件的选择,读或写的命令(控制信息);
- 读或写的数据(数据信息)。
计算机中CPU和其他器件使用总线进行交换信息,分为三类:地址总线、控制总线和数据总线。
地址总线的位宽决定了CPU的寻址能力;
数据总线的位宽决定了CPU与其他器件进行数据传送时的一次数据传输量;
控制总线的位宽决定了CPU对系统中其他器件的控制能力。
1.6.1 地址总线
CPU通过地址总线来指定存储单元,地址总线能传输多少个不同的信息,CPU就可以对多少个存储单元进行访问。
一个CPU有N根地址线,则CPU的地址总线位宽为N,CPU最多可以寻找2的N次方个内存单元。
1.6.2 数据总线
CPU和内存或其它器件之家的数据传送是通过数据总线进行的,数据总线的位宽决定了CPU和外界的数据传送速度,8根数据总线一次可以传送8位二进制数据(1个字节),16根数据总线一次可以传送16位二进制数据(2个字节)。
1.6.3 控制总线
CPU对外部器件的控制是通过控制总线来进行的,有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。控制总线决定了CPU对外部器件的控制能力。
比如前面说的内存的读写名就是CPU通过控制线来进行信号传输的。
2 寄存器
一个典型的寄存器CPU由运算器、控制器、寄存器等器件构成,这些器件通过内部总线相连。
- 运算器进行信息处理;
- 寄存器进行信息存储;
- 控制器控制各种器件进行工作;
- 内部总线连接各种器件,在它们之间进行数据的传送。
寄存器是CPU中程序员可以用指令读写的部件,通过改变各种寄存器中的内容来实现对CPU的控制。
本书主要以8086CPU进行说明,8086有14个寄存器(16位),分别为:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
- 通用寄存器:AX、BX、CX、DX
- 变址寄存器:SI、DI
- 指针寄存器:SP、BP
- 指令寄存器:IP
- 段寄存器:CS、SS、DS、ES
- 标志寄存器:PSW
2.1 通用寄存器
8086CPU的所有寄存器都是16位的,可以存放两个字节,AX、BX、CX、DX这四个寄存器通常用来存放一般性的数据,称为通用寄存器。
以AX为例,寄存器的逻辑结构如下:
一个16位寄存器可以存放一个16位的数据,数据在寄存器中的存放如下:
8086CPU为了兼容8位处理器,可以将AX、BX、CX、DX这4个寄存器分为两个独立的8位寄存器使用,以AX为例,分开使用如下:
16位寄存器和8位寄存器存放数据的情况为:
寄存器 | 寄存器中的数据 | 所表示的值 |
---|---|---|
AX | 0100111000100000 | 20000(4E20H) |
AH | 01001110 | 78(4EH) |
AL | 00100000 | 32(20H) |
2.2 字在寄存器中的存储
8086CPU可以一次性处理两种尺寸的数据:
字节(byte):一个字节8比特,可存放在8位寄存器中;
字(word):一个字由两个字节组成,这两个字节称为字的高位字节和低位字节。
举例:一个字型数据20000(4E20H),存在AX寄存器中,在AH存放高8位(4EH),在AL中存放低8位(20H)。
2.3 物理地址
CPU访问内存单元时,需要内存单元的地址,所有的内存单元构成一个一维线性空间,每一个内存单元在这个空间有唯一的地址,称为物理地址。
CPU通过地址总线送入存储器的是一个物理地址,在CPU向地址总线发出物理地址之前,必须先在内部形成物理地址,不同的CPU有不同的方式。
2.4 16位架构的CPU
8086是16位结构(16位机、字长为16位)的CPU,能够一次性处理、传输、暂时存储的信息的最大长度位16位,CPU具有以下几方面的结构特性:
- 运算器一次最多可以处理16位的数据;
- 寄存器的最大宽度位16位;
- 寄存器和运算器之间的通路为16位。
2.5 8086CPU给出物理地址的方法
8086CPU有20位地址总线,可传送20位地址,共1MB寻址能力。
8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位,表现出的寻址能力只有64KB。
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址,相关部件的逻辑结构图如下所示:
8086CPU要读写内存时:
(1)CPU中的相关部件提供两个16位的地址:段地址和偏移地址;
(2)段地址和偏移地址通过内部总线送入一个地址加法器的部件;
(3)地址加法器将两个16位地址合成一个20位的物理地址;
(4)地址加法器通过内部总线将20位物理地址送入输入输出控制电路;
(5)输入输出控制电路将20位物理地址送上地址总线;
(6)20位物理地址总线被地址总线传送到存储器。
地址加法器采用:物理地址=段地址×16+偏移地址的方法合成物理地址。
其本质含义是:CPU在访问内存时,用一个基础地址(段地址×16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址。
更一般的说,8086CPU寻址功能为:基础地址+偏移地址=物理地址的一种具体实现方式。
2.6 段的概念
内存并没有分段,段的划分来自于CPU。
由于8086CPU用“物理地址=段地址×16+偏移地址”的方式给出内存单元的物理地址,使得可以用分段的方式来管理内存。比如:
- 地址10000H~100FFH的内存单元组成一个段,段的起始地址为10000H,段地址为1000H(实际地址右移4bits),大小为100H;
- 10000H~1007FH的起始段地址为1000H,大小为80H;
- 10080H~100FFH的地址段地址为1008H,大小为80H。
(1)段地址×16必然是16的整数倍,段的起始地址必然是16的整数倍;
(2)偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB。
2.6.1 段寄存器
段地址在8086CPU的段寄存器中存放,有4个段寄存器:CS、DS、SS、ES。
- CS:代码段寄存器
- DS:数据段寄存器
- SS:栈段寄存器
- ES:附加段寄存器
2.6.2 CS和IP寄存器
CS和IP寄存器指示了当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器。
8086CPU中,任意时刻设置CS中的内容为M,IP中的内容为N,8086CPU将从内存M×16+N单元开始,读取一条指令并执行(或CPU将CS:IP指向的内容当作指令执行)。
下图为读取、执行指令的工作原理:
(1)8086CPU当前状态:CS的内容为2000H,IP中的内容为0000H;
(2)内存20000H~20009H单元存放着可执行的机器码;
下图展示了读取、执行一条指令的过程:
8086CPU的工作流程简述如下:
1)从CS:IP指向的内存单元读取指令,读取的指令进入到指令缓冲区;
2)IP=IP+所读取指令的长度,从而指向下一条指令;
3)执行指令,转到步骤1),重复。
8086CPU上电启动或复位后,CS和IP被设置为CS=FFFFH,IP=0000H,即上电启动,从内存FFFF0H单元读取指令执行,这是上电后执行的第一条指令。
2.6.3 修改CS、IP指令
程序可以通过修改CS、IP中的内容来控制CPU执行目标指令。
通常用的mov指令,称为传送指令,不能用来改变CS、IP的值;
能够改变CS、IP的指令称为转移指令,比如jmp指令,格式:
jmp 段地址:偏移地址
# 举例:
jmp 2AE3:3 # 执行后,CS=2AE3H,IP=0003H,CPU将从2AE33H处读取指令。
jmp 3:0B16 # 执行后,CS=0003H,IP=0B16H,CPU将从00B46H处读取指令。
如果想仅修改IP的内容,可以使用:
jmp 某一合法寄存器
# 举例
jmp ax # 执行前,ax=1000H,CS=2000H,IP=0003H
# 执行后,ax=1000H,CS=2000H,IP=1000H
其含义为使用寄存器中的值修改IP。
2.6.4 代码段
对于8086CPU,在编程时可以将一组内存单元定义在一个段。可以将长度为N(N≤64KB)的一组代码,存在一组地址连续、起始地址为16倍数的内存单元中。这段内存是用来存放代码的,从而定义了一个代码段。
将一段内存当作代码段,仅是编程的一种安排。CPU只认为被CS:IP指向的内存单元中的内容为指令。所以要让CPU指向所定义的代码段中的指令,必须将CS:IP指向所定义的代码段中的第一条指令的首地址。
3 寄存器(内存访问)
从访问内存角度学习寄存器。
3.1 内存中字的存储
CPU中用16位寄存器来存储一个字,高8位存放高字节,低8位存放低字节。
内存中存储时,内存单元是字节单元(一个单元一个字节),一个字需要两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
比如,存放20000(4E20H),存放如下:
地址 | 数据 |
---|---|
0 | 20H |
1 | 4EH |
2 |
字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。
高地址内存单元存放字型数据的高位字节,低地址内存单元中存放字型数据的低位地址。
3.2 DS和[address]
CPU读写一个内存单元时,要先给出内存单元的地址。
8086CPU中,内存地址由段地址和偏移地址组成。DS寄存器,用来存放要访问数据的段地址。
比如读取10000H单元的内容,将10000H(1000H:0)中的数据读取到al中:
mov bx, 1000H
mov ds, bx # 8086不支持直接将数据送入段寄存器
mov al, [0] # 将10000H中的数据读到al中
将al中的数据送入内存单元10000H:
mov bx, 1000H
mov ds, bx
mov [0], al # 将al中的数据写到10000H中
# 从内存中读取数据到寄存器的格式
mov 寄存器名,内存单元地址
# [...]表示一个内存单元,"..."表示内存单元的偏移地址,段地址自动从ds寄存器中获取
# 将寄存器中的数据送入内存单元
mov 内存单元地址,寄存器名
3.3 mov add sub指令
mov指令的常用几种格式:
指令 | 举例 |
---|---|
mov 寄存器,数据 | mov ax,8 |
mov 寄存器,寄存器 | mov ax,bx |
mov 寄存器,内存单元 | mov ax,[0] |
mov 内存单元,寄存器 | mov [0],ax |
mov 段寄存器,寄存器 | mov ds,ax |
mov 寄存器,段寄存器 | mov ax,ds |
mov 内存单元,段寄存器 | mov ax,1000H mov ds,ax mov [0],cs |
mov 段寄存器,内存单元 | mov ax,1000H mov ds,ax mov ds,[0] |
add和sub指令的常用格式:
指令 | 举例 |
---|---|
add 寄存器,数据 | add ax,8 |
add 寄存器,寄存器 | add ax,bx |
add 寄存器,内存单元 | add ax,[0] |
add 内存单元,寄存器 | add [0],ax |
sub 寄存器,数据 | sub ax,bx |
sub 寄存器,寄存器 | sub ax,bx |
sub 寄存器,内存单元 | sub ax,[0] |
sub 内存单元,寄存器 | sub [0],ax |
3.4 数据段
8086PC编程时,可以根据需要将一组内存单元定义为一个段,可以将一组长度为N(N≤64KB)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义一个数据段。
如何访问数据段中的数据?在具体操作时,用ds存放数据段的段地址,用相关指令访问数据段中的具体单元,比如将123B0H~123B9H的内存定义为数据段,要累加这个数据段中的前3个单元中的数据:
mov ax,123BH
mov ds,ax
mov al,0
add al,[0]
add al,[1]
add al,[2]
程序和数据没有区别,都是内存单元中的二进制码。当内存单元被CS:IP指定时其存储的就被当做程序执行。当内存单元被DS:[address]指定时其存储的就是数据。
3.5 栈
3.5.1 CPU提供的栈机制
栈是一种特殊的访问方式的存储空间,最后进入这个空间的数据,最先出去(LIFO,Last In First Out)。
栈的两个基本操作:入栈和出栈,入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。
8086CPU提供相关的指令来以栈的方式访问内存空间。提供的入栈和出栈指令为:PUSH(入栈)和POP(出栈)。入栈和出栈都是以字为单位进行。
入栈时,栈顶从高地址向低地址方向增长。
push ax # 将ax中的数据送入栈中
pop ax # 从栈顶取出数据送入ax
8086CPU中,栈顶的段地址存放在SS寄存器中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。PUSH指令和POP指令执行时,CPU从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.5.2 栈顶超界问题
当栈满的时候,使用push指令入栈,会发生栈顶超界问题;
当栈空的时候,使用pop指令出栈,会发生栈顶超界问题。
8086CPU不保证用户对栈的操作不会越界,CPU不知道用户的栈空间有多大,因此需要用户编程时担心越界问题,根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的越界;执行出栈的时候,也要防止栈空的时候继续出栈而导致的越界。
3.5.3 push pop指令
push和pop指令可以在内存和寄存器之间传送数据,其指令的格式如下:
push 寄存器 # 将一个寄存器中的数据入栈
pop 寄存器 # 出栈,用一个寄存器接收出栈的数据
push 段寄存器 # 将一个段寄存器中的数据入栈
pop 段寄存器 # 出栈,用一个段寄存器接收出栈的数据
push 内存单元 # 将一个内存字单元处的字入栈(以字为单位)
pop 内存单元 # 出栈,用一个内存单元接收出栈的数据
move ax,1000H
mov ds,ax # 内存单元的段地址放在ds中
push [0] # 将1000:0处的字压入栈中
pop [2] # 出栈,出栈的数据送入1000:2中
栈的综述:
(1)在SS、SP中存放栈顶的段地址和偏移地址;提供入栈和出栈指令,根据SS:SP指示的地址,按照栈的方式访问内存单元。
(2)push指令的执行步骤,①SP=SP-2;②向SS:SP指向的字单元中送入数据。
(3)POP指令的执行步骤,①从SS:SP指向的字单元中读取数据;②SP=SP+2。
(4)任意时刻,SS:SP指向栈顶元素。
(5)8086CPU只记录栈顶,栈空间的大小用户自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
(7)push、pop指令实质是一种内存传送指令,可灵活使用。
3.5.4 栈段
8086PC编程时,将一组内存单元定义为一个段。可以将长度为N(N≤64KB)的一组连续地址、起始地址为16的倍数的内存单元,当作栈空间使用,从而定义一个栈段。
将一段内存看作段,仅是编程时的一种安排,在执行push、pop指令时自动将我们定义的栈段当作栈空间来访问。
段的综述:
用户可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。
- 用一个段存放数据,将它定义为”数据段“;
- 用一个段存放代码,将它定义为”代码段“;
- 用一个段当作栈段,将它定义为”栈段“。
要让CPU按用户安排访问这些段,需要:
- 对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将用户定义的数据段中的内存当作数据来访问。
- 对于代码段,将它的段地址放在CS中,将段中的第一条指令的偏移地址放在IP中,CPU就将执行用户定义的代码段中的指令。
- 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,CPU在需要进行栈操作的时候,比如执行push、pop指令,就将用户定义的栈段当作栈空间使用。