经过主存数据排布,现在我们就需要将主存中的存放的数据,加载进入CPU进行执行。
因为主存的访问速度和CPU的计算速度还是有差距的。
所以我们需要一个接近CPU速度的存储器,进行加速CPU运算,我们直接上16位的寄存器。但是我们知道,寄存器中的数据是从主存中获取到的,那么数据是需要一个通道进行沟通,从而将主存中的数据迁移到寄存器中。而实现交流的叫做总线。
总线:
- 数据总线:在CPU与RAM之间来回传送需要处理或是需要储存的数据。
- 控制总线:将微处理器控制单元的信号,传送到周边设备。
- 地址总线:用来指定在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
由上面的转化结果来看,
- 使用八进制需要可以使用 3 个位置,表示一个八位。
- 使用十进制需要使用4个位置,表示一个十位,但是不能将 01 组合完全使用完整。
- 使用十六进制同样也是需要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位的寄存器,他名字叫 段寄存器。而我们一个程序启动需要
代码,数据,和栈
则就有如下的寄存器出现
- 代码段寄存器 CS : 存储代码段的寄存器
- 数据段寄存器 DS : 存储数据段的寄存器
- 栈段寄存器 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