Linux 是如何启动的

政采云技术团队.png

鱼针.png

前言

当你按下你们家电脑的电源按钮时,Linux 作为操作系统,需要将自己放入到内存中执行,早期工程师们称这个过程为 “Bootstrap”。

“Bootstrap” 英文直译是鞋带的意思,这里用了一个形象的比喻了计算机操作系统的启动:计算机需要操作系统作为管理者来启动程序,但操作系统本身也是一个程序,因此,操作系统需要自己启动自己。

计算机启动高度依赖计算机的硬件体系架构,本文默认描述的是在80x86架构下。

BIOS

我们知道,CPU 的执行内容都要从内存中获取,而内存在加电前是没有内容的,那么计算机通电后第一条指令从哪里来?

上世纪70年代初,人们发明了只读内存(Read Only Memory 简称 ROM ),计算机在生产过程中,厂商就会将一个系统程序烧录至 ROM 中,我们称这个系统为基本输入/输出系统(BIOS),计算机通电后第一条指令自然就是 BIOS 程序代码。

计算机使用统一的编码方式寻址,对于 CPU 而言,在做寻址等操作的时候,ROM 和 RAM 是等效的。为了能将这两种内存合并成一个整体内存供计算机使用,硬件会给ROM分配两个映射:一个是在0xffffffff(4G)处向下一点点,另一个是在0xfffff(1M)处向下一点点。

有了这个映射,CPU 就可以通过访问固定的地址获取/执行 BIOS 的代码了。

计算机通电后,通过一个特殊的硬件电路将一些寄存器(包含cs,eip)的内容设置为固定值,于是 CPU 就得到了第一条执行代码的物理地址 0xfffffff0 。

地址0xfffffff0中的指令是这样的

0xfffffff0 : ljmp  $0xf000:e05b

指令的效果是执行了一个内存的长跳,从高位(4G左右)处跳到了低位(1M左右)处。

早期80x86的体系下,段寄存器是16位,加上偏移量的4位共20位,支持的寻址空间就是1M。通过将段寄存器中的值左移4位再加上4位偏移量作为的物理地址的寻址方式称之为实模式。基于兼容考虑,现在的80x86计算机都是以实模式启动的。

在执行完上面的指令长跳后,BIOS 程序就都集中在1M以下的位置上通过实模式执行。

此时,内存里面大概是个什么情况?

内存的大致的划分如下:

  • 0到640K这部分空间用作普通 RAM ,供操作系统和应用程序使用。
  • 640K到1M这384K的空间用作 ROM 的映射,刚才说了,里面是 BIOS 程序。

最开始执行的 BIOS 程序,先是硬件自检,用来检测有什么设备以及是否正常工作。

然后初始化硬件设备,保证所有硬件设备操作不会引起 IRQ 线与 I/O 端口冲突。

接下来要做的事情就与操作系统相关了,在BIOS将控制权移交操作系统之前,需要知道操作系统被存在什么地方。自己DIY过电脑的同学都有自己安装操作系统的经验,在电脑启动前,可以进入 BIOS 设置功能,可以在 BIOS 里设置外部存储设备的启动顺序。

BIOS 根据启动顺序读取存储设备最前面的512个字节,如果这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动操作系统。这512个字节,即被称为主引导记录(Master Boot Record,缩写为 MBR )。

BIOS 将 MBR 装入到 RAM 地址的0x00007c00处开始执行。(为什么是这个位置)

到此,BIOS 程序执行完成。

启动Linux的汇编语言

setup()

上面说到,Linux 的主引导记录在0x00007c00处开始执行,它将依次执行以下内容:

  1. 把 Linux 的主引导记录这512个字节再次拷贝到了0x00090000处

  1. 然后再做一些内存的初步规划,包括给访问代码和访问数据的寄存器设置了一个基地址,访问栈寄存器设置了一个远离代码位置的地方,为执行后面的程序做基础

  2. 接下来从硬盘第二个扇区开始搬运数据,这个数据就是 Linux 的 setup() 函数,这将是下一步程序的执行入口,这些内容将从0x00090200处开始存放

  1. 接下来要做的事情就是继续从硬盘搬运数据,一直到把操作系统所有的代码全都放到内存中

此时,执行代码段被定位在了0x00090200处,即 Linux 的 setup() 函数。

setup() 汇编语言函数的代码由链接程序放在内核映像文件的偏移量0x200处。其主要功能是初始化计算机硬件设备,并为内核程序执行建立环境。

之前 BIOS 已经做了绝大多数硬件的初始化,但 Linux 并不依赖 BIOS ,它使用 setup() 函数对硬件做一次检测和初始化。

在对内存排布做最后一次整理后,内核准备要从实模式切换到保护模式了。 此时内存的排布情况大概是这样的:

在实模式下,计算机内存的寻址方式都是寄存器中的内容和偏移量直接组合成为物理地址的。

在保护模式下,这样的组合形成的地址就不是物理地址了,而是段描述符的地址,真正的物理地址包含在段描述符里。

为了能在保护模式下寻址,内核建立了一个临时中断描述符表(IDT)和一个临时全局描述符表(GDT),用来存放段描述符。

进入保护模式很简单。

首先是打开A20引脚线。前面提到过,早期的计算机寻址能力是20位的,为了兼容,计算器启动实模式下还是保持20位,即使现代计算机已经具备了32位以上的寻址能力,在A20引脚线打开之前,20位以上的部分都会被忽略掉。

然后就是将CR0寄存器中的PE位从0置为1,即从实模式切成为保护模式了。

setup() 函数的最后是一个跳转,进入新的函数 startup_32() 。

startup_32()

startup_32() 函数会将 Linux System 数据解压,因为 Linux 镜像是被压缩存放的。解压完成后的数据将会放在0x00100000处,执行代码也随之跳到这个位置。

startup_32() 函数的主要功能,是为了 Linux 的另外一种寻址方式:内存分页做准备的。

上面 setup() 函数已经将计算机从实模式下转成了保护模式,在保护模式下,内存分段寻址是8086体系下不能避开的寻址方式。

Linux 在内存管理上使用的分页模式,在兼容分段寻址上使用了一个讨巧的方法——将所有的段基地址设置为0,直接使用段内偏移地址来访问内存空间。

内核要支持内存分页,初始阶段必须先按照内存分页机制初始化一些内容。

根据内核自身占用的内存数据,创建了临时内核页表,并将线性地址与物理地址建立映射关系。

最后通过将cr3寄存器的内存设置为全局页目录地址,然后在cr0寄存器中设置PG分页标识为1。

最后将中断向量表和全局描述表的地址分别存入idtr与gdtr寄存器后,程序跳到了 start_kernel() 方法中。

Linux也由此进入了c语言的世界。

小结

Linux 从诞生到现在已经30多年,发展到现在,它涉及的领域已经十分广泛,从智能手机、路由器、嵌入式设备、各种 Linux 发行版本、服务器...

作为一款开放的操作系统,Linux 社区集中了世界上最优秀的程序员,为这个程序贡献代码,现在已经是千万行代码规模。

本文虽然只对 Linux 启动最开始执行的那一部分汇编程序做了一些阐述,尽管如此,其涉及的概念也非常多。任何一个概念都能单独拿出来写一篇论文。因此,关于这些内容的更加详尽的知识,还需要读者自行查询相关资料丰富完善。

相关资料:

www.intel.cn/content/www…

xuanxuanblingbling.github.io/ctf/pwn/202…

推荐阅读

ElasticSearch 磁盘 io 瓶颈问题解决方案探索

人工智能 NLP 简述

浅析 ElasticJob-Lite 3.x 定时任务

雪花算法详解

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png