认识寄存器

经过主存数据排布,现在我们就需要将主存中的存放的数据,加载进入CPU进行执行。

因为主存的访问速度和CPU的计算速度还是有差距的。

所以我们需要一个接近CPU速度的存储器,进行加速CPU运算,我们直接上16位的寄存器。但是我们知道,寄存器中的数据是从主存中获取到的,那么数据是需要一个通道进行沟通,从而将主存中的数据迁移到寄存器中。而实现交流的叫做总线。

总线:

  1. 数据总线:在CPU与RAM之间来回传送需要处理或是需要储存的数据。
  2. 控制总线:将微处理器控制单元的信号,传送到周边设备。
  3. 地址总线:用来指定在RAM之中储存的数据的地址。

我们知道数据都是按照 01 二进制的方式存储的。指令和数据都是二进制表示的,但是由于二进制组合亢长,则我们就需要精简他,将指令精简为了 操作码,而数据我们也需要精简,我们选择使用16进制来表示二进制。为什么不用其他进制表示,而使用了十六进制呢,我们顺便来分析一下。

二进制:计算机使用进制表示。特点是逢2进1

我们使用如下进制转化为2进制。

0001(1)0010(2)0011(3)0100(4)0101(5)0110(6)0111(7)1000(8)1001(9)1010(10)

1011(11)1100(12)1101(13)1110(14)1111(15)

八进制:0-7 逢8进1

十进制:0-9 逢10进1

十六进制:0-15逢16进1

由上面的转化结果来看,

  1. 使用八进制需要可以使用 3 个位置,表示一个八位。
  2. 使用十进制需要使用4个位置,表示一个十位,但是不能将 01 组合完全使用完整。
  3. 使用十六进制同样也是需要4个位置,表示一个十六位。而将整个组合都可以使用1占满。

观察可得,八位和十六位,一个使用的3个位置,一个使用的是4个位置。

而十位被抛弃则是因为他转化二进制计算不方便,不是2的倍数。

但是选择了十六位而没选择八位的原因,就是在于4是2的倍数,也是由于计算方便,而3是奇数不方便计算。

所以现在选用16位作为基础,因为16这个数字非常特别。

2^4也是二进制中最小的单位,也是二进制中运算的最大单位。之前就是因为 21 表示过多,所以要简化,23 5 6 7 8不行,3,5,7是奇数,6 不是2 的倍数,8虽然是2的倍数,但是也可以写成 24 * 22 ,所以我们选择 16 这个数。

既然提到了位运算总结规律:以功能的层面

与运算:不相同的全部截断 举例说明 :1010 & 1101 = 1000

或运算:组合 举例说明 :1010 | 1101 = 1111

异或运算:无进位相加 举例说明 :1010 | 1101 = 0111

而我们知道缓存的升级,将内存提升到了4GB,寻址单元也从1byte,变成了1MB的大小,而1MB则是

而存储单元是 byte 单位,是因为数据缘故,ASCII 表使用 8 个字节完全可以表示完全,所以 1 byte = 8 字节。

现在使用 16 位的寄存器去访问 1MB 的数据怎么办。之前在主存的数据排布也介绍过。

1MB = 2 20

16 位寄存器可以满足 216

它还差 4 位,怎么办?

那就再拿一个寄存器,来补这个 4 位,而补这4位的寄存器,他名字叫 段寄存器。而我们一个程序启动需要

代码,数据,和栈

则就有如下的寄存器出现

  1. 代码段寄存器 CS : 存储代码段的寄存器
  2. 数据段寄存器 DS : 存储数据段的寄存器
  3. 栈段寄存器 SS : 存储堆栈的的寄存器

而如果还有特别的需要,那么我们就叫他其他寄存器,* S ,S 表示 segment。

所以实际地址 的计算公式:

实际地址 = 基地址 + 偏移量

假设现在有两个函数。

fun1() 和 fun2(),站在高级语言的角度来说,这个主存就像一个非常长的数组一样。

当前内存假设为4GB,而数组长度为int[] arr = new [1024 * 1024 * 1024 * 4]; 这么大的地址空间。

现在进行代码加载,和数据加载。

代码被编译成一条一条的指令,并在内存中开辟了一个栈帧存放当前函数需要的数据,让进去的话就需要记录一下当前占用数组的多少长度,也就是要记录一下数组下标,从哪里开始到哪里结束,那么存放开始的这个寄存器,和结束的寄存器,一个叫栈顶寄存器,一个段栈底寄存器。现在指令被加载到主存中,之后又被加载到寄存器中,而寄存器大小有限,并不能一次性的将都执行完成,所以在执行的过程中就需要记录状态信息,就需要存储一下。

如下又引入了三个寄存器:

状态寄存器:是用于存储当前指令执行结果的各种状态信息

指令寄存器:临时放置从内存里面取得的代码数据(也就是指令)

地址寄存器:当前CPU访问的内存的地址单元

现在我们只加载一个函数。

那么这个函数可能会有 形参,实参,跳转指令。

我们现在讨论一下这个跳转指令,如果我在当前函数中调用了另一个函数,那么根据栈的规则,fun1 会被压在栈底,将fun2加载到内存中,那么如果fun2执行完成之后,而我们需要知道我们刚才fun1执行到哪里了,而这个地址我们也需要在这里保存一下。这里就是用的是指令寄存器( RIP)存起来就可以回去了。

那么形参和实参呢?

讨论这个我们其实就是讨论的值传递还是引用传递。

顺便来讨论一个 C语言的指针。

我们定义一个变量 int a,因为之前了解过高级语言就是将分配内存抽象,int 表示分配 4byte 大小的空间,而整个内存又是一个大的数组,,假设内存是从arr.length开始分配 肯定是从arr.length - 4,arr.length = 64 , 64 - 4 = 60。

这里插入一个细节:

如果分配内存是从高地址向低地址分配,那就是大端序,如果是低地址向高地址分配,那就是小端序。

那么&符号是什么?*p 是什么?

现在已经分配了 4 byte 的空间,变量的起始地址就 60 那么 & 符号就是获取这个 60 ,而这个60只是数组下标,并不是这个 4byte 中存放的数据。而 *p 呢,就是 60 也是一个值,他也需要存储,*p 就是这个 60 数字。

那么我们就发现,原来我们存储的这个下标60就是指针 指向这个地址的开始下标位置。

那么这个地址也是占用空间的,而我们能搞多少个这样的空间呢?这得看内存多大,比方说4GB。

如果是32位机 ,232 那必须是为 4 byte ,1 byte= 8位。如果是64位机,那得是 8 byte。

常量指针:指向常量的指针,他不能指向变量,也就是说他指向的数据不能被改变。

指针常量:一个指向指针的常量,常量本身是不能被改变的,也就是说这个指针的地址是不能被改变的,但是这个指针存储的数据是可以被改变的。

那么我们现在来看一个这个 形参与实参。

如果传递的是一个实参,说明传递的是一个指针,这个指针指向的数据会被当前方法改变掉。

而传入一个形参,他会新开辟一个空间,因为值传递是不能对之前的数据进行改变的。

当前假设的只是有一个程序在运行的情况,如果是两个程序在运行呢,这势必会发生争抢,那么我会怎么处理呢,就是给他增加一到屏障,也就是虚拟地址。

我们将物理地址隐藏起来,并告诉用户这么大内存你都可以使用,而程序不知道除了自己还有别的程序在运行,这个就操作系统这个软件的所干的事情,他又将内存屏蔽。

但是虚拟内存最终也是要存放到物理内存的,这个时候我们就需要做一个映射,将物理内存和虚拟内存关联起来,每个段分配一个区域号,以便于操作系统查询与分配内存给程序。而区域号加上虚拟地址就是真实的物理地址。相当于省市区结构,虽然县级和村级的编号可能一样,但是省号和区号编码不一样,他们就不是一个地区的。虽然都叫榆林市,加上陕西省榆林市和海南省榆林市,这明显就不在一个地方。

我们就需要指定一个规则,去找到这个物理地址到底存放到哪里去呢?

首先这个关联关系其实是一个数组的结构,因为内存就是一个块连续的地址空间,我们将单独为这个表开辟一段空间,他要存储每一个段在虚拟地址和物理地址的关系,将这些信息我们成为段信息,那么这个表一定会有从开始位置,不然没发找到这个表,再根据他的偏移量找到他的物理地址。

所以我们如果要得到真实的物理地址,则需要,虚拟地址加上段寄存器中存放的偏移量,才能得到真实的物理地址

而这个表就叫做 全局描述表:GDT

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值