• 作者:老汪软件技巧
  • 发表时间:2024-09-13 11:01
  • 浏览量:

XV6操作系统入门系列02-详解启动过程第零步-心理上的准备工作

任何事物都有其关键的窍门,当我们抓住了关键,事情会变得简单起来;当我们没有抓住要领,事情就会变得异常困难。

本文的主题是操作系统的启动过程。那么,理解它的关键是什么呢?

首先肯定不是 xv6 源代码使用的汇编语言或者C语言,如果哪条语法规则看不懂,我们问一下搜索引擎,ChatGPT,基本就能获得答案。学习语法是必要的工作,但不是关键。 临时抱佛脚、遇到问题再查找,甚至猜代码的作用都没有问题。

其次也不是 xv6 源代码本身。要将操作系统启动起来,涉及的代码也不少,分散在多个文件中。尤其是对于第一次接触它的人,彻底理解它的启动非常困难。当然网上有不少文章将涉及的代码一行一行注释写下来,理解成本很高。而我认为,阅读代码是必要的工作,但也不是关键。

必要的工作可以慢慢做,关键的工作要时刻做。我认为,要搞明白操作系统的启动过程,最核心的是要搞明白操作系统和CPU处理器之间的关系,也就是软件和硬件的关系。

在本文以及后面的文章中,我们会碰到很多概念。学习的关键是要搞清楚,操作系统中哪些概念是由CPU来实现的(硬件电路),哪些概念是操作系统实现的(抽象的代码运行过程)。

我们时刻要绷紧这根弦。硬件电路是一种规范性的事物,在这操作系统的视角,它不能被改变,但可以被利用。软件是我们发挥主观能动性的战场,我们需要不断反思,有没有更好的设计。

我在学习操作系统的时候,犯过这样的错误。我花了很多时间在研究硬件为什么这么设计,三极管、逻辑电路、以及CPU的各个部分的硬件是如何实现的?人的精力是有限的,要想学习操作系统,就应该把注意力放在操作系统本身。既要了解它的基本概念,也要了解它的实现细节。而关于硬件部分,我们只需要知道它的基本概念即可。当然,既不要陷入硬件电路的细节,也不要忽略硬件电路背后的逻辑概念。

第一步:分析源码寻找第一行代码

源代码根目录下的 Makefile 文件就是用来配置代码编译顺序的。当然我们也不需要精通 Makefile,只需要关注它的前面5行:

答案就在眼前!操作系统的第一行代码对应的文件是 kernel/entry.o。啊哦!糟糕!我们打开 kernel 文件夹,结果发现源代码里根本没有 kernel/entry.o。但是我们看到有一个非常类似的文件 kernel/entry.S。kernel/entry.S 就是我们要找的第一行代码,而 kernel/entry.o 则是编译后的可执行文件。

我们打开这个文件 entry.S ,学习一下它。文件开头有这样的注释:entry.S 被放在 0x80000000 的位置,并且这个位置在 kernel.ld 中配置。

接着我们打开 kernel.ld 源码来看看它是怎么配置的吧。在它的开头,就出现了我们要找的字符串 0x80000000。我们不了解链接器的语法,也能猜出来这是用来配置内核第一行代码放置的位置。

第二步-梳理程序逻辑

上一小节中,我们通过探索源码,基本上搞清楚了内核的第一行代码在0x8000_0000。涉及的所有概念都属于软件层面。

我需要补充说明一下,中间阶段生成的每一个.o文件本身也由这四个部分组成,kernel.ld就是把所有的.o文件的四个部分拆分重组。

kernel.ld 里出现的这几个段的含义:

第三步-Debug验证结果

在第一篇文章,我详细介绍了如何配置环境,以及如何调试代码。我再简述一下调试的方式。

你将看到这个画面:左半边是make qemu-gdb 的结果,右半边是 riscv64-elf-gdb 的结果。

非常非常地不幸,通过Debug,我们发现qemu启动时的第一条指令地址在 0x1000, 根本不是前面分析的 0x8000_0000。

问题出在哪里呢?本质是我们触及到了硬件部分的概念——引导程序,我们只需要了解一下Risc-V的引导程序,问题就迎刃而解了。

为了获取引导程序,我教大家一点GDB的技巧。

输入 p/x $pc 打印当前指令的地址,它存在cpu的PC寄存器里。PC全称是Program Counter,中文叫做程序计数器;输入 x/10i $pc 打印PC寄存器指向的内存地址以及下面10行汇编指令。

从GDB的输出结果可以发现:在内存中,CPU的每条指令的地址间隔4,这里指的是4个字节,因为每个字节8位,所以每条指令占据32位。

这段代码就是是存在RiscV处理器只读存储器(ROM,Read Only Memory)上的引导程序,用于初始化系统或跳转到操作系统的入口点。容我来逐行解释一下这段代码,给你节省一下时间:

auipc t0,0x0 将当前PC(程序计数器)的值加上立即数(这里是0)的结果存储到t0寄存器中,t0=0x1000。addi a2,t0,40 将t0中的值加上40,结果存入a2寄存器。 a2=t0+40=0x1000+0x28=0x1028。csrr a0,mhartid 读取控制状态寄存器(CSR) mhartid 的值到a0寄存器。mhartid 存储了当前硬件线程(hart)的ID。ld a1,32(t0) 从内存地址(t0+32=0x1000+0x0020=0x1020)处加载64位数据到a1寄存器。

因为我们启动的是一个64位的Risc-V处理器,所以CPU的硬件电路会读取 0x1020到 0x1027 这8个字节64位数据。

因为Risc-V的数据遵循小端数据(Little Endian) 的标准,所以数据从左到右,地址从大到小。

在GDB中输入x/4i 0x1020 ,可以查到当前对应的内存布局。

综上,寄存器a1的数值为0x87e0_0000 。

ld t0,24(t0) 从内存地址(t0+24=0x1000+0x0018=0x1018)出加载64为数据到t0寄存器。类似的,寄存器t0的数值为ox8000_0000 。

jr t0 跳转到t0寄存器中存储的地址,也就是 0x8000_0000。

我们通过GDB来验证一下上面的内容。这是一些新的GDB的调试技巧

谜底揭开,引导程序在初始化一些内部寄存器后,程序计数器PC寄存器将跳转到 0x8000_0000,也就是我们前面分析的内核的第一行代码在内存中的位置。

因为这段跳转程序是固定在硬件电路中的,所以操作系统内核第一行代码在内存中的位置由处理器硬件决定,xv6的源码进行了适配。

第四步-梳理硬件逻辑

上一小节中, 我们补全了xv6的整个启动流程。首先是 Risc-V 在通电后,首次读取它内部的只读寄存器的引导程序(硬件部分),引导程序会初始化处理器内部的寄存器,然后跳转到地址 0x8000_0000 处继续运行。因为操作系统的代码就放在 0x8000_0000 处,所以操作系统的代码开始执行。

总结

本文详细梳理了XV6的启动过程。处理器通电后,先运行引导程序,然后引导程序跳转到操作系统代码继续运行。其中引入了很多概念,有的属于硬件部分,有的属于软件部分。学习XV6的过程中,大家一定要把硬件和软件区分清楚。