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
网友评论