当前位置: 首页 > 科技观察

Linux如何从零开始学习Linux保护内核代码?

时间:2023-03-14 19:01:14 科技观察

从16位到32位808616位模式8038632位模式从实模式到保护模式如何进入保护模式GDT全局描述符表GDTR全局描述符表RegisterSegmentDescriptorLookupinthe7articles,我们一直在学习最原始的8086处理器的基本原理,重点是内存的寻址方式。即:CPU如何通过[段地址:偏移地址]对内存进行寻址。不知道大家有没有发现一个问题:所有的程序都可以读取和修改内存中任意位置的数据,即使这个位置不属于这个应用程序。这是很危险的,想想那些心怀恶意的黑帽黑客,他们要干坏事,想怎么说就怎么说!面对这种不安全的行为,处理器也无能为力。所以Intel从80286开始就加入了保护模式的机制。PS:相应的,之前的8086中的处理器执行模式被称为“实模式”。80286虽然没有形成一定的气候,但是为后来的80386处理器提供了基础,使得386大获成功。本篇我们将从80386处理器入手,谈谈保护模式到底保护了谁?底层采用什么机制来实现保护模式?我们的学习目标是理解下图:从16位16位模式到32位8086在8086处理器中,所有的寄存器都是16位的。正因为如此,为了得到一个20位的物理地址,处理器需要将段寄存器的内容左移4位,然后加上偏移寄存器的内容,得到一个20位的物理地址,最后最多访问1MB内存空间。例如:访问代码段时,将cs寄存器左移4位,然后加上ip寄存器,得到20位的物理地址;对于20位地址,最大寻址范围是2的20次方=1MB空间;上面的寄存器都是16位的,在这种模式下,只能分段访问内存。而且每个段的偏移地址最大只能达到64KB的范围(2的16次方)。访问代码段时,使用cs:ip寄存器;访问数据段时,使用ds寄存器;访问栈时,使用ss:sp寄存器;80386的32位模式进入32位处理器后,这些寄存器被扩展为32位:从寄存器的名字可以看出前面加了字母E,意思是Extend。对于这些32位寄存器,低16位保持与16位处理器的兼容性,即可以使用16位寄存器(例如:AX)和8位寄存器(例如:AH、AL)。注意:高16位不能单独使用。下图是32位处理器的另外4个通用寄存器(注意不能当8位寄存器使用):在32位模式下,处理器中的地址线达到32位,最大内存空间寻址能力达到4GB(2的32次方)。在32位处理器中,仍然兼容16位处理方式,此时仍然使用16位寄存器;如果不兼容,它将失去很大的市场份额;是不是觉得上面的寄存器图有漏电的地方?少了什么东西?是的,图中没有显示段寄存器(cs、ds、ss等)。这是因为在32位模式下,段寄存器的长度仍然是16位,但是对其内容的解释已经发生了非常非常大的变化。它们不再代表段的基地址,而是一个索引值和其他信息。通过这个索引值(或索引号),去一个表中找到段的真实基址(有点类似中断向量查表的方法):有的书把描述符叫做:segmentselector;还有一些书上把段寄存器中的值叫做索引值,在GDT中段描述符的偏移量叫做选择器;不用纠结名字,明白其中的道理即可;因为处理器有32条地址线,可寻址范围已经很大(4GB),所以理论上不需要8086那样的寻址方式(段地址左移4位+偏移地址)。但是由于x86处理器的基因,在32位模式下,仍然需要以段为单位访问内存。这里请大家不要混淆:刚才介绍的段寄存器的内容只是为了说明如何找到一个段的基地址,也就是说:对于8086来说,段寄存器中的内容左移后4位,是段的基地址;对于80386,段寄存器中的内容是一个表的索引号,通过这个索引号,可以找到表中相应位置的内容,这个内容中有段的基地址(怎么找,如下面所描述的);找到这个段的基地址后,在访问内存的时候,还是按照段机制+偏移量的方式。由于在32位处理器中,存放偏移地址的寄存器都是32位的,最大偏移地址可达4GB,所以我们可以将段的基地址设置为0x0000_0000:这种分段方式称为“Flatmodel”也可以理解为不切分。看到这里,是不是还记得之前的一篇文章,我们曾经画过Linux操作系统中的分段模型:现在你大概明白了:为什么这4个段的基址和段长,都一样?从实模式到保护模式如何进入保护模式CPU如何判断:当前是否在执行实模式?还是保护模式?在处理器内部,有一个寄存器CR0。该寄存器的bit0的值决定了当前的工作模式:bit0=0:实模式;bit1=1:保护模式;处理器上电后,默认工作在实模式。当操作系统准备进入保护模式时,将CR0寄存器的bit0置1,CPU开始工作在保护模式。也就是说:bit0设置为1之前,CPU是按照实模式的机制寻址的(段地址左移4位+偏移地址);当bit0设置为1时,CPU按照保护模式下的机制寻址(通过段寄存器中的索引号,在一个表中查找段的基地址,然后加上偏移地址)。GDT全局描述符表因为这个表中的每一个表项(Entry)都描述了一个段的基本信息,包括:基地址、段长度限制、安全级别等,所以我们称它为全局描述符表(GlobalDescriberTable,GDT).之所以称为全局的,是因为每个应用程序也可以将段描述符信息放在自己私有的局部描述符表(LocalDescripterTable,LDT)中,在以后的文章中会介绍到。处理器规定第一个描述符必须为空,主要是为了避免一些程序错误。从上图可以看出:GDT中每个表项的长度为8个字节,描述了一个段的具体信息,如下图:黄色部分:表示该段在内存中的基地址。绿色部分:表示该段的最大长度。当你第一次看到这张图的时候,是不是有两个疑问:为什么段的基地址不是用连续的32位来表示的呢?为什么段边界是20位?20bits只能代表1MBScope?第一个问题的答案是:历史原因(兼容性)。第二个问题的答案是:每个描述符中的标志位G提供了对段边界更细粒度的描述:如果G=0:表示段边界以字节为单位,此时,段边界的最大表示范围为1MB;如果G=1:表示段限制为4KB,此时段限制的最大表示范围为4GB(1MB乘以4KB);为了完整起见,我把所有标志位的含义总结如下,以便于参考:D/B(bit22):用于判断数据段或堆栈段使用的偏移寄存器是16位还是32位。L(bit21):只在64位系统中使用,暂时忽略。AVL(bit20):处理器不使用这个位的内容,操作系统可以用这个位来做一些事情。P(bit15):表示该段的内容当前是否驻留在物理内存中。在Linux系统中,每个应用程序都有4GB(32位处理器)的虚拟内存空间,多个应用程序可以同时存在于一个系统中。这些应用程序在虚拟内存中的代码段、数据段等最终都会映射到物理内存中。但是,物理内存空间毕竟是有限的。当物理内存紧张时,操作系统会将那些当前没有被执行的段的内容暂时保存在硬盘上(此时段描述符的P位被设置为0),这称为交换出去。当换出的段需要执行时,处理器发现P位为0,知道段中的内容不在物理内存中,于是在物理内存中寻找空闲空间,然后进行拷贝将硬盘中的内容写入物理内存,并将P位设置为1,这称为换入。DPL(bit14~13):指定段的特权级别。处理器一共支持4个特权级:0、1、2、3(最低特权级)。例如:操作系统代码段的特权级为0,而当一个应用程序刚启动时,操作系统分配给它的特权级为3,那么应用程序就不能直接转移到要执行的操作系统。在Linux操作系统中,只使用了0和3两个特权级别。S(bit12):决定这个段的类型。TYPE(bit11~8):用来描述段的一些属性,比如:可读、可写、扩展方向、代码段的执行特性等,这里的compliance属性不好理解。主要用于判断一个权限级别低的代码是否可以进入另一个权限级别高的代码。如果可以进入,当前任务的requestlevelRPL有没有变化(这个问题后面再说)。另外,操作系统可以在物理内存的换出和换入计算策略中加入A标志。这样就可以避免将最近频繁访问的物理内存换出,从而获得更好的系统性能。GDTR全局描述符表寄存器还有一个问题需要处理:GDT表本身也是数据,需要存放在内存中。那么:它存储在内存中的什么位置?CPU如何知道起始位置?在处理器内部,有一个寄存器:GDTR(GDTRegister),里面存放着两条信息:我们可以从上面的一篇文章Linux从07开始:【中断】这么重要,它的本质是什么?类比中断向量表的安装过程:程序代码将每个中断处理程序的地址放在中断向量表中的对应位置;中断向量表的起始地址放在内存地址0处;也就是说:处理器到固定地址0去查找中断向量表,这是一个固定地址。对于GDT表来说,它的起始地址是不固定的,可以放在内存中的任意位置。只要将这个位置保存在寄存器GDTR中,处理器就可以在需要的时候通过GDTR定位到GDT的起始地址。实际上,GDT不能在刚上电的时候放在内存的任何地方。因为在进入保护模式之前,处理器仍然工作在实模式,只能寻址1MB的内存空间,因此,GDT只能放在1MB以内的地址空间。进入保护模式后,可以寻址更大的地址空间。这时可以将GDT重新放入一个更大的地址空间,然后将新的起始地址存入GDTR寄存器。从GDTR寄存器的内容可以看出,它不仅存放了GDT的起始地址,还限制了GDT的长度。这个长度一共是16位,最大值是64KB(2的16次方),一个段描述符信息是8B,那么64KB的空间一共可以存放8192个描述符。这个数字对于操作系统或一般应用程序来说绰绰有余。段描述符查找原理在上面的段寄存器图中,我们只是说明了段寄存器还是16位的。在保护模式下,其内容的解释与实模式下的完全不同。我们以代码段寄存器CS为例:RPL:表示当前正在执行的代码段所请求的特权级;TI:表示在哪个表中查找该段的描述信息:全局描述符表(GDT)还是局部描述符表(LDT)?当TI=0时,在GDT中找到段描述符;当TI=1时,在LDT中查找段描述符;假设当前代码段寄存器cs的值为0x0008,处理器按照保护模式机制来解释内容:TI=0,表示在GDT中寻找段描述符;RPL=0,表示请求权限级别为0;描述符索引为1,这意味着段描述符位于GDT的第一个条目中。由于每个描述符占用8个字节,所以这个描述符的起始地址位于GDT中偏移地址8处(1*8=8);找到这个段描述符入口后,就可以从中得到这个代码段的具体信息:代码段在内存中的基地址在哪里;代码段的最大长度是多少(取指令时,如果偏移地址超过这个长度,会抛出异常);代码段的特权级别是多少,当前是否驻留在物理内存中等;另外,从上面介绍的GDTR注册内容来看,它限制了GDT中最多可以存放8192个描述符。我们可以从代码段寄存器中描述符索引域占用的13位进行计算,可以发现最多8192个段描述符。2的13次方=8192。至此,处理器已经找到保护模式下一个段的所有信息。下面的步骤是:到这个段所在的内存空间,执行里面的代码,或者读写里面的数据。我们将在下一篇文章中继续。..------完------本文主要介绍80386处理器保护模式下段寄存器的使用,以及通过段描述符查找段的具体信息。从描述的内容来看,已经越来越接近我们的最终目标:Linux操作系统中的执行方式!因为这些底层知识是Linux操作系统运行的基础。了解了这些基本内容之后,在后面学习Linux具体模块的时候,可以回过头来查看其在处理器层面的底层支持。最后,如果本文对您有帮助,请转发给您身边的技术小伙伴。也是对我继续输出文章最大的鼓励和动力!谢谢你!让我们一起出发,继续朝着我们的目标前进!