浅谈CPU和CPU对内存的访问方式

本文介绍了从16位到32位CPU的内存访问方式的演变,包括8086CPU的16位访问模式,以及32位CPU在实模式下对内存的访问,详细解析了32位CPU的全局描述符表(GDT)和分页机制。内容涵盖了CPU如何通过寄存器和段机制访问内存,以及在保护模式下如何通过权限等级和分页提高内存管理效率。
摘要由CSDN通过智能技术生成

1.写在前面

我上个月全在看汇编,导致博客好久没有更新了,但是看了《x86汇编-从实模式到保护模式》这书看起来比较吃力,我也看了好几篇也才了解的大概,凭我对这本书的简单的理解,于是今天这篇博客简单的介绍下CPU的体系结构,以及不同模式下,不同位数的CPU对内存的访问模式。

2.16位CPU以及内存的访问的方式

我们都知道,程序是一条条的指令,而CPU单次只能执行一条指令,而CPU又怎么知道每次要执行那条指令呢?于是聪明的人类想到了一个办法,就是用CPU中某一个寄存器去读取内存中的指令,读取一条执行一条,然后默认的去执行下一条。于是就有了代码段,既然我们知道CPU是16位,也就意味着最大存储的值就是0xFFFF,这也就意味只能访问64K的最大的内存,但是当时的8086CPU可以访问1M的内存,当时的8086CPU有20根地址线,最大访问的内存是1M,但是16位CPU又怎么能访问到1M的内存呢?于是当时想到了一个办法就是把一个寄存器中值左移4位,然后加上一个偏移地址,这样就组成了一个20位的地址信息,于是就达到了1M的访问空间。

既然知道了8086CPU的内存的访问方式,但是我们似乎忽略了一个知识,就是指令和数据有什么区别,CPU又如何知道它们之间的区别呢?CPU蠢的狠,它只知道执行指令,然后改变寄存器的值,但是数据和指令我们发现在二进制的上面是没有任何的区别,我前面的博客也是说清楚了,于是聪明的人类又想到一个方法,就是我把数据和指令分开存就行了呀!于是就有了数据段。

既然有了数据和指令,那么似乎可以应付很多场景,似乎什么问题的都可以解决了,但是聪明的人类发现,如果我一直重复做一件事的话,在现有的体系中,我似乎只能一直写相同的指令,比如某一个操作,我要执行一百次,那我就要写这个指令一百次,如果是一万次呢?那么这个CPU是不是就超级蠢了,只知道一条指令一条指令的往下执行,不会拐弯,这不是傻子嘛?于是聪明的人类,开始抽象啊!循环的关键因素是啥,就是循环的操作和循环的次数呀!这不就好办了,循环操作我可以用指令的表示出来,但是这个次数我是不是要存到对应的寄存器中去,于是就有了这个寄存器用来存循环次数,每次循环操作的时候,都会读取这个寄存器中的值同时将这个寄存器中的值减去1。当寄存器中的值为0的时候就结束循环了。

数据和指令还有循环都有了,似乎完美无缺了,是嘛?但是有一天,你写着写着,发现CPU还是好尼玛蠢,比如我之前写了两个数相加等等一系列指令,但是我后面的同样需要这样的功能的时候,为什么CPU不会跳到原来的地方去执行呢?还要我重新写一次,CPU的设计者说这个好办,我给你一个跳转指令就行,让你跳转到对应的地方去执行,然后执行完,你再跳转回来。开发者发现,这个功能不错,我终于可以模块化开发了,但是问题来了?跳来跳去我尼玛地址记不住了,写个程序,变成我天天要计算地址了,CPU真尼玛蠢。能不能给我自动跳呢?好嘛!那我们再抽象好嘛?实现的功能不就是调用一个模块,然后从被调用模块返回到调用模块的位置,于是这儿就只有两个地址,一个是调用模块的地址,一个被调用模块的地址,那么我们要存起来,可是往哪里存呢?于是就有了栈段,存到栈中就可以。

这儿可以看下下面的调用的图,具体的如下:

在这里插入图片描述

似乎,我们已经能用这个蠢蠢的CPU来解决所有的问题了,于是就有了早起的8086的CPU的模型,上面的提到的代码段,CPU用CS:IP寄存器来表示,数据段用DS:DI寄存器来,同时怕你还有复制的需求,于是又多了一对寄存器ES:SI,这样还剩最后一对栈段寄存器SS:SP ,还有用来存循环次数的寄存器叫做CX寄存器。还有一些CPU附带的寄存器。

至于CPU访问内存的方式,可以看下我下面的这张图。

在这里插入图片描述

3.32位CPU以及内存的访问的方式(实模式未开启分页的情况)

16位的CPU访问内存都是自由的,但是如果有了操作系统的出现,你说你改了操作系统的代码,那么操作系统就有可能直接崩溃了,那你还怎么玩,黑客想干嘛就干嘛,于是32位的寄存器才原来的基础上加了内存的访问的限制,但是原来16位的CPU被扩展成32位的CPU,寄存器的位数也是变大了。访问内存的方式也有所改变。

我们先来说说内存的访问的限制吧!我们先不看对应的实现。我们先来想想,如果你来做,你该怎么做?那么我们需要这个程序的能够访问的内存的范围以及它的基地址,同时需要对这段内存的有哪些权限,读、写、执行等等权限。既然我们知道CPU的位数上去了,我们可以使用对应的位来表示对应的功能,于是就有了对应的描述的方式,在32位的CPU中叫做GDT(全局描述符),具体的如下图:

在这里插入图片描述

接下来,就是对上面的内容进行相应的解释,具体的如下:

  • G位为粒度位,当G位是“0”时,段界限字节为单位。“1”,那么,段界限是以4KB为单位的。

  • S 位用于指定描述符的类型(Descriptor Type)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(栈段也是特殊的数据段)。

  • L 位是64 位代码段标志64-bit Code Segment),保留此位给64位处理器使用。

  • AVL 是软件可以使用的位(Available),通常由操作系统来用,处理器并不使用它。

  • P 是段存在位(Segment Present)。P 位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的P 位清零,表示段并不存在。

  • DPL 表示描述符的特权级(Descriptor PrivilegeLevel,DPL)。这两位用于指定段的特权级。共有4 种处理器支持的特权级别,分别是0、1、2、3,其中0 是最高特权级别,3 是最低特权级别。刚进入保护模式时执行的代码具有最高特权级0(可以看成是从处理器那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。每当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如3 特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由0 特权级的程序来执行,为的就是安全。

在这里插入图片描述

  • D/B 位是“默认的操作数大小”(Default Operation Size)或者“默认的栈指针大小”(Default Stack Pointer Size),又或者“上部边界”(Upper Bound)标志。该标志位对不同的段有不同的效果。对于代码段,此位称做“D”位,用于指示指令中默认的偏移地址和操作数尺寸。D=0 表示指令中的偏移地址或者操作数是16 位的;D=1,指示32 位的偏移地址或者操作数。对于栈段来说,该位被叫做“B”位,用于在进行隐式的栈操作时,是使用SP 寄存器还是ESP 寄存器。如果该位是“0”,在访问那个段时,使用SP 寄存器,否则就是使用ESP 寄存器。同时,B 位的值也决定了栈的上部边界。如果B=0,那么栈段的上部边界(也就是SP 寄存器的最大值)为0xFFFF;如果B=1 , 那 么 栈 段 的 上 部 边 界 ( 也 就 是 ESP 寄 存 器 的 最 大 值 ) 为

    0xFFFFFFFF。

既然我们有了GDT,那么这个鬼东西,CPU每次在执行的时候,是需要读取一下的,看看是否符合规则,于是CPU需要将这个GDT存起来,于是CPU中有开辟了一个寄存器用来存储这个GDT,就是寄存器GDTR,这个寄存器是48位,GDTR寄存器是48位的寄存器,低16位存的是全局描述符表边界,高32位存的是全局描述表线性基地址,其中全局描述符表边界等于GDT的表项乘8减去1

于是从就有了下图的演变过程。

在这里插入图片描述

4.32位CPU以及内存的访问的方式(实模式开启分页的情况)

由于原先的分段的方式会导致内存的分配的不合理,有的程序的分配过大,有的程序分配的过小,导致内存的利用率不高,那么有没有什么的方法来解决呢?数学上有一种方法叫做不断的拆分,化曲为直,不断的逼近,达到极限的方法,于是我们是不是可以将内存不断的拆分,划分成很小的一段,然后程序要多少空间的时候,我们只需要给它的一段,于是就有了分页的模型。

最早期的分页的模型如下:

在这里插入图片描述

每个任务都有一个虚拟内存空间,对应的内存空间去对应的页映射表中找到对应的物理内存,通过这种简单的分页的方式,达到内存的最大化的利用,每个页都有4K,对于操作系统来说,如果一个任务执行完,操作系统就会对这个任务的物理内存的空间进行回收。

但是这个映射表很大就4M大,很占内存的空间,于是又改版,就是将页映射表进行分层,于是有了页目录和页表,于是有了下面的模型

在这里插入图片描述

这样的层次化分页结构是每个任务都拥有的,或者说,每个任务都有自己的页目录和页表。页的目录的大小是4KB,在处理器内部,有一个控制寄存器CR3,存放着当前任务页目录的物理地址,故又叫做页目录基址寄存器(Page Directory Base Register,PDBR)。

既然有了分页的模式,那么怎么对地址进行转换呢?CR3 寄存器给出了页目录的物理基地址;页目录给出了所有页表的物理地址,而每个页表给出了它所包含的页的物理地址。首先将段部件送来的32 位线性地址截成3 段,分别是高10位、中间的10 位和低12 位。高10 位是页目录的索引,中间10 位是页表的索引,低12 位则作为页内偏移来用。具体的如下图

在这里插入图片描述

4.写在最后

这篇博客主要讲了从16位CPU到32位的CPU对内存的一步步的演变的过程,以及对一些的常用的问题进行思考,其实CPU不要把它看的很牛逼,它其实很蠢的。就只会傻傻的跑你给的指令,我们只需要了解它的机制就可以了,已经对内存的访问的方式,我们程序员不过是用CPU进行对数据进行对应的处理,而今天的程序,不过是程序员对数据的组织的方式达到最好的方式,它的本质永远没有变,不管是微服务还是分布式,它们只不过对数据的处理方式和对数据的组织方式达到当下时代的需求,也许不就的将来,又会有其他的组织方式和处理方式。

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值