这篇 B l o g Blog Blog 准备分析微机里面一个比较难理解的,又十分重要的知识点——实地址模式存储器的寻址,下面我们用尽量简单的语言阐述这个过程。我们开始吧!
文章打算从以下几个模块入手:
- 内存分段管理的思想(从逻辑层面理解 “段” )
- 实地址模式下内存的地址变换
- 段寄存器的作用
- 堆栈的概念
一、内存分段管理的思想
首先,引入分段管理的思想主要基于以下几个事实:
【事实一】我们知道,8088/8086 CPU 都是16位的,也就是说它们一共可以产生
2
16
2^{16}
216 种不同的二进制编码,也就是 64K 种二进制编码(地址编码)。
【事实二】内存其实是一块一块的,也就是一个单元一个单元的,CPU 要想访问到内存中的某一个单元,就必须通过这个单元的地址编码
OK,现在我们需要 8088/8086 CPU 能够管理到,或者说能够访问到 1M 的内存。是什么概念?1M 的内存,也就是它有 2 20 2^{20} 220 种不同的小单元,即如果需要访问到 1M 内存中任意一个单元,那么总共需要 CPU 能够产生 2 20 2^{20} 220 种不同的二进制编码。
那么,问题来了——现在事实摆在那:8086/8088CPU 一共就只能产生 2 16 2^{16} 216 种编码,这显然是不够的,那么我们应该怎么办呢??
我们举一个例子就好理解了:假设现在有一栋大楼,一共有100个房间(假设你现在不知道它是可以分楼层的hhh),那么最原始地,对大楼所有房间的编号不就是按顺序一个一个编码嘛:
这样做的话,你需要 100 个不同的号码才能够全部编码完毕。
可现在,你只有1~10 这10个号码,每个号码可以有若干的,但是范围就是从 1~10。
因此,我们可以先分楼层,再对于每一层的房间进行编号,和我们现在一样,我们看看结果:
这样的话,我们就可以这样对不同的房间编码:比如第二层第五间房就是 2-5,第九层第八间房就是 9-8、、、这样一来,我们只需要十种不同的号码就可以对100 间房子进行编码了!
那么,对于内存也是一样,我们先来看看一个 1M 内存:
也是一样,我们把 1M 内存划分成了 16 层,每一层有 64K 个房间。这样一来,我们就可以通过 16位的 CPU 去访问 1M 的内存了!
但是,对于这个楼层,一共是16层(在每一层都是固定不动的前提下),这样就是需要 4 位的二进制编码,也就是我们为了存楼层地址的编码,还需要设计 4 位的寄存器。这显然与我 16 位的 CPU 是不匹配的,这是第一个问题。第二个问题:我们想,假设我们第一条指令用不完第一层,但是第二条指令却不能够再使用第一层了,而是只能去使用第二层。这样就会导致指令在内存中的存储变得分散,断断续续,也造成了内存的浪费。
因此,我们引入了 “逻辑层” 的概念。也就是说:这个层,并非像我们真实盖楼房那样,改好的第一层、第二层、第 n 层就永远不会变了。内存里面的层,它是会动的,比如某一层,它可以宽一点,也可以窄一点;可以在某一时刻属于另外一层,也可以在某一时刻同时属于多个层
因此,如果按照上面右图这种方式,那么楼层地址我们也可以用 16 位二进制代码编号。因此1M 内存中每一个单元的地址可以这样表示:
段基地址相当于楼层的编码,偏移地址相当于某一层的一个房间距该层第一个房间的偏移。还是以楼房编码为例:3-4 表示第三层,4表示这个房间是该层从第一个房间开始数第4个房间。
另外,我们定义:每一层第一个内存单元的地址叫做:段首地址
二、实地址下内存的地址变换
现在问题又来了:我们知道段基地址和偏移地址都是 16 位的了,这没话说。但是我们想访问的是 1M 内存,也就是说,一共需要 20位二进制编码就够了,现在是 32 位。如果做这样的一个变换呢?
我们知道:段基地址就是一层的第一件房子,它的偏移地址就是0!也就是说它的偏移地址是 ( 0000 ) H (0000)_{H} (0000)H,可以如果对于 0 来说,一位二进制是0,二为也是0,四位也是0,都是一样的。那么我们能不能把段首地址的偏移用 4 位二进制代码来表示呢?这样一来整个单元地址不就是 20 位二进制编码表示的了吗?
注意:我们这里只是将段首地址的偏移地址用 4 位二进制表示,对于不是段首的偏移地址,我们仍然是用 16 位二进制来表示的!
这就是实地址下内存的地址变化!
举个栗子:比如我们知道一个段首的段基地址是 102 A H 102AH 102AH,那么它的逻辑地址就是: 102 A 0 H 102A0H 102A0H(即在段基地址最低位补上一个0)
举第二个栗子:假如某一个内存单元的段基地址是
101
A
H
101AH
101AH,偏移地址是
0009
H
0009H
0009H,问这个单元的地址是什么?
解:首先,我们要知道这个段的段首地址:因为段基地址是
101
A
H
101AH
101AH,段首的偏移量我们可以用四位二进制 0000,即:
0
H
0H
0H 来表示,所以段首地址就是:
101
A
0
H
101A0H
101A0H,再加上这个 4 位十六进制的偏移地址
0009
H
0009H
0009H,故最后这个单元的地址就是:
101
A
9
H
101A9H
101A9H
因此,我们完成了从 32 位的地址转换到了 20 位的地址,是不是很神奇哈哈。
那么我们得到一个结论,内存单元的物理地址的表示公式:
物理地址 = 段基地址 x 16 + 偏移地址
三、段寄存器的应用
好啦,现在我们已经有了 “逻辑段” 的概念了,任何一个内存单元的地址都可以由段基地址和偏移地址构成。还记得我们在之前的 B l o g Blog Blog 中提到的 CPU 结构吗?
我们看到上图中有一个叫做 “段寄存器” 的模块,这个模块就是用于存放相应的段基地址的
段寄存器组分为了 4 个效小的段寄存器:
- 代码段寄存器(CS):用于存放指令代码的段基地址
- 数据段寄存器(DS):用于存放操作数据的段基地址
- 附加段寄存器(ES)
- 堆栈段寄存器(SS):用于存放暂时不需要但是要保存下来的数据的段基地址
CS:存储的是将要执行的代指令的段基地址、而 IP 里面存放的就是这个指令的偏移地址。因此,CS: IP 就构成了 CPU 将要执行的指令的逻辑地址。
我们用下面这张图能够更好的观察 CPU 是如何通过 CS:IP 找到并且执行需要的代码指令的:
首先我们看看:
- CPU 先从 CS和 IP 里面分别读取到一会儿将要取出的指令的段基地址和偏移地址。
- 然后这两个地址输入进 地址加法器(完成的是合成总的逻辑地址:物理地址 = 段基地址 x 16 + 偏移地址)
- 得到了需要访问的单元地址之后,读取这个单元的内容,放入指令缓存器,准备执行
- IP = IP + 所读取指令的长度,从而指向下一条指令;
- 重复上述的步骤