再次进入startup_32: 初始化页表

http://blog.chinaunix.net/uid-1701789-id-154125.html

好了,我们就回到0x100000处开始执行解压缩之后的kernel了。但是问题是现在在0x100000处的代码在kernel tree的哪个源代码里面呢?我们就有必要来看看kernel是怎么编译的,压缩的代码到底是什么?


大家可以看一下http://david05software.javaeye.com/blog/775070。里面提到压缩的代码实际来源是vmlinux文件,这个文件是在编译后源码根目录下,不是/arch/x86/boot/compressed下的那个vmlinux文件。可以用objdump来反编译vmlinux看以下得到以下结果:


  1. c0100000 <_text>:
  2. c0100000: f6 86 11 02 00 00 40 testb $0x40,0x211(%esi)
  3. c0100007: 75 14 jne c010001d <_text+0x1d>
  4. c0100009: 0f 01 15 16 60 7d 00 lgdtl 0x7d6016
  5. c0100010: b8 18 00 00 00 mov $0x18,%eax
  6. c0100015: 8e d8 mov %eax,%ds
  7. c0100017: 8e c0 mov %eax,%es
  8. c0100019: 8e e0 mov %eax,%fs
  9. c010001b: 8e e8 mov %eax,%gs
  10. c010001d: fc cld
  11. c010001e: 31 c0 xor %eax,%eax
  12. c0100020: bf 00 60 8d 00 mov $0x8d6000,%edi
  13. c0100025: b9 04 cb 9a 00 mov $0x9acb04,%ecx
  14. c010002a: 29 f9 sub %edi,%ecx

再看arch/x86/kernel/header_32.Sentry,是不是完全一样?

  1. ENTRY(startup_32)
  2. /* test KEEP_SEGMENTS flag to see if the bootloader is asking
  3. us to not reload segments */
  4. testb $(1<<6), BP_loadflags(%esi)
  5. jnz 2f
  6. /*
  7. * Set segments to known values.
  8. */
  9. lgdt pa(boot_gdt_descr)
  10. movl $(__BOOT_DS),%eax
  11. movl %eax,%ds
  12. movl %eax,%es
  13. movl %eax,%fs
  14. movl %eax,%gs
  15. 2:
  16. /*
  17. * Clear BSS first so that there are no surprises...
  18. */
  19. cld
  20. xorl %eax,%eax
  21. movl $pa(__bss_start),%edi
  22. movl $pa(__bss_stop),%ecx
  23. subl %edi,%ecx

好了,其他的也就不多讲了,回过头来看代码。更加详细关于解压和代码定位可以参见http://www.embedu.org/Column/Column13.htm


好了,我们回到header_32.S。这可不是我们先前说过的那个boot/compressed里面的header_32.S。我们现在知道那个header_32.S的工作就是将kernel解压并重新定位到0x100000的位置。现在我们所说的header_32.S是在arch/x86/kernel/下面的。它是真正的kernel的入口。


现在就进入代码:

  1. ENTRY(startup_32)
  2. /* test KEEP_SEGMENTS flag to see if the bootloader is asking
  3. us to not reload segments */
  4. testb $(1<<6), BP_loadflags(%esi)
  5. jnz 2f
  6. /*
  7. * Set segments to known values.
  8. */
  9. lgdt pa(boot_gdt_descr)
  10. movl $(__BOOT_DS),%eax
  11. movl %eax,%ds
  12. movl %eax,%es
  13. movl %eax,%fs
  14. movl %eax,%gs

上面的代码是设置段寄存器为__BOOT_DS。我们在前面说过__BOOT_DS是指向数据段的选择字,保存在gdt中。它所指向的是0-4G的空间。这里有一个问题就是pa(boot_gdt_descr)是什么?我们就要看以下pa是什么

  1. /* Physical address */
  2. #define pa(X) ((X) - __PAGE_OFFSET)

pa是一个宏,它的作用是将x减去__PAGE_OFFSET从一个虚拟地址转化成为实际地址。__PAGE_OFFSETarch/x86/include/asm/page_32.h里面定义

  1. #define __PAGE_OFFSET _AC(CONFIG_PAGE_OFFSET, UL)

CONFIG_PAGE_OFFSET是从哪里来的呢?这是在使用编译kernel时候生成的include/config/auto.conf里面定义的。这个值在我的编译下是0xC0000000

那么为什么需要做这样的转换呢?这是因为在实际kernel的运行时,它运行在以0xC0000000起始的虚拟内存中。按照linux内存的分配状况,0-3G的内存是由应用程序所使用而3-4G的空间是由kernel使用。这也是为什么我们用objdump来反编译vmlinux的时候看到的地址是以c0100000为开始。而kernel实际存在的物理内存空间是0x100000,所以当kernel的内存管理模块还没有建立的时候,就需要在代码中进行相应的地址转换把编译的0xC0100000转换成为0x100000。这也就是使用pa的原因。


  1. 2:
  2. /*
  3. * Clear BSS first so that there are no surprises...
  4. */
  5. cld
  6. xorl %eax,%eax
  7. movl $pa(__bss_start),%edi
  8. movl $pa(__bss_stop),%ecx
  9. subl %edi,%ecx
  10. shrl $2,%ecx
  11. rep ; stosl
  12. /*
  13. * Copy bootup parameters out of the way.
  14. * Note: %esi still has the pointer to the real-mode data.
  15. * With the kexec as boot loader, parameter segment might be loaded beyond
  16. * kernel image and might not even be addressable by early boot page tables.
  17. * (kexec on panic case). Hence copy out the parameters before initializing
  18. * page tables.
  19. */
  20. movl $pa(boot_params),%edi
  21. movl $(PARAM_SIZE/4),%ecx
  22. cld
  23. rep
  24. movsl
  25. movl pa(boot_params) + NEW_CL_POINTER,%esi
  26. andl %esi,%esi
  27. jz 1f # No comand line
  28. movl $pa(boot_command_line),%edi
  29. movl $(COMMAND_LINE_SIZE/4),%ecx
  30. rep
  31. movsl
  32. 1:

接下来的代码就是初始化bss段,为未初始坏的全局和静态变量做准备。并且保存boot_params


下面的这一段和kernel的配置相关,如果没有需要配置支持Open Laptop per Child,则不需要这段代码:

  1. #ifdef CONFIG_OLPC_OPENFIRMWARE
  2. /* save OFW's pgdir table for later use when calling into OFW */
  3. movl %cr3, %eax
  4. movl %eax, pa(olpc_ofw_pgd)
  5. #endif

而下面的代码是关于hyperV的设置。和VT有关。


  1. #ifdef CONFIG_PARAVIRT
  2. /* This is can only trip for a broken bootloader... */
  3. cmpw $0x207, pa(boot_params BP_version) //如果Boot Protocal的版本小于2.07,则并不支持VT。
  4. jb default_entry
  5. /* Paravirt-compatible boot parameters. Look to see what architecture
  6. we're booting under. */
  7. /*
  8. 0x00000000 The default x86/PC environment
  9. 0x00000001 lguest
  10. 0x00000002 Xen
  11. 0x00000003 Moorestown MID
  12. */
  13. movl pa(boot_params + BP_hardware_subarch), %eax
  14. cmpl $num_subarch_entries, %eax
  15. jae bad_subarch
  16. //相对应不同的启动VT方式跳转到不同的入口。
  17. movl pa(subarch_entries)(,%eax,4), %eax
  18. subl $__PAGE_OFFSET, %eax
  19. jmp *%eax
  20. bad_subarch:
  21. WEAK(lguest_entry)
  22. WEAK(xen_entry)
  23. /* Unknown implementation; there's really
  24. nothing we can do at this point. */
  25. ud2a
  26. __INITDATA
  27. subarch_entries:
  28. .long default_entry /* normal x86/PC */
  29. .long lguest_entry /* lguest hypervisor */
  30. .long xen_entry /* Xen hypervisor */
  31. .long default_entry /* Moorestown MID */
  32. num_subarch_entries = (. - subarch_entries) / 4
  33. .previous
  34. #endif /* CONFIG_PARAVIRT */

这里,我们只看正常 x86 PC 的启动 default_entry ,不去涉及 xen lguest.所以以下就让我们看x86的default_entry

这里有首先了解一下PAE

CONFIG_X86_PAE是一个开关用来设定是否启用Intel Pentium PAEPhysical Address Extension, x86系统从Pentium Pro开始实际的地址总线从32根增加到36根,所以操作系统可以使用的地址空间理论上增加到了64G。开启了PAE就能使kernel使用大于4G的空间了。在使用PAE的情况下,最初的内存页面设置使用PMD(Page Middle Directory)

 default_entry 的代码如下。这段代码很重要,它完成了Linux页表的初始化。

  1. default_entry:
  2. #ifdef CONFIG_X86_PAE
  3. /*
  4. * In PAE mode initial_page_table is statically defined to contain
  5. * enough entries to cover the VMSPLIT option (that is the top 1, 2 or 3
  6. * entries). The identity mapping is handled by pointing two PGD entries
  7. * to the first kernel PMD.
  8. *
  9. * Note the upper half of each PMD or PTE are always zero at this stage.
  10. */
  11. #define KPMDS (((-__PAGE_OFFSET) >> 30) & 3) /* Number of kernel PMDs */
  12. //PAGE_OFFSET=0xC0000000, 所以KPMDS=3.所以现在我们只有三个PMD
  13. xorl %ebx,%ebx /* %ebx is kept at zero */
  14. //__brk_base的定义在arch/x86/kernel里。定义了64K的空间给brk段。下面代码的作用是在__brk_base段开始建立PMD表
  15. movl $pa(__brk_base), %edi
  16. movl $pa(initial_pg_pmd), %edx
  17. movl $PTE_IDENT_ATTR, %eax
  18. 10:
  19. //将edi指向内存单元的地址加上PDE_IDENT_ATTR存放到initial_pg_pmd中。由于edi实际指向__brk_base,所以initial_pg_pmd里的实际内容是__brk_base+PDE_IDENT_ATTR。由于__brk_base是低12位为0并且0x86中一个页面是4K所以指向PMD头部的指针其低12位必然永远是0,所以可以将PDE_IDENT_ATTR作为PMD的附加信息加在指向PMD的指针上。要访问PMD的时候则需要对于这个指针进行处理,将低12位进行清零。
  20. leal PDE_IDENT_ATTR(%edi),%ecx /* Create PMD entry */
  21. movl %ecx,(%edx) /* Store PMD entry */
  22. /* Upper half already zero */
  23. addl $8,%edx
  24. movl $512,%ecx
  25. 11:
  26.  //stosl将eax放置到es:di指向的地址,即PMD中去。这里eax = n*0x1000+PDE_IDENT_ATTR。每一个PMD的页面项是64位长,所以需要使用2次stosl.而一个PMD的4K大小页表可以包含512个4K页面也就是2M的实际地址空间。
  27. stosl
  28. xchgl %eax,%ebx
  29. stosl
  30. xchgl %eax,%ebx
  31. addl $0x1000,%eax
  32. loop 11b
  33. /*
  34. * End condition: we must map up to the end + MAPPING_BEYOND_END.
  35. 初始化的页面将只覆盖0到vmlinux所只用的空间加上MAPPING_BEYOND_END
  36. */
  37. movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
  38. cmpl %ebp,%eax
  39. jb 10b
  40. 1:
  41. addl $__PAGE_OFFSET, %edi
  42. movl %edi, pa(_brk_end) //将最后一个页面项+4的虚拟地址保存在_brk_end
  43. shrl $12, %eax
  44. movl %eax, pa(max_pfn_mapped) //max_pfn_mapped里面就是最终的初始化页面的个数
  45. /* Do early initialization of the fixmap area */
  46. movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
  47. movl %eax,pa(initial_pg_pmd+0x1000*KPMDS-8)

  48. #else /* Not PAE */  //如果没有PAE则编译以下代码段,具体的方法和有PAE类似,但是这时没有PMD表,而是PT表,而且每一个表项是32位
  49. page_pde_offset = (__PAGE_OFFSET >> 20);
  50. movl $pa(__brk_base), %edi
  51. movl $pa(initial_page_table), %edx
  52. movl $PTE_IDENT_ATTR, %eax
  53. 10:
  54. leal PDE_IDENT_ATTR(%edi),%ecx /* Create PDE entry */
  55. movl %ecx,(%edx) /* Store identity PDE entry */
  56. movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
  57. addl $4,%edx
  58. movl $1024, %ecx
  59. 11:
  60. stosl
  61. addl $0x1000,%eax
  62. loop 11b
  63. /*
  64. * End condition: we must map up to the end + MAPPING_BEYOND_END.
  65. */
  66. movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
  67. cmpl %ebp,%eax
  68. jb 10b
  69. addl $__PAGE_OFFSET, %edi
  70. movl %edi, pa(_brk_end)
  71. shrl $12, %eax
  72. movl %eax, pa(max_pfn_mapped)
  73. /* Do early initialization of the fixmap area */
  74. movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
  75. movl %eax,pa(initial_page_table+0xffc)
  76. #endif
  77. jmp 3f


经过初始化,基本建立了页表,在PAE被启用的情况下,页表叫做PMDPage Middle Directory),PMD的表项都是64位长的指针指向4K的物理页面。而在PAE未被启用的情况下,页表就是PT(Page Table), Pt的表项都是32位长的指针指向4K的物理页面。在页表初始化结束后,kernel在内存中的映射如下图


下面的一段代码主要是对于CPU的一些扩展功能做一定的设置,例如PAE, NX和Extended Feature Enable Register。这不是我所关心的,略过不提。

再之后,就会真正去启动分页模式:

  1. 6:
  2. /*
  3. * Enable paging
  4. */
  5. movl $pa(initial_page_table), %eax //initial_page_table与initial_pmd_table是一个地址。
  6. movl %eax,%cr3 /* set the page table pointer.. */
  7. movl %cr0,%eax
  8. orl $X86_CR0_PG,%eax
  9. movl %eax,%cr0 /* ..and set paging (PG) bit */
  10. ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */ //通过一个jmp来重置CPU再次进入保护模式并启动分页。
  11. 1:
  12. /* Set up the stack pointer */
  13. lss stack_start,%esp

至此,CPU就进入了基于页面的内存管理模式。地址的解析过程如同以下图所示:

分页字段


当前只是把物理内存进行了分页化,基本上虚拟地址进行地址转换后得到的物理地址是相同的。

接下来的代码就是初始化堆栈并且初始化IDT

  1. 1:
  2. /* Set up the stack pointer */
  3. lss stack_start,%esp //初始化堆栈
  4. /*
  5. * Initialize eflags. Some BIOS's leave bits like NT set. This would
  6. * confuse the debugger if this code is traced.
  7. * XXX - best to initialize before switching to protected mode.
  8. */
  9. pushl $0
  10. popfl

//SMP的代码我们不看
  1. #ifdef CONFIG_SMP
  2. cmpb $0, ready
  3. jz 1f /* Initial CPU cleans BSS */
  4. jmp checkCPUtype
  5. 1:
  6. #endif /* CONFIG_SMP */
  7. /*
  8. * start system 32-bit setup. We need to re-do some of the things done
  9. * in 16-bit mode for the "real" operations.
  10. */
  11. call setup_idt

我们再看一下setup_idt的code:

  1. /*
  2. * setup_idt
  3. *
  4. * sets up a idt with 256 entries pointing to
  5. * ignore_int, interrupt gates. It doesn't actually load
  6. * idt - that can be done only after paging has been enabled
  7. * and the kernel moved to PAGE_OFFSET. Interrupts
  8. * are enabled elsewhere, when we can be relatively
  9. * sure everything is ok.
  10. *
  11. * Warning: %esi is live across this function.
  12. */
  13. setup_idt:
  14. lea ignore_int,%edx
  15. movl $(__KERNEL_CS << 16),%eax
  16. movw %dx,%ax /* selector = 0x0010 = cs */
  17. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ //eax中就是实际指向0x00100000+ignore_int-__PAGE_OFFSET
  18. lea idt_table,%edi
  19. mov $256,%ecx
  20. rp_sidt:
  21. movl %eax,(%edi)
  22. movl %edx,4(%edi)
  23. addl $8,%edi
  24. dec %ecx
  25. jne rp_sidt  //初始化所有idt_table里的256个中断向量指向ignore_int
  26. .macro set_early_handler handler,trapno  //宏定义,设置trapno的中断向量到handler
  27. lea \handler,%edx
  28. movl $(__KERNEL_CS << 16),%eax
  29. movw %dx,%ax
  30. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
  31. lea idt_table,%edi
  32. movl %eax,8*\trapno(%edi)
  33. movl %edx,8*\trapno+4(%edi)
  34. .endm
  35. set_early_handler handler=early_divide_err,trapno=0 //设置除零错误中断
  36. set_early_handler handler=early_illegal_opcode,trapno=6 //设置错误运行代码中断
  37. set_early_handler handler=early_protection_fault,trapno=13 //设置13号中断
  38. set_early_handler handler=early_page_fault,trapno=14 //设置14号中断
  39. ret

接下来的一段代码就会对当前CPU进行检测,它通过使用指令cpuid来执行。我们跳过不管。我们从432行开始的代码看起:

  1. lgdt early_gdt_descr //early_gdt_descr实际引用gdt_page,gdt_page的定义可以参考http://blog.youkuaiyun.com/yunsongice/archive/2010/12/31/6110703.aspx
  2. lidt idt_descr
  3. ljmp $(__KERNEL_CS),$1f  //启用IDT和新的GDT
  4. 1: movl $(__KERNEL_DS),%eax # reload all the segment registers
  5. movl %eax,%ss # after changing gdt.
  6. movl $(__USER_DS),%eax # DS/ES contains default USER segment
  7. movl %eax,%ds
  8. movl %eax,%es
  9. movl $(__KERNEL_PERCPU), %eax
  10. movl %eax,%fs # set this cpu's percpu
  11. #ifdef CONFIG_CC_STACKPROTECTOR
  12. /*
  13. * The linker can't handle this by relocation. Manually set
  14. * base address in stack canary segment descriptor.
  15. *实际上是设置gdt_page中的GDT_STACK_CANARY的base设置为stack_candary.
  16. */
  17. cmpb $0,ready
  18. jne 1f
  19. movl $gdt_page,%eax
  20. movl $stack_canary,%ecx
  21. movw %cx, 8 * GDT_ENTRY_STACK_CANARY + 2(%eax)
  22. shrl $16, %ecx
  23. movb %cl, 8 * GDT_ENTRY_STACK_CANARY + 4(%eax)
  24. movb %ch, 8 * GDT_ENTRY_STACK_CANARY + 7(%eax)
  25. 1:
  26. #endif
  27. movl $(__KERNEL_STACK_CANARY),%eax
  28. movl %eax,%gs
  29. xorl %eax,%eax # Clear LDT
  30. lldt %ax //ldt=0
  31. cld # gcc2 wants the direction flag cleared at all times
  32. pushl $0 # fake return address for unwinder //准备跳转到initial_code,由于使用jmp,所以要在跳转之前准备好ret回来的地址。由于实际不需要回来,那就把跳转回来的地址设为0.
  33. #ifdef CONFIG_SMP //我不关心SMP怎么干的,不看
  34. movb ready, %cl
  35. movb $1, ready
  36. cmpb $0,%cl # the first CPU calls start_kernel
  37. je 1f
  38. movl (stack_start), %esp
  39. 1:
  40. #endif /* CONFIG_SMP */
  41. jmp *(initial_code) //进入C了,initial_code=i386_start_kernel

好了,至此,kernel已经准备好了页表,堆栈,IDT, GDT, LDT了,已经具备了C的运行条件了,可以告别汇编来到C代码中了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值