第2章内容,主要从CPU如何执行指令的角度讲解了8086CPU的逻辑结构、形成物理地址的方法、相关的寄存器以及一些指令。

这一章,从访问内存的角度继续学习几个寄存器。

一、内存中字的存储

CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。比如,从0地址开始存放20000,这种情况如下所示:

进制转换

§ 第3章 寄存器(内存访问) - 图1

此时的4E20 H一共有16位数据

§ 第3章 寄存器(内存访问) - 图2

现在要将这16位数据放到内存单元中,一个内存单元存放8位,即2个字节,放进去后如下图所示

§ 第3章 寄存器(内存访问) - 图3

现在,再提出一个概念,字单元:一个存放字型数据(16位)的内存单元,有两个地址连续的内存单元组成。

§ 第3章 寄存器(内存访问) - 图4

在以后的课程中,将起始地址为N的字单元简称为N地址字单元。比如,上面的例子中,字单元的起始地址为0,可以说这是0地址字单元。

§ 第3章 寄存器(内存访问) - 图5

二、DS和[address]

CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC机中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址

比如,我们要读取10000 H单元的内容,可以用如下的程序段进行。

  1. ; 下面的3条指令将10000H(1000:0)中的数据读到al
  2. mov bx,1000H
  3. mov ds,bx
  4. mov al,[0]

对上面的指令讲解,首先是mov指令的用法

§ 第3章 寄存器(内存访问) - 图6

第3种用法中,[0 ]中存放的是内存单元的偏地址0

但是,我们知道,只有一个偏移地址是不能定位一个内存单元的,内存单元的段地址是多少?指令执行时,8086CPU自动取DS中的数据为内存单元的段地址

另外,上面第1条语句和第2条语句

  1. mov bx,1000H
  2. mov ds,bx

可不可以合成如下的一句话

  1. mov ds,1000H

答案是:不能!因为不可以给段寄存器直接赋值,只能先赋值给一个寄存器吗,然后利用这个寄存器给段寄存器赋值

§ 第3章 寄存器(内存访问) - 图7

上面是将1000:0的物理地址送到al中,如果将al中的数据从到1000:0中,应该怎么做

  1. ; 下面的3条指令将al中的数据送到10000H(1000:0)
  2. mov bx,1000H
  3. mov ds,bx
  4. mov [0],al

三、字的传送

前面使用mov指令在寄存器和内存之间进行字节型数据的传送。因为8086CPU是16位结构,有16根数据线,所以可以一次性传送16位数据(即1个字,2个字节)。只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。

  1. mov bx,1000H
  2. mov ds,bx
  3. mov ax,[0] ; 1000:0处的字型数据送入ax
  4. mov [0],cx ; cx中的16位数据送到1000:0

例子:《汇编语言》第3版P62

§ 第3章 寄存器(内存访问) - 图8

四、mov、add、sub指令

前面用到了movaddsub指令,它们都带有2个操作对象,对于mov指令,它有如下几种形式

§ 第3章 寄存器(内存访问) - 图9

对于addsub指令,这两个应该差不多,下面给出几个指令,至于其指令能否像mov指令那样使用,需要验证一下

§ 第3章 寄存器(内存访问) - 图10

五、数据段

对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。

可以将一组长度§ 第3章 寄存器(内存访问) - 图11#card=math&code=N%28N%5Cleqslant%2064%5Ctext%7BKB%7D%29&id=Bnmca)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。

§ 第3章 寄存器(内存访问) - 图12

比如用123B0H~123B9H这段内存空间存放数据,就可以认为,123B0H - 123B9H这段内存是一个数据段,它的段地址为123BH,长度为10个字节。

§ 第3章 寄存器(内存访问) - 图13

PS:这段和原来的代码段的定义是一样的,所以我就可以使用原来的图了§ 第3章 寄存器(内存访问) - 图14

现在将123B0H - 123B9H的内存单元定义为数据段,现在要累加这个数据段中前3个单元中的数据,代码如下

  1. mov ax,123BH
  2. mov ds,ax ; 123BH送入DS中,作为数据段的首地址
  3. mov al,0 ; AL存放累加结果
  4. add al,[0] ; 123B:0中的数据加到AL
  5. add al,[1] ; 123B:1中的数据加到AL
  6. add al,[2] ; 123B:2中的数据加到AL

§ 第3章 寄存器(内存访问) - 图15

六、栈

这里对栈的理解就很浅显了:最先进的最后出

§ 第3章 寄存器(内存访问) - 图16

七、CPU提供的栈机制

现今的CPU都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着在8086CPU编程的时候,可以将一段内存当作栈来使用。

8086提供入栈和出栈指令

  • PUSH:入栈
  • POP:出栈
  1. ; 注意:8086CPU的入栈和出栈操作都是以字为单位进行的
  2. push ax ; 将寄存器ax中的数据送入栈中
  3. pop ax ; 从栈顶取出数据送入ax

下面举例说明,下面可以将10000H-1000FH这段内存当作栈来使用,使用下面这个动图来描述一段指令的执行过程

指令代码如下

  1. mov ax,1023H
  2. push ax
  3. mov bx,2266H
  4. push bx
  5. mov cx,1122H
  6. pushcx
  7. pop ax
  8. pop bx
  9. pop cx

§ 第3章 寄存器(内存访问) - 图17

上面可以看出pushpop指令的执行过程,但是其中有两个问题

  1. 我们将10000H-1000FH这段内存当作栈来使用,一个重要的问题是,CPU怎么知道这段内存是栈
  2. 我们使用push ax指令的时候,要将寄存器内容放在当前栈顶的上方;使用pop ax指令的时候,要从栈顶取出数据,送入寄存器。然而,poppush指令在执行的时候必须知道哪个单元是栈顶单元,怎么知道呢?

之前学习内存地址的时候,使用CS和IP来告诉CPU其物理地址,这里同样有对应的寄存器SS和SP来告诉栈顶的地址

§ 第3章 寄存器(内存访问) - 图18

现在,可以完整描述pushpop指令的功能了,例如push ax的执行,有下面两步完成

  1. SP=SP-2SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元作为新的栈顶
  2. ax中的内容送入SS:SP指令的内存单元出,ss:sp此时指向新栈顶。

下图描述了8086CPU对push指令的执行过程

§ 第3章 寄存器(内存访问) - 图19

从图中,可以看出8086CPU入栈时,栈顶从高地址向低地址方向增长。

接下来,描述pop指令的功能,例如pop ax有以下两步完成

  1. SS:SP指向的内存单元处的的数据送入ax
  2. SP=SP+2SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

§ 第3章 寄存器(内存访问) - 图20

注意:上图中,出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的2266H依然存在。但是,它以不在栈中。当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写新的数据,它将被覆盖。

八、栈顶超界的问题

通过上述的过程,我们现在知道了8086CPU用SS和SP指示栈顶的地址,并提供pushpop指令实现入栈和出栈。

但是,还有另外一个问题,SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。可是,如何保证入栈、出栈时,栈顶不会超出栈空间

栈顶超界是很危险的,既然我们将一段空间安排为栈,那么在栈空间之外的空间里可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们程序之中的,也可能是别的程序中的(毕竟一个计算机系统并不是只有我们自己的程序在运行)。但是,由于我们在入栈出栈时不小心,将这些数据、代码意外改写会引起一连串的错误。

这个时候,我们希望CPU中有记录栈顶上限和栈底的寄存器,我们通过这些寄存器来指定栈空间的范围,从而保证不会超界。

§ 第3章 寄存器(内存访问) - 图21

但是,8086CPU不保证我们对栈的操作不会超界。也就是说,8086CPU只知道栈顶在何处(由SS : SP指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知道当前要执行的命令在何处(由CS : IP指示),而不知道要执行的指令有多少条。

所以,在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致超界;执行出栈操作的时候也要注意,防止栈空的时候继续出栈而导致超界。

§ 第3章 寄存器(内存访问) - 图22

九、push、pop指令

pushpop指令是可以在寄存器和内存之间(栈空间是内存空间的一部分,是一段可以以一种特殊的方式进行访问的内存空间)传递的数据。当然,pushpop指令也可在内存单元之间传递数据

§ 第3章 寄存器(内存访问) - 图23

比如,下面这个例子:

指令执行时,CPU要知道内存单元的地址,可以在pushpop指令中只给出内存单元的偏移地址,段地址在指令执行时从ds中取得。

  1. mov ax,1000H
  2. mov ds,ax ; 内存单元的段地址要放在ds
  3. push [0] ; 1000:0处的字压入栈中
  4. pop [2] ; 出栈,出栈的数据送入1000:2

9.1 练习题

问题3.7

§ 第3章 寄存器(内存访问) - 图24

问题3.8

§ 第3章 寄存器(内存访问) - 图25

答案以及其分析:

§ 第3章 寄存器(内存访问) - 图26

问题3.9

§ 第3章 寄存器(内存访问) - 图27

答案以及分析

§ 第3章 寄存器(内存访问) - 图28

问题3.10

§ 第3章 寄存器(内存访问) - 图29

答案以及分析

§ 第3章 寄存器(内存访问) - 图30

9.2 综述

§ 第3章 寄存器(内存访问) - 图31

十、栈段

之前知道了数据段和代码段的定义,其实栈段也是一样的 § 第3章 寄存器(内存访问) - 图32

对于8086PC机,在编程时,根据需要将一组内存单元定义为一个段。将长度为§ 第3章 寄存器(内存访问) - 图33#card=math&code=N%28N%5Cleqslant%20%5Ctext%7B64KB%7D%29&id=PC7SF)一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来使用,从而定义了一个栈段。

§ 第3章 寄存器(内存访问) - 图34

将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会这种安排,就在执行pushpip等栈操作指令时自动地将我们定义的栈段当作栈空间来访问?

只能通过SS:SP指向我们定义的栈段

10.1 问题1

如果将10000H-1FFFFH这段空间当作栈段,初始状态栈是空的,此时SS=1000H,SP= 0 ?

当整个栈为空的时候,其指向的是整个栈的下一个内存单元:20000H,这个时候SS:2000H, SP:0

§ 第3章 寄存器(内存访问) - 图35

10.2 问题2

一个栈段最大可以设为多少?为什么?SP的寻址大小

通过定义我们就可以到了§ 第3章 寄存器(内存访问) - 图36,既然一个栈段大小为§ 第3章 寄存器(内存访问) - 图37,这个大小是由偏移地址来确定的,所以对应的SP的大小为§ 第3章 寄存器(内存访问) - 图38

检测

检测3.1

内存中的情况如图3.6所示:

§ 第3章 寄存器(内存访问) - 图39

各寄存器的初始值:CS=2000HIP=0DS=1000HAX=0BX=0

  1. 写出CPU执行的指令序列
    ```assembly ; 因为CS=2000H IP=0,所以执行2000:0处的代码 mov ax,6622H

; 跳转到下一个命令 jmp 0ff0:0100 ; 修改为CS:0ff0 IP:0100,即地址10000H

mov ax,2000H

mov ds,ax

mov ax,[0008] ; 将2000:8处的数据给ax,即AX=C389H,注意这里当作数据处理 mov ax,[0002] ; 同上,这里只是当作数据处理,如果当作代码处理的话,只能用jmp命令

  1. 2. 写出CPU执行每条指令后,CSIP和相关寄存器中的数值
  2. 3. 再次体会:数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?<br />其实,通过上面的例子我们也可以知道,要想确定哪些是程序,只能通过`CS``IP`的大小来确定,**如果**`**CS**`**和**`**IP**`**大小有已经确定地指向了某一区域,那么这块区域就是代码**,其余的就当作数据处理。
  3. <a name="cda56704"></a>
  4. ## 检测3.2
  5. 下面的3条指令执行后,CPU几次修改IP?都是在什么时候,最后IP中的值是多少?
  6. ```assembly
  7. mov ax,bx
  8. sub ax,ax
  9. jmp ax ; 只有这次修改IP,IP的值为0