「Linux 源码趣读」 进入内核前的苦力活
Czz Hardcore

[TOC]

前言

「Linux 0.11」内核于 1991 年 9 月 3 日发布,代码总量只有 10,000 行左右,但是麻雀虽小,五脏俱全。0.11版本的内核已经具备了基本的文件系统、进程管理等。

仅仅支持 x86 架构(Intel 80386 处理器),原因是因为 Linus Torvalds 使用的电脑为这台,因此他首先针对这一体系结构进行了开发。该版本的内核仅支持单个处理器,不支持对称多处理(SMP)。

需要较少的内存才能运行。通常,它需要至少2MB的物理内存,但可以在更小的系统上运行。这个版本不支持虚拟内存(交换空间),因此所有的内存管理都发生在物理内存中。

支持基本的文件系统,最初它使用的是Minix文件系统。硬盘大小通常非常有限,远远小于今天的标准。硬盘驱动程序和文件系统支持仅适用于有限的硬件。

有基本的输入和输出支持。它可以与键盘和显示器进行简单的交互,但不支持图形界面。它还可以与串口通信进行简单的终端交互。

Linux 0.11 最初不支持网络,因此没有网络堆栈。网络功能被后续版本添加到内核中。

简介

本篇以文档为主,记录我阅读闪客的「Linux源码趣读」过程中对每一回的笔记以及总结,所以有可能最后这篇文章会非常的冗余并且没有章法。

为了保证内容看起来不算太冗余,一部分一部分的记录。

正文

第一部分 进入内核前的苦力活

第 1 回 最开始的两行代码

0x7c00

计算机上电后的硬件初始化使得CPU能够找到主板上的固件程序 BIOS 将硬盘启动区的 512 字节数据复制到内存的 0x7c00 位置。

初始化 PC 寄存器

根据 Intel 80386 处理器手册,开机后 PC 寄存器要初始化为 0xFFFF0,这个地址线指向了 BIOS 程序所在的 ROM 区域,而 BIOS 就会作为一个数据搬运工,从启动盘的 0 盘 0 道 1 扇区将数据搬至内存,CPU就可以一句句往下执行。

CPU 地址线连接有 RAM、 ROM、I/O端口,叫做 Memory-Mapped IO。

为什么是 0x7c00?

当使用 BIOS 这种启动方式就规定了启动代码是如此。

还有更浪漫的一种说法:(From 吕恒) 侵删!!!

默认的数据段寄存器 ds 寄存器的值改为 0x07c0 方便以后的基址寻址。

这里我有话说,07在ASCII码中是bell的意思,这里代指Bell Labs(贝尔实验室),后面的 C0 可以理解成C语言(1973年第四版的Unix用C语言重写了),也可理解为 Clear Zero,Current Zero……

启动盘

启动盘的 0 盘 0 道 1 扇区,即第一扇区的 512 字节最后两字节分别为 0x55 和 0xAA,BIOS 就会认为它是一个启动盘,将这 512 字节搬至内存。

(From 吕恒) 侵删!!!

0x55对应ASCII码的U,也就是Unix的首字母。而0x55左移一位为0xAA,这里的A代指AT&T,因为Unix来自于“AT&T Unix”。意即,感谢AT&T诞生Unix。想想,机器启动时满载感恩。

第 2 回 从 0x7c00 到 0x90000

在上一回中,计算机将 0x7c00 的地址记录在了 ds 数据段寄存器中,而在接下的操作中,内核代码通过巧妙地 rep 循环指令与 movw,一次复制一个字(word,16位)复制256次,将 0x7c00 起始的 512 字节复制到 0x90000,并执行了jmpi go段间跳转至 go 标签,也就是bootsect.s编译后二进制文件中 go 标签的相对偏移位置。

第 3 回 访问内存的基础准备工作

CPU 访问内存的三种途径

访问代码的cs:ip,访问数据的ds:xxx,访问栈的ss:sp。这样就实现了通过代码寄存器cs、数据寄存器ds、栈段寄存器ss和栈基址寄存器sp,cs访问当前执行的指令代码所在段的起始地址、ds存储的程序数据如变量和数组、ss栈段的起始地址存储函数调用和局部变量、sp栈指针偏移量指示栈中下一个空闲位置。

第 4 回 将操作系统的全部代码从硬盘搬到内存

这一步的目的是通过已经存在内存的 512 字节的操作系统代码,将其余的代码一并放入内存中。

第 5 回 将重要的操作系统代码放在零地址处

将内存地址 0x10000 处开始往后到 0x90000 的内容,通通复制到内存最开始 0 地址。

这一步的主要操作是对操作系统即将管理的内存地址重新进行布局,并将一些重要的如光标位置等的局部变量,覆盖原来的 0x90000 位置起。

第 6 回 解决段寄存器的历史包袱问题

由于 16 位实模式下,是通过段基址左移 4 位使得 CPU 可以有 20 位的地址总线,但在保护模式下段基址变成了段选择子,段选择子中存储着段描述符的索引,段寄存器里存储的段选择子可以帮助操作系统去全局描述符中寻找段描述符,取出段基址,再加上偏移地址,得到最终的物理地址。首先,物理地址计算的过程不同以及现在还无从得知全局描述表在内存中的位置。需要将全局描述符保存在一个寄存器(gdtr)中。

所以这一步的主要目的是为了后面计算机进入 32 位的保护模式,操作系统设置了一个全局描述表,后面切换到保护模式时,段选择子能通过此寄存器寻找到段描述符,填补 16 位实模式下的坑。

第 7 回 六行代码进入保护模式

具体哪六行代码就不关心了,因为这个操作在现如今的平台已经不太可能用上。

  1. 配置全局描述符表和中断描述符表。
  2. 打开 A20 地址线,这一步的目的是手动扩展地址信号线 20 位的宽度,变成 32 位可用。(为了兼容性,保持 20 位地址可用,所以默认是 20 位,必须手动开启)
  3. 更改机器状态字寄存器 cr0 上的 1 位,进入保护模式。

由编译好的 Linux 操作系统二进制文件组成的 system 占据现在内存布局中的一大部分(从第 0 地址,到 0x80000 地址),这些代码都是由 head.s 以及 main.c 及其他模块的操作系统代码合并出来的。

第 8 回 重新设置中断描述符表与全局描述符表

由于后续的操作系统会将在 0x90200(setup.s) 中的内存覆盖掉,其中包含了先前设置的全局描述符表,所以这一步的目的主要是将表挪到从 0 开始的 system 中,并且给所有中断设置了一个默认的中断处理程序 ignore_int,全局描述符表中仍然只有代码段描述符和数据段描述符。

第 9 回 开启分页机制

在开启分页机制之前,通过保护模式下的段选择子 + 段偏移的计算,就已经是一个物理地址。

但在开启分页机制后,得到的只是一个线性地址,还需要经过计算机中的 MMU(分页内存管理单元)的计算(不说了,大学课本都讲过)。

开启分页机制的开关,类似于开启保护模式,更改 cr0 寄存器中的 31 位,1 表示开启。

在 Linux 0.11 的约定下,总共可以使用的内存不会超过 16 MB,及最大地址空间为 0XFFFFFF。

因此,4(页表数) x 1024(页表项数) x 4KB(一页大小) = 16 MB。

而页目录表与 4 张页表总共占据 4096 x 5 放在从 0 地址起的位置。覆盖使用过的 system 内存空间。

逻辑地址是由程序员给出的,经过分段基址的计算后变成线性地址,再经过分页机制转换后变成物理地址。

1
2
3
4
5
6
7
8
9
10
11
jmp after_pate_tables
...
after_page_tables:
push 0
push 0
push 0
push L6
push _main
jmp setup_paging
L6:
jmp L6

第 10 回 进入 main 函数前的最后一跃

在上面那段代码中,进行了压栈操作,再执行完setup_paging设置分页的操作后 CPU 会在其中的汇编 ret 到栈顶指针,也就是刚才压入的 _main 所在的内存的起始地址,至此计算机正式要进入 main 函数。

参考

8086CPU只有16位寄存器,却可以访问20位的物理地址_16位cpu寻址20位总线-CSDN博客

需要从16位的实模式转换到32位的保护模式主要有以下几个原因:

  1. 内存管理:实模式下,内存分段是相对简单的,但也受到了很多限制。保护模式提供了更灵活的内存管理机制,包括虚拟内存、分页等,使操作系统能够更好地管理内存资源。
  2. 多任务支持:实模式下,只能运行一个程序,而在保护模式下,可以支持多个任务或进程并行执行。这是多任务操作系统的基础,允许多个程序同时运行。
  3. 更高的特权级别:保护模式提供了更多的特权级别,通常有4个级别(0到3级),允许更细粒度地控制不同代码的权限。这有助于保护操作系统的核心部分,使其不容易受到用户级代码的损害。
  4. 硬件保护:在保护模式下,处理器提供了硬件支持,可以在执行指令时进行访问权限检查。这增加了系统的安全性,允许操作系统更好地保护其自身和用户数据。
  5. 扩展性:32位保护模式支持更广泛的寻址范围,允许访问更大的内存。这对于现代计算机系统非常重要,因为它们通常具有大容量的RAM。

从16位实模式切换到32位保护模式是x86处理器在引导过程中执行的一部分。这个切换包括以下步骤:

  1. 设置全局描述符表(Global Descriptor Table,GDT):在保护模式下,内存分段的管理使用GDT。首先,需要定义GDT,包括描述符的基地址、大小和属性。GDT是一个表,其中每个描述符定义了内存段的特性,如基地址、段限制、特权级等。这个表存储在内存中,并在加载GDT之前仍然在实模式下运行。
  2. 加载GDT寄存器:在实模式下,通过将GDT的基地址和大小加载到GDTR寄存器来启用GDT。这会告诉处理器在切换到保护模式时去哪里查找段描述符。
  3. 设置控制寄存器 CR0:在实模式下,通过将CR0寄存器的第0位(PE位,Protection Enable)设置为0来禁用保护模式。在切换到保护模式之前,需要将PE位设置为1以启用保护模式。
  4. 切换到保护模式:最后,通过执行JMP指令或CALL指令,将处理器切换到位于GDT中的新代码段。此时,处理器将进入32位的保护模式,其中有多个特权级,分段机制更为复杂,可以访问扩展的内存和功能。