用机器指令和汇编指令编程(二)
物理地址
CPU
访问内存空间时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,将这个唯一的地址称为物理地址。
CPU
通过地址总线送入存储器的,必须是一个内存单元的物理地址。而在CPU
向地址总线上发出物理地址之前,必须要在内部形成这个物理地址。不同的CPU
有不同的形成物理地址的方式,此处依旧讨论8086CPU
如何在内部形成内存单元的物理地址。
16位结构的CPU
8086CPU
是16
位机,也可以说8086CPU
是16
位结构的CPU
。那么什么是16
位结构的CPU
?
- 字长为
16
位。 - 运算器一次最多可以处理
16
位的数据。 - 寄存器的最大宽度为
16
位。 - 寄存器和运算器之间的通路为
16
位。
8086CPU生成物理地址
8086CPU
有20
位的地址总线,可以传送20
位地址,达到1MB
的寻址能力。8086CPU
又是16
位结构,在内部一次性处理、传输、暂时存储的地址为16
位,如果将地址简单的发出,那么只能送出16
位的地址,表现出的寻址能力只有64KB
。
8086CPU
采用一种在内部用两个16
位地址合成的方法来形成一个20
位的物理地址。
如上图所示,当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
如何知道当前要执行的指令所在的位置,可以通过CS
、IP
两个寄存器中存放着的当前指令的段地址和偏移地址得知。因此,也应该有相应的寄存器来存放栈顶的地址,8086CPU
中,有两个寄存器,段寄存器SS
和寄存器SP
,栈顶的段地址存放在SS
中,偏移地址存放在SP
中。任意时刻,SS:SP
指向栈顶地址。push
和pop
指令执行时,CPU
从SS
和SP
中得到栈顶的地址。
下图描述了8086CPU
对push ax
指令的执行过程。
SP=SP-2
,SS:SP
指向当前栈顶前面的单元,当前栈顶前面的单元成为新的栈顶。- 将
ax
中的内容送入SS:SP
指向的内存单元处,SS:SP
此时指向新栈顶。
从图中可以看出,8086CPU
中,入栈时,栈顶从高地址向低地址方向增长。
pop
指令的执行过程正好好push
指令相反,例如pop ax
,如下图所示。
- 将
SS:SP
指向的内存单元处的数据送入ax
中。 SP=SP+2
,SS:SP
指向当前栈顶下面的单元,当前栈顶下面的单元成为新的栈顶。
总结
可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。
- 对于代码段,将它的段地址放在
CS
中,将段中第一条指令的首地址存储在IP
中,这样CPU
就会执行代码段中的指令。 - 对于数据段,将它的段地址放在
DS
中,用mov
、add
、sub
等访问内存单元的指令时,CPU
就将定义的数据段中的内容当做数据来访问。 - 对于栈段,将它的段地址放在
SS
中,将栈顶单元的偏移地址放在SP
中,这样CPU
在需呀进行栈操作的时候,就将栈段当做栈空间来使用。
可见内存中并不区分什么是数据,什么是指令,完全取决于我们自己的安排。CPU
把内存中的某段当做指令,是因为CS:IP
指向了那里;CPU
把某段内存当作栈来使用,是因为SS:SP
指向了那里。