x86汇编语言-从实模式到保护模式笔记

处理器、内存和指令

Posted by BINBIN Blog on October 20, 2019

##寄存器和算数逻辑部件 为什么处理器能够自动计算,这个问题已经在我的上一本书《穿越计算机的迷雾》里讲过 了,不过这些原理讲起来很费劲,花了整整一本书的篇幅。当然,如果你没看过这本书,也没关系,下面就来简单回顾一下。回顾这些知识很有用,因为只有这样你才能知道如何安排处理器做事情。

电子计算机能做很多事情。你能够知道明天出门要穿厚一点才不挨冻,是因为电子计算机算出了天气。除此之外,它还能让你看电影、听音乐、写文章、上网。尽管表面上看来,这些用处和算数学题没什么关系,但实质上,这些功能都是以数学计算为基础的。正是因为如此,人们才会把“计算”这个词挂在嘴边,什么“云计算”、“网络计算”、“64 位计算”,等等。

如图是一个处理器:

具体参考书中所说,或者网友OS学习笔记

目前主流处理器是32位和64位的! 我目前工作接触到的处理器基本都是32位的,比如powerpc, tricore,arm 等等芯片!

内存储器

我们知道处理的计算过程实际上是借助于寄存器和算数逻辑部件进行的。数据是从保存数据的电路来的,这个就是存储器,有硬盘,u盘,内存条等等。

我们这按主要打交道就是内存条,直接和cpu连接的,内存用字节组织的!最小的单位是1byte。如下是内存访问示意图:

图中范围是0-0xFFFF!

指令和指令集

处理器的设计者用某些数来表示该处理器该做什么,这些数,我们称为指令或者叫机器指令。因为只有处理器才认得它们。比如,指令 F4H 表示让处理器停机,当处理器取到并执行这条指令后,就停止工作。指令是集中存放在内存里的,一条接着一条,处理器的工作是自动按顺序取出并加以执行。

如图 2-6 所示,从内存地址 0000H 开始(也就是内存地址的最低端)连续存放了一些指令。 同时,假定执行这些指令的是一个 16 位处理器,拥有两个 16 位的寄存器 RA 和 RB。

停机指令仅包含 1 字节的操作码 F4,而没有操作数。指令的长度不定,短的指令仅有 1 字节,而长的指令则有可能达到 15 字节

第一条指令的操作码是 B8,这表明,该指令是一条传送指令,第一个操作数是寄存器,第二个操作数是直接包含在指令中的,紧跟在操作码之后,可以立即从指令中取得,所以叫做立即数(Immediate Operand)。同时,操作码还直接指出该寄存器是 RA。 RA 是 16位寄存器,这条指令将按字进行操作。所以,当这条指令执行之后,该指令的操作数(立即数) 005DH 就被传送到 RA 中。

既然操作码中隐含了这么多的信息,那么,处理器就可以“知道”每条指令的长度。这样,当它执行第一条指令 B8 5D 00 的时候,就已经知道,这是一个 3 字节指令,下一条指令位于 3个字节之后,即内存地址 0003H 处。

地址 0001H 和 0002H 里的内容分别是 5D 和 00,如果每次读一个字节,则从地址 0001H 里读出的是 5D,从 0002H 里读出的是 00。但如果以字的方式来访问地址 0001H,读到的就会是 005DH。这种差别,跟处理器和内存之间的数据线连接方式有关。对于 Intel 处理器来说,如果访问内存中的一个字,那么,它规定高字节位于高地址部分,低字节位于低地址部分,这称为低端字节序(Little Endian)。至于其他公司的处理器, 则可能情况正好相反,称为高端字节序!

复杂一些的指令来说, 1 个字节的操作码可能不会够用。所以,第 2 条指令的操作码为 8B 1E,它隐含的意思是,这是一条传送指令,第一个操作数是寄存器,而且是 RB 寄存器,第二个操作数是内存地址,要传送到 RB 寄存器中的数存放在该地址中。同时,这是一个字操作指令,应当从第二个操作数指定的地址中取出一个字。

该指令的操作数部分是 3F 00,指定了一个内存地址 003FH。它相当于高级语言里的指针,当处理器执行这条指令时,会再次用 003FH 作为地址去访问内存,从那里取出一个字(1002H),然后将它传送到寄存器 RB。注意,“传送”这个词带有误导性。其实,传送的意思更像是“复制”,传送之后, 003FH 单元里的数据还保持原样。 通过这两条指令的比较,很容易分清指令中的“立即数”是什么意思。指令执行和操作的对象是数。如果这个数已经在指令中给出了,不需要再次访问内存,那这个数就是立即数,比如第一条指令中的 005DH;相反,如果指令中给出的是地址,真正的数还需要用这个地址访问内存才能得到,那它就不能称为立即数,比如第二条指令中的 003FH。

余下的三条指令,旁边都有注解,这里就不再一一解释了。如果一开始内 存地址 003FH 中存放的是 1002H,那么,当所有这些指令执行完后, 003FH 里就是最终的结果 105FH,最后F4指令停机。

指令和非指令的二进制数据是一模一样的,在组成内存的电路中,都是一些高低电平的组合。因为处理器是按顺序取指令,并加以执行的,这样的话,如果指令和数据存放在一起,处理器很容易将数据的二进制当成指令,从而导致错误。所以我们需要将指令与数据分开存放。分别存放在不同的区域。存放指令的区域叫做代码区,存放数据的区域叫做数据区(这就是为什么进程的虚拟地址空间中代码和数据区分开的原因)。

一个处理器能够识别的指令的集合,称为指令集。

Intel 8086处理器

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

8086的通用寄存器

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

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

可以看到,有介个寄存器,AX,BX,CX,DX,又可以分为两个8位的寄存器来使用。在后面文章中,我们会看到具体的应用。

程序的重定位问题

处理器是自动化的器件,在给出了起始地址之后,它将从这个地址开始,自动地取出每条指令并加以执行。只要每条指令都正确无误,它就能准确地知道下一条指令的地址。 这就意味着,完成某个工作的所有指令,必须集中在一起,处于内存的某个位置,形成一个段,叫做代码段。我们知道,处理器是自动取指令和执行指令的,只要每条指令都正确无误,它就能准确的知道下一条指令的地址。所以,指令必须集中在一起,形成一个段,叫做代码段。 为了做某件事而编写的指令,形成我们所知道的程序。程序中肯定需要操作大量的数据,这大量的数据也应该集中在一起,位于内存中的某个地方,叫做数据段。

段在内存中的位置并不重要,因为处理器是可控的,我们可以让处理器在内存中的任何一个位置开始取指令并加以执行。

假设上面是一个程序片段在内存中的位置,相关指令的用处已经在图中表明。这里不再赘述。但是现在有一个问题,就是我们写的程序(不管是C语言还是C++语言或者Java语言),最终要运行在内存中的位置,是无法确定的。因为你想一想,本身你的电脑中有各种程序在跑了,有qq程序在跑,微信程序在跑,或者还有微博等在跑,他们可能把你的内存都占完了,当你想运行你写的程序的时候,你的程序本想从0100H处开始存放代码段,但是可能这个时候0100H处有其他程序的代码或者指令在运行,此时你的程序只能去另一个位置存放你的代码和数据,假设你的代码下一次运行的时候代码段和数据段在如下位置:

此时你的程序的代数据段在内存的位置为1000H处,但是你会发现,你的代码段的第一条指令,还是将地址单元0100H处的内容传送到AX寄存器中。但是!!!此时0100H处的内容,并不是我们想要的内容。

产生这种情况的原因就是我们在程序中使用的是绝对内存地址。这样的程序是无法重新定位的。所以我们需要使用另一种方式访问内存:相对地址或者叫做逻辑地址。在任何时候,程序的重定位都是非常棘手的事情。在8086处理器中,这个问题得到解决。它使用了分段机制

内存分段机制

在内存分段中,段,是很多个连续的字节组成的。如图:

上图有一个7个字节的段。段的起始地址为A532。那么段中的地址从第一个字节开始,他们的地址此时就是段内的偏移地址(0000,0001,0002…)。而他们的实际的物理地址,又恰好等于段地址加上段内的偏移地址。可以用“段地址:偏移地址”,来表示段中的每一个字节的地址

为了在硬件一级提供对“段地址:偏移地址”的支持。处理器至少要提供两个段寄存器,分别是代码段寄存器CS,数据段寄存器DS

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

很重要的一点是,当处理器取指令执行指令的时候,它是把指令中指定的内存地址看成是段内偏移地址的。而不是内存的物理地址。那么当处理器遇到一条访问内存的指令时,它就会将DS中的数据段起始地址加上指令中提供的段内偏移地址得到访问内存所需要的物理地址的。

如下图所示:

代码段地址为1020H,数据段地址为1000H,在代码段中有一条指令:A1 02 00,它的功能是将地址0002H处的的一个字传送到寄存器AX。在这里处理器将0002H看成是段内偏移地址,段地址在DS中,应该在执行这条指令之前就已经用别的地址将数据段地址传送到DS寄存器中了。当处理器执行到指令A1 02 00时,处理器会将DS中的内容和指令中指定的便宜地址相加,得到内存中的一个物理地址,这个物理地址就是处理器要去访问的内存地址,就可以从该地址获取一个字:00A0H。

如果下一次执行该程序,代码段和数据段发生变化,只需要将程序的代码段地址和数据段地址分别传送到CS和DS就可以正常的执行程序了。

8086 处理器内部组成框图

看一下8086处理器内部组成框图:

上图中,我们知道8086内部有8个16位通用寄存器,分别为AX,BX,CX,DX,SI,DI,BP,SP。ALU是算数逻辑部件,用于算数逻辑运算或者数据传送。标志寄存器是用于控制各种依赖于标志位的标志来进行相应的指令执行的,这个可以等到以后的文章中进行详细的说明,目前不需要理解。处理器可以自动执行,依赖于控制器的控制。

指令队列缓冲器,又名:指令预取队列。什么意思呢?就是当你的CPU在忙于做其他事情(一般是指执行那些不需要去内存中取的指令)而并没有去内存中取指令执行时,此时指令队列缓冲器(指令预取队列)就会去内存中取出6个字节的指令放到指令队列缓冲器,那么当CPU该执行内存中的指令时,它就会就近向指令队列缓冲器中去取指令,这样的话就会比去内存中取指令要快很多,毕竟内存还是通过外部总线与内存条相连接的。

CS是代码段寄存器。DS是数据段寄存器。SS是栈段寄存器(很重要,以后会讲)。ES是额外的段寄存器,它的用处相当于补充段寄存器,当你的DS在使用时,但是你还要访问另一个数据段,那么此时就可以使用ES寄存器(后面的文章中我们就用ES寄存器指向显卡的内存区域,用来寻址显存,从而控制显卡)。IP寄存器,是存代码段的段内偏移地址的。那么指令的地址此时就可以用“CS:IP”来表示。

上面的与总线控制逻辑相连的16位总线,实际上是16位的数据总线与20位的地址总线的低16位复用的。20位代表的是20位的地址总线是(当然,低16位是与数据总线复用的)。

8086的内存分段机制

前面讲了如何从逻辑地址转换到物理地址,以使得程序的运行和它在内存中的位置是无关的。上述策略在很多处理器上应用得到了支持。但是在8086处理器上,由于8086处理器是16位处理器,如果按照正常计算,给它提供16根地址线的话,那么8086处理器就只能用于寻址最多64KB的内存空间(65536字节=2的15次方+1)。在当时的年代64KB的内存还是不够的。所以8086处理器就将16根地址线扩展到20根地址线。从而使得可寻址的空间变为1M(16*64KB)也就是 2的20次方。

但是有一个问题就是16位的段地址加上16位的段内偏移地址,还是16位的,并不是20位的。所以有一个解决办法就是在计算内存的物理地址时,先将段地址先左移4位,然后加上段内偏移地址,这样就可以得到20位的物理地址,就可以将整个1M的地址空间表示完全。

8086在进行分段时,并不是每一个地址都可以作为段地址,地址必须是16的倍数,才能作为段地址。

如下图,是其中的一种情况,我们从0地址开始分段,每段16字节(从任何地址只要这个地址是16的倍数,都可以作为段地址,每一个段内最低有16字节(可以分为65536个段)的内存,最高有65536(可以分为16个段)个字节的内存)。

接着,你的任务是定义段地址并设置处理器的段寄存器,其中最重要的是段地址的选取。 因为偏移地址总是要求从 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 范围内。也就是说,同一个物理地址,实际上对应着多个逻辑地址。

习题: 数据段寄存器 DS 的值为25BCH 时,计算 Intel 8086可以访问的物理地址范围。

答案: 对应的地址是0x25BC0,范围是25BC:0000 ~ 25BC:FFFF 所以也就是0x25bc0 + 0xFFFF = 0x35BBF 25BC0 - 35BBF 就是物理地址范围!