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