找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 6239|回复: 0

计算机的引导过程

[复制链接]

238

主题

329

帖子

2119

积分

超级版主

Rank: 8Rank: 8

积分
2119
QQ
发表于 2016-3-17 10:29:16 | 显示全部楼层 |阅读模式
本帖最后由 xiaoyu 于 2016-3-17 13:24 编辑

原文标题:How Computers Boot Up

原文地址:http://duartes.org/gustavo/blog/


[注:本人水平有限,只好挑一些国外高手的精彩文章翻译一下。一来自己复习,二来与大家分享。]


引导(Booting)是一个复杂的,充满技巧的,涉及多个阶段,又十分有趣的过程。下图列出了此过程的概要:
IMG_6385.JPG

引导过程概要


当你按下计算机的电源键后(现在别按!),机器就开始运转了。一旦主板上电,它就会初始化自身的固件(firmware)——芯片组和其他零零碎碎的东西——并尝试启动CPU。如果此时出了什么问题(比如CPU坏了或根本没装),那么很可能出现的情况是电脑没有任何动静,除了风扇在转。一些主板会在CPU故障或缺失时发出鸣音提示,但以我的经验,此时大多数机器都会处于僵死状态。一些USB或其他设备也可能导致机器启动时僵死。对于那些以前工作正常,突然出现这种症状的电脑,一个可能的解决办法是拔除所有不必要的设备。你也可以一次只断开一个设备,从而发现哪个是罪魁祸首。


如果一切正常,CPU就开始运行了。在一个多处理器或多核处理器的系统中,会有一个CPU被动态的指派为引导处理器(bootstrap processor简写BSP),用于执行全部的BIOS和内核初始化代码。其余的处理器,此时被称为应用处理器(application processor简写AP),一直保持停机状态直到内核明确激活他们为止。虽然Intel CPU经历了很多年的发展,但他们一直保持着完全的向后兼容性,所以现代的CPU可以表现得跟原先1978年的Intel 8086完全一样。其实,当CPU上电后,它就是这么做的。在这个基本的上电过程中,处理器工作于实模式,分页功能是无效的。此时的系统环境,就像古老的MS-DOS一样,只有1MB内存可以寻址,任何代码都可以读写任何地址的内存,这里没有保护或特权级的概念。


CPU上电后,大部分寄存器的都具有定义良好的初始值,包括指令指针寄存器(EIP),它记录了下一条即将被CPU执行的指令所在的内存地址。尽管此时的Intel CPU还只能寻址1MB的内存,但凭借一个奇特的技巧,一个隐藏的基地址(其实就是个偏移量)会与EIP相加,其结果指向第一条将被执行的指令所处的地址0xFFFFFFF0(长16字节,在4GB内存空间的尾部,远高于1MB)。这个特殊的地址叫做复位向量(reset vector),而且是现代Intel CPU的标准。


主板保证在复位向量处的指令是一个跳转,而且是跳转到BIOS执行入口点所在的内存映射地址。这个跳转会顺带清除那个隐藏的、上电时的基地址。感谢芯片组提供的内存映射功能,此时的内存地址存放着CPU初始化所需的真正内容。这些内容全部是从包含有BIOS的闪存映射过来的,而此时的RAM模块还只有随机的垃圾数据。下面的图例列出了相关的内存区域:

IMG_6386.PNG

引导时的重要内存区域


随后,CPU开始执行BIOS的代码,初始化机器中的一些硬件。之后BIOS开始执行上电自检过程(POST),检测计算机中的各种组件。如果找不到一个可用的显卡,POST就会失败,导致BIOS进入停机状态并发出鸣音提示(因为此时无法在屏幕上输出提示信息)。如果显卡正常,那么电脑看起来就真的运转起来了:显示一个制造商定制的商标,开始内存自检,天使们大声的吹响号角。另有一些POST失败的情况,比如缺少键盘,会导致停机,屏幕上显示出错信息。其实POST即是检测又是初始化,还要枚举出所有PCI设备的资源——中断,内存范围,I/O端口。现代的BIOS会遵循高级配置与电源接口(ACPI)协议,创建一些用于描述设备的数据表,这些表格将来会被操作系统内核用到。


POST完毕后,BIOS就准备引导操作系统了,它必须存在于某个地方:硬盘,光驱,软盘等。BIOS搜索引导设备的实际顺序是用户可定制的。如果找不到合适的引导设备,BIOS会显示出错信息并停机,比如“Non-System Disk or Disk Error”没有系统盘或驱动器故障。一个坏了的硬盘可能导致此症状。幸运的是,在这篇文章中,BIOS成功的找到了一个可以正常引导的驱动器。


现在,BIOS会读取硬盘的第一个扇区(0扇区),内含512个字节。这些数据叫做主引导记录(Master Boot Record简称MBR)。一般说来,它包含两个极其重要的部分:一个是位于MBR开头的操作系统相关的引导程序,另一个是紧跟其后的磁盘分区表。BIOS 丝毫不关心这些事情:它只是简单的加载MBR的内容到内存地址0x7C00处,并跳转到此处开始执行,不管MBR里的代码是什么。

IMG_6387.PNG

主引导记录


这段在MBR内的特殊代码可能是Windows 引导装载程序,Linux 引导装载程序(比如LILO或GRUB),甚至可能是病毒。与此不同,分区表则是标准化的:它是一个64字节的区块,包含4个16字节的记录项,描述磁盘是如何被分割的(所以你可以在一个磁盘上安装多个操作系统或拥有多个独立的卷)。传统上,Microsoft的MBR代码会查看分区表,找到一个(唯一的)标记为活动(active)的分区,加载那个分区的引导扇区(boot sector),并执行其中的代码。引导扇区是一个分区的第一个扇区,而不是整个磁盘的第一个扇区。如果此时出了什么问题,你可能会收到如下错误信息:“Invalid Partition Table”无效分区表或“Missing Operating System”操作系统缺失。这条信息不是来自BIOS的,而是由从磁盘加载的MBR程序所给出的。因此这些信息依赖于MBR的内容。


随着时间的推移,引导装载过程已经发展得越来越复杂,越来越灵活。Linux的引导装载程序Lilo和GRUB可以处理很多种类的操作系统,文件系统,以及引导配置信息。他们的MBR代码不再需要效仿上述“从活动分区来引导”的方法。但是从功能上讲,这个过程大致如下:


1、 MBR本身包含有第一阶段的引导装载程序。GRUB称之为阶段一。

2、 由于MBR很小,其中的代码仅仅用于从磁盘加载另一个含有额外的引导代码的扇区。此扇区可能是某个分区的引导扇区,但也可能是一个被硬编码到MBR中的扇区位置。

3、 MBR配合第2步所加载的代码去读取一个文件,其中包含了下一阶段所需的引导程序。这在GRUB中是“阶段二”引导程序,在Windows Server中是C:/NTLDR。如果第2步失败了,在Windows中你会收到错误信息,比如“NTLDR is missing”NTLDR缺失。阶段二的代码进一步读取一个引导配置文件(比如在GRUB中是grub.conf,在Windows中是boot.ini)。之后要么给用户显示一些引导选项,要么直接去引导系统。

4、 此时,引导装载程序需要启动操作系统核心。它必须拥有足够的关于文件系统的信息,以便从引导分区中读取内核。在Linux中,这意味着读取一个名字类似“vmlinuz-2.6.22-14-server”的含有内核镜像的文件,将之加载到内存并跳转去执行内核引导代码。在Windows Server 2003中,一部份内核启动代码是与内核镜像本身分离的,事实上是嵌入到了NTLDR当中。在完成一些初始化工作以后,NTDLR从“c:/Windows/System32/ntoskrnl.exe”文件加载内核镜像,就像GRUB所做的那样,跳转到内核的入口点去执行。


这里还有一个复杂的地方值得一提(这也是我说引导富于技巧性的原因)。当前Linux内核的镜像就算被压缩了,在实模式下,也没法塞进640KB的可用RAM里。我的vanilla Ubuntu内核压缩后有1.7MB。然而,引导装载程序必须运行于实模式,以便调用BIOS代码去读取磁盘,所以此时内核肯定是没法用的。解决之道是使用一种倍受推崇的“虚模式”。它并非一个真正的处理器运行模式(希望Intel的工程师允许我以此作乐),而是一个特殊技巧。程序不断的在实模式和保护模式之间切换,以便访问高于1MB的内存同时还能使用BIOS。如果你阅读了GRUB的源代码,你就会发现这些切换到处都是(看看stage2/目录下的程序,对real_to_prot 和 prot_to_real函数的调用)。在这个棘手的过程结束时,装载程序终于千方百计的把整个内核都塞到内存里了,但在这后,处理器仍保持在实模式运行。

至此,我们来到了从“引导装载”跳转到“早期的内核初始化”的时刻,就像第一张图中所指示的那样。在系统做完热身运动后,内核会展开并让系统开始运转。

由于我习惯以事实为依据讨论问题,所以文中会出现大量的链接引用Linux 内核2.6.25.6版的源代码(源自Linux Cross Reference)。如果你熟悉C的语法,这些代码就会非常容易读懂;即使你忽略一些细节,仍能大致明白程序都干了些什么。最主要的障碍在于对一些代码的理解需要相关的背景知识,比如机器的底层特性或什么时候、为什么它会运行。我希望能尽量给读者提供一些背景知识。为了保持简洁,许多有趣的东西,比如中断和内存,文中只能点到为止了。在本文的最后列出了Windows的引导过程的要点。


当Intel x86的引导程序运行到此刻时,处理器处于实模式(可以寻址1MB的内存),(针对现代的Linux系统)RAM的内容大致如下:

IMG_6388.PNG

引导装载完成后的RAM内容


引导装载程序通过BIOS的磁盘I/O服务,已经把内核镜像加载到内存当中。这个镜像只是硬盘中内核文件(比如/boot/vmlinuz-2.6.22-14-server)的一份完全相同的拷贝。镜像分为两个部分:一个较小的部分,包含实模式的内核代码,被加载到640KB内存边界以下;另一部分是一大块内核,运行在保护模式,被加载到低端1MB内存地址以上。


如上图所示,之后的事情发生在实模式内核的头部(kernel header)。这段内存区域用于实现引导装载程序与内核之间的Linux引导协议。此处的一些数据会被引导装载程序读取。这些数据包括一些令人愉快的信息,比如包含内核版本号的可读字符串,也包括一些关键信息,比如实模式内核代码的大小。引导装载程序还会向这个区域写入数据,比如用户选中的引导菜单项对应的命令行参数所在的内存地址。之后就到了跳转到内核入口点的时刻。下图显示了内核初始化代码的执行顺序,包括源代码的目录、文件和行号:

IMG_6389.JPG

与体系结构相关的Linux内核初始化过程


对于Intel体系结构,内核启动前期会执行arch/x86/boot/header.S文件中的程序。它是用汇编语言书写的。一般说来汇编代码在内核中很少出现,但常见于引导代码。这个文件的开头实际上包含了引导扇区代码。早期的Linux不需要引导装载程序就可以工作,这段代码是从那个时候留传下来的。现今,如果这个引导扇区被执行,它仅仅给用户输出一个“bugger_off_msg”之后就会重启系统。现代的引导装载程序会忽略这段遗留代码。在引导扇区代码之后,我们会看到实模式内核头部(kernel header)最开始的15字节;这两部分合起来是512字节,正好是Intel硬件平台上一个典型的磁盘扇区的大小。


在这512字节之后,偏移量0x200处,我们会发现Linux内核的第一条指令,也就是实模式内核的入口点。具体的说,它在header.S:110,是一个2字节的跳转指令,直接写成了机器码的形式0x3AEB。你可以通过对内核镜像运行hexdump,并查看偏移量0x200处的内容来验证这一点——这仅仅是一个对神志清醒程度的检查,以确保这一切并不是在做梦。引导装载程序运行完毕时就会跳转执行这个位置的指令,进而跳转到header.S:229执行一个普通的用汇编写成的子程序,叫做start_of_setup。这个短小的子程序初始化栈空间(stack),把实模式内核的bss段清零(这个区域包含静态变量,所以用0来初始化它们),之后跳转执行一段又老又好的C语言程序:arch/x86/boot/main.c:122。


main()会处理一些登记工作(比如检测内存布局),设置显示模式等。然后它会调用go_to_protected_mode()。然而,在把CPU置于保护模式之前,还有一些工作必须完成。有两个主要问题:中断和内存。在实模式中,处理器的中断向量表总是从内存的0地址开始的,然而在保护模式中,这个中断向量表的位置是保存在一个叫IDTR的CPU寄存器当中的。与此同时,从逻辑内存地址(在程序中使用)到线性内存地址(一个从0连续编号到内存顶端的数值)的翻译方法在实模式和保护模式中是不同的。保护模式需要一个叫做GDTR的寄存器来存放内存全局描述符表的地址。所以go_to_protected_mode()调用了setup_idt() 和 setup_gdt(),用于装载临时的中断描述符表和全局描述符表。


现在我们可以转入保护模式啦,这是由另一段汇编子程序protected_mode_jump来完成的。这个子程序通过设定CPU的CR0寄存器的PE位来使能保护模式。此时,分页功能还处于关闭状态;分页是处理器的一个可选的功能,即使运行于保护模式也并非必要。真正重要的是,我们不再受制于640K的内存边界,现在可以寻址高达4GB的RAM了。这个子程序进而调用压缩状态内核的32位内核入口点startup_32。startup32会做一些简单的寄存器初始化工作,并调用一个C语言编写的函数decompress_kernel(),用于实际的解压缩工作。


decompress_kernel()会打印一条大家熟悉的信息“Decompressing Linux…”(正在解压缩Linux)。解压缩过程是原地进行的,一旦完成内核镜像的解压缩,第一张图中所示的压缩内核镜像就会被覆盖掉。因此解压后的内核也是从1MB位置开始的。之后,decompress_kernel()会显示“done”(完成)和令人振奋的“Booting the kernel”(正在引导内核)。这里“Booting”的意思是跳转到整个故事的最后一个入口点,也是保护模式内核的入口点,位于RAM的第二个1MB开始处(偏移量0x100000,此值是由芬兰Halti山巅之上的神灵授意给Linus的)。在这个神圣的位置含有一个子程序调用,名叫…呃…startup_32。但你会发现这一位是在另一个目录中的。


这位startup_32的第二个化身也是一个汇编子程序,但它包含了32位模式的初始化过程:

1、 它清理了保护模式内核的bss段。(这回是真正的内核了,它会一直运行,直到机器重启或关机。)

2、 为内存建立最终的全局描述符表。

3、 建立页表以便可以开启分页功能。

4、 使能分页功能。

5、 初始化栈空间。

6、 创建最终的中断描述符表。

7、 最后,跳转执行一个体系结构无关的内核启动函数:start_kernel()。

下图显示了引导最后一步的代码执行流程:

IMG_6390.JPG

与体系结构无关的Linux内核初始化过程


start_kernel()看起来更像典型的内核代码,几乎全用C语言编写而且与特定机器无关。这个函数调用了一长串的函数,用来初始化各个内核子系统和数据结构,包括调度器(scheduler),内存分区(memory zones),计时器(time keeping)等等。之后,start_kernel()调用rest_init(),此时几乎所有的东西都可以工作了。rest_init()会创建一个内核线程,并以另一个函数kernel_init()作为此线程的入口点。之后,rest_init()会调用schedule()来激活任务调度功能,然后调用cpu_idle()使自己进入睡眠(sleep)状态,成为Linux内核中的一个空闲线程(idle thread)。cpu_idle()会在0号进程(process zero)中永远的运行下去。一旦有什么事情可做,比如有了一个活动就绪的进程(runnable process),0号进程就会激活CPU去执行这个任务,直到没有活动就绪的进程后才返回。


但是,还有一个小麻烦需要处理。我们跟随引导过程一路走下来,这个漫长的线程以一个空闲循环(idle loop)作为结尾。处理器上电执行第一条跳转指令以后,一路运行,最终会到达此处。从复位向量(reset vector)->BIOS->MBR->引导装载程序->实模式内核->保护模式内核,跳转跳转再跳转,经过所有这些杂七杂八的步骤,最后来到引导处理器(boot processor)中的空闲循环cpu_idle()。看起来真的很酷。然而,这并非故事的全部,否则计算机就不会工作。


在这个时候,前面启动的那个内核线程已经准备就绪,可以取代0号进程和它的空闲线程了。事实也是如此,就发生在kernel_init()开始运行的时刻(此函数之前被作为线程的入口点)。kernel_init()的职责是初始化系统中其余的CPU,这些CPU从引导过程开始到现在,还一直处于停机状态。之前我们看过的所有代码都是在一个单独的CPU上运行的,它叫做引导处理器(boot processor)。当其他CPU——称作应用处理器(application processor)——启动以后,它们是处于实模式的,必须通过一些初始化步骤才能进入保护模式。大部分的代码过程都是相同的,你可以参考startup_32,但对于应用处理器,还是有些细微的不同。最终,kernel_init()会调用init_post(),后者会尝试启动一个用户模式(user-mode)的进程,尝试的顺序为:/sbin/init,/etc/init,/bin/init,/bin/sh。如果都不行,内核就会报错。幸运的是init经常就在这些地方的,于是1号进程(PID 1)就开始运行了。它会根据对应的配置文件来决定启动哪些进程,这可能包括X11 Windows,控制台登陆程序,网络后台程序等。从而结束了引导进程,同时另一个Linux程序开始在某处运行。至此,让我祝福您的电脑可以一直正常运行下去,不出毛病。


在同样的体系结构下,Windows的启动过程与Linux有很多相似之处。它也面临同样的问题,也必须完成类似的初始化过程。当引导过程开始后,一个最大的不同是,Windows把全部的实模式内核代码以及一部分初始的保护模式代码都打包到了引导加载程序(C:/NTLDR)当中。因此,Windows使用的二进制镜像文件就不一样了,内核镜像中没有包含两个部分的代码。另外,Linux把引导装载程序与内核完全分离,在某种程度上自动的形成不同的开源项目。下图显示了Windows内核主要的启动过程:

IMG_6391.JPG

Windows内核初始化过程


自然而然的,Windows用户模式的启动就非常不同了。没有/sbin/init程序,而是运行Csrss.exe和Winlogon.exe。Winlogon会启动Services.exe(它会启动所有的Windows服务程序)、Lsass.exe和本地安全认证子系统。经典的Windows登陆对话框就是运行在Winlogon的上下文中的。


本文是引导启动系列话题的最后一篇。感谢每一位读者,感谢你们的反馈。我很抱歉,有些内容只能点到为止;我打算把它们留在其他文章中深入讨论,并尽量保持文章的长度适合blog的风格。下次我打算定期的撰写关于“Software Illustrated”的文章,就像本系列一样。最后,给大家一些参考资料:

l 最好也最重要的资料是实际的内核代码,Linux或BSD的都成。

l Intel出版的杰出的软件开发人员手册,你可以免费下载到。

l 《理解Linux内核》是本好书,其中讨论了大量的Linux内核代码。这书也许有点过时有点枯燥,但我还是将它推荐给那些想要与内核心意相通的人们。《Linux设备驱动程序》读起来会有趣得多,讲的也不错,但是涉及的内容有些局限性。最后,网友Patrick Moroney推荐Robert Love所写的《Linux内核开发》,我曾听过一些对此书的正面评价,所以还是值得列出来的。

l 对于Windows,目前最好的参考书是《Windows Internals》,作者是David Solomon和Mark Russinovich,后者是Sysinternals的知名专家。这是本特棒的书,写的很好而且讲解全面。主要的缺点是缺少源代码的支持。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表