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看以下得到以下结果:
- c0100000 <_text>:
- c0100000: f6 86 11 02 00 00 40 testb $0x40,0x211(%esi)
- c0100007: 75 14 jne c010001d <_text+0x1d>
- c0100009: 0f 01 15 16 60 7d 00 lgdtl 0x7d6016
- c0100010: b8 18 00 00 00 mov $0x18,%eax
- c0100015: 8e d8 mov %eax,%ds
- c0100017: 8e c0 mov %eax,%es
- c0100019: 8e e0 mov %eax,%fs
- c010001b: 8e e8 mov %eax,%gs
- c010001d: fc cld
- c010001e: 31 c0 xor %eax,%eax
- c0100020: bf 00 60 8d 00 mov $0x8d6000,%edi
- c0100025: b9 04 cb 9a 00 mov $0x9acb04,%ecx
- c010002a: 29 f9 sub %edi,%ecx
再看arch/x86/kernel/header_32.S的entry,是不是完全一样?
- ENTRY(startup_32)
- /* test KEEP_SEGMENTS flag to see if the bootloader is asking
- us to not reload segments */
- testb $(1<<6), BP_loadflags(%esi)
- jnz 2f
- /*
- * Set segments to known values.
- */
- lgdt pa(boot_gdt_descr)
- movl $(__BOOT_DS),%eax
- movl %eax,%ds
- movl %eax,%es
- movl %eax,%fs
- movl %eax,%gs
- 2:
- /*
- * Clear BSS first so that there are no surprises...
- */
- cld
- xorl %eax,%eax
- movl $pa(__bss_start),%edi
- movl $pa(__bss_stop),%ecx
- 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的入口。
现在就进入代码:
- ENTRY(startup_32)
- /* test KEEP_SEGMENTS flag to see if the bootloader is asking
- us to not reload segments */
- testb $(1<<6), BP_loadflags(%esi)
- jnz 2f
- /*
- * Set segments to known values.
- */
- lgdt pa(boot_gdt_descr)
- movl $(__BOOT_DS),%eax
- movl %eax,%ds
- movl %eax,%es
- movl %eax,%fs
- movl %eax,%gs
上面的代码是设置段寄存器为__BOOT_DS。我们在前面说过__BOOT_DS是指向数据段的选择字,保存在gdt中。它所指向的是0-4G的空间。这里有一个问题就是pa(boot_gdt_descr)是什么?我们就要看以下pa是什么
- /* Physical address */
- #define pa(X) ((X) - __PAGE_OFFSET)
pa是一个宏,它的作用是将x减去__PAGE_OFFSET从一个虚拟地址转化成为实际地址。__PAGE_OFFSET在arch/x86/include/asm/page_32.h里面定义
- #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的原因。
- 2:
- /*
- * Clear BSS first so that there are no surprises...
- */
- cld
- xorl %eax,%eax
- movl $pa(__bss_start),%edi
- movl $pa(__bss_stop),%ecx
- subl %edi,%ecx
- shrl $2,%ecx
- rep ; stosl
- /*
- * Copy bootup parameters out of the way.
- * Note: %esi still has the pointer to the real-mode data.
- * With the kexec as boot loader, parameter segment might be loaded beyond
- * kernel image and might not even be addressable by early boot page tables.
- * (kexec on panic case). Hence copy out the parameters before initializing
- * page tables.
- */
- movl $pa(boot_params),%edi
- movl $(PARAM_SIZE/4),%ecx
- cld
- rep
- movsl
- movl pa(boot_params) + NEW_CL_POINTER,%esi
- andl %esi,%esi
- jz 1f # No comand line
- movl $pa(boot_command_line),%edi
- movl $(COMMAND_LINE_SIZE/4),%ecx
- rep
- movsl
- 1:
接下来的代码就是初始化bss段,为未初始坏的全局和静态变量做准备。并且保存boot_params。
下面的这一段和kernel的配置相关,如果没有需要配置支持Open Laptop per Child,则不需要这段代码:
- #ifdef CONFIG_OLPC_OPENFIRMWARE
- /* save OFW's pgdir table for later use when calling into OFW */
- movl %cr3, %eax
- movl %eax, pa(olpc_ofw_pgd)
- #endif
而下面的代码是关于hyperV的设置。和VT有关。
- #ifdef CONFIG_PARAVIRT
- /* This is can only trip for a broken bootloader... */
- cmpw $0x207, pa(boot_params BP_version) //如果Boot Protocal的版本小于2.07,则并不支持VT。
- jb default_entry
- /* Paravirt-compatible boot parameters. Look to see what architecture
- we're booting under. */
- /*
- 0x00000000 The default x86/PC environment
- 0x00000001 lguest
- 0x00000002 Xen
- 0x00000003 Moorestown MID
- */
- movl pa(boot_params + BP_hardware_subarch), %eax
- cmpl $num_subarch_entries, %eax
- jae bad_subarch
- //相对应不同的启动VT方式跳转到不同的入口。
- movl pa(subarch_entries)(,%eax,4), %eax
- subl $__PAGE_OFFSET, %eax
- jmp *%eax
- bad_subarch:
- WEAK(lguest_entry)
- WEAK(xen_entry)
- /* Unknown implementation; there's really
- nothing we can do at this point. */
- ud2a
- __INITDATA
- subarch_entries:
- .long default_entry /* normal x86/PC */
- .long lguest_entry /* lguest hypervisor */
- .long xen_entry /* Xen hypervisor */
- .long default_entry /* Moorestown MID */
- num_subarch_entries = (. - subarch_entries) / 4
- .previous
- #endif /* CONFIG_PARAVIRT */
这里有首先了解一下PAE。
CONFIG_X86_PAE是一个开关用来设定是否启用Intel Pentium PAE(Physical Address Extension), x86系统从Pentium Pro开始实际的地址总线从32根增加到36根,所以操作系统可以使用的地址空间理论上增加到了64G。开启了PAE就能使kernel使用大于4G的空间了。在使用PAE的情况下,最初的内存页面设置使用PMD(Page Middle Directory)
default_entry 的代码如下。这段代码很重要,它完成了Linux页表的初始化。
- default_entry:
- #ifdef CONFIG_X86_PAE
- /*
- * In PAE mode initial_page_table is statically defined to contain
- * enough entries to cover the VMSPLIT option (that is the top 1, 2 or 3
- * entries). The identity mapping is handled by pointing two PGD entries
- * to the first kernel PMD.
- *
- * Note the upper half of each PMD or PTE are always zero at this stage.
- */
- #define KPMDS (((-__PAGE_OFFSET) >> 30) & 3) /* Number of kernel PMDs */
- //PAGE_OFFSET=0xC0000000, 所以KPMDS=3.所以现在我们只有三个PMD
- xorl %ebx,%ebx /* %ebx is kept at zero */
- //__brk_base的定义在arch/x86/kernel里。定义了64K的空间给brk段。下面代码的作用是在__brk_base段开始建立PMD表
- movl $pa(__brk_base), %edi
- movl $pa(initial_pg_pmd), %edx
- movl $PTE_IDENT_ATTR, %eax
- 10:
- //将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位进行清零。
- leal PDE_IDENT_ATTR(%edi),%ecx /* Create PMD entry */
- movl %ecx,(%edx) /* Store PMD entry */
- /* Upper half already zero */
- addl $8,%edx
- movl $512,%ecx
- 11:
- //stosl将eax放置到es:di指向的地址,即PMD中去。这里eax = n*0x1000+PDE_IDENT_ATTR。每一个PMD的页面项是64位长,所以需要使用2次stosl.而一个PMD的4K大小页表可以包含512个4K页面也就是2M的实际地址空间。
- stosl
- xchgl %eax,%ebx
- stosl
- xchgl %eax,%ebx
- addl $0x1000,%eax
- loop 11b
- /*
- * End condition: we must map up to the end + MAPPING_BEYOND_END.
- * 初始化的页面将只覆盖0到vmlinux所只用的空间加上MAPPING_BEYOND_END
- */
- movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
- cmpl %ebp,%eax
- jb 10b
- 1:
- addl $__PAGE_OFFSET, %edi
- movl %edi, pa(_brk_end) //将最后一个页面项+4的虚拟地址保存在_brk_end
- shrl $12, %eax
- movl %eax, pa(max_pfn_mapped) //max_pfn_mapped里面就是最终的初始化页面的个数
- /* Do early initialization of the fixmap area */
- movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
- movl %eax,pa(initial_pg_pmd+0x1000*KPMDS-8)
-
- #else /* Not PAE */ //如果没有PAE则编译以下代码段,具体的方法和有PAE类似,但是这时没有PMD表,而是PT表,而且每一个表项是32位
- page_pde_offset = (__PAGE_OFFSET >> 20);
- movl $pa(__brk_base), %edi
- movl $pa(initial_page_table), %edx
- movl $PTE_IDENT_ATTR, %eax
- 10:
- leal PDE_IDENT_ATTR(%edi),%ecx /* Create PDE entry */
- movl %ecx,(%edx) /* Store identity PDE entry */
- movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
- addl $4,%edx
- movl $1024, %ecx
- 11:
- stosl
- addl $0x1000,%eax
- loop 11b
- /*
- * End condition: we must map up to the end + MAPPING_BEYOND_END.
- */
- movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
- cmpl %ebp,%eax
- jb 10b
- addl $__PAGE_OFFSET, %edi
- movl %edi, pa(_brk_end)
- shrl $12, %eax
- movl %eax, pa(max_pfn_mapped)
- /* Do early initialization of the fixmap area */
- movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
- movl %eax,pa(initial_page_table+0xffc)
- #endif
- jmp 3f
经过初始化,基本建立了页表,在PAE被启用的情况下,页表叫做PMD(Page Middle Directory),PMD的表项都是64位长的指针指向4K的物理页面。而在PAE未被启用的情况下,页表就是PT(Page Table), Pt的表项都是32位长的指针指向4K的物理页面。在页表初始化结束后,kernel在内存中的映射如下图
下面的一段代码主要是对于CPU的一些扩展功能做一定的设置,例如PAE, NX和Extended Feature Enable Register。这不是我所关心的,略过不提。
再之后,就会真正去启动分页模式:
- 6:
- /*
- * Enable paging
- */
- movl $pa(initial_page_table), %eax //initial_page_table与initial_pmd_table是一个地址。
- movl %eax,%cr3 /* set the page table pointer.. */
- movl %cr0,%eax
- orl $X86_CR0_PG,%eax
- movl %eax,%cr0 /* ..and set paging (PG) bit */
- ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */ //通过一个jmp来重置CPU再次进入保护模式并启动分页。
- 1:
- /* Set up the stack pointer */
- lss stack_start,%esp
至此,CPU就进入了基于页面的内存管理模式。地址的解析过程如同以下图所示:

当前只是把物理内存进行了分页化,基本上虚拟地址进行地址转换后得到的物理地址是相同的。
接下来的代码就是初始化堆栈并且初始化IDT
- 1:
- /* Set up the stack pointer */
- lss stack_start,%esp //初始化堆栈
- /*
- * Initialize eflags. Some BIOS's leave bits like NT set. This would
- * confuse the debugger if this code is traced.
- * XXX - best to initialize before switching to protected mode.
- */
- pushl $0
- popfl
- #ifdef CONFIG_SMP
- cmpb $0, ready
- jz 1f /* Initial CPU cleans BSS */
- jmp checkCPUtype
- 1:
- #endif /* CONFIG_SMP */
- /*
- * start system 32-bit setup. We need to re-do some of the things done
- * in 16-bit mode for the "real" operations.
- */
- call setup_idt
我们再看一下setup_idt的code:
- /*
- * setup_idt
- *
- * sets up a idt with 256 entries pointing to
- * ignore_int, interrupt gates. It doesn't actually load
- * idt - that can be done only after paging has been enabled
- * and the kernel moved to PAGE_OFFSET. Interrupts
- * are enabled elsewhere, when we can be relatively
- * sure everything is ok.
- *
- * Warning: %esi is live across this function.
- */
- setup_idt:
- lea ignore_int,%edx
- movl $(__KERNEL_CS << 16),%eax
- movw %dx,%ax /* selector = 0x0010 = cs */
- movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ //eax中就是实际指向0x00100000+ignore_int-__PAGE_OFFSET
- lea idt_table,%edi
- mov $256,%ecx
- rp_sidt:
- movl %eax,(%edi)
- movl %edx,4(%edi)
- addl $8,%edi
- dec %ecx
- jne rp_sidt //初始化所有idt_table里的256个中断向量指向ignore_int
- .macro set_early_handler handler,trapno //宏定义,设置trapno的中断向量到handler
- lea \handler,%edx
- movl $(__KERNEL_CS << 16),%eax
- movw %dx,%ax
- movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
- lea idt_table,%edi
- movl %eax,8*\trapno(%edi)
- movl %edx,8*\trapno+4(%edi)
- .endm
- set_early_handler handler=early_divide_err,trapno=0 //设置除零错误中断
- set_early_handler handler=early_illegal_opcode,trapno=6 //设置错误运行代码中断
- set_early_handler handler=early_protection_fault,trapno=13 //设置13号中断
- set_early_handler handler=early_page_fault,trapno=14 //设置14号中断
- ret
接下来的一段代码就会对当前CPU进行检测,它通过使用指令cpuid来执行。我们跳过不管。我们从432行开始的代码看起:
- lgdt early_gdt_descr //early_gdt_descr实际引用gdt_page,gdt_page的定义可以参考http://blog.youkuaiyun.com/yunsongice/archive/2010/12/31/6110703.aspx
- lidt idt_descr
- ljmp $(__KERNEL_CS),$1f //启用IDT和新的GDT
- 1: movl $(__KERNEL_DS),%eax # reload all the segment registers
- movl %eax,%ss # after changing gdt.
- movl $(__USER_DS),%eax # DS/ES contains default USER segment
- movl %eax,%ds
- movl %eax,%es
- movl $(__KERNEL_PERCPU), %eax
- movl %eax,%fs # set this cpu's percpu
- #ifdef CONFIG_CC_STACKPROTECTOR
- /*
- * The linker can't handle this by relocation. Manually set
- * base address in stack canary segment descriptor.
- *实际上是设置gdt_page中的GDT_STACK_CANARY的base设置为stack_candary.
- */
- cmpb $0,ready
- jne 1f
- movl $gdt_page,%eax
- movl $stack_canary,%ecx
- movw %cx, 8 * GDT_ENTRY_STACK_CANARY + 2(%eax)
- shrl $16, %ecx
- movb %cl, 8 * GDT_ENTRY_STACK_CANARY + 4(%eax)
- movb %ch, 8 * GDT_ENTRY_STACK_CANARY + 7(%eax)
- 1:
- #endif
- movl $(__KERNEL_STACK_CANARY),%eax
- movl %eax,%gs
- xorl %eax,%eax # Clear LDT
- lldt %ax //ldt=0
- cld # gcc2 wants the direction flag cleared at all times
- pushl $0 # fake return address for unwinder //准备跳转到initial_code,由于使用jmp,所以要在跳转之前准备好ret回来的地址。由于实际不需要回来,那就把跳转回来的地址设为0.
- #ifdef CONFIG_SMP //我不关心SMP怎么干的,不看
- movb ready, %cl
- movb $1, ready
- cmpb $0,%cl # the first CPU calls start_kernel
- je 1f
- movl (stack_start), %esp
- 1:
- #endif /* CONFIG_SMP */
- jmp *(initial_code) //进入C了,initial_code=i386_start_kernel
好了,至此,kernel已经准备好了页表,堆栈,IDT, GDT, LDT了,已经具备了C的运行条件了,可以告别汇编来到C代码中了。

3074

被折叠的 条评论
为什么被折叠?



