linux1.0内核代码学习(五) 之保护模式编程

时间:2024-04-08 21:42:57

linux1.0内核是压缩的zImage映像文件,其内部格式组织如下: 

linux1.0内核代码学习(五) 之保护模式编程

其启动各个阶段在内存中的位置如下:

linux1.0内核代码学习(五) 之保护模式编程

 

下图是指zboot/zSystem中程序调用自带的解压缩程序,将tools/zSystem解压到0x100000的内存地址处,并跳转到0x100000处执行boot/head.s程序。这个阶段已经处于32位保护模式运行,使用的gdt、idt表是setup程序中设置的临时表,开启分段内存保护模式,但没有开启分页内存保护模式。

 linux1.0内核代码学习(五) 之保护模式编程

 下图表示在1M(0x100000)以上内存空间中执行boot/head.s程序时的内存位置图,head.s程序主要做得事情如下:

按照顺序初始化具有256项的中断描述符表IDT为哑中断;再次检查A20地址线是否开启;将setup.s程序在0x90000处保存的参数拷贝到标号empty_zero_page=0x105000开始的头2k内存空间;如果有命令行参数,则将命令行参数拷贝到0x105800开始的2k内存空间;检查cpu类型是386/486/还是其他类型;设置页目录表并开启分页保护模式;加载全局描述符表寄存器和中断描述符表寄存器覆盖掉setup.s程序中设置的临时表;设置堆栈指针;最后跳转到start_kernel开始内核c语言部分的执行。页目录表位于地址0x101000~0x102000-1处共4K内存空间,有1024个页目录表项,每个页目录表项可以寻址4M的物理内存空间,因此这个页目录表项可以寻址4G的内存空间。初始化页目录表的第一项和第768项的内容为页表0的地址(即pg0=0x102000),页表0(pg0)初始化了0~4M的物理内存空间,这个时候内核是位于0~4M的物理空间里面的,对于执行内核的初始化寻址已经够用了,剩下的物理内存的页表在后续的程序中再初始化。而将页目录表的第768项也设置为指向pg0,是因为在后面设置全局描述符表(gdt表)的时候将内核的代码段基地址设置在了0xC0000000=3G的虚拟地址处,而在开启了分页的内存管理机制后,虚拟地址3G的寻址将会找到页目录表中的第768项(因为768*4M=3G),而768项里面保存的是pg0页表,pg0是0~4M的物理内存,而内核代码正好处于这个内存地址中。

linux1.0内核代码学习(五) 之保护模式编程

下面具体来分析一下在开启了分页内存管理模式的32位保护模式下取一条指令的地址映射过程,当然在开始分析之前要先来点基础知识,这里只切几张图,能把过程将清楚就OK了。

linux1.0内核代码学习(五) 之保护模式编程

 linux1.0内核代码学习(五) 之保护模式编程

 linux1.0内核代码学习(五) 之保护模式编程
 linux1.0内核代码学习(五) 之保护模式编程

linux1.0内核代码学习(五) 之保护模式编程

 linux1.0内核代码学习(五) 之保护模式编程
 linux1.0内核代码学习(五) 之保护模式编程
 
 上面几张图差不多就可以讲清楚整个流程了,选取head.s中的代码段如下:

        lgdt gdt_descr     /*加载全局描述符表寄存器*/

lidt idt_descr     /*加载中断描述符表寄存器*/

ljmp $(KERNEL_CS),$1f

1: movl $(KERNEL_DS),%eax # reload all the segment registers /*改变gdt表后重新加载*/

mov %ax,%ds # after changing gdt.                      /*所有的段寄存器*/

mov %ax,%es

mov %ax,%fs

mov %ax,%gs

lss stack_start,%esp  /*将stack_start地址处低4字节的值赋值给esp,高2字节的值赋值

给ss,即(stack_start)-->ss:esp*/

xorl %eax,%eax

lldt %ax        /*加载局部描述符表的段选择符。段选择符长度为16位(2个字节);位0-1表示

请求的特权级0-3,linux操作系统只用到2级:0级(内核级)和3级(用户级);

位2用于选择全局描述符表(0)还是局部描述符表(1);位3-15是描述符表项的索引,

指出选择第几项描述符。这里ax=0x0000(0b0000,0000,0000,0000)表示请求特权

级0,使用全局描述符表GDT中第0个段描述符项,该项指出代码的基地址是0。*/

pushl %eax # These are the parameters to main :-)

pushl %eax       /*这些是调用main函数的参数,这里是init/main.c中的函数start_kernel*/

pushl %eax

cld # gcc2 wants the direction flag cleared at all times

call start_kernel

L6:

jmp L6 # main should never return here, but

# just in case, we know what happens.

/*main程序绝对不应该返回到这里,不过为了以防万一,所以添加了该语句,这样

                  我们就知道发生什么问题了。*/

gdt:

.quad 0x0000000000000000 /* NULL descriptor */

.quad 0x0000000000000000 /* not used */

.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */

.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */

.quad 0x00cbfa000000ffff /* 0x23 user   3GB code at 0x00000000 */

.quad 0x00cbf2000000ffff /* 0x2b user   3GB data at 0x00000000 */

.quad 0x0000000000000000 /* not used */

.quad 0x0000000000000000 /* not used */

.fill 2*NR_TASKS,8,0 /* space for LDT's and TSS's etc */

 

 

我们具体来看ljmp $(KERNEL_CS),$1f这条指令的执行,段地址是段选择符KERNEL_CS指示的,段内偏移地址为$1f。从内核的启动过程我们知道head.s位于0x100000~0x100FFF这段4K的内存地址中,$1f的地址我们假设为0x00100xxx,首先这条指令的段地址是通过段选择符KERNEL_CS=0x10来选择的,这里0x10的含义(0b0000,0000,0001,0000)是请求特权级0(位0-1=0)、选择全局描述符表(位2=0)、选择表中第2项(位3-15=2),它正好指向gdt表中的代码段描述符项(.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */)。可知段的基地址为0xc0000000,然后加上假设的$1f的地址0x00100xxx,则为0xC0100xxx,如果只开启了分段内存管理,那么这个地址就是物理地址,cpu就可以从这个地址取指令来执行了。但是,现在开启了分页模式,根据上面的图示可以知道还需要进行二次寻址,即将0x00100xxx拿来二次寻址,22~31这10位为0,寻址到页目录表的第0项,内容为pg0=0x00102007,表示其页表基地址为0x00102000,在用12~21(01,0000,0000)这10bit来在这个页表中进行偏移到第256项找到物理内存的基地址即256*4K=0x100000(我们知道pg0中映射的物理内存空间为0~4M地址空间),最后的0~11这12个位是偏移地址,基地址+偏移地址=0x00100xxx,正好是我们要执行的指令地址,整个寻址地址变换过程就是这样。感觉保护模式就像饶了一个大圈一样!!!