4.2.2 第三次启动保护模式
很多人就有疑问, 刚才我们设置好了中断描述符表,那内核怎么用它呢?不好意思,刚才在略过了的checkCPUtype过程中有一步非常重要的步骤,现在把它补上:
418 is386: movl $2,%ecx # set MP
419 2: movl %cr0,%eax
420 andl $0x80000011,%eax # Save PG,PE,ET
421 orl %ecx,%eax
422 movl %eax,%cr0
423
424 call check_x87
425 lgdt early_gdt_descr
426 lidt idt_descr
427 ljmp $(__KERNEL_CS),$1f
428 1: movl $(__KERNEL_DS),%eax # reload all the segment registers
429 movl %eax,%ss # after changing gdt.
430
431 movl $(__USER_DS),%eax # DS/ES contains default USER segment
432 movl %eax,%ds
433 movl %eax,%es
434
435 movl $(__KERNEL_PERCPU), %eax
436 movl %eax,%fs # set this cpu's percpu
425行重新加载一个全局描述符表,early_gdt_descr:
707 ENTRY(early_gdt_descr)
708 .word GDT_ENTRIES*8-1
709 .long gdt_page /* Overwritten for secondary CPUs */
同样也是包含段限和全局描述符表地址,gdt_page在arch/x86/include/asm/desc.h定义:
35
36struct gdt_page {
37 struct desc_struct gdt[GDT_ENTRIES];
38} __attribute__((aligned(PAGE_SIZE)));
为什么需要重新加载一个全局描述符表?很简单,因为现在起到分页了,所以,要重新定位全局描述符表的位置,GDT的第三次段设置是在开启并设置了了页面寻址之后进行的。设置本身很简单,也是最终的设置,而且还同时设置了IDT。而由于aligned(PAGE_SIZE),所以gdt_page正好是一个页面的开始位置。GDT_ENTRIES的值是32,定义在arch/x86/include/asm/segment.h:
110#define GDT_ENTRIES 32
所以全局描述符表式每个表项为desc_struct结构的,32个表项的数组,它的每个表项是:
22struct desc_struct {
23 union {
24 struct {
25 unsigned int a;
26 unsigned int b;
27 };
28 struct {
29 u16 limit0;
30 u16 base0;
31 unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
32 unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
33 };
34 };
35} __attribute__((packed));
正好看到第二个联合体,熟悉吧,描述符结构。哦,你不熟悉,那就看看博客“Intel 80286工作模式” 。8个字节,所以gdt表共占8*32=256个字节,相对于一个页面的4096字节,仅仅占了一个页面前1/16的空间。
回到426行,把我们刚才设置好的中断门描述符表idt_descr通过lidt加载上,然后427~436行分别将段选择子__KERNEL_CS、__KERNEL_DS、__USER_DS、__KERNEL_PERCPU加载到寄存器cs、ss、ds/es、fs。注意这里一个细节,加载cs寄存器重来都不使用mov指令,而是通过ljmp $(__KERNEL_CS),$1f来设置。还有,这时候,gdt是早已初始化了的。
那么,这个gdt是在哪里初始化的呢,请看arch/x86/kernel/cpu/common.c文件:
86DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = { 87#ifdef CONFIG_X86_64 88 /* 89 * We need valid kernel segments for data and code in long mode too 90 * IRET will check the segment types kkeil 2000/10/28 91 * Also sysret mandates a special GDT layout 92 * 93 * TLS descriptors are currently at a different place compared to i386. 94 * Hopefully nobody expects them at a fixed place (Wine?) 95 */ 96 [GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff), 97 [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff), 98 [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff), 99 [GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff), 100 [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff), 101 [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff), 102#else 103 [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xc09a, 0, 0xfffff), 104 [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), 105 [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xc0fa, 0, 0xfffff), 106 [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f2, 0, 0xfffff), 107 /* 108 * Segments used for calling PnP BIOS have byte granularity. 109 * They code segments and data segments have fixed 64k limits, 110 * the transfer segment sizes are set at run time. 111 */ 112 /* 32-bit code */ 113 [GDT_ENTRY_PNPBIOS_CS32] = GDT_ENTRY_INIT(0x409a, 0, 0xffff), 114 /* 16-bit code */ 115 [GDT_ENTRY_PNPBIOS_CS16] = GDT_ENTRY_INIT(0x009a, 0, 0xffff), 116 /* 16-bit data */ 117 [GDT_ENTRY_PNPBIOS_DS] = GDT_ENTRY_INIT(0x0092, 0, 0xffff), 118 /* 16-bit data */ 119 [GDT_ENTRY_PNPBIOS_TS1] = GDT_ENTRY_INIT(0x0092, 0, 0), 120 /* 16-bit data */ 121 [GDT_ENTRY_PNPBIOS_TS2] = GDT_ENTRY_INIT(0x0092, 0, 0), 122 /* 123 * The APM segments have byte granularity and their bases 124 * are set at run time. All have 64k limits. 125 */ 126 /* 32-bit code */ 127 [GDT_ENTRY_APMBIOS_BASE] = GDT_ENTRY_INIT(0x409a, 0, 0xffff), 128 /* 16-bit code */ 129 [GDT_ENTRY_APMBIOS_BASE+1] = GDT_ENTRY_INIT(0x009a, 0, 0xffff), 130 /* data */ 131 [GDT_ENTRY_APMBIOS_BASE+2] = GDT_ENTRY_INIT(0x4092, 0, 0xffff), 132 133 [GDT_ENTRY_ESPFIX_SS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), 134 [GDT_ENTRY_PERCPU] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), 135 GDT_STACK_CANARY_INIT 136#endif 137} }; |
首先讲讲DEFINE_PER_CPU_PAGE_ALIGNED宏,在include/linux/percpu-defs.h中:
#define DEFINE_PER_CPU_PAGE_ALIGNED(type, name) /
DEFINE_PER_CPU_SECTION(type, name, ".page_aligned") /
__aligned(PAGE_SIZE)
#define DECLARE_PER_CPU_SECTION(type, name, sec) /
extern __PCPU_ATTRS(sec) __typeof__(type) name
#define __PCPU_ATTRS(sec) /
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) /
PER_CPU_ATTRIBUTES
展开就是:
extern __percpu __attribute__((section(PER_CPU_BASE_SECTION .page_aligned))) /
PER_CPU_ATTRIBUTES __typeof__(struct gdt_page) gdt_page __aligned(PAGE_SIZE)
看到了吗?在编译阶段gdt[GDT_ENTRIES]就被编译成了一个常量,在32位x86体系中其值为103~135行的代码,将这个常量最后链接进PER_CPU_BASE_SECTION中。
另外,GDT_ENTRY_INIT宏,在arch/x86/include/asm/desc_defs.h中定义:
#define GDT_ENTRY_INIT(flags, base, limit) { { { /
.a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), /
.b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | /
((limit) & 0xf0000) | ((base) & 0xff000000), /
所以经过编译链接以后,gdt就包含14个全局描述符,其中:
#define GDT_ENTRY_KERNEL_BASE 12
#define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0)
表示内核代码段的索引,即gdt[12]为代码段描述符。
#define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1)
表示内核数据段的索引,即gdt[13]为数据段描述符。
#define GDT_ENTRY_DEFAULT_USER_CS 14
表示用户代码段的索引,即gdt[14]为用户代码段描述符。
#define GDT_ENTRY_DEFAULT_USER_CS 14
表示用户数据段的索引,即gdt[15]为用户数据段描述符。
#define GDT_ENTRY_PNPBIOS_BASE (GDT_ENTRY_KERNEL_BASE + 6)
#define GDT_ENTRY_PNPBIOS_CS32 (GDT_ENTRY_PNPBIOS_BASE + 0)
#define GDT_ENTRY_PNPBIOS_CS16 (GDT_ENTRY_PNPBIOS_BASE + 1)
#define GDT_ENTRY_PNPBIOS_DS (GDT_ENTRY_PNPBIOS_BASE + 2)
#define GDT_ENTRY_PNPBIOS_TS1 (GDT_ENTRY_PNPBIOS_BASE + 3)
#define GDT_ENTRY_PNPBIOS_TS2 (GDT_ENTRY_PNPBIOS_BASE + 4)
gdt[18]、gdt[19]、gdt[20]、gdt[21]、gdt[22]分别表示PNP BIOS的32位代码段、16位代码段、数据段、两个任务段。
#define GDT_ENTRY_APMBIOS_BASE (GDT_ENTRY_KERNEL_BASE + 11)
gdt[23]、gdt[24]、gdt[25]分别表示高级电源管理APM的32位代码段、16位代码段和数据段。
#define GDT_ENTRY_ESPFIX_SS (GDT_ENTRY_KERNEL_BASE + 14)
gdt[26]表示堆栈修复段的段描述符。
#define GDT_ENTRY_PERCPU (GDT_ENTRY_KERNEL_BASE + 15)
gdt[26]表示每CPU段的段描述符。
最后由于我们的.config没有定义CONFIG_CC_STACKPROTECTOR,所以GDT_STACK_CANARY_INIT就为空。所以,Linux启动以来自此加载的gdt已有以上若干个段的描述符在编译vmlinux的时候初始化了,其他没被初始化的地方暂时保留。
我们知道Linux x86的分段管理是通过GDTR来实现的,那么现在就来总结一下Linux启动以来到现在,共设置了几次GDTR:
1. 第一次还是cpu处于实模式的时候,运行arch/x86/boot/pm.c下setup_gdt()函数的代码。该函数,设置了两个GDT项,一个是代码段可读/执行的,另一个是数据段可读写的,都是从0-4G直接映射到0-4G,也就是虚拟地址和线性地址相等。
2. 第二次是在内核解压缩以后,用解压缩后的内核代码arch/x86/kernel/head_32.S再次对gdt进行设置,这一次的设置效果和上一次是一样的。
3. 第三次同样是在arch/x86/kernel/head_32.S中,只不过是在开启了页面寻址之后,通过分页寻址得到编译好的全局描述符表gdt的地址。这一次效果就跟前两次不一样了,为内核最终使用的全局描述符表,同时也设置了IDT。