Linux内核启动分析(中)

来源:互联网 发布:linux下安装配置jdk 编辑:程序博客网 时间:2024/06/05 10:55

Linux内核启动分析(中)

0. linux-3.2.0-32

1. bzImage由setup和vmlinux两部分组成,setup是实模式下的代码,vmlinux是保护模式下的代码。

2. BIOS把Boot Loader加载到0x7C00的地方并跳转到这里继续执行之后,BootLoader就会把实模式代码setup加载到0x07C00之上的某个地址上,其中setup的前512个字节是bootsector(引导扇区),现在这个引导扇区的作用并不是用来引导系统,而是为了兼容及传递一些参数。之后Boot Loader跳转到setup的入口点,入口点为_start例程(根据arch/x86/boot/setup.ld可知)。

3. 实模式设置(setup)阶段用于体系结构相关的硬件初始化工作,涉及的文件有arch/x86/boot/header.S、链接脚本setup.ld、arch/x86/boot/main.c。header.S第一部分定义了bstext、.bsdata、.header这3个节,共同构成了vmlinuz的第一个512字节(即引导扇区的内容)。常量BOOTSEG和SYSSEG定义了引导扇区和内核的载入地址。

4. 内核映像文件vmlinuz:包含有linux内核的静态链接的可执行文件,传统上,vmlinux被称为可引导的内核镜像。vmlinuz是vmlinux的压缩文件。其构成如下:
    (1)第一个512字节(以前是在arch/i386/boot/bootsect.S);
    (2)第二个,一段代码,若干个不多于512字节的段(以前是在arch/i386/boot/setup.S);
    (3)保护模式下的内核代码(在arch/x86/boot/main.c)。


      在linux下,GRUB stage 2 通过读取/boot/vmlinuz-3.2.0.32-generic-pae 文件到内存并跳转到kernel启动入口,对应上图中的kernel setup。Intel架构的kennel启动初始代码在arch/x86/boot/header.S中。是用纯汇编写的,kernel中的大部分用的是C而只有少部分性能关键的位置才会用汇编。看一下header.S文件就会发现前512字节组成了boot sector:.bstext .bsdata.header(15字节);现在他存在的意义只限于兼容古石器时代,没有bootloader的情况;如果有幸执行到这段code他只会打印“bugger_off_msg”的内容,然后reboot。

 

      在这512字节之后,偏移0x200处,就是实模式下kenerl的入口地址。在header.S:110处,这里简单的放着一个jump指令(直接用机器码写的):0xeb(其中jump对应的机器码是EB,查看#hexdump -c vmlinuz-xxx | less)。你可以试一下,确保咱这不是在YY。跳转到header.S:241处:start_of_setup标签处。这里会设置栈、初始化bss段(这有实模式需要的静态变量),然后就跳转到arch/x86/boot/main.c:128处(calll main)。


      main()函数做一些内存检测、设置视频模式等。调用copy_boot_params()复制启动参数到boot_params,其存放在boot sector的.bsdata中,也可以在

GRUB stage 1.5时用户输入;调用console_init();检查堆尾init_heap();设置BIOS mode,set_bios_mode();内存布局检测,detect_memory();设置键盘频率,keyboard_set_repeat(),就是说要怎么对待当你恩下一个键不放手的事件。。。最后调用go_to_protected_mode()中protectec_mode_jump()(arch/x86/boot/pmjump.S)进入保护模式。在准备好进入保护模式之前最主要的俩件事是:中断和内存。在实模式下中断向量表始终从地址0开始,但是保护模式下CPU使用IDTR索引的IDT(Interrupt Descriptor Table),其中放着中断向量,因而进入保护模式前要设置好IDT。同样的保护模式下的内存访问也和实模式下的访问区别甚远,前者会使用段模式和页模式对内存进行管理(具体的可参见相关资料,http://zhidao.baidu.com/question/55293031.html;或者linux-0.11完全注释的前几章讲的很棒),CPU会用GDTR来索引GDT(Glogal Descriptor Table),其中其中放着若干个描述符,通过他们来访问进程的地址空间。

      go_to_protected_mode()就会调用setup_idt(), setup_gdt(),设置临时的中断描述符表和全局描述符表,内核真正运行起来时还会重置的。protected_mode_jump()也是用汇编写的。他是通过设置CPU的CR0寄存器的PE位实现的。这个时候是没有打开页模式的,上一篇提到过,段模式是必须的而页模式则是可选的,在初期页模式暂时用不着的,分页模式是在arch/kernel/head_32.S中的startup_32中才打开的。这样一置位PE我们就拥有4G的RAM了。接着他会调用32位内核的入口函数(startup_32(),那是怎么进去的?go_to_protected_mode() --> protected_mode_jump(boot_params.hdr.code32_start,  (u32)&boot_params + (ds() << 4));,  code32_start在header.s中为0x100000,就是说你的保护模式下的kernel位置,也是入口点...)  --> startup_32(), code32_start在build.c中被赋值的 ) :arch/x86/kernel/head_32.S/startup_32()中的decompress_kernel(),是个C函数,来解压kernel。

      decompress_kernel()会打印我们熟悉的“Decompressing Linux...”,一旦成功解压缩会打印:“done.”“Booting the kernel.”,会覆盖之前压缩的内核。也就是说解压缩的内核是从1MB处开始的。最后就会跳到最终的入口点:0x100000处,这个地方也有个startup_32,但文件位置是:/arch/x86/kernel/head_32.S:87不再是../boot/下了。

      这第二个startup_32也是用汇编写的,因为他包含一些32bit模式的初始化(硬件相关)。清零bss段,boot_params以及boot_command_line,设置GDT,建立页表打开页模式,初始化栈,建立IDT,等等,然后最终跳到最后的架构无关的kernel启动入口:start_kernel()!整个世界一下子清净了...startup_32--> i386_start_kernel(arch/x86/kernel:32) --> start_kernel()(arch/x86/kernel:467)...在start_kernel()之前是体系架构相关的,之后是架构独立的,起着承前启后的作用。


      start_kernel()近乎所有代码都是用C写的、架构无关的。他即是教练,指挥着一系列初始化函数完成规定动作:初始化各个子系统和数据结构。包括:调度器、内存模块,等等。待得一切准备就绪后,start_kernel()会调用rest_init(),创建一个进程,调用kernel_init()。还会调用schedule()来启动任务调度器,或者没事可做时调用cpu_idle()睡觉去。后者会一直保持运行,空转直到有新的任务需要调度。

      从摁下电源键那一刻起,从BIOS到MBR到boot sector到bootloader到real-mode kernel到protected-modekernel,一直跳一直跳一直跳一直跳直到这里的cpu_idle(),难道只是为了看他空转?当然了,不是!

      还有另一条路,在kernel_init()中还会激活剩余的CPU核心,前一篇咱说过,初始启动代码只是在一个核心上执行的。最后kernel_init()会调用init_post()来执行用户态进程,顺序是:/sbin/init、/etc/ini和/bin/sh。如果全都失败,内核就惊慌了!正常情况下init会以PID1来运行。他会根据配置文件来启动相关进程,加载X11,打开登录会话,网络守护进程或者其他什么的。接着你就可以上网聊天、搜教育资源了!


参考:

[1]   http://duartes.org/gustavo/blog/post/kernel-boot-process

原创粉丝点击