心渐渐失空

导航

Intel:从屌丝逆袭成业界大佬

原创文章,转载请标明出处哈,Thanks♪(・ω・)ノ。
参考《Linux内核情景分析》《深入理解计算机系统》《深入理解linux内核》《Orange'S:一个操作系统的实现》
 
Intel处理器在1978年从简单的16位处理器发展起来的;现在已经成为主流的处理器;Inter处理器俗称x86;
加入新特性后,16位体系结构转变成支持32位数据和地址的IA32,它还具有向后兼容的特性,而现代编译器和操作系统根本不使用这些特性;
将IA32扩展到64位,成为x86-64;最初64位机是又AMD开发出来的;64位机支持256TB(2^48(保留了一部分))的ARM寻址;

实模式起源:

8位CPU地址总线是16位(64KB寻址空间),16位CPU地址总线是20位(1MB寻址空间)。
CPU内部的ALU宽度只有16位,地址总线却宽20位,如何填补呢?
1:可以像8位CPU那样,增设一些20位操作数的指令专用于地址运算操作。(造成CPU内部结构不均匀)
2:当时的PDP-11小型机那样,结合其MMU(内存管理单元)可以将16位地址映射到24位地址空间。
3:Intel的分段方法。(主流)在8086CPU中设置了4个段寄存器:CS、DS、SS和ES,分别用于可执行代码段、数据段、堆栈段和其他段。每个寄存器16位,对应地址总线中的高16位,每条“访内”指令中的“内部地址”都是16位,但是在送上地址总线之前都在CPU内部自动地与某个段寄存器中的内容相加,形成一个20位的实际地址。(段寄存器值右边加四个0成为20位地址+16位指令中的地址=放入地址总线的地址)(通过修改段寄存器的值,可以访问任何内存地址,缺乏内存访问限制,不安全,所以相对从80286开始实现的“保护模式”,8086的这种安全缺陷的模式就被称为“实地址模式”)
 
8086(1978)(16位)(实地址模式)
8088(16位)(实地址模式)
80286(16位,寻址方式:实地址模式到保护模式的过度)(80286只能从实模式转入保护模式,却不能从保护模式转回实模式)
//进入现代32位CPU,但是仍要兼容之前的实模式和段寄存器。
(80386)i386(32位)(实地址模式和保护模式)
(80486)i486(32位)
Pentium (586)
PentiumPro (686)
Pentium II
Pentium III
Pentium 4
Pentium 4E
Core 2(2006)
Core i7(2008)
......
除了这些处理器,中间还产生过其他一些处理器。每个处理器上能运行的指令集,在它之后的处理器上也同样能运行。例如32位操作系统能运行在后来的64位处理器上。

保护模式起源:

32位CPU:保留CS、DS、SS和ES这4个16位段寄存器,又添加了FS(用于任意段)和GS(用于任意段)两个段寄存器。为了实现保护模式(访问限制),需要一个数据结构。在保护模式下,改变段寄存器的功能,使其从一个单纯的基地址变成指向一个数据结构的指针。(数据结构记录段的基地址、访问权限等信息),内部地址(汇编指令的操作数,ALU中的值)经过数据结构后得到放上地址总线的地址,地址转换原理:
(1)根据上下文判断内部地址是想访问哪个段,例如call指令就是访问代码段。
(2)根据段寄存器找到数据结构:“地址段描述结构”。
(3)从地址段描述结构中得到基地址。
(4)基地址加上指令中的地址作为位移,看看是否越界。
(5)根据指令的性质和段描述符中的访问权限来确定是否越权。
(6)将指令中发出的地址作为位移,与基地址相加得到物理地址。
段描述结构存储在内存中,在实际使用时却将其装载到CPU的一组影子结构中。
上述原理的实际实现:
80386开始,CPU中增设两个寄存器:全局性的段描述表寄存器GDTR(global descriptor table register)和局部的段描述表寄存器LDTR(local descriptor table register),分别用来指向存储在内存中的一个段描述结构数组(段描述表)。访问这两个新增寄存器的专用指令设计成“特权指令(读:LGDT/LLDT;写:SGDT/SLDT。只允许在系统状态下使用)”。用户态下的指令访问不了G/L DTR寄存器,所以找不到段描述符表基址,访问不了“影子”寄存器,所以不知道段基址,所以比较安全。在改变段寄存器内容时,CPU会加以检查以确保程序的当前执行权限和段寄存器的RPL都不低于所要访问的那一段内存的权限DPL。
段寄存器的高13位用作访问段描述表中具体描述结构的下标(index,偏移个数),低3位作用如下图。
根据段寄存器的第2位,选择GDTR或LDTR。使用GDTR或LDTR中的段描述表指针和段寄存器的高13位(下标)结合,决定了具体的段描述表项在内存中的位置。每个段描述符表项都是8字节(含有段的基地址和段大小和其他一些信息,结构如下图)。一个段描述表最多有8192(2^13)项。
段描述符(6个段寄存器)->段描述符表指针(G/L DTR)->段描述符表项(8B)->段起始地址指针->段及段内偏移。
段寄存器内容改变时,CPU就把这个段寄存器中新内容关联的段描述符表项装进CPU内部的一个“影子”描述项(8B),每个段寄存器对应一个“影子”描述项,“影子”描述项供CPU内部使用,程序只可见段寄存器。(相当于把将来可能要使用的段描述符表项预先缓存到CPU内部,这样就不需要每次寻址都跑到内存去找描述符表项(8B)了)。这样一来,所有将来可能使用到的内容都缓存到寄存器了,分别是6个程序可见的段寄存器,G/L DTR,“影子”描述项。CPU根据这3种寄存器中的值来寻址。
 
在80386段式内存管理的基础上,如果把所有段寄存器都指向(经过G/L DTR)同一个描述符(8B),在该描述符中将段基地址设为0,段长度设为最大,这样一来物理地址就与逻辑地址相同。CPU放到地址总线上的地址就是指令中的地址值,此种特例叫做“平面(Flat)”地址。linux内核源代码(gcc)采用的就是平面地址,这样就相当于只使用了硬件分段机制的安全机制,而实际没有分段,段与段是重叠的。
 
段式虚存管理:
当段描述符装入CPU时(由于段寄存器内容更新导致段描述符缓存到CPU内部“影子”段寄存器),如果8B段描述项中的P标志为0,则表示该段内容不在内存中(在磁盘上),此时CPU产生一次异常,相应的服务程序便可以从磁盘交换区将这一段内容读入内存中,并设置此段描述符的段基地址,并将P标志改为1。内存中暂时不用的段可以写入磁盘,并将对应描述符P标志改为0。
 

页式内存管理:

80年代中期开始,页式内存管理进入各种操作系统。Intel从80286开始实现保护模式(即段式内存管理),光有段式内存管理没有页式内存管理是不够的,因此在不久后的80386中实现了对页式内存管理的支持。80386完善了80286的段式内存管理同时实现了页式内存管理。
逻辑地址:指令的操作数,指令中的地址,存放在通用寄存器。
物理地址:正真放到地址总线上的地址值。
页式存储管理本来不需要建立在段式存储管理基础之上,它们是两种不同的机制。但是在80386中保护模式的实现是与段式存储密不可分的,所以为了不抛弃保护模式,80386的系统结构决定了它的页式存管只能建立在段式存管基础上。也就是在段式存管映射而成的地址上再加上一层映射。由于逻辑地址经过段式存管映射而成的地址不再直接放到地址总线,所以Intel就称之为“线性地址”,线性地址再由页式存管映射成物理地址。如果不使用页式存管,则线性地址直接用作物理地址。
 
80386把线性地址空间划分成4KB的页面,每个页面可以被映射到物理存储空间中任意一块4KB大小的区间(边界4KB对齐)。连续的线性地址经过映射后在物理空间不一定是连续的(灵活性)。一旦启用了页式存管,所有的线性地址都要经过页式映射,包括GDTR与LDTR中给出的地址也不例外。
 
以前32位线性地址就当做物理地址直接放到地址总线到主存进行寻址,现在线性地址不直接放到总线,而有了新的解释:
页面目录dir(10)+页面表(10)+页内偏移offset(12)
页面目录有1024(2^10)个项,每个目录项指向一个页面表,每个页面表中有1024(2^10)个页面描述项,类似于G/L DTR。又增加了一个新的寄存器CR3作为指向当前页面目录的指针。
线性地址->物理地址:
1、从CR3取得页面目录基地址;
2、以线性地址中的dir位段为下标,在目录中取得相应的页面表的基地址;
3、以线性地址的page位段为下标,在所得到的页面表中取得相应的页面描述项;
4、将页面描述项中给出的页面基地址与线性地址中的offset位段相加得到物理地址。
目录只有一个,页表有多个。
为什么要使用两个层次呢?这是出于空间效率的考虑(节省空间)。如果将dir和page合并在一起,则此时页面表大小就将是1MB(2^20),但实际上一个进程不需要4G全部空间,所以大部分表项是空的,用不到,造成了主存空间的浪费。设置成二层寻址,则页表可以视需要而设置,如果目录中的某项为空,就不必设立相应的页表(1KB),这样就节省了大量的用于存储页表的空间,占用内存越小的程序效果越明显,如果一个进程真的使用了4G的存储空间,则二层寻址会比一层寻址多花1KB的空间用来存目录。另外,一个目录或者每一个页表都是1024项,每一项都是4B,所以总大小都是4KB,恰好可以放到一个页面中,不需要跨快存储,正是因为这样,在64位Alpha CPU中页面大小是8KB目录表项和页面表项的大小都变成了8B,每个表都是1024项。页目录、页表都不会跨页面存储,否则系统将变得复杂。
平台名(64位)
页大小
寻址使用位数
分页级别数
线性地址分级
alpha
8k(还支持其他页大小)
43
3
[10]+[10]+[10]+[13]
ia64
4k(还支持其他页大小)
39
3
[9]+[9]+[9]+[12]
ppc64
4k
41
3
[10]+[10]+[9]+[12]
sh64
4k
41
3
[10]+[10]+[9]+[12]
x86_64
4k
48
4
[9]+[9]+[9]+[9]+[12]
80386目录项结构:
typedef struct {
    unsigned int ptba:20; /*页表基地址的高20位,因为页面是4K对齐的,所以20位就能找到基地址*/
    unsigned int avail:3;/*供系统程序员使用*/
    unsigned int g:1;/*global,全局性页面*/
    unsigned int ps:1;/*页面大小,0表示4KB*/
    unsigned int reserved:1;/*保留,永远是0*/
    unsigned int a:1;/*accessed,已被访问过*/
    unsigned int pcd:1;/*关闭(不使用)缓冲存储器*/
    unsigned int pwt:1;/*Write Through,用于缓冲存储器*/
    unsigned int u_s:1;/*为0时表示系统(或超级)权限,为1时表示用户权限*/
    unsigned int r_w:1;/*只读或可写*/
    unsigned int p:1;/*为0时表示相应的页面不在内存中*/
} 目录项;
页表项的结构基本上与目录项的结构相同,也是32位结构,中间有两个位解释方式不一样。
typedef struct {
    unsigned int ptba:20; /*页表基地址的高20位,因为页面是4K对齐的,所以20位就能找到基地址*/
    unsigned int avail:3;/*供系统程序员使用*/
    unsigned int g:1;/*global,全局性页面*/
    unsigned int reserved:1;/*保留,永远是0*/
    unsigned int D:1;/*Dirty,表示该页面已经被写过,所以已经脏了(与读入主存时数据不一致的页面)*/
    unsigned int a:1;/*accessed,已被访问过*/
    unsigned int pcd:1;/*关闭(不使用)缓冲存储器*/
    unsigned int pwt:1;/*Write Through,用于缓冲存储器*/
    unsigned int u_s:1;/*为0时表示系统(或超级)权限,为1时表示用户权限*/
    unsigned int r_w:1;/*只读或可写*/
    unsigned int p:1;/*为0时表示相应的页面不在内存中*/
} 页表项;
页表项或者目录项结构中,当p标志为0时,表示此4KB页面不在内存中,根据其他一些有关寄存器的设置,CPU可以产生一个“页面错”异常(缺页中断),内核中有关异常的服务程序就可以从磁盘上的页面交换区将相应的页面读入内存,并更新相应页表项(或者目录项)的基地址和p标志;相反,也可以将暂时不用的页面写入磁盘的交换区(swap分区),并设置p标志为0。这样就能实现页式虚存。Pentium处理器开始,Intel引入了PSE页面大小扩充机制,当目录项的ps位为1时,页面大小就是4MB,页面表就不再使用,这时低22位全用来做页内偏移。
 
i386 CPU中,还有个寄存器CR0,其最高位PG是页式映射机制的总开关,当PG为1时,CPU开启页式存储管理的映射机制。
从Pentium Pro开始,Intel又做了扩充,Intel在另一个控制寄存器CR4中又增加了一位PAE,当PAE位为1时,地址总线的宽度就变成了36位(增加了4位,能寻址64GB物理空间),页式存储管理的映射机制也自然地有所改变(略)。除存储管理外,80386还有很强的高速缓冲存储和流水线功能,但是对软件(操作系统内核等)而言,很大程度上是透明的。
 
为了支持64位CPU,gcc增加了新的基本数据类型:long long int。

posted on 2018-08-08 16:49  心渐渐失空  阅读(229)  评论(0编辑  收藏  举报