购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.5 古老的Intel 8086 处理器

任何时候,一旦提到Intel 公司的处理器,就不能不说8086。8086 是Intel 公司第一款16 位处理器,诞生于1978 年,所以说它很古老。

但是,在Intel 公司的所有处理器中,它占有很重要的地位,是整个Intel 32 位架构处理器(IA-32)的开山鼻祖。首先,最重要的一点是,它是一款非常成功的产品,设计先进,功能很强,卖得很好。

其次,8086 的成功使得市场上出现了大量针对它开发的软件产品。这样,当Intel 公司要设计新的处理器时,它不得不考虑到兼容性的问题。要使得老的软件也能在新的处理器上很好地运行,必须要具备指令集和工作模式上的兼容性和一致性。Intel 公司很清楚,如果新处理器和老处理器不兼容,那么,新处理器越多,它扔掉的拥趸也就越多,要不了多久,这公司就不用再开了。

所以,当我们讲述处理器的时候,必须要从8086 开始;而且,要学习汇编语言,针对8086的汇编技术也是必不可少的。

2.5.1 8086 的通用寄存器

8086 处理器内部有8 个16 位的通用寄存器,分别被命名为AX、BX、CX、DX、SI、DI、BP、SP。“通用”的意思是,它们之中的大部分都可以根据需要用于多种目的。

如图2-7 所示,因为这8 个寄存器都是16 位的,所以通常用于进行16 位的操作。比如,可以在这8 个寄存器之间互相传送数据,它们之间也可以进行算术逻辑运算;也可以在它们和内存单元之间进行16 位的数据传送或者算术逻辑运算。

同时,如图2-7 所示,这8 个寄存器中的前4 个,即AX、BX、CX 和DX,又各自可以拆分成两个8 位的寄存器来使用,总共可以提供8 个8 位的寄存器AH、AL、BH、BL、CH、CL、DH 和DL。这样一来,当需要在寄存器和寄存器之间,或者寄存器和内存单元之间进行8 位的数据传送或者算术逻辑运算时,使用它们就很方便。

图2-7 8086 的通用寄存器

将一个16 位的寄存器当成两个8 位的寄存器来用时,对其中一个8 位寄存器的操作不会影响到另一个8 位寄存器。举个例子来说,当你操作寄存器AL 时,不会影响到AH 中的内容。

2.5.2 程序的重定位难题

我们知道,处理器是自动化的器件,在给出了起始地址之后,它将从这个地址开始,自动地取出每条指令并加以执行。只要每条指令都正确无误,它就能准确地知道下一条指令的地址。这就意味着,完成某个工作的所有指令,必须集中在一起,处于内存的某个位置,形成一个段,叫做代码段。事情是明摆着的,要是指令并没有一条挨着一条存放,中间夹杂了其他非指令的数据,处理器将因为不能识别而出错。

为了做某件事而编写的指令,它们一起形成了我们平时所说的程序。程序总要操作大量的数据,这些数据也应该集中在一起,位于内存中的某个地方,形成一个段,叫做数据段。

注意,我们并没有改变内存的物理性质,并不是真的把它分成几块。段的划分是逻辑上的,从本质上来说,是如何看待和组织内存中的数据。

段在内存中的位置并不重要,因为处理器是可控的,我们可以让它从内存的任何位置开始取指令并加以执行。这里有一个例子,如图2-8 所示,我们有一大堆数字,现在想把它们加起来求出一个总和。

图2-8 程序的代码段和数据段示例

假定我们有16 个数要相加,这些数都是16 位的二进制数,分别是0005H、00A0H、00FFH、…。为了让处理器把它们加起来,我们应该先在内存中定义一个数据段,将这些数字写进去。数据段可以起始于内存中的任何位置,既然如此,我们将它定在0100H 处。这样一来,第一个要加的数位于地址0100H,第二个要加的数位于地址0102H,最后一个数的地址是011EH。

一旦定义了数据段,我们就知道了每个数的内存地址。然后,紧挨着数据段,我们从内存地址0120H 处定义代码段。严格地说,数据段和代码段是不需要连续的,但这里把它们挨在一起更自然一些。为了区别数据段和代码段,我们使用了不同的底色。

代码段是从内存地址0120H 处开始的,第一条指令是A1 00 01,其功能是将内存单元0100H里的字传送到AX 寄存器。指令执行后,AX 的内容为0005H。

第二条指令是03 06 02 01,功能是将AX 中的内容和内存单元0102H 里的字相加,结果在AX 中。由于AX 的内容为0005H,而内存地址0102H 里的数是00A0H,这条指令执行后,AX 的内容为00A5H。

第三条指令是03 06 04 01,功能是将AX 中的内容和内存单元0104H 里的字相加,结果在AX 中。此时,由于AX 里的内容是00A5H,内存地址0104H 里的数是00FFH,本指令执行后,AX 的内容为01A4H。

后面的指令没有列出,但和前2 条指令相似,依次用AX 的内容和下一个内存单元里的字相加,一直到最后,在AX 中得到总的累加和。在这个例子中,我们没有考虑AX 寄存器容纳不下结果的情况。当累加的总和超出了AX 所能表示的数的范围(最大为FFFFH,即十进制的65535)时,就会产生进位,但这个进位被丢弃。

在内存中定义了数据段和代码段之后,我们就可以命令处理器从内存地址0120H 处开始执行。当所有的指令执行完后,就能在AX 寄存器中得到最后的结果。

看起来没有什么问题,一切都很完美,不是吗?那本节标题中所说的难题又从何而来呢?

这里确实有一个难题。

在前面的例子中,所有在执行时需要访问内存单元的指令,使用的都是真实的内存地址。比如A1 00 01,这条指令的意思是从地址为0100H 的内存单元里取出一个字,并传送到寄存器AX。在这里,0100H 是一个真实的内存地址,又称物理地址。

整个程序(包括代码段和数据段)在内存中的位置,是由我们自己定的。我们把数据段定在0100H,把代码段定在0120H。

问题是,大多数时候,整个程序(包括代码段和数据段)在内存中的位置并不是我们能够决定的。请想一想你平时是怎么使用计算机的,你所用的程序,包括那些用来调整计算机性能的工具、小游戏、音乐和视频播放器等,都是从网上下载的,位于你的硬盘、U 盘或光盘中。即使有些程序是你自己编写的,那又如何?当你双击它们的图标,使它们在Windows 里启动之前,内存已经被塞了很多东西,就算你是刚刚打开计算机,Windows 自己已经占用了很多内存空间,不然的话,你怎么可能在它上面操作呢?

在这种情况下,你所运行的程序,在内存中被加载的位置完全是随机的,哪里有空闲的地方,它就会被加载到哪里,并从那里开始被处理器执行。所以,前面那段程序不可能恰好如你所愿,被加载到内存地址0100H,它完全可能被加载到另一个不同的位置,比如1000H。但是,同样是那个程序,一旦它在内存中的位置发生了改变,灾难就出现了。

图2-9 在指令中使用绝对内存地址的程序是不可重定位的

如图2-9 所示,因为程序现在是从内存地址1000H 处被加载的,所以,数据段的起始地址为1000H。这就是说,第一个要加的数,其地址为1000H,第二个则为1002H,其他以此类推。代码段依然紧挨着数据段之后,起始地址相应地是1020H。

只要所有的指令都是连续存放的,代码段位于内存中的什么地方都可以正常执行。所以,处理器可以按你的要求,从内存地址1020H 处连续执行,但结果完全不是你想要的。

请看第一条指令A1 00 01,它的意思是从内存地址0100 处取得一个字,将其传送到寄存器AX。但是,由于程序刚刚改变了位置,它要取的那个数,现在实际上位于1000H,它取的是别人地盘里的数!

这能怪谁呢?发生这样的事情,是因为我们在指令中使用了绝对内存地址(物理地址),这样的程序是无法重定位的。为了让你写的程序在卖给别人之后,可以在内存中的任何地方正确执行,就只能在编写程序的时候使用相对地址或者逻辑地址了,而不能使用真实的物理地址。当程序加载时,这些相对地址还要根据程序实际被加载的位置重新计算。

在任何时候,程序的重定位都是非常棘手的事情。当然,也有好几种解决的办法。在8086 处理器上,这个问题特别容易解决,因为该处理器在访问内存时使用了分段机制,我们可以借助该机制。

2.5.3 内存分段机制

如图2-10 所示,整个内存空间就像长长的纸条,在内存中分段,就像从长纸条中裁下一小段来。根据需要,段可以开始于内存中的任何位置,比如图中的内存地址A532H 处。

在这个例子中,分段开始于地址为A532H 的内存单元处,这个起始地址就是段地址。

这个分段包含了6 个存储单元。在分段之前,它们在整个内存空间里的物理地址分别是A532H、A533H、A534H、A535H、A536H、A537H。

但是,在分段之后,它们的地址可以只相对于自己所在的段。这样,它们相对于段开始处的距离分别为0、1、2、3、4、5,这叫做偏移地址。

于是,当采用分段策略之后,一个内存单元的地址实际上就可以用“段:偏移”或者 “段地址:偏移地址”来表示,这就是通常所说的逻辑地址。比如,在图2-10 中,段内第1 个存储单元的地址为A532H:0000H,第3 个存储单元的地址为A532H:0002H,而本段最后一个存储单元的地址则是A532H:0005H。

图2-10 段地址和偏移地址示意图

为了在硬件一级提供对“段地址:偏移地址”内存访问模式的支持,处理器至少要提供两个段寄存器,分别是代码段寄存器(Code Segment,CS)和数据段寄存器(Data Segment,DS)。

对CS 内容的改变将导致处理器从新的代码段开始执行。同样,在开始访问内存中的数据之前,也必须首先设置好DS 寄存器,使之指向数据段。

除此之外,最重要的是,当处理器访问内存时,它把指令中指定的内存地址看成是段内的偏移地址,而不是物理地址。这样,一旦处理器遇到一条访问内存的指令,它将把DS 中的数据段起始地址和指令中提供的段内偏移相加,来得到访问内存所需要的物理地址。

如图2-11 所示,代码段的段地址为1020H,数据段的段地址为1000H。在代码段中有一条指令A1 02 00,它的功能是将地址0002H 处的一个字传送到寄存器AX。在这里,处理器将0002H 看成是段内的偏移地址,段地址在DS 中,应该在执行这条指令之前就已经用别的指令传送到DS 中了。

当执行指令A1 02 00 时,处理器将把DS 中的内容和指令中指定的偏移地址0002H 相加,得到1002H。这是一个物理地址,处理器用它来访问内存,就可以得到所需要的数00A0H。

如果一下次执行这个程序时,代码段和数据段在内存中的位置发生了变化,只要把它们的段地址分别传送到CS 和DS,它也能够正确执行。

图2-11 从逻辑地址到物理地址的转换过程

2.5.4 8086 的内存分段机制

前面讲了如何从逻辑地址转换到物理地址,以使得程序的运行和它在内存中的位置无关。这种策略在很多处理器中得到了支持,包括8086 处理器。但是,由于8086 自身的局限性,它的做法还要复杂一些。

如图2-12 所示,8086 内部有8 个16 位的通用寄存器,分别是AX、BX、CX、DX、SI、DI、BP、SP。其中,前四个寄存器中的每一个,都还可以当成两个8 位的寄存器来使用,分别是AH、AL、BH、BL、CH、CL、DH、DL。

图2-12 8086 处理器内部组成框图

在进行数据传送或者算术逻辑运算的时候,使用算术逻辑部件(ALU)。比如,将AX 的内容和CX 的内容相加,结果仍在AX 中,那么,在相加的结果返回到AX 之前,需要通过一个叫数据暂存器的寄存器中转。

处理器能够自动运行,这是控制器的功劳。为了加快指令执行速度,8086 内部有一个6 字节的指令预取队列,在处理器忙着执行那些不需要访问内存的指令时,指令预取部件可以趁机访问内存预取指令。这时,多达6 个字节的指令流可以排队等待解码和执行。

8086 内部有4 个段寄存器。其中,CS 是代码段寄存器,DS 是数据段寄存器,ES 是附加段(Extra Segment)寄存器。附加段的意思是,它是额外赠送的礼物,当需要在程序中同时使用两个数据段时,DS 指向一个,ES 指向另一个。可以在指令中指定使用DS 和ES 中的哪一个,如果没有指定,则默认是使用DS。SS 是栈段寄存器,以后会讲到,而且非常重要。

IP 是指令指针(Instruction Pointer)寄存器,它只和CS 一起使用,而且只有处理器才能直接改变它的内容。当一段代码开始执行时,CS 指向代码段的起始地址,IP 则指向段内偏移。这样,由CS 和IP 共同形成逻辑地址,并由总线接口部件变换成物理地址来取得指令。然后,处理器会自动根据当前指令的长度来改变IP 的值,使它指向下一条指令。

当然,如果在指令的执行过程中需要访问内存单元,那么,处理器将用DS 的值和指令中提供的偏移地址相加,来形成访问内存所需的物理地址。

8086 的段寄存器和IP 寄存器都是16 位的,如果按照原先的方式,把段寄存器的内容和偏移地址直接相加来形成物理地址的话,也只能得到16 位的物理地址。麻烦的是,8086 却提供了20根地址线。换句话说,它提供的是20 位的物理地址。

提供20 位地址线的原因很简单,16 位的物理地址只能访问64KB 的内存,地址范围是0000H~FFFFH,共65536 个字节。这样的容量,即使是在那个年代,也显得捉襟见肘。注意,这里提到了一个表示内存容量的单位“KB”。为了方便,我们通常使用更大的单位来描述内存容量,比如千字节(KB)、兆字节(MB)和吉字节(GB),它们之间的换算关系如下:

所以,65536 个字节就是64KB,而20 位的物理地址则可以访问多达1MB 的内存,地址范围从00000H 到FFFFFH。问题是,16 位的段地址和16 位的偏移地址相加,只能形成16 位的物理地址,怎么得到这20 位的物理地址呢?

为了解决这个问题,8086 处理器在形成物理地址时,先将段寄存器的内容左移4 位(相当于乘以十六进制的10,或者十进制的16),形成20 位的段地址,然后再同16 位的偏移地址相加,得到20 位的物理地址。比如,对于逻辑地址F000H:052DH,处理器在形成物理地址时,将段地址F000H 左移4 位,变成F0000H,再加上偏移地址052DH,就形成了20 位的物理地址F052DH。

这样,因为段寄存器是16 位的,在段不重叠的情况下,最多可以将1MB 的内存分成65536个段,段地址分别是0000H、0001H、0002H、0003H,……,一直到FFFFH。在这种情况下,如图2-13 所示,每个段正好16 个字节,偏移地址从0000H 到000FH。

图2-13 1MB 内存可以划分为65536 个16 字节的段

同样在不允许段之间重叠的情况下,每个段的最大长度是64KB,因为偏移地址也是16 位的,从0000H 到FFFFH。在这种情况下,1MB 的内存,最多只能划分成16 个段,每段长64KB,段地址分别是0000H、1000H、2000H、3000H,…,一直到F000H。

以上所说的只是两种最典型的情况。通常情况下,段地址的选择取决于内存中哪些区域是空闲的。举个例子来说,假如从物理地址00000H 开始,一直到82251H 处都被其他程序占用着,而后面一直到FFFFFH 的地址空间都是自由的,那么,你可以从物理内存地址82251H 之后的地方加载你的程序。

接着,你的任务是定义段地址并设置处理器的段寄存器,其中最重要的是段地址的选取。因为偏移地址总是要求从0000H 开始,而82260H 是第一个符合该条件的物理地址,因为它恰好对应着逻辑地址8226H:0000H,符合偏移地址的条件,所以完全可以将段地址定为8226H。

但是,举个例子来说,如果你从物理内存地址82255H 处加载程序,由于它根本无法表示成一个偏移地址为0000H 的逻辑地址,所以不符合要求,段不能从这里开始划分。这里面的区别在于,82260H 可以被十进制数16(或者十六进制数10H)整除,而82255H 不能。通过这个例子可以看出,8086 处理器的逻辑分段,起始地址都是16 的倍数,这称为是按16 字节对齐的。

段的划分是自由的,它可以起始于任何16 字节对齐的位置,也可以是任意长度,只要不超过64KB。比如,段地址可以是82260H,段的长度可以是64KB。在这种情况下,该段所对应的逻辑地址范围是8226H:0000H~8226H:FFFFH,其所对应的物理地址范围是82260~9225FH。

同时,正是由于段的划分非常自由,使得8086 的内存访问也非常随意。同一个物理地址,或者同一片内存区域,根据需要,可以随意指定一个段来访问它,前提是那个物理地址位于该段的64KB 范围内。也就是说,同一个物理地址,实际上对应着多个逻辑地址。

检测点2.3

1. INTEL 8086 处理器有( )个16 位通用寄存器,分别是(     )。其中,有些还可以分开来作为两个独立的8 位寄存器来用,这几个8 位寄存器分别是(    )。

2. 选择题(可多选):INTEL 8086 处理器取指令时,使用段寄存器( )和指令指针寄存器( )。方法是,将段寄存器的值( ),加上指令指针寄存器的当前值,形成物理地址访问内存。

A.CS B.DS C.IP D.左移4位 E.右移4位 F.乘以1 G.除以10H

3. 物理地址132FEH 对应的逻辑地址是(可多选):

A.132FH:000EH   B.1300H:02FEH   C.1000H:32FEH   D.1320H:00FEH

E.102FH:03E0H   F.0FE0H:34FEH 02OJ6J45mfYIhBFJpEL8jcCMcNWBxJ5tKAAOgQA/6Fcj0BZMz0LdhdSDBIEp+qOE

点击中间区域
呼出菜单
上一章
目录
下一章
×