在stage2开始的活动中,我们一定要设置好C环境。GRUB下可以自由输入的指令,像kernel、boot、initrd等,都是由C来实现的。这样对保护模式的需求就呼之欲出了,最起码的,C语言通过保护模式实现了更大范围的硬件支持。 |h6, .#n 位于stage2中的asm.s汇编文件更多的是提供了一些基本的汇编子程序,可以称之为GRUB基本函数库。而实模式切换到保护模式的函数ENTRY(real_to_prot)以及保护模式到实模式的ENTRY(prot_to_real)就位于asm.s中。 $W:_ f_%[P 如果程序通过调用ENTRY(real_to_prot)函数切入到保护模式下,那么这个子函数ENTRY(real_to_prot)是怎样具体工作的呢?首要的,是要建立合适的全局描述符表GDT,不要忘了,保护模式之所以不同于实模式首先在于寻址模式的变化。保护模式通过GDT来间接寻找内存地址。GDT标号位于asm.s的末尾,内容当然是要包含各种门描述符和段的描述符。其第一个8字节的位置并不使用。GDT的内容如下: [~0x '_048 ***************************************************************** tSpkl 2fs gdt: EXZ0|h6?1 .word 0, 0 -zh6?#8 .byte 0, 0, 0, 0 ;(GDT的第一个8字节不使用) E6<G! .word 0xFFFF, 0 ;代码段描述符,可以看到段界限低16位为0xFFFF WVLF!56 .byte 0, 0x9A, 0xCF, 0 ;第6、7字节为1100111110011010,段界限粒度G为1,段界限高四位都为1,可寻址到0xFFFFF*4K,即4G字节;段基址为0;权限为执行/读, mIMrk]1 .word 0xFFFF, 0 ;数据段描述符 M;)4BP%b .byte 0, 0x92, 0xCF, 0 ;权限为读/写 #H |./E0 .word 0xFFFF, 0 ;16位实模式代码段描述符 w}+?[""{!8 .byte 0, 0x9E, 0, 0 ;段界限粒度G为0,段界限为0xFFFF,可寻址到0xFFFF,即64K字节;段基址为0;权限为执行/读、一致码段 4EF(e,tT .word 0xFFFF, 0 ;16位实模式数据段描述符 4] PvZ8@ .byte 0, 0x92, 0, 0 ;权限为读/写 ]1 1>U| ***************************************************************** W9YpFB@ 可以看到,GDT一共包含了4个有效的段描述符(包含空描述符的话,一共是5个)。一个段描述符会指出段的32位基地址和20位段界限(即段长)。以第一个有效的段描述符为例,它的形式应该是: [!n#zU& ***************************************************************** 6jUl[<# 字节 二进制码 >Okk0%K 0 1 1 1 1 1 1 1 1 a)gYGeiz 1 1 1 1 1 1 1 1 1 &Lp6AE[vx 2 0 0 0 0 0 0 0 0 Nf 2Cv;, 3 0 0 0 0 0 0 0 0 z:58go;g 4 0 0 0 0 0 0 0 0 V_]up]Z>X 5 1 0 0 1 1 0 1 0 AGJWwlkj 6 1 1 0 0 1 1 1 1 n'Uo{Q[e 7 0 0 0 0 0 0 0 0 RPm/"`s ***************************************************************** pn)B\IC{ 它的含义或者它描述了一个段基址为0x00000000(32位)、段长为0xF0000乘以4k(20位长度在此处表示一个段中的页数,此刻相当于4GB)、存在于内存中、特权级为2的、可读的、系统代码段。更详细的情况可以参考段描述符的格式。其中,第6字节的D位表示缺省操作数的大小。这也是我们称后两个段描述符为伪实模式代码段或数据段的原因。 vqp.3FDB ENTRY(real_to_prot)函数还使用48位指针gdtdesc指向该GDT。gdtdesc是在asm.s的最后部分,内容如下: 1n B* **************************************************** y3vd3nER gdtdesc: X%Ys%< .word 0x27 ;GDTR界限,可以描述(0x27+1)/8=5个描述符,和上面一致 @@}{C(_ .long gdt ;GDTR基地址,指向gdt B8 EhWxm **************************************************** >1"votA) 因为我们在ENTRY(real_to_prot)函数中使用lgdt gdtdesc指令将这个gdtdesc(GDT descriptor,GDT描述符)装入到GDTR寄存器中,这是一个48位的寄存器,用来保存GDT的32位基地址和16位GDT的界限(也即长度,除以8字节就得到描述符的数目),此处gdtdesc的内容被装载到GDTR中,含义在上面的注释中。 (#C;EG0% 那么现在实模式到保护模式的第一步(加载全局描述符表)就完成了,接下来需要设置cr0寄存器。其中,cr0的0位是PE位(protected enable),如果置1,则保护模式启动。很明显的,ENTRY(real_to_prot)函数要这么做。通过movl %cro,%eax;orl $CR0_PE_ON,%eax; Z>}ImWD[ movl %eax,%cr0来使cr0寄存器变为0。 (#(,1hX{t 看样子,保护模式已经ok了。然而,gemfield不得不提醒的是,实模式和保护模式中的段寄存器——虽然都为16位,但含义却是不一样的。实模式下,段寄存器装载的是段基址,而在保护模式下,却装载的是索引,对于全局描述符表GDT的索引。因此,如果不对各段寄存器从新设置的话,怎么能够想象保护模式已经诞生了呢? (I#]T: [ 首先cs寄存器比较特殊,它不能通过直接赋值的方法来改变值,一般是通过跳转指令来实现。所以在实模式的最后一条指令执行的时候,其实跳转指令已经被预取到队列中了。在执行完实模式的最后一条指令后,程序切换到保护模式,这条跳转指令就是执行的第一条指令。 dug.i` gemfield通过ljmp $PROT_MODE_CSEG,$protcseg指令来改变cs的值,$PROT_MODE_C在share.h中被定义为0x8,此处ljmp的含义是程序跳转到cs:ip=0x8:$protcseg的位置,自然的,cs被设置为0000000000001000(16位),那么这个选择子(或者索引值)的含义就是指向GDT的、特权级为0的、是全局描述符表中第2个描述符。这个描述符正是gemfield在本文第四段介绍的那个。那么新的指令地址不就因此而改变了吗?没错,cs指向的描述符给出了0x00000000的段基址,还要和IP这个32位的偏移量相加($protcseg),这样新的地址重新指向了protcseg标号。 ^^h 6s*y 那么程序接着从protcseg处开始执行,对ds、es、fs、gs、ss进行重新设置,赋值0x10,与上一段的含义类似,指向GDT中的保护模式数据段描述符。 bJ|]~O? 还有一点很明显的,保护模式并没有对堆栈描述符进行设置,因此对堆栈的操作就显得至关重要了。调用ENTRY(real_to_prot)子函数时程序本来就要进行压栈,现在进入保护模式,就把栈中保存的地址上的数据拿出来放在新栈(protstack)中继续使用,这样ret时原来的数据能继续在保护模式下使用。 Kr&xOg 而从保护模式进入实模式的过程和ENTRY(real_to_prot)类似。首先加载gdtdesc为进入伪实模式做准备(类似于飞船的气闸仓)。然后将保护模式下的堆栈esp内容保存至protstack堆栈中,然后将堆栈中内存地址上的数据放在STACKOFF中,然后设置新的堆栈,也就是STACKOFF,而这个堆栈中已经保存了需要的数据。 <+}[+*kd 接着通过movw $PSEUDO_RM_DSEG,%ax指令将段寄存器设置为0x20,以及下面指令将cs置为$PSEUDO_RM_CSEG(0x18)来将cs设置为0x18,来进行数据段选择符和代码段选择符的索引。然后再将cr0清零,进入实模式。然后再通过ljmp $0,$realcseg来将cs清零,并将IP指向下面的标号realcseg。到此时,我们已经完全进入真正的实模式了,然后将段寄存器清零。开中断,并返回。 I"p 4FV1- 这么一个大气的切换过程确实令人叹为观止,虽然grub中模式的切换有些简单,比如没有加载中断描述符表和局部描述符表等,从而不得不关闭中断。但不管怎样,这样艺术性的代码不可多得,那就让gemfield和大家一起享受这代码之美吧。 z=Uw&7.6]
|