基于x86体系结构分析linux的启动过程

来源:互联网 发布:网络竞彩诈骗 编辑:程序博客网 时间:2024/05/20 05:57

仅考虑32位体系结构

不考虑多核多处理器

要求1:分析流程按照开机-->BIOS-->grub-->Linux的顺序进行,到start_kernel结束

 

第一步——>加载BIOS:

 

打开计算机电源后,计算机会首先加载BIOS信息,这是因为BIOS中包含了CPU的相关信息、设备启动顺序信息、硬盘信息、内存信息、时钟信息、PnP特性等等。BIOS所做的工作如下:

1.检测连接硬件,比如显卡,内存,磁盘等等,检测的目的是以后把这些设备信息提供给操作系统;
2.寻找启动磁盘,每一种BIOS都会有开机启动菜单,可以在菜单里设置以哪个设备启动系统
比如:光驱,硬盘,网络等等,这个菜单可以设置多个选项,依照设置次序在设备上寻找启动信息;

读取所指定的硬盘上的MBR(主引导记录,在硬盘上第0磁道第一个扇区),并将其拷贝到0x7c00地址所在的物理内存(RAM)中,这部分内容其实就是Boot Loader,即操作系统内核映像,对应到电脑上的lilo或者grub.

 

第二步——>grub

 

grub就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。

grub是通过将两阶段的引导加载程序转换成三阶段的引导加载程序来实现加载Linux内核功能的。阶段 1 (MBR)引导了一个阶段 1.5 的引导加载程序,它可以理解包含 Linux 内核映像的特殊文件系统。这方面的例子包括reiserfs_stage1_5(要从 Reiser 日志文件系统上进行加载)或 e2fs_stage1_5(要从 ext2 或 ext3 文件系统上进行加载)。当阶段 1.5 的引导加载程序被加载并运行时,阶段 2 的引导加载程序就可以进行加载了。当阶段 2 加载之后,GRUB 就可以在请求时显示可用内核列表(在 /etc/grub.conf 中进行定义,同时还有几个软符号链接/etc/grub/menu.lst 和 /etc/grub.conf)。将第二阶段的引导加载程序加载到内存中之后,就可以对文件系统进行查询了,并将默认的内核映像和 initrd 映像加载到内存中。当这些映像文件准备好之后,阶段 2 的引导加载程序就可以调用内核映像了。


     Linux的引导扇区内容是采用汇编语言编写的程序,其源代码在arch/i386/boot中(不同体系的CPU有其各自的boot目录),它最重要的两个程序文件为:

  ◎bootsect.S,引导扇区的主程序,汇编后的代码不超过512字节,即一个扇区的大小;

◎setup.S, 引导辅助程序;

下面分析这两个文件到底做了什么:

Bootsect首先将“自己”从被RAM的0x7c00处搬到0x90000处,然后建立运行环境,即将DS,ES,SS都指向0x90000处,与CS看齐,同时初始化SP;然后将setup读到0x90200处(setup的image将会读入至程序所指定的内存绝对地址0x90200处),打印“Loading”,读入内核(vmlinuz)到0x100000(bzImage)处,然后跳到setup(boot/Setup.S)处运行。Setup部分首先设置一些系统的硬件设备(例如建立idt, gdt表),然后将核心从0x10000处移至0x1000处,这时系统转入保护模式,开始执行位于0x1000处的代码,正式进入内核。

 

第三步——>加载linux内核

 

首先是内核的加压缩,ox1000处的代码来自于arch/i386/boot/compressed/head.S,它用来初始化寄存器和调用decompress_kernel()(这个函数将内核vmlinuz解压到0x100000)程序,解压后的数据被装入到0x100000处,而arch/x86/kernel/head.S中的startup_32的地址正是0x100000,所以现在从这个startup_32还是执行,它实现了如下功能:

1.首先将ds,es,fs,gs指向系统数据段KERNEL_DS(KERNEL_DS 在asm/segment.h中定义,表示全局描述符表中中的第三项)。
2 数据段全部清空。

3 setup_idt为一段子程序,将中断向量表全部指向ignore_int函数,该函数打印出:          unknown interrupt 当然这样的中断处理函数什么也干不了。

4 察看数据线A20是否有效,否则循环等待(地址线A20是x86的历史遗留问题,决定是否能访问1M以上内存)。

5 拷贝启动参数到0x5000页的前半页,而将setup.s取出的bios参数放到后半页。

6 检查CPU类型。

7 初始化页表,只初始化最初几页。

 1>将swapper_pg_dir(0x2000)和pg0(0x3000)清空swapper_pg_dir作为整个系统的页目录;

 2>将pg0作为第一个页表,将其地址赋到swapper_pg_dir的第一个32位字中;

 3>同时将该页表项也赋给swapper_pg_dir的第3072个入口,表示虚拟地址0xc0000000也指向pg0;

 4>将pg0这个页表填满指向内存前4M;

 5>进入分页方式;
8 装入新的gdt和ldt表。

9 刷新段寄存器ds,es,fs,gs。

10 使用系统堆栈,即预留的0x6000页面。

11 执行start_kernel函数,这个函数是第一个C编制的 函数。

到此处为止,内核又有了一个新的开始。

 

要求2:首先给出Linux映像的make过程分析,说明grub将跳转到哪个Linux源文件中的哪处开始执行

Makefile的主要流程如下:

1.使用命令行或者图形界面配置工具,对内核进行裁减,生成.config配置文件;

2. 保存内核版本信息到 include/linux/version.h;

3. 产生符号链接 include/asm,指向实际目录 include/asm-$(ARCH);

4. 为最终目标文件的生成进行必要的准备工作;

5. 递归进入 /init 、/core、 /drivers、 /net、 /lib等目录和其中的子目录来编译生成所有的目标文件;

6. 链接上述过程产生的目标文件生成vmlinux,vmlinux存放在内核代码树的根目录下;

7. 最后根据 arch/$(ARCH)/Makefile文件定义的后期编译的处理规则建立最终的映象bootimage,包括创建引导记录、准备initrd映象和相关处理。

 

 

Make命令首先从顶层的makefile中开始执行,这个makefile产生vmlinux文件和内核模块(modules):

只使用make命令,即没有任何参数的情况之下,make会执行的是Makefile文件中的默认规则,即all:vmlinux这个规则。

然后找到vmlinux目标: 

这样可以看出vmlinux的依赖的几项内容了,对应这样的几个依赖文件,分别进行分析。

这些就是以上三个依赖项的定义了。

l  vmlinux-lds的定义已经很清楚了,就是对应目录arch/x86/kernel/下的vmlinux.lds了

l  至于vmlinux-init的定义,就得到arch/x86/makefile文件中去看了,因为顶层的Makefile文件把这个Makefile文件也include进去了。

其中head-y如下:

head-y:=arch/x86/kernel/head_$(BITS).o

head-y+=arch/x86/kernel/head$(BITS).o

head-y+=arch/x86/kernel/head.o

head-y+=arch/x86/kernel/init_task.o

至于BITS,按照要求之考虑32为的情况,即把BITS代换为32就可以了。所以,head-y有三个重要的文件组成,即head_32.S,head32.c,init_task.o文件。这也说明了其是与体系结构相关的。

其中init-y如下:


这样可以看出,init-y是与体系结构无关的。一种涉及到了一个patsubs函数替换的工作。

其中还有core-y如下:

core-y      := usr/

……

core-y      += kernel/ mm/ fs/ ipc/ security/crypto/block/

……

vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y)$(init-m)\

            $(core-y) $(core-m) $(drivers-y)$(drivers-m) \

            $(net-y) $(net-m) $(libs-y)$(libs-m)))

core-y      := $(patsubst %/, %/built-in.o,$(core-y))

vmlinux-main:=$(core-y) $(libs-y) $(drivers-y) $(net-y)

经过分析,分析core-y的定义,分析core-y中既包含体系结构相关的,又包含体系结构无关的内容。

其中libs-y如下:

 

 

至于$(vmlinux-lds)$(vminux-init)$(vmlinux-main)这三个,都是依赖于$(vmlinux-dirs)的。而vmlinux-dirs又是依赖于上面说到的init-y,init-m,core-y,core-m,dirvers-m,ner-y,net-m,libs-y,l

libs-m等,并且定义时候要对这些文件进行排序和过滤,即使用了sort和filter函数,通过这些就可以生成想要的一些文件见了。

接下来,就应该是编译过程中的链接操作了。即把生成的这些文件链接起来,生成最终的目标文件才行。在Makefile文件中,使用了如下的call操作,分别调用相关的链接规则。

$(callif_changed_rule,vmlinux__)

$(call cmd,vmlinux__)

其中又有quiet_cmd_vmlinux。

 

在确定了$(vmlinux-lds)$(vminux-init)$(vmlinux-main)都成功生成了之后,才可以执行该操作函数的。quiet_cmd_vmlinux这个主要是用来在编译时进行显示用的,可以看出显示结果为LD target列表,真正的命令为cmd_vmlinux__,通过这个命令将变量vmlinux-init和vmlinux-main指定的目标链接成vmlinux文件。链接脚本由vmlinux-lds指定,即 –T 后跟连接的脚本。

综上,这样编译vmlinux的主要流程如下:

最后生成在内核代码树根目录下的vmliux目标文件。

若是makebzImage,则可以在arch/x86/makefile文件中找到相应规则,

然后可以看到再到arch/x86/boot/makefile文件中的bzImage,

可以看出bzImage和vmliunx.bin和setup.bin都相关,而setup.bin由set.elf转化而来,setuo.elf是由一系列.o文件链接而成,vmlinux.bin则是由compressed/vmlinux经过objcopy转化而来的,在arch/x86/boot/compressed的目录下,分析其中的Makefile文件了。其中:

 

$(obj)/vmlinux:$(src)/vmlinux_$(BITS).lds$(obj)/head_$(BITS).o $(obj)/misc.o $(obj)/piggy.oFORCE

则vmlinux又由head_32.o misc.o piggy.o经过vmlinux_32.lds链接组成的。

misc.o的作用就是一个解压缩的功能。而piggy.o是由vmlinux.scr和vmlinux.bin.gz经过ld链接生成。vmlinux.bin.gz是vmlinux.bin经过gzip压缩之后生成,而vmlinux.bin是由顶层vmlinux经过objcopy得到得。

之后,利用objcopy把arch/x86/boot/cmpressed目录下的vmlinux文件转换成二进制的vmlinux文件,保存在arch/x86/boot/目录下了。

接着,利用build工具把arch/x86/boot/cmpressed目录下的setup.bin和vmlinux.bin拼接成bzImage.这样就生成了bzImage.

 

Grub将跳转到arch/x86/boot/compressed/head_32.S的startup_32处开始执行。

要求3:从make过程,给出Linux中的启动相关的几个关键源文件的执行顺序

1. arch/i386/boot/header.S;

2. arch/i386/boot/main.c;

3. arch/i386/boot/compressed/head_32.S;

4. arch/i386/kernel/head_32.S;

5. init/main.c

要求4:从第一个源文件开始给出主要流程,到start_kernel结束

1.header.S的主要流程:

①    设置setup header参数部分

②     start_of_setup

          1):设置堆栈
         2):检查setup中的标签
         3):清除BSS段
         4):调用C入口main

2.boot/main.c的主要流程

copy_boot_params();复制 boot header到"zeropage"
  validate_cpu();确保支持当前运行的CPU
  set_bios_mode();告诉BIOS什么CPU我们将要去运行
  detect_memory();检测Memory
  keyboard_set_repeat();设置键盘 repeatrate (Why ?)
  set_video();设置 Video mode
  query_mca();获得 MCA 信息
  query_ist();获得 Query IntelSpeedStep (IST) 信息
  query_apm_bios();获得APM 信息
  query_edd();获得EDD信息
  go_to_protected_mode();最后一件事,也是最重要的一件事,进入保护模式

3.boot/compressed/head_32.S的主要流程

在保护模式下,首先到0x00001000处,即startup_32处开始执行

1) 首先初始化段寄存器和临时堆栈;
       2) 清除eflags寄存器的所有位;
       3) 将_edata和_end区间的所有内核未初始化区填充0;
       4) 调用decompress_kernel( )函数解压内核映像。首先显示"Uncompressing Linux..."信息,解压完成后显示 "OK, booting the kernel."。内核解压后,如果时低地址载入,则放在0x00100000位置;否则解压后的映像先放在压缩映像后的临时缓存里,最后解压后的映像被放置到物理位置0x00100000处;
5) 跳转到0x00100000物理内存处执行;

4.kernel/head_32.S

该函数未Linux第一个进程建立执行环境,操作如下:
       1) 初始化ds,es,fs,gs段寄存器的最终值;
       2) 用0填充内核bss段;
       3) 初始化swapper_pg_dir数组和pg0包含的临时内核页表:
       4) 建立进程0idle进程的内核模式的堆栈;
       5) 再次清除eflags寄存器的所有位;
       6) 调用setup_idt()用非空的中断处理函数填充IDT表;
       7) 将从BIOS获取的系统参数传递到操作系统的第一个页面帧;
       8) 识别处理器的模式;
       9) 将GDT和IDT表的地址加载到gdtr和idtr寄存器中;
       10) 跳转到start_kernel函数,这个函数是第一个C编制的函数,内核又有了一个新的开始。

5.init/main.c
       主要是执行start_kernel函数:

1) 调度器初始化,调用sched_init();
       2) 调用build_all_zonelists函数初始化内存区;
       3) 调用page_alloc_init()和mem_init()初始化伙伴系统分配器;
       4) 调用trap_init()和init_IRQ()对中断控制表IDT进行最后的初始化;
       5) 调用softirq_init() 初始化TASKLET_SOFTIRQ和HI_SOFTIRQ;
       6) Time_init()对系统日期和时间进行初始化;
       7) 调用kmem_cache_init()初始化slab分配器;
       8) 调用calibrate_delay()计算CPU时钟频率;
       通过调用kernel_thread()启动进程1init进程的内核线程,然后该线程再创建其他的内核线程执行/sbin/init程序。

原创粉丝点击