汇编语言是很多相关课程(如:数据结构、操作系统、微机原理等)的重要基础。概括地说,如果想从是计算机科学方面的工作的话,汇编语言的基础是必不可少的。原因很简单,我们的工作平台、研究对象都是机器,汇编语言是人和计算机沟通的最直接的方式,它描述了机器最终所要执行的指令序列。
汇编语言是和具体的微处理器(CPU)相联系的,每一种微处理器的汇编语言都不一样,只能通过一种常用的、结构简洁的微处理器的汇编语言来进行学习,从而达到学习汇编的两个最根本的目的:充分获得底层编程的体验。深刻理解机器运行程序的机理
汇编课程的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活地控制系统进行工作
汇编语言是直接在硬件之上工作的编程语言,我们首先要了解硬件系统的结构,才能有效地应用汇编语言对其编程
机器语言是机器指令的集合。机器指令展开来讲就是一台机器可以正确执行的命。电子计算机的机器指令是一列二进制数字。计算机将之转变为一系列高低电平,以使计算机的电子器件受到驱动,进行运算。
上面说的计算机指的是可以执行机器指令,进行运算的机器,这是早期计算机的概念
现在,在我们常用的PC机(个人计算机),有一个芯片来完成上面所说的计算机的功能。这个芯片就是我们常说的CPU(Central Processing Unit,中央处理单元),CPU是一种微处理器。
以后我们提到的 计算机是指由 CPU 和其他受 CPU 直接或间接控制的芯片、器件、设备组成的计算机系统,比如我们最常见的PC机
每一种微处理器,由于硬件设计和内部结构的不同,就需要用不同的电平脉冲来控制 ,使它工作。所以每一种微处理器都有自己的机器指令集,也就是机器语言
早期的程序设计均使用机器语言。程序员们将0、1数字编成的程序代码打在纸带或卡片上,1打孔,0不打孔,再将程序通过纸带机或卡片机输入计算机,进行运算。不难看出,用这种方式编写程序不是一件简单的工作。晦涩难懂和不易查错…
程序员们很快就发现了使用机器语言带来的麻烦,它是如此难于辨别和记忆,给整个产业的发展带来了障碍,于是汇编语言产生了
汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表达方法上。汇编指令是: 机器指令便于记忆的书写格式
此后,程序员们就用汇编指令编写源程序。但是计算机能读懂的只有机器指令,这时,就需要有一个能够将汇编指令转换成机器指令的翻译程序,这样的程序我们称其为编译器。
程序员用汇编语言写出源程序,再用汇编编译器将其编译为机器码,由计算机最终执行
汇编语言发展至今,有以下 3 类指令组成:(汇编语言的核心是汇编指令,它决定了汇编语言的特性)
(1) 汇编指令:机器码的助记符,有相对应的机器码
(2) 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
(3) 其他符号:如+、-、*、/ 等,没有对应的机器码,由编译器识别
CPU 是计算机的核心部件,它控制整个计算机的运作并进行运算,要想让一个 CPU工作,就必须向它提供指令和数据。
指令和数据在存储器中存放,也就是我们平时所说的内存 (机器指令主要存放在存储器中)。在一台 PC 机中内存的作用仅次于 CPU。离开了内存,性能再好的 CPU 也无法工作,这就像再聪明的大脑,没有了记忆也无法进行思考。
需要注意的是:磁盘不同于内存,磁盘上的数据或程序如果不读到内存中,就无法被 CPU 使用。
要灵活地利用汇编语言编程,我们首先要了解 CPU 是如何 从 内存 中读取信息,以及向 内存 中写入信息的
指令和数据是应用上的概念。在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。CPU 在工作的时候把有的信息看作 指令,有的信息看作 数据,为同样的信息赋予了不同的意义。
存储器 被划分成 若干个存储单元。每个存储单元从 0 开始顺序编号,例如一个存储器有 128 个存储单元,编号从 0 ~ 127。每个存储单元可以存储一个 Byte(字节),即 8 个二进制位。
计算机最小的信息单位是 bit(比特),也就是一个二进制位,微机存储器的容量是以 字节 为最小单位来计算的。对于拥有 128 个存储单元的存储器,我们可以说,它的容量是 128个字节1 KB = 1024B,1MB = 1024KB,1GB = 1024MB,1TB = 1024GB
磁盘的容量单位同内存的一样,实际上以上单位是微机中常用的计量单位
以上讲到,存储器被划分为多个存储单元,存储单元从 0 开始编号。这些编号可以看作存储单元在存储器中的地址。
CPU 要从内存中读取数据,首先要指定存储单元的地址。也就是说它要先确定它要读取哪一个存储单元中的数据。
另外,在一台微机中,不只有存储器这一种器件 。CPU 在读写数据 时还要指明 ,它对哪一个器件进行操作,进行哪种操作,是从中读写数据,还是写入数据
可以,CPU 要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行下面 3 类信息的交互:
- 地址信息:存储单元的地址
- 控制信息:器件的选择,读或写的命令
- 数据信息:读或写的数据
那么 CPU 是通过什么将地址、数据和控制信息传到存储器芯片中的呢? 电子计算机能处理、传输的信息都是电信号,电信号当然要用导线传送。在计算机中专门有连接 CPU 和其他芯片的导线,通常称为总线。总线从物理上来讲,就是一根根导线的集合。根据传送信息的不同,总线从逻辑上又分为3类,地址总线、控制总线和数据总线。
如图:CPU从 3 号单元中读取数据的过程:
(1) CPU 通过地址线将地址信息 3 发出
(2) CPU 通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据
(3) 存储器将 3 号单元中的数据 8 通过数据线送入 CPU
现在我们知道,CPU 是通过地址总线来指定存储器单元的。那么地址总线上能传送多少个不同的信息,也就决定 CPU 能对多少个存储单元进行寻址。
若一个 CPU 有 10 根地址总线,而 一根导线可以传送的稳定状态只有两种,高电平或是低电平。用二进制表示就是 1 或 0,10根导线可以传送 10 位二进制数据。而 10 位二进制数就可以表示出 2 的 10次方个不同的数据。最小数为 0,最大数为 1023。
一个 CPU 有 N 根地址线,则可以说这个 CPU 的地址总线的宽度为 N,这样的 CPU 最多可以寻找 2 的 N 次方个内存单元
如图,为 一个具有 10 根地址线的 CPU 向内存发出地址信息 11 时 10 根地址线上传递的二进制信息:
CPU 与内存或其他器件之间的数据传送是通过数据总线来进行的。
数据总线的宽度决定了 CPU 和外界的数据传送速度。8 根数据总线一次可传送一个 8 位二进制数据(即一个字节)。16根数据总线一次可传送两个字节。
8088CPU 的数据总线宽度为 8,8086CPU 的数据总线宽度为 16。
如图,分别为 8088CPU(数据总线宽度8)、8086CPU(数据总线宽度为16)数据传送情况:
CPU 对外部器件的控制是通过控制总线来进行的。在这里控制总线是个总称,控制总线是一些不同控制线的集合。有多少根控制总线,就意味着 CPU 提供了外部器件的多少种控制。所以,控制总线的宽度决定了 CPU 对外部器件的控制能力。
前面的内存读写命令是由几根控制线综合发出的,其中有一根称为 “读信号输出” 的控制线负责由 CPU 向外传送读信号,CPU 向该控制线上输出低电平表示将要读取信号;有一根称为 “写信号输出” 的控制线则负责传送写信号。
什么是内存地址空间呢?比如,一个 CPU 的地址总线宽度为 10,那么可以寻址 1024 个内存单元,这 1024 个可寻到的内存单元就构成这个 CPU 的内存地址空间。
在每一台 PC 机中,都有一个主板,主板上有核心器和一些主要器件,这些器件 通过总线(地址总线、数据总线、控制总线)相连。这些器件有 CPU、存储器、外围芯片组、扩展插槽等。扩展插槽上一般插有 RAM内存条 和 各类接口卡。
计算机系统中,所有可用程序控制其工作的设备,必须受到 CPU 的控制。CPU 对外部设备都不能直接控制,如显示器、音箱、打印机等。直接控制这些设备进行工作的是插在扩展插槽上的接口卡。
扩展插槽通过总线和 CPU 相连,所以接口卡也通过总线同 CPU 相连。CPU 可以直接控制这些接口卡,从而实现 CPU 对外设的间接控制。
简单地讲,就是 CPU 通过总线向接口卡发送命令,接口卡根据 CPU 的命令控制外设进行工作。
一台 PC 机中,装有多个存储器芯片,这些芯片从物理连接上看是独立的、不同的器件。
1.从读写属性上看分为两类:随机存储器(RAM) 和 只读存储器(ROM)。随机存储器: 可读可写,但必须带电存储,关机后存储的内容丢失。
只读存储器: 只能读取不能写入,关机后其中的内容不丢失。
2.从功能和连接上分:
随机存储器:
用于存放供 CPU 使用的绝大部分程序和数据,主随机存储器一般由两个位置上的 RAM 组成,装在主板上的 RAM 和插在扩展插槽上的 RAM
装有 BIOS(Basic Input/Output System,基本输入/输出系统)的 ROM:
BIOS 是由系统和各类接口卡(如显卡、网卡等)厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。
例如:主板上的 ROM 中存储着主板的 BIOS(通常称为系统 BIOS);显卡上的 ROM 中存储着显卡的 BIOS;如果网卡上装有 ROM,那其中就可以存储网卡的 BIOS
接口卡上的 RAM:
某些接口卡需要对大批量输入、输出数据进行暂时存储,在其上装有 RAM。
最典型的是显示卡上的 RAM,一般称为显存。显示卡随时将显存中的数据向显示器上输出。换句话说,我们将需要显示的内容写入显存,就会出现在显示器上
如图,为 PC 系统中各类存储器的逻辑连接情况:
上述的那些存储器,在物理上是独立的器件,但是在以下两点上相同。
- 都和 CPU 的总线相连
- CPU 对它们进行或写的时候都通过控制线发出内存读写命令
也就是说,CPU 在操控它们的时候,把它们都当作内存来对待,把它们总的看作一个由若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间
如图,展示了 CPU 将系统中各类存储器看作一个逻辑存储器的情况:
上图中,所有的物理存储器被看作一个由若干个存储单元组成的逻辑存储器。每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU 在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据
假设,上图8中的内存地址空间的地址段分配如下。
地址0~7FFFH的32KB空间为主随机存储器的地址空间;
地址8000H9FFFH的8KB空间为显存地址空间:
地址A000H~FFFFH的24KB空间为各个ROM的地址空间。
这样,CPU向内存地址为1000H的内存单元中写入数据,这个数据就被写入主随机
存储器中;CPU向内存地址为8000H的内存单元中写入数据,这个数据就被写入显存中,然后会被显卡输出到显示器上;CPU向内存地址为C000H的内存单元中写入数据的操作是没有结果的,C000H单元中的内容不会被改变,C000H单元实际上就是ROM存储器中的一个单元。内存地址空间的大小受CPU地址总线宽度的限制。8086CPU的地址总线宽度为20,可以传送 220 个不同的地址信息(大小从 0 至 220-1)。即可以定位 220 个内存单元,则8086PC的内存地址空间大小为1MB。同理,80386CPU的地址总线宽度为32,则内存地址空间最大为4GB。
我们在基于一个计算机硬件系统编程的时候,必须知道这个系统中的内存地址空间分配情况:
因为当我们想在某类存储器中读写数据的时候,必须知道它的第一个单元的地址和最后一个单元的地址,才能保证读写操作是在预期的存储器中进行。比如,我希望向显示器输出一段信息,那么必须将这段信息写到显存中,显卡才能将它输出到显示器上。要向显存中写入数据,必须知道显存在内存地址空间中的地址。
不同的计算机系统的内存地址空间的分配情况是不同的。如下图,展示了 8086PC 机内存地址空间分配的基本情况:
上图告诉我们:
1. 从地址0~9FFF℉的内存单元中读取数据,实际上就是在读取主随机存储器中的数据;
2. 向地址A0000~BFFF℉的内存单元中写数据,就是向显存中写入数据,这些数据会被显示卡输出到显示器上;
3. 我们向地址C0000FFFF℉的内存单元中写入数据的操作是无效的,因为这等于改写只读存储器中的内容。
内存地址空间:
最终运行程序的是 CPU,我们用汇编语言编程的时候,必须要从 CPU 的角度考虑问题。对 CPU 来讲,系统中的所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受 CPU 寻址能力的限制。这个逻辑存储器即是我们所说的内存地址空间
一个典型的 CPU 由运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。前面所说的总线,是相对 CPU 内部而言的外部总线。
内部总线实现 CPU 各个器件之间的联系,外部总线实现 CPU 和主板上其他器件的联系,简单地说,在 CPU 中:
- 运算器进行信息处理
- 寄存器进行信息存储
- 控制器控制各种器件 ,在它们之间进行数据的传送
- 内部总线连接各种器件,在它们之间进行数据的传送
不同的 CPU,寄存器的个数、结构是不同的。8086CPU 有 14 个寄存器,每个寄器有一个名称。这些寄器是: AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW
8086CPU 的所有寄器都是 16 位的,可以存放两个字节。AX、BX、CX、DX 这 4 个寄存器通常用来存放一般性的数据,被称为通用寄存器。
以 AX 为例,寄存器的逻辑结构如图(一个 16 位的寄器可以存储一个 16 位的数据)
:
8086CPU 的上一代 CPU 中的存储器都是 8 位的,为了保证兼容,使原来基于上供 CPU 编写的程序稍加修改就可以运行在 8086 之上,8086CPU 的 AX、BX、CX、DX 这 4 个寄存器都可分为两个可独立使用的 8 位寄器来用:
- AX 可分为 AH 和 AL
- BX 可分为 BH 和 BL
- CX 可分为 CH 和 CL
- DX 可分为 DH 和 DL
以 AX 为例,8086CPU 的 16 位寄存器分为两个 8 位寄存器的情况如图:
AX 的低 8 位构成了 AL 寄存器,高 8 位构成了 AH 寄存器。AH 和 AL寄存器都是可以独立使用的 8 位寄器。
16 位寄存器及所分成的两个 8 位寄器的数据存储情况如图:
出于兼容性的考虑,8086CPU 可以一次性处理以下两种尺寸的数据:
- 字节: 记为 byte,一个字节由 8 个 bit组成,可以存放在 8 位寄存器中。
- 字: 记为 word,一个字由两个字节组成,这两个字节分别称为这个字的高位字节和低位字节
如下图所示(一个字由两个字节组成):
一个字可以存在一个16位寄存器中,这个字的高位字节和低位字节自然就存在这个寄存器的高8位寄存器和低8位寄存器中。
如图,一个字型数据 20000,存在 AX 寄存器中,在AH中存储了它的高 8 位,在AL中存储了它的低 8 位。AH 和 AL 中的数据,既可以看成是一个字型数据的高 8 位和低 8 位,这个字型数据的大小 20000 又可以看成是两个独立的字节型数据,它们的大小分别是 78 和 32。
关于数制的讨论:
作何数据,到了计算机中都是以二进制的形式存放的。但为了描述不同的问题,又经常将它们用其他进制来表示。
比如,寄存器 AX 中的数据是 0100111000100000,这就是 AX 中的信息本身,可以用不同的逻辑意义来看待它,可以将它看作一个数值,大小是 20000。
当然,二进制数 0100111000100000 本身也可表示一个数值的大小,但人类习惯的是十进制,用十进制 20000来表示这个信息可以使我们直观地感受这个数值的大小。
十六进制的一位相当于二进制数的四位,如 0100111000100000 可表示成:4(0100)、E(1100)、2(0010)、0(0000)四位十六进制数。
一个内存单元可存放 8 位数据,CPU 中的寄存器又可存放 n 个 8 位的数据。也就是说,计算机中的数据大多是由 1~N 个 8 位数据构成的。很多时候,需要直观地看出组成数据的各个字节数据的值,用十六进制来表示数据可以直观地看出这个数据是由哪些 8 位数据构成的。
比如 20000 写成 4E20 就可以更直观地看出这个数据是由 4E 和 20 这两个 8 位数据构成的,如果 AX 中存放 4E20,则 AH 里是 4E,AL 里是 20。这种表示方法便于许多问题的直观分析。在以后的课程中,我们多使用十六进制来表示一个数据。
在下面,为了区别不同的进制,在十六进制表示的数据的后面加 H,在二进制表示的数据后面加 B,十进制表示的数据后面什么也不加。如:可用 3 种不同的进制 表示 AX 里的数据,二进制: 0100111000100000,十六进制:AE20H,十进制: 20000
注意:在写一条汇编指令或一个寄存器的名称时不区分大小写
程序段中指令的执行情况之一:
程序段中的最后一条指令 add ax,bx,在执行前ax和bx中的数据都为 8226H,相加后所得的值为:1044CH,但是 ax 为 16 位寄存器,只能存放 4 位十六进制的数据,所以最高位的 1 不能在 ax 中保存,ax 中的数据为:044CH。
程序段中指令的执行情况之二:
程序段中的最后一条指令add al,93H,在执行前,al中的数据为 C5H,相加后所得的值为:158H,但是l为8位寄存器,只能存放两位十六进制的数据,所以最高位的1丢失,x中的数据为:0058H。(这里的丢失,指的是进位值不能在8位寄存器中保存,但是CPU并不真的丢弃这个进位值)
注意,此时al是作为一个独立的8位寄存器来使用的,和ah没有关系,cPU在执行这条指令时认为ah和al是两个不相关的寄存器。不要错误地认为,诸如add al,93H的指令产生的进位会存储在ah中,add al,93H进行的是8位运算。
同时,在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的(8 位寄存器只能与 8 位寄存器作运算)
我们知道,CPU 访问内存单元时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。
CPU 通过地址总线送入存储器,必须是一个内存单元的物理地址。在 CPU 向地址总线上发出物理地址之前,必须要在内部先形成这个物理地址。不同的 CPU 可以有不同的形成物理地址的方式。
我们说 8086CPU 的上一代 CPU(8080、8085)等是 8 位机,而 8086 是16 位机,也可以说 8086 是 16 位结构 CPU。
概括地讲,16位结构(16位机、字长为 16 位等常见说法,与 16 位结构的含义相同)描述了一个 CPU 具有下面几方面的结构特性:
- 运算器一次最多可以处理 16 位的数据
- 寄存器的最大宽度为 16 位
- 寄存器和运算器之间的通路为 16 位
8086 是 16 位结构的 CPU,这也就是说,在 8086 内部,能够一次性处理、传输、暂时存储的信息的最大长度是 16 位的。内存单元的地址在送上地址总线之前,必须在 CPU 中处理、传输、暂时存放,对于 16 位 CPU,能一次性处理、传输、暂时存储 16 位的地址
8086CPU 有 20 位地址总线,可以传送 20 位地址,达到 1MB 寻址能力。而 8086CPU 又是 16 位结构,在内部一次性处理、传输、暂时存储的地址为 16 位。从 8086CPU 的内部结构来看,如果将地址从内部简单地发出,那么它只能送出 16 位的地址,表现出的寻址能力只有 64KB。
8086CPU 采用一种在内部用两个 16 位地址合成的方法来形成一个 20 位的物理地址,如下图所示:
如图所示,当 8086CPU 要读写内存时:
(1) CPU 中相关部件提供两个 16 位的地址,一个称为段地址,另一个称为偏移地址;
(2) 段地址和偏移地址通过内部总线送往一个称为地址加法器的部件;
(3) 地址加法器将两个 16 位地址合成为一个 20 位的物理地址;
(4) 输入输出控制电路 将 20 位物理地址送上地址总线
(5) 20 位物理地址被地址总线传送到存储器
地址加法器采用 物理地址 = 段地址 x 16 + 偏移地址 的方法用段地址和偏移地址合成物理地址。
例如:8086CPU 要访问地址为 123C8H 的内存单元,此时,地址加法器的工作f过程中如下图所示(图中皆为十六进制表示):
由段地址 x16 引发的讨论:
“段地址x16” 有一个更为常用的说法是左移 4 位。计算机中的所有信息都是以二进制的形式存储的,段地址当然也不例外。机器只能处理二进制信息,“左移 4 位” 中的位,指的是二进制位。
观察上面移位次数和各种形式数据的关系,我们可以发现:
(1) 一个数据的二进制形式左移 1 位,相当于该数据乘以 2;
(2) 一个数据的二进制形式左移 N 位,相当于该数据乘以 2 的 N 次方;
(3) 地址加法器如何完成段地址 x16 的运算?就是将以二进制形式存放的段地址左移 4 位。
二进制的数,其位权是以 2 为底的幂,左移一位就是 *2。十进制的数,其位权是以 10 为底的幂,左移一位就是 *10。【一个 X 进制的数据左移 1 位,相当于乘以 X】
“段地址 x 16 + 偏移地址 = 物理地址” 的本质含义是:CPU 在访问内存时,用一个基础地址(段地址x16) 和 一个相对于基础地址的 偏移地址 相加,给出内存单元的物理地址。
更一般地说,8086CPU 的这种寻址功能是 “基础地址+偏移地址=物理地址” 寻址模式的一种具体实现方案。8086CPU 中,段地址x16 可看作是基础地址
下面,用一个与 CPU 无关的例子做进一步的比喻说明:
比如说,学校、体育馆、图书馆同在一条笔直的单行路上,学校位于路的起点。你要去图书馆,问我那里的地址,我可以用两种方式告诉你图书馆的地址:
- (1) 从学校走 2826m 到图书馆。这 2826 可以认为是图书馆的物理地址
- (2) 从学校走 2000m 到图书馆,从体育馆再走 826m 图书馆。第一个距离 2000m,是相对于起点的基础地址,第二个距离 826m 是相对于基础地址的偏移地址(以基础地址为起点的地址)
第一种方式是直接给出物理地址 2826m,而第二种方式是用基础地址和偏移地址相加得到物理地址的。
如果再为上面的例子加一些限制条件,比如,只能通过纸条来互相通过。你问我图书馆的地址我只能将它写在纸上告诉你。显然,我必须有一张可以容纳 4 位数据的纸条,才能写下 2826 这个数据。
可不巧的是,我没有能容纳 4 位数据的纸条,仅有两张可以容纳 3 位数据的纸条。这样我只能以这种方式告诉你 2826 这个数据
在第一张纸上写上200(段地址),在第二张纸上写上826(偏移地址)。假设我们事前对这种情况又有过相关的约定:你得到这两张纸后,做这样的运算:200(段地址)×10+826(偏移地址)=2826(物理地址)。
8086CPU就是这样一个只能提供两张 3 位数据纸条的CPU。8086CPU 有 20 位地址总线,可以传送 20 位地址,但是 8086CPU 内部又只能送出 16 位,因此通过这样的方式最终能够得到 20 位地址。
“段地址” 并不是内存进行了分段,内存并没有分段,段的划分来自于 CPU,由于 8086CPU 用 “基础地址(段地址)+偏移地址=物理地址” 的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。
如下图:
我们可以认为,地址 10000H~100FFH 的内存单元组成一个段,该段的起始地址(基础地址)为 10000H,段地址为 1000H,大小为 100H;我们也可以认为地址 10000H1007FH、10080H10FFH 的内存单元组成两个段,它们的起始地址(基础地址)为 10000H 和 10080H,段地址为:1000H 和 1008H,大小都为 80H。
以后,在编程时可以根据需要,将若干个地址连续的内存单元看作一个段,用段x16 定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。
有两点需要注意:段地址x16 必然是 16 的倍数,所以一个段的起始地址也一定是 16 的倍数;偏移地址为 16 位,16位地址的寻址能力为 64KB,所以一个段的长度最大为 64KB
其它:
观察下面的地址:
结论:CPU 可以用不同的段地址和偏移地址形成同一个物理地址
比如 CPU 要访问 21F60H 单元,则它给出的段地址 SA 和偏移地址 EA 满足 SAx16+EA=21F60H 即可
“数据在 21F60H 内存单元中。” 这句话对于 8086CPU 机一般不这样讲,取而代之的是两种类似的说法:1.数据存在内存 2000:1F60 单元中;2.数据存在内存的 2000H 段中的 1F60H 单元中。这两种描述都表示 “数据在内存 21F60H 单元中”
可以根据需要,将地址连续、起始地址为 16 的倍数的一组内存单元定义为一个段
前面提到,8086CPU 在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址。这里,需要看一下,是什么部件提供段地址。
段地址在 8086CPU 的段寄存器中存放。8086CPU 有 4 个寄存器:CS、DS、SS、ES。当 8086CPU 要访问内存时由这 4 个段寄存器提供内存单元的段地址。
CS 和 IP 是 8086CPU 中两个最关键的寄存器,它们指示了 CPU 当前要读取指令的地址。CS 为代码段寄存器,IP 为指令指针寄存器。
在 8086CPU 机中,任意时刻,设 CS 中的内容为 M,IP 中的内容为 N,8086CPU 将从内存 Mx16+N 单元开始,读取一条指令并执行
也可以这样表述:8086机中,任意时刻,CPU 将 CS:IP 指向的内容当作指令执行
如图,展示了 8086CPU 读取、执行指令的工作原理:
下面的一组图,展示了 8086CPU 读取、执行一条指令的过程(对8086CPU的描述,是在逻辑结构、宏观过程的层面上进行的)
通过上面的过程展示 ,8086CPU 的工作过程可以简单描述如下:
(1) 从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器;
(2) IP=IP+所读取指令的长度,从而指向下一条指令;
(3) 执行指令。转到步骤(1),重复这个过程
在 8086CPU 加电启动或复位后(即 CPU 刚开始工作时) CS 和 IP 被设置为 CS=FFFFH,IP=0000H,即在 8086CPU 机刚启动时,CPU 从内存 FFFF0H 单元中读取指令执行,FFFF0H 单元中的指令是 8086PC 机开机后执行的第一条指令
现在,我们更清楚了CS 和 IP 的重要性,它们的内容提供了 CPU要执行指令的地址。
我们在前面讲过,在内存中,指令和数据没有任何区别,都是二进制信息,CPU
在工作的时候把有的信息看作指令,有的信息看作数据。现在,如果提出一个问题:CPU根据什么将内存中的信息看作指令?如何回答?
我们可以说,CPU将CS:P指向的内存单元中的内容看作指令,因为,在任何时候,CPU将CS、P中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,执行。如果说,内存中的一段信息曾被CPU执行过的话,那么,它所在的内存单元必然被CS:P指向过。
在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制。CPU从何处执行指令是由CS、P中的内容决定的,程序员可以通过改变CS、IP中的内容来控制CPU执行目标指令。
我们如何改变 CS、IP 的值呢?move 指令不能用于设置 CS、IP 的值,原因很简单,因为 8086CPU 没有提供这样的功能。8086CPU 为 CS、IP 提供了另外的指令来改变它们的值。能够改变 CS、IP 的内容的指令被统称为转移指令(我们以后会深入研究)。我们现在介绍一个最简单的可以修改 CS、IP 的指令:jmp指令。若想同时修改 CS、IP 的内容,可用形如 “jmp段地址:偏移地址”的指令完成,如
- jmp2AE3:3,执行后:CS=2AE3H,IP-0003H,CPU将从2AE33H处读取指令。
- jmp3:0B16,执行后:CS=0003H,IP=0B16H,CPU将从00B46H处读取指令。
“jp段地址:偏移地址”指令的功能为:用指令中给出的段地址修改CS,偏移地
址修改P。若想仅修改P的内容,可用形如“jmp某一合法寄存器”的指令完成,如:
- jmp ax,指令执行前:ax=l000H,CS=2000H,IP=0003H
指令执行后:ax=1000H,CS=2000H,IP=1000H- jmp bx,指令执行前:bx=0B16H,CS-2000H,IP-0003H
指令执行后:bx=0B16H,CS=2000H,IP=0B16H
“jmp某一合法寄存器” 指令的功能为:用寄存器中的值修改IP。
jmp ax,在含义上好似:mov IP,.ax。
前面讲过,对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N≤64KB)的一组代码,存在一组地址连续、起始地址为 16 的倍数的内存单元中,我们可以认为,这段内存是用来存放代码的,从而定义了一个代码段。比如,将:
这段长度为10个字节的指令,存放在 123B0H~ 123B9H的一组内存单元中,我们就可以认为,123B0H~123B9H这段内存是用来存放代码的,是一个代码段,它的段地址为 123BH,长度为10个字节。
如何使得代码段中的指令被执行呢?将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就自动地将我们定义的代码段中的指令当作指令来执行。CPU只认被CS:IP指向的内存单元中的内容为指令。所以,要让CPU执行我们放在代码段中的指令,必须要将CS:IP指向所定义的代码段中的第一条指令的首地址。对于上面的例子,我们将一段代码存放在123B0H~123B9H内存单元中,将其定义为代码段,如果要让这段代码得到执行,可设CS=123BHIP=0000H。
(1) 什么是 Debug?
Debug是 DOS、Windows 都提供的实模式(8086方式)程序的调试工具。使用它,可以查看 CPU 各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
(2) 我们用到的Debug功能。
用 Debug 的 R 命令查看、改变CPU寄存器的内容;
用 Debug 的 D 命令查看内存中的内容;
用 Debug 的 E 命令改写内存中的内容;
用 Debug 的 U 命令将内存中的机器指令翻译成汇编指令;
用 Debug 的 T 命令执行一条机器指令:
用 Debug 的 A 命令以汇编指令的格式在内存中写入一条机器指令。
一、R命令:查看、修改寄存器中的内容
查看:
修改:
二、D命令:查看内存中的内容
查看:
[d 段地址:偏移地址]、[d 段地址:偏移地址 结尾偏移地址]
[d](将输出上一次连续的内存中的内容)
将显示3部分内容:(1) 中间是从指定地址开始的 128 个内存单元的内容,用十六进制的格式输出,每行的输出从 16 的整数倍的地址开始,最多输出 16 个单元的内容
(2) 左边是每行的起始地址
(3) 右边是每个内存单元中的数据对应的可显示的 ASCII 码字符
三、E命令:改写内存中的内容(可以写入数据、指令,在内存中,它们实际上没有区别)
修改:
[e 起始地址 数据 数据 数据 … ]
[也可以提问的方式,e 起始地址 回车,依次进行,空格不修改,输入数据空格进行修改]
(可以写入字符:字符 ‘a’、数值2、字符 ‘c’)
四、用 E 命令向内存中写入机器码(二进制信息),用 U 命令查看内存中机器码的含义,用 T 命令执行内存中的机器码
1. 用 E 命令向内存中写入机器码(二进制信息)
2. 用 U 命令查看内存中机器码的含义
3. 将 CS:IP 指向 1000:0
4. 用 T 命令执行内存中的指令(每次执行一条机器码,执行后 IP=IP+指令长度)
五、A 命令:直接以汇编指令的形式在内存中写入机器指令
若没有给出地址,则默认的预设地址是 ds:ip
前面,我们主要从 CPU 如何执行指令的角度讲解了 8086CPU 的逻辑结构、形成物理地址的方法。
现在,我们从访问内存的角度继续学习几个寄存器。
CPU 中,用 16 位寄存器来存储一个字。高 8 位存放高位字节,低 8 位存放低位字节。在内存中存储时,由于内存单元是字节单元 (一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
内存中字的存储,如下图:
我们用 0、1 两个内存单元存放数据20000(4E20H)。0、1 两个内存单元用来存储一个字,这两个单元可以看作一个起始地址为 0 的字单元(存放一个字的内存单元,由 0、1 两个字节单元组成)。对于这个字单元来说,0 号单元是低地址单元,1 号单元是高地址单元,则字型数据 4E20H 的低位字节存放在 0 号单元中,高位字节存放在 1 号单元中。
同理,将 2、3 号单元看作一个字单元,它的起始地址为 2。在这个字单元中存放数据 18(0012H),则在 2 号单元上存放低位字节 12H,在 3 号单元中存放高位字节 00H。
我们提出字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
在以后的课程中,我们将起始地址为 N 的字单元简称为 N 地址字单元。比如一个字单元由 2、3 两个内存单元组成,则这个字单元的起始地址为 2,我们可以说这是 2 地址字单元
任何两个地址连续的内存单元,N 号单元和 N+1 号单元,可以将守卫剑阁看成两个内存单元,也可看成一个地址为 N 的字单元中的高位字节单元
CPU 要读写一个内存单元的时候,必须先给出这个内存单元的地址,在 8086CPU 中,内存地址由段地址和偏移地址组成。8086CPU 中有一个寄存器,通常用来存放要访问数据的段地址。
关于 DS 段寄存器:
- 不能用之前的类似 “mov ax,100” 这样的指令来完成,只能用一个寄存器来进行中转,即先将一个数据(1000H) 送入一个一般的寄存器(bx),再将 bx 中的内容送入 ds(ov ds,bx)
mov 的另一种方式:
- 通常: 1.将数据直接送入寄存器(mov al,10H);2.将一个寄存器中的内容送入另一个寄存器(mov al,[0])
- 另一种方式:[…]表示一个内存单元,[…]中表示内存单元的偏移地址。指令执行时,8086CPU 自动取 dsss 中的数据为内存单元的段地址。
使用 ds 将 10000H(1000:0) 中的数据读到 al 中,指令如下图:
(1) 将段地址存入到 bx 寄存器中
(2) 将 bx 寄存器中的段地址送入到 ds 段寄存器中
(3) 使用 mov al,[0] 将内存单元为 1000:0 中的字节数据送入到 al 中
同理,将 al 中的数据送入到内存单元 10000H 中:
前面我们用 mov 指令在寄存器和内存之间进行字节型数据的传送。因为 8086CPU 是16 位结构,有 16 根数据线,所以,可以一次性传送 16 位的数据,也就是说可以一次性传送一个字。只要在 mov 指令中给出 16 位的寄存器就可以进行 16 位数据的传送了。
在寄存器和内存之间进行字数据的传送时,如( mov ax,[0]、mov [0],ax,add ax,[0] ):
- 连续的两个内存单元中(字单元),如 [10000H, 10001H],其中 10000H 为低 8 位,10001H 为高 8 位
- 高 8 位数据 与 ah 进行运算,如移动时,高 8 位(10001H) 移入 ah 寄存器。
- 低 8 位数据 与 al 进行运算,如移动时,低 8 位(10000H) 移入 al 寄存器。
如下图两个案例:
指令:
分析:
指令:
分析:
sub命令: bx = bx - 1000:2(内存单元) [减法]
前面我们用到 mov、add、sub指令,它们都带有两个操作对象。
到现在,我们知道,mov 指令可以有如下几种形式:
1. 既然有 “mov 段寄器,寄存器” ,那么也应该有 “mov 寄存器,段寄存器”
2. 既然有 “mov 内存单元,寄存器” ,那么也应该有 “mov 内存单元,段寄存器”
3. “mov 段寄存器,内存单元” 也应该可行
推测:
1. 既然有 “mov 段寄器,寄存器” ,那么也应该有 “mov 寄存器,段寄存器”
2. 既然有 “mov 内存单元,寄存器” ,那么也应该有 “mov 内存单元,段寄存器”
3. “mov 段寄存器,内存单元” 也应该可行
对于 8086CPU 机,在编程时,可以根据,将一组内存单元定义为一个段。我们可以将一组长度为 N(N<=64KB) 、地址连续、起始地址为 16 的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如用 123B0H~123BH 这段内存空间来存放数据,我们就可以认为,123B0H~123B9H 这段内存是一个数据段,它的段地址为 123BH,长度为 10 个字节
如何访问数据段中的数据呢?将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作的时候,用 ds 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
在这里,我们对栈的研究角度仅限于这个角度:栈是一种具有特殊的访问方式的 存储空间。它的特殊性在于,最后进入这个空间的数据,最先出去,也就是 后进先出。
同时,栈永远只对栈顶元素进行操作(出栈、入栈)
入栈:
出栈:
从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。
如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为:LIFO(Last InFirst Out,后进先出)。
现今的 CPU 都有栈的设计,8086CPU 也不例外。8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,在基于 8086CPU 编程的时候,可以将一段内存当作栈来使用。
8086CPU 提供入栈和出栈指令,最基本的两个是 PUSH(入栈) 和 POP(出栈):
- push ax 表示将寄存器 ax 中的数据送入栈中
- pop ax 表示从栈顶取出数据送入 ax。
- 8086CPU 的入栈和出栈操作都是以字为单位进行的
下面举例说明,我们可以将 10000H~1000FH 这段内存当作栈来使用:
两个问题,
其一:CPU 如何知道 10000H~1000FH 这段空间被当作栈来使用?8086CPU 中,有两个寄存器,段寄存器 SS 和寄存器 SP,栈顶的段落地址存放在 SS 中,偏移地址存放在 SP 中。
CPU 执行栈指令所在栈的位置就是 SS:SP,因此也就把该地址指向的内存单元当作栈空间使用其二:入栈、出栈指令执行时,CPU 如何知道哪个单元是栈顶单元?
任意时刻,SS:SP 指向栈顶元素。push 指令 和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址
现在,我们可以完整地描述 push 和 pop 指令的功能了,例如 push ax:
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
栈空的状态:
push ax 指令:
任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以 SS:SP 只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。
pop指令:
(1) 将 SS:SP 指向的内存单元处的数据送出到 ax 中
(2) SP=SP+2,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
注意:出栈后,SS:SP 指向新的栈顶 1000EH,pop 操作前的栈顶元素,1000CH 处的 2266H 依然存在,但是,它已不在栈中。当再次执行 push 等入栈指令后,SS:SP 移至 1000CH,并在里面写入新的数据,它将被覆盖
我们现在知道,8086CPU用SS和SP指示栈顶的地址,并提供push和pop指令实现入栈和出栈。
但是,还有一个问题需要讨论,就是SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。可是,如何能够保证在入栈、出栈时,栈顶会超出栈空间?
执行 push 指令后,栈顶超出栈空间的情况如下:
图中,
(1) 将10010H~1001FH当作栈空间,该栈空间容量为16字节(8字),初始状态为空,SS=1000H、SP=0020H,SS:SP指向10020H;
(2) 在执行8次push ax后,向栈中压入8个字,栈满,SS:SP指向10010H;
(3) 再次执行push ax:sp=sp-2,SS:SP指向1000EH,栈顶超出了栈空间,ax中的数据送入1000EH单元处,将栈空间外的数据覆盖。
执行 pop 指令后,栈顶超出栈空间的情况如下:
图中,
(1) 将10010H~1001FH当作栈空间,该栈空间容量为16字节(8字),当前状态为满,SS=1000H、SP=0010H,SS:SP指向10010H;
(2) 在执行8次pop ax后,从栈中弹出8个字,栈空,SS:SP指向10020H;
(3) 再次执行pop ax::sp=Sp+2,SS:SP指向10022H,栈顶超出了栈空间。此后,如果再执行push指令,10020H、10021H中的数据将被覆盖。
栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
8086CPU不保证我们对栈的操作不会超界。这也就是说,8086CPU只知道栈顶何处(由SS:SP指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知当前要执行的指令在何处(由CSIP指示),而不知道要执行的指令有多少。从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。
&、我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push和pop指令的格式可以是如下形式:
- push 寄存器 (将一个寄存器中的数据入栈)
- pop 寄存器 (出栈,用一个寄存器接收出栈的数据)
当然也可以是如下形式:
- push 段寄存器 (将一个段寄存器中的数据入栈)
- pop 段寄存器 (出栈,用一个段寄存器接收出栈的数据)
push和pop也可以在内存单元和内存单元之间传送数据,我们可以:
- push 内存单元 (将一个内存字单元处的字入栈(注意:栈操作都是以字为单位)
- pop 内存单元 (出栈,用一个内存字单元接收出栈的数据)
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从 ds 中取得。
我们要十分清楚的是,push和pop指令同mov指令不同,CPU执行mov指令只需步操作,就是传送,而执行push、pop指令却需要两步操作。执行push时,CPU的两步操作是:先改变SP,后向SSSP处传送。执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP。
注意,push,pop等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为:O~FFFFH。
提供:SS、SP指示栈顶;改变 SP 后写内存的入栈指令;读内存后改变 SP 的出栈指令。这就是8086CPU提供的栈操作机制。
前面讲过(参见2.8节),对于 8086PC 机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为 N(N≤64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。比如,我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1001H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时自动地将我们定义的栈段当作栈空间来访问。如何使得如 push、pop 等栈操作指令访问我们定义的栈段呢?前面我们已经讨论过,就是要将 SS:SP 指向我们定义的栈段。
一个栈段最大可以设为多少?为什么?
思考后看分析。
分析:
这个问题显而易见,提出来只是为了提示我们将相关的知识融会起来。首先从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP,所以栈顶的变化范围是O~FFFFH,从栈空时候的SP-O,一直压栈,直到栈满时SP-O:如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。所以一个栈段的容量最大为 64KB。
关于 D 命令:
我们知道段地址是放在段寄存器中的,在 D 命令后面直接给出段地址,是 Debug 提供的一种直观的操作方式。D 命令是由 Debug 执行的,Debug 在执行 “d 1000:0” 这样的命令时,也会先将段地址 1000H 送入段寄存器中。
CPU 在访问内存单元的时候从哪里得到内存单元的段地址?从段寄存器中得到
段寄存器有4个:CS、DS、SS、ES,将段地址送入哪个段寄存器呢?
首先不能是CS,因为CS:IP必须指向Debug处理D命令的代码,也不能是SS,因为SS:SP要指向栈顶。这样只剩下了DS和ES可以选择,放在哪里呢?
我们知道,访问内存的指令如“mov ax,[0]”等一般都默认段地址在ds中,所以Debug在执行如“d段地址:偏移地址”这种D命令时,将段地址送入ds中比较方便。
D命令也提供了一种符合CPU机理的格式:“d段寄存器:偏移地址”,以段寄存器
中的数据为段地址SA,列出从SA:偏移地址开始的内存区间中的数据。以下是几个
例子。
D 命令中使用段存器:
在 E、A、U 中使用段寄存器: