Chinaunix首页 | 论坛 | 博客
  • 博客访问: 104614265
  • 博文数量: 19283
  • 博客积分: 9968
  • 博客等级: 上将
  • 技术积分: 196062
  • 用 户 组: 普通用户
  • 注册时间: 2007-02-07 14:28
文章分类

全部博文(19283)

文章存档

2011年(1)

2009年(125)

2008年(19094)

2007年(63)

分类: LINUX

2008-03-22 10:18:26


系统引导和初始化概述


相关代码(引导扇区的程序及其辅助程序,以x86体系为例):

\linux-2.4.22\arch\i386\boot\bootsect.S:Linux引导扇区的源代码;512字节

\linux-2.4.22\arch\i386\boot\setup.S:辅助程序;

\linux-2.4.22\arch\i386\boot\video.S:辅助程序,用于引导过程中的屏幕显示.

\linux-2.4.22\arch\i386\boot\compressed\head.S,

\linux-2.4.22\arch\i386\boot\compressed\misc.c :用于对内核映像解压缩

-----------------------------------------

\linux-2.4.22\arch\i386\kernel\head.s系统初始化入口

\linux-2.4.22\init\main.c 系统初始化入口


参考文档:\linux-2.5.75\Documentation\i386\boot.txt

过程描述

系统加电,CPU RESET 跳到地址0xFFFFFFF0,此处是BIOS

BIOS完成它的操作把第一扇区的内容读入到0x7c00

就是bootsect.S 把自己移到绝对地址0x90000处,并调转到那里继续执行

通过BIOS提供的读磁盘调用"int 0x13"从磁盘上读入setup和内核的映像

将(boot/setup.S)2kB字节的代码读入内存0x90200处, 然后跳转到setup的代码中做执行内核映像的准备

从0x100000开始执行startup_32()进行初始化(head.s)

---〉start_kernel()[main.c]-cpu_idle()

start_kernel()创建的进程init()被调度执行,完成

1.指令的跳转及其机理

l 80x86/Pentium的地址映射

0–640 KB MS-DOS Area.

640–768 KB Video Buffer Area.

768–896 KB in 16-KB sections (total of eight sections) - Expansion Area.

896 -960 KB in 16-KB sections (total of four sections) - Extended System BIOS Area.

960-KB–1-MB memory (BIOS Area) - System BIOS Area.

扩展内存区:由1M到4GB-1

— High BIOS area from 4 GB to 4 GB–2 MB

Intel 82875P Memory Controller Hub (MCH)

memery的监测和初始化:在对内存接口做操作前,必须初始化MCH DRAM寄存器.MCH必须配制成针对所安装的内存的类型进行操作.对内存类型和大小的检测是通过ICH5上的System Management Bus (SMBus)来完成.这个两线的总线通过DRAM DIMM上的Serial Presence Detect端口获得DRAM的类型和大小信息.

BIOS需要确定每行内存的大小和类型来配置MCH内存接口.

2.x86MPU 启动时的初始化

复位输入提供一种初始化的硬件手段.通过复位接口电路向mpu提供信号,Reset要保持1至少15个CLK2周期,当返回0后,MPU启动内部初始化程序,进入实地址模式.初始化完成后,标志寄存器设为0xUUUU0002(u代表未定义,实模式下9位标志可用,这里是奇偶标志为1);指令指针设为0x0000FFF0,CS寄存器设为0xF000,DS,SS,ES,FS和GS寄存器都设为0x0000,指令队列清空.

实模式下地址的形成:段基址+指令指针偏移

MPU在识别出Reset信号后把数据总线设在高阻状态,地址线强行设为1.由于清空中断标志是初始化的一部分,外部中断被禁止.因为代码段寄存器为0xF000,指令指针为0x0000FFF0,地址线A20-A31全部是1,从而复位后实模式程序从地址0xFFFFFFF0开始(只用于实模式高地址位忽略,从地址0xFFFF0开始.该地址处可以包含一条转移指令跳到启动程序处.

物理地址为0xfffffff0的代码将被执行.这个地址被存储在一个只读存储器(ROM)里.BIOS(基本输入/输出系统)实际上是一段存储在ROM里的程序.它包含了一系列可以被某些操作系统调用,用于处理计算机各种硬件设备的中断驱动和低级程序.其中微软的DOS就是这样的一种操作系统.

所有的BIOS程序都是在实模式下运行的.但是,Linux内核是在保护模式下运行,而不是在实模式下.

因此,一旦初始化完成后,Linux就不再使用BIOS,而是完全由自己来为计算机上的所有硬件提供驱动程序.

那么什么时候Linux使用保护模式 为什么BIOS不能使用相同的模式 BIOS使用实模式是因为其在操作过程中使用的是实模式地址,并且在计算机刚打开电源时,只有实模式地址可用.一个实模式地址由段地址和偏移地址组成,因此,相应的物理地址就为段地址×(2×8)+偏移.

这是不是意味着在整个启动过程中,Linux就从来不使用BIOS了呢 答案是否定的.在启动阶段,Linux从硬盘或者其它外部设备加载内核时,需要使用BIOS.

3.BIOS的作用:

BIOS要对硬件进行一系列彻底的检测.这个步骤主要是检查系统安装有哪些设备,以及它们工作是否正常.通常把这个步骤叫做自检(Power-On Self-Test,POST),这时会显示版本及其它很多相关的硬件信息. BIOS要对硬件进行初始化.这一步非常重要,因为它要保证所有的硬件设备在IRQ(中断请求)和I/O端口操作时都没有冲突.等这步完成以后,它会显示一个已经安装的PCI设备表.

接着到了操作系统,BIOS将查找一个可以引导的操作系统.这取决于BIOS的设置,它可以从软盘,硬盘或者光盘启动.

一旦发现一个合法的设备,BIOS就会把其第一扇区的内容复制到物理地址,即从0x00007c00开始的内存中,然后跳至刚加载的地址并执行之.

BIOS调用一个专门的程序,这个程序的任务就是把操作系统的内核调入内存.这个程序就叫做自举程序(Boot Loader).

4.详细启动和初始化过程描述

1)从软盘启动Linux

从软盘启动时,存储在软盘第一扇区的指令将被加载并执行.这个指令然后就会把其余的内核复制到内存中.

Linux内核可以装在1.44MB的软盘里,不过为了减少磁盘占用量,它们都进行了压缩.这个压缩过程是在编译时完成的,而解压缩的过程则由自举程序完成.

从软盘启动Linux时,自举程序要做的工作非常简单.它是一个位于/usr/src/linux-2.4.2/arch/i386/boot/bootsect.S的汇编语言文件.当我们编译Linux内核源代码,或者获取一个新的内核时,这个可执行的汇编代码就会被放在内核程序的前端.由此可见,要制作一个可启动的Linux软盘其实很简单.我们只要从磁盘的第一个扇区拷贝Linux内核,就可以创建一个可启动软盘.

当BIOS加载软盘的第一个扇区时,它实际上拷贝的是自举程序.自举程序由BIOS调用(跳到物理地址为0x00007c00的位置),然后执行以下的操作:

(1)把自已从地址0x00007c00移动到0x00090000;

(2)使用地址0x00003ff4,创建"实模式"栈;

(3)设置磁盘参数表,这里使用的是BIOS提供的软盘驱动程序; 通过调用BIOS程序显示"Loading"信息;

(4)自举程序调用BIOS程序来加载软盘上内核的setup()函数,并把它放在起始地址为0x00090200的内

存中;

(5)接下来自举程序调用一个BIOS程序,这个程序从软盘加载剩余的内核程序,并将其放入起始地址为0x00010000(所谓的低地址)或者0x00100000(所谓的高地址);

(6)然后,跳转到setup()函数.

2)从硬盘启动Linux

当系统从硬盘启动时,启动过程又有所不同.硬盘的第一个扇区(0头0道1扇区)叫做MBR(Master Boot Record),其上存储着分区表和一个小程序.这个程序加载存储由操作系统的第一扇区来开始启动.

Linux是一个高度灵活且非常优秀的软件,所以在MBR里,它使用一个叫做LILO的程序来代替上述的那个程序.LILO允许用户选择所要启动的操作系统.

Boot Sector 也就是硬盘的第一个扇区, 它由 MBR (Master Boot Record),DPT (Disk Partition Table) 和 Boot Record ID 三部分组成.

MBR 又称作主引导记录占用 Boot Sector 的前 446 个字节 ( 0 to 0x1BD ),存放系统主引导程序 (它负责从活动分区中装载并运行系统引导程序).

DPT 即主分区表占用 64 个字节 (0x1BE to 0x1FD), 记录了磁盘的基本分区信息. 主分区表分为四个分区项, 每项 16 字节, 分别记录了每个主分区的信息(因此最多可以有四个主分区). Boot Record ID 即引导区标记占用两个字节 (0x1FE and 0x1FF), 对于合法引导区, 它等于 0xAA55, 这是判别引导区是否合法的标志.

Boot Sector 的具体结构如下图所示:

0000 |------------------------------------------------|

| |

| Master Boot Record |

| |

| 主引导记录( 446字节) |

01BD | |

01BE |------------------------------------------------|

01CD | 分区信息 1(16字节) |

01CE |------------------------------------------------|

01DD | 分区信息 2(16字节) |

01DE |------------------------------------------------|

01ED | 分区信息 3(16字节) |

01EE |------------------------------------------------|

01FD | 分区信息 4(16字节) |

|------------------------------------------------|

| 01FE | 01FF |

| 55 | AA |

|------------------------------------------------|

当PC的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址.PC机的BIOS将执行某些系统的检测,在物理地址0处开始初始化中断向量. BIOS首先检查0000:7dfe的位置是不是0x55AA,若不是就去启动其他介质.

一般来说,Linux是从硬盘启动的.这就需要不同的自举程序.在Intel系统里,用得最多的自举程序就是LILO.对于其它的体系结构,还存在着别的自举程序.LILO可以安装在MBR上(请注意:在安装Red Hat Linux时,有一个步骤会让用户选择把LILO安装到MBR或者引导扇区)或一个活动分区的引导扇区上.

由于LILO太大,MBR无法容纳,所以它被分成两部分.MBR(或者磁盘分区的引导扇区)包含有一个小的自举程序,它被BIOS载入到起始地址为0x00007c00的内存中.然后,这个小程序再把自己移到0x0009a000地址处,接着设置实模式栈,最后加载第二部分的LILO自举程序(请注意:实模式栈地址范围是0x0009b000 到 0x0009a200).

第二部分的LILO会从磁盘读取所有可用的操作系统,并且给用户列出,以选择所要启动的系统.一旦用户选择完成,自举程序就会加载相应的扇区内容到内存中并且执行之. 自举程序bootsect.S

自举程序bootsect.S被BIOS调用时(跳到物理地址为0x00007c00处),要执行以下操作:

(1)把自已从地址0x00007c00移动到0x00090000;

(2)使用地址0x00003ff4,创建"实模式"栈;

(3)设置磁盘参数表.

(4)通过调用BIOS程序显示"Loading Linux"信息;

(5)自举程序BIOS调用来加载的setup()函数,并把它放在起始地址为0x00090200的内存中;

(6)自举程序BIOS调用加载剩余的内核程序,并将其放入起始地址为0x00010000或者0x00100000;

(根据内核类型,对于小内核zImage放在 0x10000 ,大内核bzImage放在0x100000)

此处是如何判断要加载的内核是什么的

(7)然后,跳转到setup()函数.

Setup()函数的功用

Setup()函数可以在/linux-2.4.22/arch/i386/boot/setup.S文件中找到.

Setup()函数代码是在完整的内核自举程序加载以后,才会跳到相应的函数代码处.在内核文件中, 其偏移地址是0x200.这使得自举程序很容易找到这段代码,并将其拷贝到起始物理地址为0x00090200的内存中.

这个Setup()文函数到底是做什么用的 在计算机时里,内核要正确地操作所有硬件就必需首先要检测到它们,并且以一种有序的方式进行初始化.Setup()函数初始化所有的硬件设备,从而为内核操作它创造了一个环境.

但是,前面我们不是已经提到过BIOS会检测所有的硬件吗 虽然BIOS初始化了所有的硬件,但是Linux内核并不放心,它还要以自己的方式对所有的硬件进行初始化.Linux内核之所以要设计成这样,是为了增强可移植性和稳定性.这也是Linux内核要优于很多目前可用的Unix和类Unix内核的原因之一,并且也使得它在很多方面表现的非常出众.

Setup()函数主要完成以下任务:

(1)首先是检测系统可用内存的总量,它是通过BIOS程序来完成检测的;

(2)设置键盘重复延迟时间和重复速度;

(3)检测视频卡;

(4)重新初始化硬盘控制器和硬盘参数;

(5)检测一个MCA;

(5)检测一个PS/2定点设备(鼠标总线);

(6)检测高级电源管理器(APM)BIOS支持;

(7)检测内核在内存中的位置,如果在低地址0x00010000,就将其移到高地址0x00001000,如在高地址则不做任何移动;

(8)设置设备中断描述表(IDT)和全局描述表(GDT);

(9)如已经有了浮点单位(FPU),则重置之;

(10)重新调用程序中断控制器;

(11)通过设置cr0状态寄存器的PE位,把CPU从"实模式"切换到"保护模式";

(12)跳转到stratup_32( )汇编语言函数.

因为在内核中不能做BIOS调用,内存信息由setup通过INT 0X15 来加以查询,并根据获得的信息生成一张物理内存构成图,称为e820图,再通过参数块传给内核,使内核知道系统中内存资源的配置.因为在做int 0x15来查询内存构成是要把调用参数之一设置成0xe820,所以叫e820图.

l 第一个stratup_32( )函数

在启动过程中要用到两个stratup_32( )函数,虽然它们都是汇编语言函数,但是却是两个完全不同的函数.我们这里所说的函数包含在/usr/src/linux-2.4.2/arch/i386/boot/compressed/head.S文件里.

Setup()文件执行后,这个函数就被加载到物理地址为0x00100000或者物理地址为0x00001000的内存中(取决于内核是载入高或者低内存).

当执行这个函数时,会执行以下的操作:

(1)初始化段寄存器和一个临时栈.

(2)内核中没有初始化的数据都用0填充.它是通过symbols _edata和 _end来识别的.

(3)执行decompress_kernel( )函数.这个函数用于对Linux内核解压缩.这个时候,屏幕上将显示"Uncompressing Linux……"信息.解压缩完成后,就会显示"OK, booting the kernel"信息.现在有一个问题,就是解完压缩的内核被放置在什么位置 答案是如果Linux内核被加载低地址,那么解压缩的内核将被置于物理地址为0x00100000的地方.如果在高地址,则内核会被先解压到一个临时缓冲区中,待完成后再将其加载到物理地址为0x00100000的地方.

(4)最后,跳转到物理地址为0x00100000的地方执行.

到此为止,代码执行操作就由另外一个startup_32( )函数来接管.也就是说,第二个startup_32( )函数接管了启动过程.

第二个startup_32( )函数完成的功能解压缩Linux内核的工作由另外一个startup_32( )函数来完成.该函数位于/usr/src/linux-2.4.2/arch/i386/kernel/head.S文件中.

这时你可能会说两个不同的函数用同一个名字不会出错吗 答案是不会的.因为两个函数都是到自己初始地址去执行,并且都有自己的执行环境,所以不会出错.

下面我们来看一下第二个startup_32( )函数的功能.当执行这个函数时,实际上是为第一个Linux进程(process 0)设置环境.这个函数将执行下面的操作:

(1) 寄存器将以最后的值进行初始化;

(2)为process 0设置内核模式栈;

(3)调用并且执行setup_idt( )函数,该函数将把所有的IDT填充空值;

(4)把从BIOS中获得的参数放在第一页的框架中;

(5)识别处理器的模式;

(6)使用GDT和IDT表加载gdtr和idtr寄存器;

(7)最后跳到start_kernel( )函数.

l start_kernel( )函数功能

start_kernel( )函数完成Linux内核的初始化工作.这个函数执行后,所有的基本内核组件都将被初始化.这也是整个启动过程的最后一步.

该函数将完成以下的功能:

(1)输出Linux版本信息(printk(linux_banner))

(2)设置与体系结构相关的环境(setup_arch()) ――>页表结构初始化(paging_init())

(3)提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options())

(4)使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())

(5)使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ())

(6)核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init())

(7)时间,定时器初始化(包括读取CMOS时钟,估测主频,初始化定时器中断等,time_init())

(8)控制台初始化(为输出信息而先于PCI初始化,console_init())

(9)初始化可安装模块机制,计算出内核符号表的大小init_modules()

(10)剖析器数据结构初始化(prof_buffer和prof_len变量)

(11)核心Cache初始化(描述Cache信息的Cache,kmem_cache_init())

(12)延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay())

(13)内存初始化(设置内存上下界和页表项初始值,mem_init())

(14)创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init())

(15)创建页cache(内存页hash表初始化,pgtable_cache_init())

(16)根据物理内存的大小计算出允许创建线程(包括进程)的数量fork_init(num_mappedpages);

(17)proc_caches_init(); vfs_caches_init(num_physpages);buffer_init(num_physpages). 都是为有关的管理机制建立起专用的slab缓冲区队列.

(18)分配空间建立起缓冲页面杂凑表page_hash_table ( page_cache_init())

(19)对Sys V 进程间通信机制的初始化ipc_init();

(20)创建信号队列cache("signal_queue",signals_init())

(21)检查体系结构漏洞(对于alpha,此函数为空,check_bugs())

(22)SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init())

启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init())

至此start_kernel()结束,基本的核心环境已经建立起来了

init()函数作用

init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化.过程如下:

总线初始化(比如pci_init()) ;

网络初始化(所有协议的初始化过程,sock_init());

创建事件管理核心线程(start_context_thread()函数启动context_thread()过程,并重命名为 keventd);

启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,do_initcalls())

do_initcalls()-partion_setup()-device_init()是所有外设初始化的总入口;

文件系统初始化(filesystem_setup(),主要是devfs);

安装root文件系统(mount_root())

至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后, init()打开/dev/console设备,重定向stdin,stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序.

init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了.此时系统中存在着六个运行实体:

start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现

6,模式向保护模式的转换以及内核使用物理地址向虚拟地址的转换CPU在跳转到bootsect时还处于16位实地址模式,然后在setup的执行过程中转入32位保护模式的段式寻址方式.在bootsect和setup中都利用BIOS提供的调用来完成一些比较大的操作,如读磁盘,取BIOS在加电自检是搜集到的有关内存的信息等等.一旦转入内核映像本身的执行就不再需要BIOS了.

模式转换的控制:

x86保护模式比实模式多了几个寄存器:全局描述符表寄存器(GDTR),中断描述符表寄存器(IDTR), 局部描述符表寄存器(LDTR)和任务寄存器(TR).另外,一些寄存器的功能得到了扩展,例如指令指针成为EIP,长度32位;标志寄存器(EFLAGS)的更多位得到了利用.四个控制寄存器CR0-CR3都得到了利用.

GDTR(48位)在物理存储器地址空间定义了全局描述符表GDT(每个描述符8字节).

IDTR(长度同GDTR)定义了中断描述符表IDT.(256个中断,每个中断门8字节)

在从实模式转到保护模式前必须将GDTR的基址BASE和限长limit(16位)的值装入GDTR;IDTR也是.

这两个表的装入和保存有特殊的指令LGDT SGDT LIDT SIDT

16位的LDTR并不直接定义一个局部描述符表,他只是一个指向GDT中LDT描述符的选择符.选择符装入LDTR相应的描述符就能从全局存储器中读出来装入局部描述符表高速缓存,该描述符为当前任务创建 了一个LDT。

控制寄存器

CR0的低五位是机器状态字MSW,包含保护模式配置和状态信息.PE(保护模式允许)位重启时清零允许实模式操作,为进入保护模式将PE设为1.一旦处于保护模式就不能再设回到实模式,除非重启.MP(数学协处理器存在)设为1表示该系统有一个数学协处理器.如果用到了软件模拟器执行数学运算,那么EM(模拟)位应设为1,一次只能设置其中的1位.R(扩展类型)表示用的是80287还是80387,R为1表示80387.TS(任务切换)在做任务切换的时候自动设置.

在保护模式下支持分页,CR0的PG位设为1表示允许分页,CR3包含页目录基址寄存器PDBR.

任务寄存器(TR)是在保护模式下的任务切换中用到.该寄存器存放16位的选择符用来指示全局描述符表中描述符的位置,当选择符装入TR,相应的任务状态段(TSS)描述符自动由存储器读出并装入到任务描述符缓存中.

几条重要的系统指令:LGDT SGDT LIDT SIDT LMSW SMSW

指令LMSW和SMSW分别用于装入和保存机器状态字信息.他们是用于从实模式转换到保护模式的指令,需要将MSW的最右位设为1.

SMSW AX;

OR AX,1;

LMSW AX;

地址的转换

在正常运行时整个内核映像都应该在系统空间,系统空间地址实连续的线性的,虚拟地址和物理地址间有个固定的位移:0xC0000000.内核影像的起点是_stext,引导核压缩后的整个映像放在内存中从0x100000即1Mb开始的空间.CPU执行内核映像的入口start_32就在内核映像开头的地方,因此物理地址是0x100000,虚拟地址就成了0xC0100000.

CPU在进入start_32时运行于保护模式下的段式寻址方式.段描述表中与__KERNEL_CS和__KERNEL_DS

Cr1

页目录基址寄存器(PDBR)

缺页线性地址

保留 保留

保留

PG TS EM MP PE R

31 23 15 7 0

Cr3

Cr2

Cr0

相对应的描述项所提供的基地址都是0,所以实际产生的是线性地址.其中代码段寄存器CS已在进入 start_32之前设置成__KERNEL_CS,数据段寄存器尚未设置为__KERNEL_DS.不过虽然代码段已设置,从而 start_32的地址为0xC0100000.但是在转入这个入口时使用的指令是"ljmp 0X10000"而不是"ljmp startup_32",所以装入COU寄存器IP的地址是物理地址0x100000而不是虚拟地址,CPU在进入startup_32后会继续以物理地址取指令.只要不在代码段中引用某个地址,例如项某个地址做绝对转移,或者调用某个子程序,就可以一直这样运行下去,而与CS的内容无关,此外,CPU的中断已在进入startup_32前关闭。

阅读(310) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~