x86体系结构下Linux-2.6.26启动流程

来源:互联网 发布:俄罗斯 中国 知乎 编辑:程序博客网 时间:2024/05/16 06:20

PB09210183 何春晖
内核映像编译流程分析
源码目录结构
Makefile分析
结论
系统开机到Linux内核流程分析
BIOS
Bootloader(以GRUB-0.97为例)
Linux

内核映像编译流程分析

源码目录结构

  • arch:体系结构相关代码
  • include:头文件
  • init:内核初始化
  • lib:内核库
  • kernel:内核核心
  • mm:内存管理
  • fs:文件系统
  • driver:设备驱动
  • ipc:进程间通信
  • net:网络
  • ...

Makefile分析

Makefile分布在源码顶层和各个上述目录下。配置好内核后,键入make编译时,将从顶层的Makefile开始寻找规则并执行编译命令。

编译目标

进入顶层Makefile阅读,在107行,可见其默认目标为_all:

# That's our default target when none is given on the command linePHONY := _all_all:
接下来经过一些判断后来到141行:
# If building an external module we do not care about the all: rule# but instead _all depend on modulesPHONY += allifeq ($(KBUILD_EXTMOD),)_all: allelse_all: modulesendif
由于make后无参数,因此实际编译的目标是all。

all目标在498行起定义:

# The all: target is the default when no target is given on the# command line.# This allow a user to issue only 'make' to build a kernel including modules# Defaults vmlinux but it is usually overridden in the arch makefileall: vmlinuxifdef CONFIG_CC_OPTIMIZE_FOR_SIZEKBUILD_CFLAGS   += -OselseKBUILD_CFLAGS   += -O2endififneq (CONFIG_FRAME_WARN,0)KBUILD_CFLAGS += $(call cc-option,-Wframe-larger-than=${CONFIG_FRAME_WARN})endif# Force gcc to behave correct even for buggy distributions# Arch Makefiles may override this settingKBUILD_CFLAGS += $(call cc-option, -fno-stack-protector)include $(srctree)/arch/$(SRCARCH)/Makefile

不过在518行有一句include,由于我们是编译x86体系结构内核,因此实际文件名为arch/x86/Makefile。进入查看,可见在该Makefile的第208行起也有一个all的定义:

# Default kernel to buildall: bzImage# KBUILD_IMAGE specify target image being built                    KBUILD_IMAGE := $(boot)/bzImagezImage zlilo zdisk: KBUILD_IMAGE := arch/x86/boot/zImagezImage bzImage: vmlinux        $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)        $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot        $(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/bzImage

这是什么回事呢?根据顶层Makefile中的注释可知,若在编译之前没有事先配置体系结构,则默认目标为顶层的vmlinux;若已经配置过体系结构(这里是i386),则arch/x86下的Makefile中的all将覆盖原来的定义,转而编译bzImage目标。而bzImage目标将先编译顶层vmlinux,再在arch/x86/boot下生成bzImage文件。

vmlinux的生成

从上面可见,尽管默认目标有所不同,但是都会跑去生成vmlinux。所以我们回到顶层目录的Makefile去看vmlinux的生成。

在Makefile的第629行到第654行有一段关于vmlinux生成的注释。其大意是说vmlinux由vmlinux-init和vmlinux-main两个变量中的对象、外加kallsym.o链接而成。这可以在从第805~806行中得到印证:

# vmlinux image - including updated kernel symbolsvmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o) FORCE

vmlinux-init、vmlinux-main、vmlinux-lds定义在656~659行:

vmlinux-init := $(head-y) $(init-y)vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)vmlinux-all  := $(vmlinux-init) $(vmlinux-main)vmlinux-lds  := arch/$(SRCARCH)/kernel/vmlinux.lds

xxx-y在452~456行:

init-y          := init/drivers-y       := drivers/ sound/net-y           := net/libs-y          := lib/core-y          := usr/
和第606~627行:
core-y          += kernel/ mm/ fs/ ipc/ security/ crypto/ block/vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \                     $(core-y) $(core-m) $(drivers-y) $(drivers-m) \                     $(net-y) $(net-m) $(libs-y) $(libs-m)))vmlinux-alldirs := $(sort $(vmlinux-dirs) $(patsubst %/,%,$(filter %/, \                     $(init-n) $(init-) \                     $(core-n) $(core-) $(drivers-n) $(drivers-) \                     $(net-n)  $(net-)  $(libs-n)    $(libs-))))init-y          := $(patsubst %/, %/built-in.o, $(init-y))core-y          := $(patsubst %/, %/built-in.o, $(core-y))drivers-y       := $(patsubst %/, %/built-in.o, $(drivers-y))net-y           := $(patsubst %/, %/built-in.o, $(net-y))libs-y1         := $(patsubst %/, %/lib.a, $(libs-y))libs-y2         := $(patsubst %/, %/built-in.o, $(libs-y))libs-y          := $(libs-y1) $(libs-y2)
将两处定义合起来看,含义是,跑到init、kernel、mm、fs等目录下取生成各个目录的built-in.o。如果体系结构已经指定(此处为i386),还会在arch/xxx/Makefile(此处为arch/x86/Makefile)中得到head-y的定义和libs-y、core-y等的补充定义,并生成。

最后,将体系结构相关的head、各子目录下的built-in.o链接,经过一些处理,就得到了顶层vmlinux文件。

bzImage的生成

接上上节分析,bzImage伪目标会递归到arch/x86/boot/Makefile中来生成bzImage文件。

在arch/x86/boot/Makefile的第77~93行:

$(obj)/bzImage: IMAGE_OFFSET := 0x100000$(obj)/bzImage: ccflags-y := -D__BIG_KERNEL__$(obj)/bzImage: asflags-y := $(SVGA_MODE) $(RAMDISK) -D__BIG_KERNEL__$(obj)/bzImage: BUILDFLAGS   := -bquiet_cmd_image = BUILD   $@cmd_image = $(obj)/tools/build $(BUILDFLAGS) $(obj)/setup.bin \            $(obj)/vmlinux.bin $(ROOT_DEV) > $@$(obj)/zImage $(obj)/bzImage: $(obj)/setup.bin \                              $(obj)/vmlinux.bin $(obj)/tools/build FORCE        $(call if_changed,image)        @echo 'Kernel: $@ is ready' ' (#'`cat .version`')'OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE        $(call if_changed,objcopy)
和第111~120行:
LDFLAGS_setup.elf       := -T$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE        $(call if_changed,ld)OBJCOPYFLAGS_setup.bin  := -O binary$(obj)/setup.bin: $(obj)/setup.elf FORCE        $(call if_changed,objcopy)$(obj)/compressed/vmlinux: FORCE        $(Q)$(MAKE) $(build)=$(obj)/compressed IMAGE_OFFSET=$(IMAGE_OFFSET) $@
指出了下面几个事实:
  • 启动时内核被装载到0x100000(1M)
  • bzImage由setup.bin和vmlinux.bin组成(tools/build好像是一个校验程序)

从arch/x86/boot中的Makefile文件看,setup.bin中有bootloader载入映像后将直接跳转的位置。

而对于arch/x86/boot/vmlinux.bin,我们结合arch/x86/boot/compressed/Makefile去看,具体过程不再详细说明,流程为:

  1. 将顶层vmlinux剥去comment信息得到文件arch/x86/boot/compressed/vmlinux.bin
  2. 将...compressed/vmlinux.bin用gzip压缩成...compressed/vmlinux.bin.gz
  3. 将...compressed/vmlinux.bin.gz作为纯数据链接生成...compressed/piggy.o
  4. 将head_32.o、misc.o和piggy.o链接生成...compressed/vmlinux
  5. 将...boot/compressed/vmlinux文件剥去所有elf信息,得到纯二进制文件...boot/vmlinux.bin

至此bzImage生成完毕。

结论

内核生成简要过程如下:

  1. 进入各目录按照配置文件各生成一个built-in.o文件
  2. 在顶层目录将各个built-in.o文件链接成为vmlinux
  3. 编译体系结构setup代码,并与带自解压的、压缩过的vmlinux相链接,得到bzImage

系统开机到Linux内核流程分析

BIOS

系统上电复位后,CPU从0xfffffff0开始取指令。而这一块是ROM区,其中存有跳转指令,跳转至ROM中的BIOS代码开始运行。

BIOS运行POST(上电自检)并初始化硬件设备,然后根据配置,从软盘/硬盘/CD-ROM等设备上把对应设备的第一个扇区的内容读到RAM(0x7c00)处,然后跳转到0:0x7c00处执行。

这里将GRUB的stage1读入了0:0x7c00处。

Bootloader(以GRUB-0.97为例)

stage1会根据情况从磁盘中读入stage2到RAM并跳转到stage2的start(或stage1.5,最终进入stage2)。

stage2从grub-0.97/stage2/asm.S的start开始执行,收集一些信息后进入保护模式,最终转入grub-0.97/stage2/stage2.c的cmain函数执行。

用户输入"kernel bzImage"命令后,最终进入grub-0.97/stage2/boot.c的load_image函数。

load_image先读入文件头部并判断类型,到第219行判断为Linux内核,开始通过启动协议装载内核和填写参数。

最终:

  • linux_data_real_addr = 0x90000,即setup代码的装载地址;
  • (447行)读入自解压vmlinux部分,装载到0x100000。

用户再输入"boot"命令,最终进入grub-0.97/stage2/asm.S的big_linux_boot。该函数先退回实模式,再长跳到0x9020:0,即setup入口,执行。

Linux

首先说明,这里分析的是big kernel模式编译、从GRUB-0.97载入的情形。

准备16位C代码环境

bootloader的长跳会跳转到arch/x86/boot/header.S中的_start开始执行。

_start处是一个手写的短跳指令,跳转到start_of_setup处执行。

start_of_setup行为为:

  1. 重置磁盘控制器
  2. 设置C语言运行环境
    • 保证%es == %ds
    • 设置堆栈%ss:%sp指向正确位置
    • 调整%cs == %ds
    • 将BSS段清零
  3. 检查签名
  4. 跳转到C代码main函数

16位C代码

main函数在arch/x86/boot/main.c中。

main函数的行为为:

  1. 拷贝启动参数
  2. 设置堆
  3. 收集硬件信息(内存布局等)
  4. 设置硬件状态(video等)

最后转到go_to_protected_mode继续执行。

go_to_protected_mode在arch/x86/boot/pm.c中。主要任务为(假设无hook):

  1. 关中断、“屏蔽”NMI
  2. 打开A20地址线
  3. 设置FPU
  4. 编程8259A,禁止所有外部中断
  5. 设置GDT与IDT
  6. 长跳到32位代码入口
GDT与IDT的设置

GDT与IDT设置在arch/x86/boot/pm.c中113行起始的一段代码中:

static void setup_gdt(void){        /* There are machines which are known to not boot with the GDT           being 8-byte unaligned.  Intel recommends 16 byte alignment. */        static const u64 boot_gdt[] __attribute__((aligned(16))) = {                /* CS: code, read/execute, 4 GB, base 0 */                [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),                /* DS: data, read/write, 4 GB, base 0 */                [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),                /* TSS: 32-bit tss, 104 bytes, base 4096 */                /* We only have a TSS here to keep Intel VT happy;                   we don't actually use it for anything. */                [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),        };        /* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead           of the gdt_ptr contents.  Thus, make it static so it will           stay in memory, at least long enough that we switch to the           proper kernel GDT. */        static struct gdt_ptr gdt;        gdt.len = sizeof(boot_gdt)-1;        gdt.ptr = (u32)&boot_gdt + (ds() << 4);        asm volatile("lgdtl %0" : : "m" (gdt));}/* * Set up the IDT */static void setup_idt(void){        static const struct gdt_ptr null_idt = {0, 0};        asm volatile("lidtl %0" : : "m" (null_idt));}

可见启动阶段GDT表共三项,分别为4G代码段、4G数据段、无用的TSS。而IDT直接为空。

32位代码跳转

32位代码跳转是由arch/x86/boot/pmjump.S中的protected_mode_jump实现的。 从实模式到保护模式的分界线为第47行的手写长跳指令,之后in_pm32代码在保护模式运行。 在in_pm32中简要地设置了TR和LDT后,就转到真正的32位入口(1M)了。

32位入口

根据前面bzImage中vmlinux的生成过程、bootloader载入映像过程和链接脚本 .../compressed/vmlinux_32.lds 可知,当前内存中1M处的代码是arch/x86/boot/compressed/head_32.S中的startup_32。

startup_32的首要工作是将带自解压的vmlinux拷贝到更高的位置(89~100行),以便接下来的解压不会把自身覆盖掉:

/* Copy the compressed kernel to the end of our buffer * where decompression in place becomes safe. */        pushl %esi        leal _end(%ebp), %esi        leal _end(%ebx), %edi        movl $(_end - startup_32), %ecx        std        rep        movsb        cld        popl %esi

值得注意的是,拷贝时是从高地址到低地址,这样可以保证即使搬运前后的地址范围有重合也没有问题。复制完后跳到新的relocated处执行(117~150行):

relocated:/* * Clear BSS */        xorl %eax,%eax        leal _edata(%ebx),%edi        leal _end(%ebx), %ecx        subl %edi,%ecx        cld        rep        stosb/* * Setup the stack for the decompressor */        leal boot_stack_end(%ebx), %esp/* * 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 boot_heap(%ebx), %eax        pushl %eax        # heap area as third argument        pushl %esi        # real mode pointer as second arg        call decompress_kernel        addl $20, %esp        popl %ecx

这里是准备C代码环境、传递参数,然后调用arch/x86/boot/compressed/misc.c中的decompress_kernel进行解压。该函数解压过程还显示经典的"Decompressing Linux... done."、"Booting the kernel."。

之后,再次跳转(还是1M),进入解压后的真正的内核入口。

真正的内核入口

现在转入的是arch/x86/kernel/head_32.S的startup_32。

首先又是做一些很琐碎的搬动参数、清空内存之类的操作,然后进入default_entry开始设置页表。

不考虑PAE,进入228~255行:

page_pde_offset = (__PAGE_OFFSET >> 20);        movl $pa(pg0), %edi        movl $pa(swapper_pg_dir), %edx        movl $PTE_ATTR, %eax10:        leal PDE_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, %ecx11:        stosl        addl $0x1000,%eax        loop 11b        /*         * End condition: we must map up to and including INIT_MAP_BEYOND_END         * bytes beyond the end of our own page tables; the +0x007 is         * the attribute bits         */        leal (INIT_MAP_BEYOND_END+PTE_ATTR)(%edi),%ebp        cmpl %ebp,%eax        jb 10b        movl %edi,pa(init_pg_tables_end)        /* Do early initialization of the fixmap area */        movl $pa(swapper_pg_fixmap)+PDE_ATTR,%eax        movl %eax,pa(swapper_pg_dir+0xffc)

10处是建立PDE项,11是建立PTE项。此处会建立从0~INIT_MAP_BEYOND_END的对等映射,且虚拟地址__PAGE_OFFSET(3G)的目录项共享从0开始的虚拟地址的PTE。因此3G~3G+INIT_MAP_BEYOND_END映射到物理地址0~INIT_MAP_BEYOND_END。

注意因为链接时vmlinux起始地址为3G+1M,而物理地址为1M,因此要用pa宏来引用物理地址。

接下来跳过SMP启动代码和CR4设置,来到326~339行:

6:/* * Enable paging */        movl $pa(swapper_pg_dir),%eax        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 */1:        /* Set up the stack pointer */        lss stack_start,%esp

这里开启分页,并跳到标号1,设置新堆栈。从标号1起,代码开始在3G以上空间运行。

接下来跳转到setup_idt设置IDT表(485~515行):

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 */        lea idt_table,%edi        mov $256,%ecxrp_sidt:        movl %eax,(%edi)        movl %edx,4(%edi)        addl $8,%edi        dec %ecx        jne rp_sidt.macro        set_early_handler handler,trapno        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        set_early_handler handler=early_page_fault,trapno=14        ret

这里将0、6、13、14号异常处理程序设置为early_xxx,若接下来发生这几种类型的异常,将死机。而对其他向量,都置为ignore_int,即为忽略。

再接下来又是琐碎的检测CPU的工作,并加载IDT,最后终于到达452行:

        jmp i386_start_kernel

这个符号在arch/x86/kernel/head32.c中:

void __init i386_start_kernel(void){        start_kernel();}

内核初始化

start_kernel在init/main.c第534行定义。它依次初始化各个模块(页表、中断向量等再次被修改到正常运行的状态)并变身0号进程,最终到达rest_init(455行):

static void noinline __init_refok rest_init(void)        __releases(kernel_lock){        int pid;        kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);        numa_default_policy();        pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);        kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);        unlock_kernel();        /*         * The boot idle thread must execute schedule()         * at least once to get things moving:         */        init_idle_bootup_task(current);        preempt_enable_no_resched();        schedule();        preempt_disable();        /* Call into cpu_idle with preempt disabled */        cpu_idle();}

0号进程fork出kernel_init。kernel_init将试图准备环境,并执行根文件系统上的init程序(1号进程)来完成用户的启动。0号进程则在最后进入cpu_idle,成为idle进程,内核初始化完成。

结论

从GRUB入口开始到初始化完成的流程如下:

  1. arch/x86/boot/header.S::_start
  2. arch/x86/boot/main.c::main
  3. arch/x86/boot/pm.c::go_to_protected_mode
  4. arch/x86/boot/pmjump.S::protected_mod_jump
  5. arch/x86/boot/compressed/head_32.S::startup_32 (1M)
  6. arch/x86/boot/compressed/misc.c::decompress_kernel
  7. arch/x86/kernel/head_32.S::startup_32 (1M,之后3G+1M附近)
  8. arch/x86/kernel/head32.c::i386_start_kernel
  9. init/main.c::start_kernel, rest_init
0 0