Linux内核启动过程(2.6.23) bootloader部分

1052阅读 0评论2012-04-13 zachary027
分类:

Linux内核启动过程(2.6.23)


!!!!!!版权所有,如需要转载,请注明出处,作者:乔迁!!!!!!
内核版本号:2.6.23
当PC按下电源,CPU加电后,自动从0xFFFF0处开始执行代码,这个地址位于BIOS中。接着BIOS开始一系列系统检测,并在内存物理地址0处初 始化中断向量,供Linux内核启动过程中进行调用。此后,它将启动设备的第一个扇区(磁盘引导扇区,512Bytes)读入内存绝对地址0x7C00 处,并跳到这个地方开始执行(arch/i386/boot/header.s)。
注意,现在程序现在运行在16位实模式下,使用64k的段。 segment(段地址) : offset(偏移)构成了 逻辑地址,段地址乘以 16 再加上 偏移 ,就得到 linear address(线性地址)。header.s(arch/i386/boot/header.s)中包含了大量的.h(头文件)。
 
18 #include
 19 #include
 20 #include
 21 #include
 22 #include
 23 #include
 24 #include "boot.h"
这些头文件包含了许多重要的宏和声明,在Linux 内核的其他程序中,都会用到。
  .section ".header", "a"
          .globl  hdr
  hdr:
上面开始了header.s的最重要的部分,hdr数据段存储了Linux kernel启动过程所需的信息数据,它的结构struct boot_params定义在 include/asm-i386/bootparam.h中。struct boot_params非常重要,它包含了其他的一些struct,并不是很复杂。下面开始了header.s程序,开始了header.s的真正的执行。
 .section ".inittext", "ax"
 start_of_setup:
它首先初始化 Disk Controller(磁盘控制器),是通过 int 0x13进行的。然后设置寄存器,Zero the bss(初始化数据段),接着 call main(调用 main)跳转到:arch/i386/boot/main.c的main()函数开始执行。(终于见到我们的可亲可爱的C了,不过别高兴太早喔:~)
下面我们看看main()都做了什么(注:linux kernel 中有太多main函数,程序并不是总是从main开始执行)
        copy_boot_params();        复制 boot header 到 "zeropage"
        validate_cpu();               确保支持当前运行的CPU
        set_bios_mode();            告诉BIOS什么CPU我们将要去运行
        detect_memory();            检测Memory
        keyboard_set_repeat();      设置键盘 repeat rate (Why ?)
        set_video();                  设置 Video mode
        query_mca();                 获得 MCA 信息
        query_voyager();             Voyager ?
        query_ist();                   获得  Query Intel SpeedStep (IST) 信息
        query_apm_bios();            获得APM 信息
        query_edd();                  获得EDD信息
        go_to_protected_mode()  最后一件事,也是最重要的一件事,进入保护模式
上面只是函数调用的大致顺序,并不是真正的程序,通过这些函数我们可以一目了然地看到内核的执行过程(还是C好啊,不像汇编,得一行一行地看),当然你要了解更多的细节,可以追踪到每一个函数中去。它通过上方面的检测,不断地填充struct boot_params结构,记住,struct boot_params 是很重要的喔 。其他函数我们就不研究了,单看最后一个,go_to_protected_mode(),究竟在哪儿?  arch/i386/boot/pm.c
void go_to_protected_mode(void)
依次调用了如下函数:
        realmode_switch_hook();     Hook before leaving realmode
        move_kernel_around();       把 Kernel/setup 移到 它们最终的地方 
        enable_a20()                 开启 a20 门
        reset_coprocessor();          重置 coprocessor(协处理器)
        mask_all_interrupts();         Mask(屏蔽所有中断)
        setup_idt();                    开始转入 保护模式……
        setup_gdt();
        protected_mode_jump(boot_params.hdr.code32_start,  (u32)&boot_params + (ds() << 4));
  
protected_mode_jump,传入了boot_params->hdr ->code32_start作为第一个参数, 该参数在header.s
(arch/i386/boot/header.s) 设置如下 :
 
code32_start:                           # here loaders can put a different
                                        # start address for 32-bit code.
#ifndef __BIG_KERNEL__
                .long   0x1000           0x1000 = default for zImage
#else
                .long   0x100000        # 0x100000 = default for big kernel
#endif
显然是跳转到 0x1000或0x100000处继续执行。
第二个参数,就是 boot_params的线性地址,注意,现在仍是实地址模式,线性地址为段地址乘16加上偏移。

(啊,天哪,又到 汇编里去了,AT&T汇编), 在arch/i386/boot/pmjump.S中,
.globl  protected_mode_jump
        .type   protected_mode_jump, @function

        .code16
终于找到了,
arch/i386/boot/pmjump.S并不大,只有54行。我们看看它做了什么:
        xorl    %ebx, %ebx              # Flag to indicate this is a boot
        movl    %edx, %esi              # 传递过来的boot_params地址,转移到 ESI寄存器
        movl    %eax, 2               # Patch ljmpl instruction 要跳转的地址 放到 2f 处 
........
1:   
        movw    $__BOOT_DS, %cx     

        movl    %cr0, %edx
        orb     $1, %dl    # Protected mode (PE) bit  设置CR0的PE位,进入保护模式!!
        movl    %edx, %cr0
很简单嘛 :~)
  # Jump to the 32-bit entrypoin   进入32位程序段

        .byte   0x66, 0xea              # ljmpl opcode  跳转指令码 (为什么不用汇编呢?不太清楚)
2     .long                        # offset            偏移地址  
        .word   __BOOT_CS               # segment    段    类似于 ljmp segment : offset;

        .size   protected_mode_jump, .-protected_mode_jump
现在我们到哪儿了呢,想必大家都迷住了吧。我找了半天,终于找到了: arch/i386/boot/compressed/head.s
 **********   head.S    contains the 32-bit startup code.
NOTE!!! Startup happens at absolute address 0x00001000, which is also where
 * the page directory will exist. The startup code will be overwritten by
 * the page directory.
必定是跳转到这里了,Startup 被加载到绝对地址 0x00001000处。head.s的作用想必大家已经猜到了吧。既然在compressed文件夹中,肯定是和压缩有关的啦。对!

.text

#include
#include
#include
#include

.section ".text.head","ax",@progbits
        .globl startup_32                 定义了startup_32函数,从这儿开始执行

startup_32:                                  程序开始……
接下来,startup_32要检测是不是被加载到预定的位置了呢,怎么检查呢,这里用了一个技巧:
        leal (0x1e4+4)(%esi), %esp            还记得 ESI中存的是boot_params的地址吗?这条指令就是把                                                  栈指针指向boot_params的一个成员,而这个成员                                                             u32 scratch就是专为程序检查是否被加载到预定位置设置的作                                                  为栈使用的
                                       
        call 1f                                    当 call时,一条指令的指针要入栈,就是保存到 scratch中了
1:      popl %ebp                               把指令指针放到 EBP中,EBP存的是当前的位置
        subl $1b, %ebp            1b指的是指令在程序段中的偏移,两者之差,就是程序被加载的address了


* %ebp contains the address we are loaded at by the boot loader and %ebx
 * contains the address where we should move the kernel image temporarily
 * for safe in-place decompression.
 * 现在 EBP中含有我们被加载的地址,而 EBX 中含有我们应该暂时把内核 image移动到的安全位置的地址

接着,就开始复制并移动内核,接着跳到移动后的地址开始执行,并为解压过程设置栈(stack)
 
* Do the decompression, and jump to the new kernel..
*开始解压缩,然后跳到新的内核开始执行
        movl output_len(%ebx), %eax               #将参数依次入栈
        pushl %eax
        pushl %ebp      # output address
        movl input_len(%ebx), %eax
        pushl %eax      # input_len
        leal input_data(%ebx), %eax
        pushl %eax      # input_data
        leal _end(%ebx), %eax
        pushl %eax      # end of the image as third argument
        pushl %esi      # real mode pointer as second arg
        call decompress_kernel
        addl $20, %esp         恢复 栈
        popl %ecx
如果需要,还要再移动内核,然后:
 * Jump to the decompressed kernel.

        xorl %ebx,%ebx
        jmp *%ebp               EBP 中有解压后新内核的地址

   先说下内核的的解压缩。
   内核解压缩的 
decompress_kernel  在arch/i386/boot/compressed/misc.c中定义,
asmlinkage void decompress_kernel(void *rmode, unsigned long end,
                        uch *input_data, unsigned long input_len, uch *output)
{
……
……
        putstr("Uncompressing Linux... ");       很高兴啊,Uncompressing Linux...
        gunzip();                               是专门解压内核的gzip,它涉及到同目录下的relocs.c文件,
                                                还有 lib/inflate.c(主要的解压例程)
        putstr("Ok, booting the kernel.\n");      很熟悉吧,Ok,booting the kernel.
        return;
}

好了,继续下去,现在Linux应该是跳转到新的解压缩后的位置开始执行了吧(跳来跳去真辛苦啊
到哪儿了呢? 还是汇编 ~:(     arch/i386/kernel/head.s           head.s文件,578L(行代码,不小啊,不过这算是最后一个啦,坚持哈) 
head.s 虽然大,但很有条理:
    重新设置段
    把BSS段设置为0
    把 boot_params的参数重新COPY到安全的地方
    Initialize page tables 初始化页表
    为SMP设置页表(如果有的话)
 * Enable paging 
        movl $swapper_pg_dir-__PAGE_OFFSET,%eax
        movl %eax,%cr3          # set the page table pointer..设置页指针
        movl %cr0,%eax
        orl $0x80000000,%eax
        movl %eax,%cr0        # ..and set paging (PG) bit  启动分页
        ljmp $__BOOT_CS,$1f     # Clear prefetch and normalize %eip 设置EIP
1:
        lss stack_start,%esp       
#Set up the stack pointer   设置 栈 指针

        pushl $0                     #
Initialize eflags.   把EFLAGES设置为0
        popfl

   检测 CPU 类型
   setup_idt   设置 IDT表
   include arch/i386/xen/xen-head.S
…………

360(Line)         jmp start_kernel         跳转到    init/main.c
现在,终于熬过了 AT&T汇编 ,是不是很有成就感呢,下面的start_kernel要是也讲习 懂了,Linux内核的其他部分,就不在话下了,加油啊,我现在水平还不够,只能列出start_kernel源码,大家一同分析
513 asmlinkage void __init start_kernel(void)
........

至此,内核正常启动起来了。太高兴了,现在还没有研究到  start_kernel 内部,再接再厉!!


在此,记录一篇比较详细的文章:
linux 启动过程综述:
http://www.ibm.com/developerworks/cn/linux/kernel/startup/index.html
上一篇:MRC 和MCR 指令
下一篇:10年IT人生酸甜苦辣的经历!一个老程序员的心理话