第二次启动保护模式

4.1.3 第二次启动保护模式

我们在链接vmlinux一节中看到,定义phys_startup_32为程序入口点,这个入口点才是解压缩后内核的真正开始的地方,而在链接脚本中,phys_startup_32是逻辑地址startup_32的物理地址。从进入保护模式那一刻起,程序就是用逻辑地址了,不过在启动分页机制之前,逻辑地址向物理地址的转换很简单,仅仅是vapa操作,即物理地址转成逻辑地址和逻辑地址转成物理地址。

 

第二个startup_32arch/x86/kernel/head_32.S85我们就开始分析它了

  85ENTRY(startup_32)

  86        /* test KEEP_SEGMENTS flag to see if the bootloader is asking

  87                us to not reload segments */

  88        testb $(1<<6), BP_loadflags(%esi)

  89        jnz 2f

  90

  91/*

  92 * Set segments to known values.

  93 */

  94        lgdt pa(boot_gdt_descr)

  95        movl $(__BOOT_DS),%eax

  96        movl %eax,%ds

  97        movl %eax,%es

  98        movl %eax,%fs

  99        movl %eax,%gs

 

跟前面一样BP_loadflags的第七位没有设置所以重新设置一下保护模式的环境。我们前面在setup_gdt()函数中 中看到过通过C语言设置保护模式的方法,这里再通过汇编代码复习一下。94行,加载全局描述符表寄存器,将boot_gdt_descr的物理地址所对应的内容加载到GDTR寄存器以作为全局描述符表的基地址。

 

看到94行,著名的pa宏第一次出现了,来自同一个文件的第26行:

#define pa(X) ((X) - __PAGE_OFFSET)

 

最重要的__PAGE_OFFSET出现了,下面好几个地方都有__PAGE_OFFSET,这是因为要引用某个变量所在的地址,那么必须找到物理地址,这就是pa宏的作用。__PAGE_OFFSET被定义为CONFIG_PAGE_OFFSET,在32x86保护模式下,默认为0xC0000000

 

而此刻因为没有分页,所以实际上变量的偏移的值都是实际的n再加上0xC0000000(因为内核最终要分页,所以链接的时候都是相对这个偏移,一会再说说),所以如果不减去__PAGE_OFFSET,那么比如上面boot_gdt_descr的值就是 0xC0100000+n。由于n是个不大的值,是vmlinuxboot_gdt_desc 相对保护模式开始的偏移。这样,boot_gdt_descr - __PAGE_OFFSET之后就是n,这正是boot_gdt_descr所在的物理地址。

 

全局描述符表在哪儿?看到696行:

696boot_gdt_descr:

 697        .word __BOOT_DS+7

 698        .long boot_gdt - __PAGE_OFFSET

 699

 700        .word 0                         # 32-bit align idt_desc.address

 

由于GDTR是一个长度为48bit的寄存器,内容为一个32位的基地址和一个16位的段限。其中32位的基址是指GDT在内存中的地址。被GDTR通过94行指令加载的内容就是上面的内容,先是697行的段限。__BOOT_DS还记得吧,初始化阶段的数据段选择子,其值为0x88,加上7就是0x8f,这个值占据了2个字节的空间,16位,作为段限。

 

GDTR32位基地址是初始化阶段的全局描述符表现性地址boot_gdt的物理地址,这个地址定位到716行:

716ENTRY(boot_gdt)

 717        .fill GDT_ENTRY_BOOT_CS,8,0

 718        .quad 0x00cf9a000000ffff        /* kernel 4GB code at 0x00000000 */

 719        .quad 0x00cf92000000ffff        /* kernel 4GB data at 0x00000000 */

这个四个字节作为段基地址;最后2个字节是段描述符段限的其他部分。这里我们又学个新知识,看上面有个.fillGNU汇编程序提供了很多这样的指令,这种指令都是以句点(.)为开头,后跟指令名(小写字母),.fill的形式是 .fill repeat , size , value

 

其中,repeatsize value都是常量表达式。Fill的含义是反复拷贝size个字节。Repeat可以大于等于0size也可以大于等于0,但不能超过8,如果超过8,也只取8。把repeat个字节以8个为一组,每组的最高4个字节内容为0,最低4字节内容置为value。所以我们看到由于GDT_ENTRY_BOOT_CS定义为2,所以717行代码意思是申请两个8字节的空间,其内容为0

 

.quad也是个新知识,表示零个或多个bignums(用逗号分隔),对于每个bignum,其缺省值是8字节整数。如果bignum超过8字节,则打印一个警告信息;并只取bignum最低8字节。例如,718行对全局描述符表的填充就用到这个指令,第一个8字节大数是0x00cf9a000000ffff,表示内核4GB代码段的描述符内容,起始地址为0x00000000;第二个8字节大数是0x00cf92000000ffff,表示内核4GB数据段的描述符内容,起始地址同样也为0x00000000。注意,这里还处于初始化阶段,不存在用户代码段和数据段。

 

有关其他有用的GNU汇编程序指令请参考我的博客“Linux 中的汇编语言”

http://blog.csdn.net/yunsongice/archive/2010/10/08/5927895.aspx

 

boot_gdt就是初始化阶段描述符表的内容,698行的运算就是得到它的物理地址。如果对描述符、选择子这些概念还不熟悉的同学,请查阅博客“Intel 80286工作模式”

http://blog.csdn.net/yunsongice/archive/2010/10/04/5920447.aspx

 

注意,在进入保护模式后,Linux进行了第二次段寻址的设置,也就是第二次启动保护模式,这一次设置的原因是在之前的处理过程中,指令地址是从物理地址0x100000开始的,而此时整个vmlinux的编译链接地址是从虚拟地址0xC0000000开始的,所以需要在这里重新设置boot_gdt的位置。

 

随后95~99行就是设置4个数据段寄存器的值,存放数据段选择子__BOOT_DS

 

101 2:

102/*

 103 * Clear BSS first so that there are no surprises...

 104 */

 105        cld

 106        xorl %eax,%eax

 107        movl $pa(__bss_start),%edi

 108        movl $pa(__bss_stop),%ecx

 109        subl %edi,%ecx

 110        shrl $2,%ecx

 111        rep ; stosl

 112/*

 113 * Copy bootup parameters out of the way.

 114 * Note: %esi still has the pointer to the real-mode data.

 115 * With the kexec as boot loader, parameter segment might be loaded beyond

 116 * kernel image and might not even be addressable by early boot page tables.

 117 * (kexec on panic case). Hence copy out the parameters before initializing

 118 * page tables.

 119 */

 120        movl $pa(boot_params),%edi

 121        movl $(PARAM_SIZE/4),%ecx

 122        cld

 123        rep

 124        movsl

 125        movl pa(boot_params) + NEW_CL_POINTER,%esi

 126        andl %esi,%esi

 127        jz 1f                   # No comand line

 128        movl $pa(boot_command_line),%edi

 129        movl $(COMMAND_LINE_SIZE/4),%ecx

 130        rep

 131        movsl

 

105行,没有问题,清除方向标志,使得SI->DI107108行,将__bss_start__bss_stop的物理地址分别计算出来并传递给ediecx寄存器。然后再讲这两个值一相减,得到BSS内核未初始化数据段的长度。当然,为了执行rep指令,还要右移两位,就得到了32位一个传输单元的BSS段长度。随后执行rep指令,还记得这个汇编指令把,把DS:SI指向的内存单元传输到ES:DI执行的内存单元,长度是ecx

 

120~123行,再把edi指向boot_params的保护模式下物理地址,ecx设置成boot_params的长度,然后rep拷贝。注意这个PARAM_SIZE,正好1页大小,在arch/x86/include/asm/Setup.h中定义:

#define PARAM_SIZE 4096        /* sizeof(struct boot_params) */

 

我们看到protected_mode_jump函数把setup中的boot_params的参数放到%esi中了复制一份,这类却再一次。为什么复制两次呢?你想想啊,解压缩以后跳到了解压缩后的代码,再经过上述重置保护模式环境的代码后,GDTR和数据段寄存器中存放的内容已经变了,如果不做这么一个复制,那么后面的代码永远也找不到这个boot_params了。

 

注意,注释里写得很清楚,为什么这里要进行这么一个拷贝,因为%esi指向的内存区还是实模式下存放的那个boot_params,经历了这么久以后一直都没变过,而且最关键的是保护模式的环境已经改变了,vmlinux的编译链接地址已经是从虚拟地址0xC0000000开始了,所以这样的拷贝是必须地。

 

最后125~131行的代码是复制启动参数的NEW_CL_POINTER部分到boot_command_line,这个参数负责命令行,执行的过程跟前面差不多,我们就不细说了。

 

CONFIG_PARAVIRT主要用来针对bootloader损坏的情况,没有在我们的.config里面,所以略过134~165行代码。而又由于我们没有启动强大的x83内存扩展,所以CONFIG_X86_PAE也不用考虑,再略过177~225行的代码,直接来到227行。

 

 

 

你可能感兴趣的:(linux,struct,汇编,command,语言,Parameters)