物理地址

CPU访问内存空间时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,将这个唯一的地址称为物理地址。

CPU通过地址总线送入存储器的,必须是一个内存单元的物理地址。而在CPU向地址总线上发出物理地址之前,必须要在内部形成这个物理地址。不同的CPU有不同的形成物理地址的方式,此处依旧讨论8086CPU如何在内部形成内存单元的物理地址。

16位结构的CPU

8086CPU16位机,也可以说8086CPU16位结构的CPU。那么什么是16位结构的CPU

  • 字长为16位。
  • 运算器一次最多可以处理16位的数据。
  • 寄存器的最大宽度为16位。
  • 寄存器和运算器之间的通路为16位。

8086CPU生成物理地址

8086CPU20位的地址总线,可以传送20位地址,达到1MB的寻址能力。8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位,如果将地址简单的发出,那么只能送出16位的地址,表现出的寻址能力只有64KB

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。

Alt text

如上图所示,当8086CPU要读写内存时需要以下几个步骤。

  • CPU中相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址。
  • 段地址和偏移地址通过内部总线送入一个被称为地址加法器的部件。
  • 地址加法器将两个16位地址合成一个20位的物理地址。
  • 地址加法器通过内部总线将20位物理地址送入输入输出控制电路。
  • 输入输出控制电路将20位物理地址送上地址总线。
  • 20位物理地址被地址总线传送到存储器。

地址加法器采用物理地址 = 段地址 × 16 + 偏移地址的方法用段地址和偏移地址合成物理地址。对于一个用16进制表示的地址,×16意味着左移一位。

段的概念

注意“段地址”这个名称中包含了“段”的概念,但是实际上内存并没有分段,段的划分来自于CPU,由于8086CPU采用基础地址(段地址 × 16) + 偏移地址 = 物理地址的方式给出内存的物理地址,使得CPU可以用分段的方式来管理内存。

在编程时可以根据需要将若干地址连续的内存单元看作是一个段,用段地址×16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。有两点需要注意:段地址×16必然是16的倍数,所以一个段的起始地址也一定是16的倍数;偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的最大长度为64KB

代码段

前面已经提到,在编程时可以根据需要将一组内存单元定义为一个段,可以将长度为N(N <= 64KB)的一组代码存储在一组地址连续的内存单元中,可以认为这段内存是用来存放代码的,从而定义了一个代码段。

mov ax,0000   (B8 00 00)
add ax,0123H  (05 23 01)
mov bx,ax     (8B D8)
jmp bx        (FF E3)

如上这段代码为10个字节的指令,假设存放在123B0H ~ 123B9H的一组内存单元中,可以认为123B0H ~ 123B9H这段内存就是用来存放代码的,是一个代码段,它的段地址为123BH,长度为10个字节。

把一段内存作为代码段只是在编程上的一种安排,CPU并不会自动将代码段中的指令当做指令来执行。CPU只认被CS:IP指向的内存单元中的内容为指令。所以,要让CPU执行代码段中的指令,必须要将CS:IP指向所定义的代码段中的第一条指令的首地址。对于上面的例子,也就是让CS:IP指向123BH:0000H

数据段

类似的,假设定义123B0H ~ 123B9H这段内存用来存放数据,可以认为123B0H ~ 123B9H这段内存是一个数据段,它的段地址为123BH,长度为10个字节。

8086CPU中的DS寄存器通常用来存放要访问的数据的段地址,再根据相关的指令访问数据段的具体单元。

mov ax,123BH
mov ds,ax
mov al,0
add al,[0]
add al,[1]
add al,[2]

对于以上这段代码,具体执行流程如下。

  • DS寄存器不支持直接传入数据,因此将AX寄存器作为中转。将123BH送入DS,作为数据段的段地址。
  • 清零AL寄存器,AL寄存器用于存放累加结果。
  • 将数据段第一个单元(偏移地址为0,字节为单位)中的数值加入到AL中。
  • 将数据段第二个单元(偏移地址为1,字节为单位)中的数值加入到AL中。
  • 将数据段第三个单元(偏移地址为2,字节为单位)中的数值加入到AL中。

注意:[0][1][2]都是以字节为单位编址的,一个字节为一个单元,8086CPU中一个字包含两个单元。在内存和寄存器之间传送数据时,如mov ax,[0],因为此时接收数据的寄存器为16位的AX寄存器,CPU会读取[0][1]两个单元的数据存放至AX寄存器,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。

栈段

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

8086CPU提供入栈和出栈指令,最基本的两个是push(入栈)和pop(出栈)。8086CPU的入栈和出栈操作都是以字为单位进行的。字型数据用两个内存单元存放,高地址单元存放高8位,低地址单元内存放低8位。

一个问题:CPU如何知道哪个单元是栈顶单元?首先考虑CPU如何知道当前要执行的指令所在的位置,可以通过CSIP两个寄存器中存放着的当前指令的段地址和偏移地址得知。因此,也应该有相应的寄存器来存放栈顶的地址,8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶地址。pushpop指令执行时,CPUSSSP中得到栈顶的地址。

下图描述了8086CPUpush ax指令的执行过程。

  • SP=SP-2SS:SP指向当前栈顶前面的单元,当前栈顶前面的单元成为新的栈顶。
  • ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。

Alt text

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

pop指令的执行过程正好好push指令相反,例如pop ax,如下图所示。

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

Alt text

总结

可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。

  • 对于代码段,将它的段地址放在CS中,将段中第一条指令的首地址存储在IP中,这样CPU就会执行代码段中的指令。
  • 对于数据段,将它的段地址放在DS中,用movaddsub等访问内存单元的指令时,CPU就将定义的数据段中的内容当做数据来访问。
  • 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需呀进行栈操作的时候,就将栈段当做栈空间来使用。

可见内存中并不区分什么是数据,什么是指令,完全取决于我们自己的安排。CPU把内存中的某段当做指令,是因为CS:IP指向了那里;CPU把某段内存当作栈来使用,是因为SS:SP指向了那里。

参考