美文网首页
安卓手机启动时发生的那些事儿——上篇

安卓手机启动时发生的那些事儿——上篇

作者: 邱穆 | 来源:发表于2020-11-06 21:49 被阅读0次

谈到安卓手机,最先映入眼前的,肯定是开机过程,而安卓系统又是建立在Linux内核之上的,那么开机的时候,到底是怎么启动的呢?发生了哪些事情呢?本篇文章,笔者就和大家一起学习学习。

一、概览

在安卓手机上,整个系统的启动可以分为三个过程,如下:

  • BIOS和BootLoader阶段
  • Linux内核启动
  • Android系统启动

第一阶段主要由硬件和汇编语言完成,第二部分主要由C语言完成,第三部分主要由java完成,很多文档只会讲解了第三阶段的任务,我们就要追本溯源,看看从加电开始的启动过程。

二、启动过程

2.1 BIOS和BootLoader启动

这一部分一般由硬件厂商负责设计和实现,以x86为例,加电后,cpu工作在实模式下,该模式下cpu的寻址空间为1M,寄存器都复位为默认值,其中CS:IP的复位值是FFFF:0000,也就是说从这里开始执行指令,那么主板设计值必须保证这个地址映射到的位置是BIOS芯片的程序地址,而不是RAM。当运行BIOS上面的程序后,物理地址前1KB内存中会建立实模式的中断向量表,随后的一部分来保存BIOS启动阶段检测到的硬件信息。最后BIOS根据配置信息将BootLoader加载到物理地址0x07c00处,然后跳转到这里开始执行BootLoader。
而在ARM上,执行类似BIOS操作的,是固化在ROM上的Boot程序,这部分程序加载BootLoader到RAM然后跳转执行。
Header.s是这个阶段执行的重要汇编代码,一共两个512字节(boot sector和setup,分别加载到内存地址0X00090000和0X0009200,同时把Linux小内核映像加载到内存地址0X00010000或者Linux大内核映像加载到内存地址0X00100000,最后跳转到header.S代码的setup代码。第一个512字节的内容是为了兼容软驱时代而存在的。它正好被放在一个磁盘扇区之内。真正的kernel入口从第二个512字节开始,当今的bootloader把控制权交到这个入口。
Header.s主演完成以下任务:

  • 设置实模式堆栈(为运行程序做好准备)
  • 检查setup中的标签
  • 清除BSS段
  • 调用C入口main(位于boot/main.c)
    关键代码如下:
# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
        pushw   %ds
        pushw   $6f
        lretw
6:

# Check signature at end of setup
        cmpl    $0x5a5aaa55, setup_sig
        jne     setup_bad

# Zero the bss
        movw    $__bss_start, %di
        movw    $_end+3, %cx
        xorl    %eax, %eax
        subw    %di, %cx
        shrw    $2, %cx
        rep; stosl

# Jump to C code (should not return)
        calll   main

2.2 kernel启动

执行main.c函数,其实已经是kernel的一部分,不过还有很多工作没有做,main()会处理一些登记工作,复制参数,建立内存映射等等,然后通过go_to_protected_mode()跳转到保护模式,而go_to_protected_mode()需要设置临时的IDT(中断描述符表)、GDT(全局描述表),因为保护模式和实模式的IDT和GDT是不同的,所有要提前设置好。重要代码如下:

void main(void)
{
    /* First, copy the boot header into the "zeropage" */
    copy_boot_params();

    /* End of heap check */
    init_heap();

    /* Make sure we have all the proper CPU support */
    if (validate_cpu()) {
        puts("Unable to boot - please use a kernel appropriate "
             "for your CPU.\n");
        die();
    }

    /* Tell the BIOS what CPU mode we intend to run in. */
    set_bios_mode();

    /* Detect memory layout */
    detect_memory();

    /* Set keyboard repeat rate (why?) */
    keyboard_set_repeat();

    /* Query MCA information */
    query_mca();

    /* Voyager */
#ifdef CONFIG_X86_VOYAGER
    query_voyager();
#endif

    /* Query Intel SpeedStep (IST) information */
    query_ist();

    /* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
    query_apm_bios();
#endif

    /* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
    query_edd();
#endif

    /* Set the video mode */
    set_video();

    /* Do the last things and invoke protected mode */
    go_to_protected_mode();
}

void go_to_protected_mode(void)
{
    /* Hook before leaving real mode, also disables interrupts */
    realmode_switch_hook();

    /* Move the kernel/setup to their final resting places */
    move_kernel_around();

    /* Enable the A20 gate */
    if (enable_a20()) {
        puts("A20 gate not responding, unable to boot...\n");
        die();
    }

    /* Reset coprocessor (IGNNE#) */
    reset_coprocessor();

    /* Mask all interrupts in the PIC */
    mask_all_interrupts();

    /* Actual transition to protected mode... */
    setup_idt();//中断描述符表
    setup_gdt();//全局描述符表
    protected_mode_jump(boot_params.hdr.code32_start,
                (u32)&boot_params + (ds() << 4));
}

最后一步的protected_mode_jump()会跳转到pmjump.s中执行,这一步通过设置cr0来进入保护模式,然后进入setup header中指定的code32_start,code32_start在header.s的第二部分,对应压缩镜像的head_32.s。
head_32.s代码如下:

        __HEAD
ENTRY(startup_32)
        cld
        /*
         * Test KEEP_SEGMENTS flag to see if the bootloader is asking
         * us to not reload segments
      */
      testb   $(1<<6), BP_loadflags(%esi)
      jnz     1f

      cli
      movl    $__BOOT_DS, %eax
      movl    %eax, %ds
      movl    %eax, %es
      movl    %eax, %fs
      movl    %eax, %gs
      movl    %eax, %ss

这部分代码调用startup_32函数,调用decompress_kernel()解压Linux内核映像,然后跳转到kernel,关键代码如下:

 * 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

/*
 * Jump to the decompressed kernel.
 */
    xorl %ebx,%ebx
    jmp *%ebp

在vmlinux.lds中定义了start_kernel的位置,总之最后会调用start_kernel()函数。
start_kernel函数完整的初始化了所有Linux内核,包括进程调度、内存管理、系统时间等,最后调用kernel_thread()创建init进程。
到这一步的流程可以如图所示:

Linux启动
到了这一步,Linux内核基本启动完成,不过还有一点需要注意的是,Linux
下有 3 个特殊的进程, idle(swapper)(进程 PID = 0 )、 init 进程( PID =
1 )和 kthreadd(PID = 2)。其功能和特点如下:
  • idle(swapper)进程由系统自动创建,运行在内核态
    idle进程其pid =0,其前身是系统创建的第一个进程,也是唯一一个没有通过 fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换,常常被称为交换进程。
  • init 进程由idle通过kernel_thread创建,在内核空间完成初始化后,加载init程序,并最终转变为用户空间的init进程
    由0号进程创建,完成系统的初始化,是系统中所有其它用户进程的祖先进程。Linux 中的所有进程都是有init进程创建并运行的。首先 Linux 内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成后,init将变为守护进程监视系统其他进程。
  • kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间,负责所有内核线程的调度和管理
    它的任务就是管理和调度其他内核线程 kernel_thread ,会循环执行一个kthreadd函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread,我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程。

三、小结

到这一步,Linux内核已经完成了启动,下面将要启动Android系统,我将在下一篇文章中和各位一起学习,同时希望各位可以指正小弟文章中不严谨的地方。

相关文章

网友评论

      本文标题:安卓手机启动时发生的那些事儿——上篇

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