美文网首页
Brief notes of Magenta's Booting

Brief notes of Magenta's Booting

作者: oceanken | 来源:发表于2017-11-28 19:40 被阅读0次

    Magenta使用的bootloader是一个UEFI的实现,目前支持通过QEMU、Raspberry Pi 3启动(官方说还支持Acer 和 NUC 设备启动)。本文介绍了Magenta在X86_64平台上从load kernel到boot kernel 及 user boot的过程。

    1. // bootloader/build.mk中通过向链接器指定bootloader的entry:

    ifeq ($(call TOBOOL,$(USE_CLANG)),true)
        EFI_LDFLAGS := /subsystem:efi_application /entry:efi_main /libpath:out
    else
       EFI_LINKSCRIPT := $(LOCAL_DIR)/build/efi-x86-64.lds
       EFI_LDFLAGS := -nostdlib -T $(EFI_LINKSCRIPT) -pie -Lout
    endif
    

    可以看出,如果使用clang来编译,直接使用entry来指定最终链接后的程序入口;如果未使用clang,则在程序段中直接定义了ENTRY:

    // bootloader/build/efi-x86-64.lds
    OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")
    OUTPUT_ARCH(i386:x86-64)
    ENTRY(efi_main)
    SECTIONS
    {
        . = 0x2000;
    ......
    

    最终,都会在链接后的程序段中定义bootloader的入口为efi_main。

    2. efi_main在/bootloader/src/osboot.c中实现,它首先初始化efi对象和封装在efi_system_table结构中的efi_runtime_services和efi_boot_services对象,通过这些对象可以调用EFI相关协议接口,从而实现简单的文件、驱动、网络、图形输出、字符IO、USB IO等控制,进一步了解可以查询UEFI协议定义,这里不深入讨论。
    1) efi_main在完成XEFI初始化后,即通过EFI加载cmdline,从而通过cmdline读取“bootloader.fbres”参数,该参数以“WxH”的形式返回,定义了图形输出的宽高。随后系统切换到GFX模式并绘制开机Logo。
    2) 接下来,efi_main通过cmdline设定默认从网络启动系统,并检查网络接口是否可用。
    3) 通过efi文件系统从磁盘加载内核镜像magenta.bin文件。
    4) 随后打印启动菜单
    5) 如果选择启动magenta.bin,则试图加载用户镜像ramdisk.bin并调用boot_kernel启动内核。

    3. boot_kernel在/bootloader/src/magenta.c中实现,它主要是将内核文件映射到内存,分配并初始化ZeroPage,获取物理内存的e820表,然后调用start_kernel()进行内核初始化。
    1) load_kernel()首先检测内核镜像大小,小于1024字节的文件将不被接受;然后检测镜像起始偏移0x202处的magic number是否为0x53726448,如果不是,则不是一个magenta内核,拒绝加载;随后检查偏移0x206处 的高两字节的Version号。
    2) load_kernel()然后在物理地址0xFF000处为内核分配ZeroPage和cmdline,并将kernel镜像中的header拷贝到ZeroPage;随后在物理地址0x100000处为内核分配页表,并将镜像中的除头部外的数据拷贝到内存,从而完成内存镜像从磁盘到内存的映射。请注意,这里的内存分配均是在UEFI中完成,是直接操作物理内存。具体的内核内存映射将在后续文中介绍。
    3) 完成内核镜像加载后,将利用efi_graphics_output_protocol得到的图形输出设备信息初始化FrameBuffer相关参数。
    4) 随后通过EFI_BOOT_SERVICES读取物理内存分布及使用情况,并设定e820table基址并初始化e820表。随后,退出bootloader,准备执行内核代码。
    5) 随后,最后调用start_kernel()跳转到内核代码段执行。

    4. start_kernel()在/bootloader/src/magenta.c中实现,它仅仅是完成向内核入口的跳转,然后便进行一个无限循环(实际不上会有机会执行)。

    static void start_kernel(kernel_t* k) {
    // 64bit entry is at offset 0x200
    uint64_t entry = (uint64_t)(k->image + 0x200);
    
    // ebx = 0, ebp = 0, edi = 0, esi = zeropage
    __asm__ __volatile__(
    "movl $0, %%ebp \n"
    "cli \n"
    "jmp *%[entry] \n" ::[entry] "a"(entry),
    [zeropage] "S"(k->zeropage),
    "b"(0), "D"(0));
    for (;;)
        ;
    }
    

    它的实现代码不长,这里全部贴出来。实际上就是向内核代码偏移0x200的地方跳转。接下来我们看看0x200的代码段是怎么样,它定义在/magenta/kernel/arch/x86/64/start.S中。

    5. 开启内核之门。

    //magenta/kernel/arch/x86/64/start.S
    .section ".text.boot"
    .code32
    ......
    
    .align 8
    .code64
    farjump64:
    /* branch to our high address */
    mov $high_entry, %rax
    jmp *%rax
    
    high_entry:
    /* zero our kernel segment data registers */
    xor %eax, %eax
    mov %eax, %ds
    mov %eax, %es
    mov %eax, %fs
    mov %eax, %gs
    mov %eax, %ss
    
    /* load the high kernel stack */
    mov $(_kstack + 4096), %rsp
    
    /* reload the gdtr */
    lgdt _gdtr
    
    /* set up the idt */
    mov $_idt, %rdi
    call idt_setup
    lidt _idtr
    /* assign this core CPU# 0 and initialize its per cpu state */
    mov $0, %rdi
    call x86_init_percpu
    /* call the main module */
    call lk_main
    ......
    
    /* 64bit entry point from a secondary loader */
    .align 8
    .org 0x200
    FUNCTION(_entry64)
    mov %esi, PHYS(_zero_page_boot_params)
    ......
    /* long jump to our code selector and the high address */
    push $CODE_64_SELECTOR
    push $high_entry
    lretq
    

    从上述代码可以看出,偏移0x200处定义了函数_entry64,即64位平台的内核入口。_entry64中,进行了x86页表初始化,然后启用PGE和设定PML4T(Page Map Level4 Table),这里涉及到x86的分页和64位寻址的机制,不深入展开。在_entry64的结尾处,将high_entry地址置于栈顶,随后lretq指令将high_entry地址出栈并写入%eip,CPU开始执行high_entry处的指令。在寄存器清零、设定内核栈、加载gdtr、设定中断向量及初始化cpu后,便调用lk_main()进行内核初始化。

    6. lk_main()在kernel/top/main.c中实现。lk_main()中,lk_init系统对各通过LK_INIT_HOOK(kernel/include/lk/init.h)向lk_init注册的模块进行初始化。这里我们可以看到启动的Welcome Message也这里打印,heap初始化和kernel初始化都这里进行。Heap初始化过程和内核中VMM模块暂不作介绍。kernel_init()在magenta\kernel\kernel\init.c中实现,对mp、thread和timer进行了初始化。等系统heap和thread等初始化完成后,内核创建了一个名为“bootstrap2”的内核thread进行第二阶段的启动过程,同时将当前内核线程休眠,准备从内核启动向系统启动过渡。

    //kernel/top/main.c
    
    /* called from arch code */
    void lk_main(ulong arg0, ulong arg1, ulong arg2, ulong arg3)
    {
    ......
    // get us into some sort of thread context
    thread_init_early();
    
    // deal with any static constructors
    call_constructors();
    
    // early arch stuff
    lk_primary_cpu_init_level(LK_INIT_LEVEL_EARLIEST, LK_INIT_LEVEL_ARCH_EARLY - 1);
    arch_early_init();
    ......
    #if WITH_SMP
        dprintf(INFO, "\nwelcome to lk/MP\n\n");
    #else
        dprintf(INFO, "\nwelcome to lk\n\n");
    #endif
    
    dprintf(INFO, "git revision [%s]\n", MAGENTA_GIT_REV);
    dprintf(INFO, "boot args 0x%lx 0x%lx 0x%lx 0x%lx\n",
    lk_boot_args[0], lk_boot_args[1], lk_boot_args[2], lk_boot_args[3]);
    
    // bring up the kernel heap
    lk_primary_cpu_init_level(LK_INIT_LEVEL_TARGET_EARLY, LK_INIT_LEVEL_HEAP - 1);
    dprintf(SPEW, "initializing heap\n");
    heap_init();
    
    // initialize the kernel
    lk_primary_cpu_init_level(LK_INIT_LEVEL_HEAP, LK_INIT_LEVEL_KERNEL - 1);
    kernel_init();
    lk_primary_cpu_init_level(LK_INIT_LEVEL_KERNEL, LK_INIT_LEVEL_THREADING - 1);
    
    // create a thread to complete system initialization
    dprintf(SPEW, "creating bootstrap completion thread\n");
    
    thread_t *t = thread_create("bootstrap2", &bootstrap2, NULL, DEFAULT_PRIORITY, 
        DEFAULT_STACK_SIZE);
    thread_set_pinned_cpu(t, 0);
    thread_detach(t);
    thread_resume(t);
    
    // become the idle thread and enable interrupts to start the scheduler
    thread_become_idle();
    }
    

    7. bootstrap2()开始系统启动过程。在bootstrap2()中继续进行platform、target的初始化,最后调用userboot的hook方法userboot_init初始化userboot。

    8. userboot_init()在kernel/lib/userboot/userboot.cpp中实现。首先进行console初始化,随后调用attempt_userboot()启动第一个用户态进程。

    void userboot_init(uint level) {
    \#if !WITH_APP_SHELL
        dprintf(INFO, "userboot: console init\n");
        console_init();
    \#endif
    attempt_userboot(user_bootfs, user_bootfs_len);
    }
    

    9. attempt_userboot()中涉及到VMM和Process相关的调用 ,这里暂不深入,待后续文章介绍。它的主要工作是:
    1) 创建用户栈,将ramdisk和bootfs映射到虚拟地址空间
    2) 创建bootstrap message,准备VMO、JobDispatcher、Resource等内核对象的Handles,通过message传递给将要创建的第一个用户进程(userboot)。
    3) 找到vdso的入口,并设定entry启动第一个用户进程。
    4) 通过Channel向用户态userboot进程发送bootstrap消息。

    10. vdso的入口在/system/core/userboot/rules.mk中定义:

    # userboot is a reentrant DSO (no writable segment) with an entry point.
    MODULE_LDFLAGS := -T $(BUILDDIR)/rodso.ld -e _start
    

    即/system/core/userboot/start.c中的“_start”方法,它的入参即为attempt_userboot()中创建的channel。_start()方法随即调用bootstrap()开始启动用户进程。

    11. /system/core/userboot/start.c中bootstrap()主要工作为:
    1) 读取内核线程bootstrap2发过来的bootstrap message。
    2) 将bootstrap消息解析为环境变量信息,为用户子进程提供类似辅助向量的内核信息。
    3) 创建子进程,为其分配栈空间,并复用当前进程的内核对象Handles。
    4) 搜索从bootstrap message中传递过来的bootfs_vmo,找到其中的ELF文件(bin/devmgr), 从bootfs中搜索并加载链接器lib/ld.so.1,由loader加载其他的共享库。
    5) 创建子进程的首线程,并向子进程传递内核bootstrap消息,随后启动子进程。
    6) devmgr启动后,userboot退出。

    12. devmgr启动。其实现位于/system/core/devmgr目录。devmgr会启动"/boot/bin/netsvc"、"/boot/bin/sh"、"/boot/bin/mxsh"、 "/boot/autorun"、 "/system/autorun"、"/system/bin/application_manager"等系统进程,待console和shell准备好后,完成启动流程。

    附: QEMU下Magenta启动时console输出:


    image.png

    相关文章

      网友评论

          本文标题:Brief notes of Magenta's Booting

          本文链接:https://www.haomeiwen.com/subject/zfgebxtx.html