首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

  • 24-12-06 00:05
  • 2952
  • 12372
juejin.cn

前一篇博客讲到了 bootloader ,本文将继续了解内核镜像是如何被 Bootloader 加载进内存,并最终启动的。

原文地址: manybutfinite.com/post/how-co… 
注:文章发布于 2008 年,部分内容可能已经过时

为使阅读顺畅,在翻译的过程中进行了一些删改和补充。修改较小处不单独标出,只有较大量的补充会进行标注

在本文中会给出很多到 LXR / The Linux Cross Reference 中 2.6.25.6 版本内核代码链接,只要你熟悉类 C 语法的语言,读起这些内核源码就应该不会太困难——即使无法理解某些细节,也不影响大致了解主要逻辑。我也会尽量讲出链接中代码相关的上下文信息(比如要在什么时候、为什么运行等),让你读起来更容易一些。出于简洁方面的考量,本文只能将很多有趣的东西(比如中断和内存)一带而过。本文最后还会简要介绍 Windows 的启动过程。

接上篇:从用户按下启动按钮的那一刻到现在,我们的 x86 处理器还仍然工作在实模式下,只能寻址 1MB 的内存——而加载完成后的现代 Linux 系统的内存是下面这样的,显然需要大于 1MB 的内存:

image.png

boot loader 运行完成后的内存

内核镜像会被 bootloader 通过 BIOS 磁盘 I/O 服务加载进内存,该镜像就是硬盘上包含镜像的文件的一份拷贝(如 /boot/vmlinuz-2.6.22-14-server)。镜像会被分为两部分:较小的、包含实模式代码的那部分被加载到前 640K 内存之内,较大的、运行于保护模式的主要部分则被加载到内存的第 1 MB 之后。

内核的启动过程,从上图中的实模式内核头( real-mode kernel header )开始。这一区域的内存被用来实现 bootloader 和内核之间的 Linux 启动协议 。该内存中的某些值是由 bootloader 设置的,其中有一些易用性相关的信息,比如以人类可读字符串形式存储的内核版本等。也有像实模式部分内核代码大小这样的关键信息。bootloader 还会向这个区域中写入一些别的值,比如用户在启动菜单中输入的命令行参数等。在 boot loader 运行完成后,其就已经准备好了内核头所需的所有参数,接下来就可以跳转到内核入口点了。

下图结合源码目录、文件以及代码所在行,展示了内核初始化的代码序列:

image.png

架构相关的 Linux 内核初始化过程

Intel 架构的早期阶段内核启动代码定义在 arch/x86/boot/header.S 中。该代码由汇编语言编写,汇编语言在启动阶段的代码中非常常见,而在这之后的主要部分中则用得很少。该文件最开始包含了引导扇区的代码——这些代码是从 Linux 可以不依赖引导程序启动的那些日子里遗留下来的。如果尝试执行现在的启动扇区代码,它就只会在为用户打印一行 "bugger_off_msg" 之后重启。因此,现代 bootloader 都忽略了这段经典代码。

在启动扇区后,是实模式内核头的前 15 bytes 。这两部分加起来一共有 512 bytes ,这是 Intel 硬件中一个典型的磁盘扇区的大小。

在这 512 bytes 之后的偏移量 0x200 处,是 Linux 内核的第一条指令:实模式入口点。代码位于 header.S:110 ,这是一个 2 byte 的跳转指令,直接以机器码的形式写作 0x3aeb 。你可以通过对自己的 Linux 内核镜像 hexdump 一把,来看看这个位置的东西。boot loader 运行完成后会跳转到这个位置,然后跳转到 header.S:229 处名为 start_of_setup 的汇编程序。这一小段程序会创建一个栈,将实模式内核的 bss 段置零(该段存放静态变量,初始值为 0 )并跳转到 arch/x86/boot/main.c:122 的 C 代码。

main() 函数会检测内存布局,设置显示模式等,最后调用 go_to_protected_mode() 函数。

在 CPU 可以被设置为保护模式前,仍然有些必备的准备工作。这些工作主要包括两部分:中断和内存。

  • 在实模式下,处理器的 中断向量表 的内存地址总是内存地址 0 ,而在保护模式下,中断向量表的地址则会保存在 CPU 的 IDTR 寄存器中。
  • 在实模式和保护模式下,逻辑内存地址(软件所使用的地址值)到线性内存地址(从 0 到内存最高地址)的转换也是不同的。保护模式下需要在 GDTR 寄存器中保存 全局描述附表(Global Descriptor Table,GDT)的内存地址。

因此,go_to_protected_mode() 函数需要调用 setup_idt() 建立临时中断描述符表,再调用 setup_gdt() 建立全局描述符表。

译注:

bss(Block Starting Symbol)是对象文件、可执行文件或汇编代码中,包含【未初始化的静态分配变量】的内存部分。通常情况下,对象文件的这部分都只保存了长度而没有实际的数据,其占用的内存会在加载器加载程序时再分配。

中断描述符表(Interrupt Descriptor Table, IDT):中断描述符表是 x86 架构下的 "中断向量表"。其保存每个异常 / 中断与它们相应的处理程序的关联。

全局描述附表(Global Descritpor Table, GDT)是 Intel x86 系列处理器用于定义程序运行时不同内存区域功能的数据结构,其中包括各个区域的基址、大小、访问权限(如是否可执行、可写等)—— Intel 将这些内存区域称为 "段"( segment )

现在终于可以进入保护模式了,模式转换通过另一段汇编程序 protected_mode_jump 进行。这段代码通过设置 CPU 中的 CR0 寄存器的 PE 位来启动保护模式。此时还没有启用分页(paging) —— 分页是处理器的一个可选特性(在保护模式下同样可选),目前还不需要启动该特性。到这一步,就不再受 640K 内存屏障的限制,可以寻址 4GB 的内存了。该汇编程序接下来会调用 32 位压缩内核的入口点 —— startup_32 ——这段程序会初始化一些寄存器,然后调用 C 函数 decompress_kernel() 解压内核。

decompress_kernel() 将打印一条类似于 "Decompressing Linux..." 之类的信息。内核解压是在原地进行的,一旦完成就会覆盖上面第一张图片中的压缩内核——也就是说,解压后的内容也是从 1MB 的内存地址开始的,解压完成后会打印一行 done. ,继而打印 "Booting the kernel."。 "Booting" 意味着跳转到最终的 Linux 入口点——这就是保护模式的内核入口点,位于 RAM 的第 2MB (0x100000)。

位于此处的是另一个 startup_32 汇编程序,其中包含着 32 位模式的初始化代码。该程序会初始化保护模式内核(直到关机前都会一直运行的真正内核)的 bss 段内存、设置最终的内存全局描述附表、构建页表、启动分页、初始化栈、创建最终的中断描述符表,最后跳转到与架构无关的内核启动函数 start_kernel() 。下面的图展示了启动最后阶段的代码流程

image.png

架构无关 Linux 内核初始化过程

start_kernel() 看起来就更像是普通的内核代码了——基本全部由 C 语言编写,与具体机器架构无关。该函数进行了一系列的调用,来初始化各种内核子系统和数据结构。包括调度器、内存区、时间管理等。在完成绝大部分的初始化工作后, start_kernel() 将调用 rest_init() 。rest_init() 会创建一个内核线程并传入 kernel_init() 函数。然后 rest_init() 会再调用 schedule() 启动任务调度,并通过调用 cpu_idle() 函数进入睡眠状态——cpu_idle() 是内核的空闲线程,该线程由 0 号进程管理,并和该进程一起一直运行。只要有可运行进程出现时,0 号进程就会继续运行,直到无事可做时再返回。

idle 循环是我们从上电后 CPU 执行的第一个 jump 开始,一直跟踪的代码的最后一步。从重置向量,到 BIOS、MBR、bootloader、实模式内核再到保护模式内核,一步步跳转到 boot 处理器的 cpu_idle() 。接下来是全新的开始。

至此,前面创建的内核线程就已经就绪了,其将取代 0 号进程和它的空闲线程。内核线程在 kerenl_init() 开始运行: kernel_init() 需要初始化系统中剩余的 CPU (这些 CPU 之前一直处于待机状态)—— 到目前为止,我们看过的所有代码都是由一个被称为 "启动处理器 boot processor" 的 CPU 执行的。其余被称为 "应用 CPU " 的处理器也是从实模式开始运行的,经过几次初始化后才算启动完成。该过程中很多的逻辑都是相同的(参阅代码 startup_32 ),只在一些小分支不太一样。最终,kernel_init() 会调用 init_post() ,该段代码会尝试按下面的顺序启动一个用户模式进程:/sbin/init、/etc/init、/bin/init 和 /bin/sh ,如果全都启动失败了,内核就会 panic 。但幸运的是,启动通常都会成功。成功启动的进程将作为 PID 1 开始运行,其会检查自己的配置文件来确定启动哪些进程——例如 X11 Windows、控制台的登录程序、网络守护进程等。至此,启动完成。

在相同的架构下, Windows 的启动过程和 Linux 有很多相似之处——需要面对很多相同的问题,并做一系列必须完成的初始化。启动阶段最大的区别在于,Windows 将所有实模式内核代码和部分最初的保护模式代码都打包放在了 boot loader 中(C:\NTLDR)。因此,与 Linux 将一个内核镜像分到了内存中的两个区域不同,Windows 直接提供了两个二进制镜像。另一个不同点是 Linux 实现了 bootloader 和内核的完全分离。下图中展示了 Windows 内核启动的主要部分:

image.png

Windows 内核初始化

Windows 用户模式的启动与 Linux 非常不同——Windows 没有 /sbin/init ,而是通过 Csrss.exe 和 Winlogin.exe 启动。Winlogin 会启动 Services.exe ,其会启动所有的 Windows 服务以及 Lsass.exe —— 本地安全身份认证子系统。经典的 Windows 登录对话窗口就运行在 Winlogin 的上下文中。

系统启动系列就此完结。感谢大家的阅读和反馈。下面给出一些有用的资源:

  • 最有用的资源永远是内核源码。Linux 的和各种 BSD 的都好;
  • Intel 出版了非常棒的 软件开发者手册 ,可以免费下载;
  • 理解 Linux 内核 是一本很好的书,它能够带你了解很多内核源码。虽然有点老了,但依旧值得推荐。Linux 设备驱动 更有趣一些,但覆盖范围较小。评论区推荐的 Linux 内核开发 也是一本广受好评的好书,应该值得一看;
  • 目前最好的 Windows 参考书是 David Solomon 和 Mark Russinovich 的 Windows Internals ,后者是 Sysinternals 的大佬。这是一本好书,只可惜没有系统源码。

补充内容:评论区中的 "Nix" 对初始根文件系统( initial root file system, initrfs )进行了补充。为了简便,本文没有提到这部分内容。

译注,补充翻译如下

最初的根文件系统,默认是从内核代码树的 usr/ 子文件夹中的内容组合而来的(体现为一个 cpio 压缩包),并链接到内核镜像中(也可以将这个压缩的文件系统镜像作为一个单独的文件使用)。启动过程(定义在 init/main.c:do_basic_setup() )的一部分会执行 initcalls —— 这些调用会以函数指针数组的形式保存,并在启动时调用,该数组由链接器构造。

这些 initcalls 中的一个就是 init/initramfs.c:populate_rootfs() ,该函数会初始化不可换页的内存文件系统,该文件系统挂载在 / ("真正的"根文件系统是挂载在这个根之上的,稍后会提到)。在系统成功启动后, rootfs 也不会被卸载,而是会一直挂在 /proc/mounts 。上面的 init/initramfs.c:populate_rootfs() 函数会解压这个压缩的文件系统,并在该文件系统上执行 /init 。一旦找到真正的根文件系统,就会立即 chroot 过去并执行真正的 init ,这样 "早期用户空间" 的启动就完成了。因此——寻找根文件系统的过程是完全可自定义的:可以从 RAID 阵列进行组合,也可以从网络拉取一部分组件(我已经这么做了,这样可以用于极端情况下的系统恢复)。

但如果没能找到,系统就还是没有一个包含 /sbin/init 的文件系统。在调用 init_post() 之前,系统可以调用 init/do_mounts.c 中的 prepare_namespace() 函数,该函数可以尝试通过多种方式来找到一个根文件系统:比如暂停一段时间,等待保存在低速硬件中的根文件系统就绪(例如 SCSI 磁盘或 USB 盘),又比如自动检测 RAID (这么做其实有点危险,因为此时无法确定此时组合阵列的方式是否正确:推荐的 RAID 启动方式是使用可自定义的启动进程,并使用 mdadm 工具来进行组合),再比如通过内核命令行的 root= 来挂载指定的块设备。甚至可以直接要求用户插入一张包含根文件系统的软盘。

注:本文转载自juejin.cn的marsCatXDU_李经纬的文章"https://juejin.cn/post/7444450350189838374"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

143
阅读
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top