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

1,551 阅读4分钟

谈到安卓手机,最先映入眼前的,肯定是开机过程,而安卓系统又是建立在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系统,我将在下一篇文章中和各位一起学习,同时希望各位可以指正小弟文章中不严谨的地方。