您的位置:首页 > 其它

第二次启动保护模式

2010-12-31 22:28 316 查看

4.1.3 第二次启动保护模式

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


第二个startup_32在arch/x86/kernel/head_32.S的85行,我们就开始分析它了:
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,在32位x86保护模式下,默认为0xC0000000。

而此刻因为没有分页,所以实际上变量的偏移的值都是实际的n再加上0xC0000000(因为内核最终要分页,所以链接的时候都是相对这个偏移,一会再说说),所以如果不减去__PAGE_OFFSET,那么比如上面boot_gdt_descr的值就是 0xC0100000+n。由于n是个不大的值,是vmlinux中boot_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位,作为段限。

而GDTR的32位基地址是初始化阶段的全局描述符表现性地址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个字节是段描述符段限的其他部分。这里我们又学个新知识,看上面有个.fill。GNU汇编程序提供了很多这样的指令,这种指令都是以句点(.)为开头,后跟指令名(小写字母),.fill的形式是 .fill repeat , size , value

其中,repeat、size 和value都是常量表达式。Fill的含义是反复拷贝size个字节。Repeat可以大于等于0。size也可以大于等于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->DI。107、108行,将__bss_start和__bss_stop的物理地址分别计算出来并传递给edi和ecx寄存器。然后再讲这两个值一相减,得到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_PAR***IRT主要用来针对bootloader损坏的情况,没有在我们的.config里面,所以略过134~165行代码。而又由于我们没有启动强大的x83内存扩展,所以CONFIG_X86_PAE也不用考虑,再略过177~225行的代码,直接来到227行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: