分类: LINUX
2016-05-18 14:20:52
[本博客连载并且会持续修正,转载请注明出处:http://blog.chinaunix.net/uid-26583089-id-5729093.html]
我们经常听说“硬盘驱动”、“网卡驱动”等,但没听过“CPU驱动”、“内存驱动”,因为CPU、内存正是由内核直接控制的。我自己把内核比喻成“CPU和内存驱动,以及其它硬件驱动接口的调用者”,由C语言和汇编编写。所以“预备知识”一章,主要是从硬件角度介绍intel CPU寻址方式(段式内存管理、页式内存管理),以及C语言、汇编语言。
逻辑运算单元宽度(CPU位数)、数据总线宽度、地址总线宽度
4004:ALU宽度=8,数据总线宽度=8,地址总线宽度=16
8086:ALU宽度=16,数据总线宽度=16,地址总线宽度=20
80386:ALU宽度=32,数据总线宽度=32,地址总线宽度=32
最好的情况就是:ALU宽度、数据总线宽度、地址总线宽度都一致。多宽的数据,配多宽的运算单元,而且我们希望一个地址(指针)能正好保存在一个基本类型变量里。那么,4004、8086为什么“不听话”?
想一想,4004地址总线如果设计成8位,那寻址空间岂不太小啦,所以设计成16位,估计地址是用2个8位的变量保存吧。8086地址总线20位,是由于intel工程师预见64KB太小,所以增加到1MB,正因为这样的一个地址总线宽度,诞生了“段寄存器”以及段式寻址方式。到了80386的时候,终于“听话”了,地址总线和数据总线宽度一样,按道理可以丢弃“段寄存器”了,但为了向前兼容,必须保留“段寄存器”。另外!还要支持保护模式。
80386必须保留“段寄存器”,同时为了节约CPU资源,决定保护模式下也能利用到这些“段寄存器”,于是决定在“段寄存器”的基础上建立保护模式,当然,保护模式下“段寄存器”的含义就改变了,不再表示“段地址”,而是变成了“段选择子+进程权限”。
保护模式,必须保证对关键信息的维护,只能内核有权限,这样就依赖“权限划分+特权指令”。如果只有权限划分,只有版主有权进入穿越时空的大门,但世界上根本没有这扇门,那么大家还是平等的;如果没有权限划分,那么大家更是平等的,特权指令也就没有意义。
80386在电路层,将每个程序“分外之事”必须依赖的指令设计为特权指令,并提供权限划分的机制,内核最先执行,“执掌”特权,应用进程由内核创建,被设置为较低的权限,这样,应用程序与内核虽同在硬件执行,但“待遇”是不一样的,应用程序就不能“为所欲为”。
80386段式内存管理机制(保护模式)
先根据指令的性质(取指令地址/取普通数据),决定选用哪个段寄存器(CS/DS SS ES)。
保护模式下,段寄存器含义如下:
3~15表示段描述符在描述符表(由GDTR/LDTR指向)中的下标,TI表示用GDTR还是LDTR指向的描述符表,RPL表示进程的特权级别。
结合段寄存器中的3~15、TI,找到段描述符:
到此为止,段地址、段长度,以及比实模式多得多的段描述信息都有了,然后对比段寄存器中的RPL与描述符中的DPL,判断没有越权,就可以用最开始触发这次寻址过程的指令中的地址作为段偏移,找到最终的地址了。关于图里面的字段都是什么意思,一定要去《Linux内核源代码情景分析》看看哟。
另外,访问或修改GDTR/LDTR就被设计为前面提到的“特权指令”,而且老的CPU里没有这样的寄存器,也不用担心兼容问题。RPL、DPL就是与“权限划分”相关的设计。
疑问:
修改段寄存器又不是特殊指令,不担心用户进程运行时,把段寄存器的RPL修改成00吗?
这个是论坛里的nswcfd大牛从Intel手册帮我找的:
IF DS, ES, FS, or GS is loaded witch non-NULL selector
THEN
IF segment selector index is outside descriptor table limits
or segment is not a data or readable code segment
or ((segment is a data or nonconforming code segment))
or ((RPL > DPL) and (CPL > DPL))
THEN
GP(selector);
FI;
80386页式内存管理机制
段式内存管理缺点:
段式模式,内核是通过一个一个长短不一的段管理内存的,交换分区开启的情况下,磁盘也不方便管理这些段数据;
段分配的过大,内存利用不充分,而且和交换分区换入换出时,交换的数据也相应过大;分配的小,就要经常切换段,而每次切换,都会往CPU中的“影子”(为了不是每次访问段描述符都大老远跑到内存)复制一遍,而且每个段描述符index只有13位,最多寻找到8192个描述符,可能出现不够用的情况。
所以,Linux内核决定支持绕过80386的段式管理机制,采用页式管理方式。
什么叫“绕过”?
前面已经分析过80386在段式管理的基础上建立保护模式,既然硬件上保留这样的机制,软件即使“嫌弃”,不接收它的“好处”,但规则还是要遵守,比如:虚拟地址→线性地址(段式寻址)→物理地址(页式寻址),段式寻址的过程,仍然要保证不能越权。
为什么可以“绕过”?
① 不影响寻址正确性:每个进程都是用从0开始2^32长度的段,所以线性地址与虚拟地址一样,相当于啥都没做,虚拟地址直接进入页式寻址一样;
② “绕过”的内容,页式管理也提供,比如每个进程虽然没有自己独立的“段”了,但有自己独立的“页”。
线性地址含义:
然后依次通过CR3和22~31中的index找到页目录表、通过目录表和12~21中的index找到页表、通过页表和0~11段内偏移找到最终地址。有意思的是,用于实现页式管理机制的页目录、页表,也是设计成一页的大小。
为什么这么多级映射?
就如同一张大纸,能撕成10页,而一个进程用于记录映射关系只需要3页,就不用拿一张大纸,下面7页的面积空着又得不到利用了(数组的后半部分活生生的空着)。
页式管理可以保证安全的依据是什么?
页式管理的单元是页(大小通常为4KB),起始地址都是以4KB对齐,而不管CR3、目录表中的每个pte、页表中的每个pte,都是32位的,而使用12~31位就可以确定所指页的地址,所以低12位,就可以用于描述页的其它信息,实现安全自然也就有依据了。
我个人感觉理解“虚拟地址、物理地址”,以及接下来的第二章说明的“用户空间、内核空间”,简直就是学习内核的敲门砖。而且本章只是说明硬件如何根据事先安排好的段描述符表和页目录、页表,将虚拟地址映射为物理地址,第二章“存储管理”,正是从软件的角度,说明内核如何为每个进程安排这些映射关系表,相信等学习完第二章,再结合本章的内容,就会明白内存管理到底是怎么回事。
另外,学习的过程中,始终要提醒自己搞清楚,哪些是硬件完成的,哪些是软件完成的,而硬件提供给软件的“接口”,就是一些寄存器,以及相应位置的内存,比如内核设置好GDTR/LDTR,并在相应内存位置设置好地址映射关系,就指示硬件为将来完成虚拟地址到物理地址的映射做好了准备。