本文出自 “New World” 博客,请务必保留此出处http://snower.blog.51cto.com/2918921/550845
研究x86其实主要的目的还是操作系统,即使在有原码的情况下你还得要看懂汇编,要看懂汇编就得明白体系结构,最好再懂点儿硬件原理……到此热身运动基本完成,开始看操作系统原码吧!值得注意的这些准备活动还都只是看懂,而世界上最遥远的距离不是“生与死”,也不是“曾经柜台上有一袋盐摆在我面前我没有珍惜,等到一位大妈将柜台扑倒才追悔莫及”,而是“看懂了与写出来的距离”。
当然,如果你只想对操作系统做定性的分析或者关注的是其中的某些算法,那么恭喜你,不用受x86复杂的体系结构煎熬了……我所说x86的复杂也是相对于DSP,因为DSP的应用程序开发人员不会去写恶意的代码,这使得DSP不用过多的考虑安全性而把重心放在性能上。
x86不同,一方面,它的市场定位和跑在其上的windows系列操作系统(漏洞太多)注定让那些
Intel的设计师们想方设法的把内存的物理地址掩藏起来,这也是保护模式的主要意义;另一方面,保护之意也可以理解为避免因程序设计上的缺陷而导致的非恶意进程越权行为。
根本上说是CPU对进程特权优先级的管理。可以将x86的保护模式理解为寻址方式的模型,或者说是一种内存管理机制。
在学习保护模式的过程中我主要有以下几点疑惑:
1.对x86中段的理解?2.什么叫一致性代码段?这里又可以引出系统调用的问题。3.系统调用到底是个啥?4.中断号、中断向量、IRQ三者之间的关系?5.跳转指令中,call和jmp有什么区别?6.从实模式跳转到保护模式之前,要将cs寄存器保存起来,而cs的初始值从何而来?7.特权级转移的过程为什么要伴随着堆栈的变化?如果不变换堆栈,有什么方案可以替换?8.调用门过程中,被调用者堆栈需要存储调用者的eip和cs,那个eip和cs的存储的先后顺序有影响吗?9.内存的分段机制已经很好的保护了内存,那么还要分页机制有什么用啊?分页机制的妙处何在?10.一条指令能做多少事?11.保护机制,“保护”二字都体现在哪?
问题1.对x86中段的理解?
段是一段具有相同属性的代码或者数据的集合,实际上对于CPU而言并不知道段的存在,CPU只知道从内存中取得一个字节的数据,然后将这个字节直接做为指令执行(如ldgdr)或者再从内存和寄存器中取得操作数拼接成指令执行(如mov ax cs)。如果CPU不需要段的话,那么可以说段并非必须的。在某种情况(如不需要内存保护,直接对物理地址进行操作)下,我们可以把整个内存都看成一个段;也可以按常用的代码段、数据段、堆栈段来分;当然也可以自己再随便再添加N个段,看你!
x86中,如果没有全局描述符表和局部描述符表,那么段没有任何意义。这两个表中的段描述符定义了每个段的基地址、段界限和访问特权级等属性信息(由于此内容并非本文重点,这里不再赘述),前面说过我们自己可以添加任意多个段,可如果你不添加相应的段描述符那自定义的段将会被编译器默认为代码段,该自定义段也就没有任何用处。
一般来说常用的段有三个:代码段(.code或.text)、数据段(.data)、堆栈段(.stack)。它们各有各的段基址、段界限和段属性,对应的选择子也分别存储在cs、ds和ss段寄存器中。这三个段也构成了x86的保护模式的基本构架,
CPU主要的寻址操作都在这三个段中;
寻址空间的范围和大小取决于段描述符段基址和段属性初始化的值;应用程序对一块内存的访问权限也主要是由这三个段描述符决定的。这也就保证了应用程序无法访问不该访问的内存空间,从而对内存起到了保护作用,从另一角度讲这也算是一种内存管理方案。
/*问题2:什么叫一致性代码段?*/(暂时对原帖不感兴趣的问题)
出于内存保护的目的,各个段都有自己的特权级,高特权级进程访问低特权级的段自是没问题,可低特权级进程就不能访问高特权级段吗?当然能,系统调用就是一种从低特权级代码跳转到高特权级代码过程,一般是从应用程序的代码(特权级一般为3)跳转到操作系统内核某函数接口(特权级一般为0)的代码,有关系统调用的故事详见问题3。
如果想从一个低优先级跳转到高优先级代码,需要目标代码段是一致性代码段,而这时你也许会认为应用程序此时运行在高优先级下了,那你就错了!一致性代码段最大的特点就是即使你跳转到高优先级的一致性代码段,但当前任进程的优先级不变,仍为低优先级。比如你当前运行的应用程序(为一个进程)优先级为3,运行过程中需要跳转到一个优先级为0的一致性代码段,OK,跳转成功之后,当前进程的优先级仍为3。
(这里其实有许多保护机制的概念,如描述符、CPL、DPL、RPL等,我只想概念性的讲一下,所以省略这些细节,你一定也不会很想看到这种涉及到寄存器级的东西)
一句话总结一下:一致性代码段是在两个不同特权级代码段之间做跳转的时候用到的定义,当从一个特权级较低的代码段跳转到特权级较高的代码段的时候,特权级较高的代码段可以临时的将特权级降到较低特权级,进而允许这次跳转并继续执行,那么就说该目标代码段是一致性代码段,否则是不一致性代码段。
也许你明白了,这种一致性代码段的机制是为了避免低优先级进程通过跳转而变向的提高自己的优先级,进而在高优先级特权下对你的操作系统你的计算机为所欲为!而明白这些也就可以了,详细的如果你有兴趣可以去查手册。
ps:下面的问题原帖只讲了问题4,我暂时不感兴趣,其他没有讲,额滴个神,服了。。。
问题3:系统调用到底是个啥?
系统调用是操作系统为应用程序提供的一种接口函数,比如说任务挂起函数。同样出于安全性考虑,操作系统不想让用户直接调用内核函数或者接口,于是就制定了某种规则,基于中断机制的系统调用就是其中的一种。我对系统调用的理解:
与其说系统调用是一种函数,到不如说系统调用是一种协议,一种由操作系统制定的应用程序如何调用内核函数接口的规则。
要不然你就别翻译成系统调用,这种蹩脚的翻译真是让读者一头雾水,本来《操作系统概念》就号称计算机专业的天书,再加上这些莫名其妙的定义,我们除了更莫名其妙之外还有其他的结果吗?相同的例子还有“任务上下文”,笔者当初看到这个定义的感觉简直就如刘姥姥穿越到计算中心——太TM扯了!心中不住的叹息,学个OS怎么还把中学给文章分段的那套用上了,这任务的上下文该分成“总分总”啊?还是“总分”啊?翻译的仁兄,您不是上帝派来玩儿我的吧……咳咳,回来接着说系统调用(没办法,权威的书都是这翻译的,我也只能沿用了)。
虽然系统调用已经有了新的实现方式,但学习需要循序渐进,我们就从经典开始。
在奔腾Pro以前,x86中的系统调用是通过中断机制实现的,对应的中断号是0x80。中断主要分为外部中断和陷阱:外部中断是由硬件通过相应引脚,发送中断信号给CPU产生的,比如说时钟中断,外部中断也叫硬中断;陷阱是由trap指令产生的,由编程人员用手敲的,比如说这里讲到的0x80号中断,陷阱又叫做软中断。
Linux中常见的系统如fork(),当你调用一个fork()去创建一个进程时,实际上你离Linux的内核还很远,因为这个fork()只是一个接口,它做的只是以下三步:
1.把系统调用的编号存入EAX;
2.把函数参数存入其它通用寄存器;
3.触发0x80号中断(int 0x80)。
中断触发后,0x80对应的中断处理子程序(ISR)开始运行,并调用真正实现fork操作的内核接口函数,最后将返回值存入到EAX中。
以上就是Linux在x86中,中断机制实现的系统调用的主要过程。
当然,也有人把实现真正功能的那些内核接口函数叫作系统调用,不过我觉得那都是浮云,以后你看到这些不懂的名词就看一下英文原文,看不懂它什么意思也没事儿,你就把它当个名词就得了,哪怕你叫它“小黑”都成,然后再看它的详细解释,别跟着译者的翻译理解就行了,要不然真成天书了。
ps:下面的问题原帖只讲了问题4,我暂时不感兴趣,其他没有讲,额滴个神,服了。。。