Linux0.11内核笔记 一

Linux0.11内核笔记 一


《Linux内核设计的艺术》笔记

1. 概述


从开机到main函数的执行分三步完成,目的是实现从启动盘加载操作系统程序,完成执行main函数所需要的准备工作。

第一步,启动BIOS,准备实模式下的中断向量表和中断服务程序;

第二步,从启动盘加载操作系统程序到内存,加载操作系统程序的工作就是利用第一步中准备的中断服务程序实现的;

第三步,为执行32为的main函数做过渡工作。

  • 实模式(Real Mode)是Intel80286和之后的80x86兼容CPU的操作模式。实模式的特性是一个20位的存储器地址空间(2^20=1048576,即1MB的存储器可被寻址),可以在直接软件方位BIOS以及周边硬件,没有硬件支持的分页机制和实时多任务的概念。

流程简述:
执行main函数之前,先要执行三个由汇编代码生成的程序,即bootsect、setup和head。之后,才执行由main函数开始的用C语言编写的操作系统内核程序。
BIOS:检测硬件、加载中断向量表和中断服务程序---目的加载内核程序准备、把bootsect(Linux代码)载入到内存
bootsect:内存规划、加载setup代码到内存、将system模块载入内存、确认根设备
setup:通过BIOS提取内核运行所需的机器系统数据、关中断并将system移动到内存地址起始位置0x00000、设置中断描述符表和全局描述符表、打开A20,实现32位寻址、为保护模式下执行head.s做准备
调用main的准备工作、 创建了内核分页机制

1.1 启动BIOS

1.1.1 BIOS启动原理

Intel 80x86系列的CPU可以分别在16位实模式和32位保护模式下运行。为了兼容也为了解决最开始的启动问题,Intel将所有80x86系列的CPU,包括最新型号的CPU的硬件都设计为加电即进入16位实模式状态运行。同时,还有一个关键点——将CPU硬件逻辑设计为加电瞬间强行将CS的值置为0xF000、IP的值置为0xFFF0,这样CS:IP的值就指向0xFFFF0这个地址位置——实模式内存寻址空间为:0x00000~0xFFFFFBIOS程序的入口就是0xFFFFF0。即BIOS程序的第一条指令就设计在这里

  • IP/EIP:指令指针寄存器,存在CPU中,记录将要执行的指令在代码段内的偏移地址,和CS组合即为将要执行的指令的内存地址。实模式为绝对地址,指令指针为16为,即IP;保护模式下为线性地址,指令指针为32位,即EIP

  • CS:代码段寄存器,存在CPU中,指向CPU当前执行代码在内存中的区域(定义了存放代码的存储器的起始地址)

  • 8086CPU中的计算公式为(CS<<4)|IP,即CS左移4位,然后加上IP

为什么要设计CS和IP寄存器?
“CPU发展是连续迭代的过程,新的设计要兼容旧的设计”
在上古CPU中,类似于现在的小型单片机,是没有CS和IP寄存器的,因为内存少的可怜,比如16位的CPU,最大就能访问2^16 byte,即64Kb的内存,也就是地址线、数据线、寄存器都是16位,访问内容完全都是统一的,一点都不会乱,而且那会儿64Kb的内存空间已经足够用了。
随着应用程序的发展,对内存的需求也更多了(2021年,4G内存都沦落到乞丐配置了),所以相比上一代的64Kb,8086处理器的设计目标是1M的大内存空间,相当于提升到上一代的16倍,1M的空间对应的地址总线就是20位。
一个很现实的问题就摆在Intel设计人员面前,地址线宽度是20位,但是CPU中的算数逻辑运算单元(ALU) 仍然是16位,而且很尴尬的是,当时的制造技术很难把ALU加工到20位,即便是有能力加工到20位,也无法兼容上一代的CPU了,当然也有其他的方案,比如增设一些20位的指令和寄存器,专门用于地址的运算和操作,但是那样又造成CPU内存结构的不均匀,基于上面的原因,Intel的工程师设计了一种在当时看来很巧妙的方法,即分段方法。也就是前面提到的CS:IP结合的算法,CS和IP都是16位,CS左移4位,然后与IP相加,得到20位的地址。这样就实现了从16位内存地址到20位实际地址的转换,段式内存管理带来了显而易见的优势,程序地址不再需要硬编码了,调试错误也更容易定位了,也能支持更大的内存了。
1.1.2 BIOS加载中断向量表和中断服务程序

BIOS程序被固化在计算机主机板上的一块很小的ROM芯片里。通常不同的主机板所用的BIOS也有所不同。就启动部分而言,各种类型BIOS的基本原理大致相似。书中选用的BIOS程序只有8KB,所占地址段为0xFE000~0xFFFFF。注意前面提到上电后,CS:IP指向0xFFFF0这个位置,意味着BIOS从这里开始启动。随着BIOS程序的执行,屏幕上会显示显卡的信息,内存的信息……说明BIOS程序在检测显卡、内存……这期间有一项对启动(boot)操作系统至关重要的工作,那就是BIOS在内存中建立中断向量表和中断服务程序。

BIOS程序在内存中最开始的位置(0x00000)用1KB的内存空间(0x00000~0x003FF)构建中断向量表,在紧挨着它的位置用256字节的内存空间构建BIOS数据区(0x00400~0x004FF),并在大约57KB以后的位置(0x0E05B)加载了8KB左右的与中断向量表相应的若干中断程序。

中断向量表中有256个中断向量,每个中断向量占4字节,其中两个字节是CS的值,两个字节是IP的值。每个中断向量都指向一个具体的中断服务程序

1.2 加载操作系统内核程序并为保护模式做准备

1.2.1 整体概述

从现在开始,要执行真正的boot操作了,即把软盘中的操作系统程序加载至内存。对于Linux0.11操作系统而言,计算机将分三批次逐次加载操作系统中内核代码。

  • 第一批由BIOS中断int 0x19把第一扇区bootsect的内容加载到内存;

  • 第二批、第三批在bootsect指挥下,分别把其后的4个扇区和随后的240个扇区的内容加载至内存

1.2.2 加载第一部分内核代码——引导程序(bootsect)

现在我们基本上都是将硬盘设置为启动盘。Linux0.11是1991年设计的操作系统,那时常用的启动设备时软驱以及其中的软盘。

经过执行一系列BIOS代码之后,计算机完成了自检等操作。由于我们把软盘设置为启动设备,计算机硬件体系结构的设计与BIOS联手操作,会让CPU接收到一个int 0x19中断。CPU接收到这个中断后,会立即在中断向量表中找到int 0x19中断向量。接下来,中断向量把CPU指向0x0E6F2,这个位置就是int 0x19相对应的中断服务程序入口地址。这个中断服务程序的作用就是把软盘第一扇区中的程序(512B)加载到内存中的指定位置。这个中断服务程序是BIOS事先设计好的,代码是固定的,与Linux操作系统无关,这段BIOS程序任务就是找到软盘并加载第一扇区。

int 0x19中断向量所指向的中断服务程序,即启动加载服务程序,将软驱0号磁头对应盘面的0磁道1扇区的内容复制至内存0x07C00处。

这个扇区里的内容就是Linux 0.11的引导程序,也就是bootsect,其作用就是陆续把软盘中的操作系统程序载入内存。这样制作的第一扇区就称为启动扇区(boot sector)。第一扇区程序的载入,标志着Linux 0.11中的代码即将发挥作用了。

这是非常关键的动作,从此计算机开始和软盘上的操作系统程序产生关联。第一扇区中的程序由bootsect.s中的汇编程序汇编而成(以后简称bootsect)。这是计算机自开机以来,内存中第一次有了Linux操作系统自己的代码,虽然只是启动代码。 至此,已经把第一批代码bootsect从软盘载入计算机的内存了。下面的工作就是执行bootsect把软盘的第二批、第三批代码载入内存。

注意:
BIOS程序固化在主机板上的ROM中,是根据具体的主机板而不是根据具体的操作系统设计的。 理论上,计算机可以安装任何适合其安装的操作系统,既可以安装Windows,也可以安装Linux。不难想象每个操作系统的设计者都可以设计出一套自己的操作系统启动方案,而操作系统和BIOS通常是由不同的专业团队设计和开发的,为了能协同工作,必须建立操作系统和BIOS之间的协调机制。 与已有的操作系统建立一一对应的协调机制虽然麻烦,但尚有可能,难点在于与未来的操作系统应该如何建立协调机制。现行的方法是 “两头约定”“定位识别”。 对操作系统(这里指Linux 0.11)而言,“约定”操作系统的设计者必须把最开始执行的程序“定位”在启动扇区(软盘中的0盘面0磁道1扇区),其余的程序可以依照操作系统的设计顺序加载在后续的扇区中。 对BIOS而言,“约定”接到启动操作系统的命令,“定位识别”只从启动扇区把代码加载到0x07C00 (BOOTSEG)这个位置(参见Seabios 0.6.0/Boot.c文件中的boot_disk函数)。至于这个扇区中是否是启动程序、是什么操作系统,则不闻不问、一视同仁。如果不是启动代码,只会提示错误,其余是用户的责任,与BIOS无关。 这样构建协调机制的好处是站在整个体系的高度,统一设计、统一安排,简单、有效。只要BIOS和操作系统的生产厂商开发的所有系统版本全部遵循此机制的约定,就可以各自灵活地设计出具有自己特色的系统版本。
1.2.3 加载第二部分内核代码——setup
  1. bootsect对内存规划 bootsect第一个任务就是对内存进行规划,包括要加载的setup程序的扇区数(SETUPLEN)以及被加载到的位置(SETUPSEG);启动扇区被BIOS加载的位置(BOOTSEG)及将要移动到的新位置(INITSEG);内核(kernel)被加载的位置(SYSSEG)、内核的末尾位置(ENGSEG)及根文件系统设备号(ROOT_DEV)。设置这些位置就是为了确保将要载入内存的代码与已经载入内存代码及数据各在其位置。

  1. 复制bootsect

bootsect启动程序将它自身(全部512B内容)从内存0x07C00处复制至内存0x90000处。

由于“两头约定”和“定位识别”,所以在开始时bootsec“t被迫”加载到0x07C00位置。现在将自身移至0x90000处,说明操作系统开始根据自己的需要安排内存了

从现在起,操作系统已经不需要完全依赖BIOS,可以按照自己的意志把自己的代码安排在内存中自己想要的位置。bootsect的第一步操作,即规划内存并把自身从0x07C00的位置复制到0x90000的位置的动作已经完成了。

DS/ES/FS/GS/SS:这些段寄存器存在于CPU中,其中SS(Stack Segment)指向栈段,此区域将按栈机制进行管理。 SP(Stack Pointer):栈顶指针寄存器,指向栈段的当前栈顶。 注意:很多计算机书上使用“堆栈”这个词。本书用堆、栈表示两个概念。栈表示stack,特指在C语言程序的运行时结构中,以“后进先出”机制运作的内存空间;堆表示heap,特指用C语言库函数malloc创建、free释放的动态内存空间。
  1. 将setup程序加载到内存中

bootsect程序执行它的第二步工作:将setup程序加载到内存中

这个中断服务程序的执行过程与前面的int 0x19中断向量所指向的启动加载服务程序不同。

  • int 0x19中断向量所指向的启动加载服务程序是BIOS执行的,而int 0x13的中断服务程序是Linux操作系统自身的启动代码bootsect执行的。

  • int 0x19的中断服务程序只负责把软盘的第一扇区的代码加 载到0x07C00位置,而int 0x13的中断服务程序则不然,它可以根据设计者的意图,把指定扇区的代码加载到内存的指定位置。针对服务程序的这个特性,使用int 0x13中断时,就要事先将指定的扇区、加载的内存位置等信息传递给服务程序

现在,操作系统已经从软盘中加载了5个扇区的代码。等bootsect执行完毕后,setup这个程序就要开始工作了

1.2.4 加载第三部分内核代码——system模块

接下来,bootsect程序要执行第三批程序的载入工作,即将系统模块载入内存。这次载入从底层技术上看,与前面的setup程序的载入没有本质的区别。比较突出的特点是这次加载的扇区数是240个,足足是之前的4个扇区的60倍,所需时间也是几十倍。到此为止,第三批程序已经加载完毕,整个操作系统的代码已全部加载至内存。bootsect的主体工作已经做完了,还有一点小事,就是要再次确定一下根设备。

根文件系统设备(Root Device):Linux 0.11使用Minix操作系统的文件系统管理方式,要求系统必须存在一个根文件系统,其他文件系统挂接其上,而不是同等地位。Linux 0.11没有提供在设备上建立文件系统的工具,故必须在一个正在运行的系统上利用工具(类似FDISK和Format)做出一个文件系统并加载至本机。因此Linux 0.11的启动需要两部分数据,即系统内核镜像和根文件系统。注意:这里的文件系统指的不是操作系统内核中的文件系统代码,而是有配套的文件系统格式的设备,如一张格式化好的软盘。

因为本书假设所用的计算机安装了一个软盘驱动器、一个硬盘驱动器,在内存中开辟了2 MB的空间作为虚拟盘(见第2章的main函数),并在BIOS中设置软盘驱动器为启动盘,所以,经过一系列检测,确认计算机中实际安装的软盘驱动器为根设备,并将信息写入机器系统数据。第2章中main函数一开始就用机器系统数据中的这个信息设置根设备,并为“根文件系统加载”奠定基础。

setup程序现在开始执行。它做的第一件事情就是利用BIOS提供的中断服务程序从设备上提取内核运行所需的机器系统数据,其中包括光标位置、显示页面等数据,并分别从中断向量0x41和0x46向量值所指的内存地址处获取硬盘参数表1、硬盘参数表2,把它们存放在0x9000:0x0080和0x9000:0x0090处。

到此为止,操作系统内核程序的加载工作已经完成。接下来的操作对Linux 0.11而言具有战略意义。系统通过已经加载到内存中的代码,将实现从实模式到保护模式的转变,使Linux 0.11真正成为“现代”操作系统。

1.3 开始向32位模式转变,为main函数的调用做准备

1.3.1 整体概述

接下来,操作系统要使计算机在32位保护模式下工作。这期间要做大量的重建工作,并且持续工作到操作系统的main函数的执行过程中。在本节中,操作系统执行的操作包括打开32位的寻址空间、打开保护模式、建立保护模式下的中断响应机制等与保护模式配套的相关工作、建立内存的分页机制,最后做好调用main函数的准备。

1.3.2 关中断并将system移动到内存地址起始位置0x00000

这个准备工作先要关闭中断,即将CPU的标志寄存器(EFLAGS)中的中断允许标志(IF)置0。这意味着,程序在接下来的执行过程中,无论是否发生中断,系统都不再对此中断进行响应,直到下一章要讲解的main函数中能够适应保护模式的中断服务体系被重建完毕才会打开中断,而那时候响应中断的服务程序将不再是BIOS提供的中断服务程序,取而代之的是由系统自身提供的中断服务程序。

setup程序做了一个影响深远的动作:将位于0x10000的内核程序复制至内存地址起始位置0x00000处!

废除BIOS的中断向量表,等同于废除了BIOS提供的实模式下的中断服务程序。
收回刚刚结束使用寿命的程序所占内存空间。
让内核代码占据内存物理地址最开始的、天然的、有利的位置。
1.3.3 设置中断描述符表和全局描述符表

setup程序继续为保护模式做准备。此时要通过setup程序自身提供的数据信息对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。

32位的中断机制和16位的中断机制,在原理上有比较大的差别。最明显的是16位的中断机制用的是中断向量表,中断向量表的起始位置在0x00000处,这个位置是固定的;32位的中断机制用的是中断描述符表(IDT),位置是不固定的,可以由操作系统的设计者根据设计要求灵活安排,由IDTR来锁定其位置。GDT是保护模式下管理段描述符的数据结构,对操作系统自身的运行以及管理、调度进程有重大意义,后面的章节会有详细讲解。因为,此时此刻内核尚未真正运行起来,还没有进程,所以现在创建的GDT第一项为空,第二项为内核代码段描述符,第三项为内核数据段描述符,其余项皆为空。IDT虽然已经设置,实为一张空表,原因是目前已关中断,无需调用中断服务程序。此处反映的是数据“够用即得”的思想。创建这两个表的过程可理解为是分两步进行的:1)在设计内核代码时,已经将两个表写好,并且把需要的数据也写好。2)将专用寄存器(IDTR、GDTR)指向表。此处的数据区域是在内核源代码中设定、编译并直接加载至内存形成的一块数据区域。专用寄存器的指向由程序中的lidt和lgdt指令完成,具体操作见图1-18。值得一提的是,在内存中做出数据的方法有两种:1)划分一块内存区域并初始化数据,“看住”这块内存区域,使之能被找到;2)由代码做出数据,如用push代码压栈,“做出”数据。此处采用的是第一种方法。
1.3.4 打开A20,实现32位寻址

打开A20,意味着CPU可以进行32位寻址,最大寻址空间为4 GB。内存条范围的变化:从5个F扩展到8个F,即0xFFFFFFFF——4 GB。

实模式下CPU寻址范围为0~0xFFFFF,共1 MB寻址空间,需要0~19号共20根地址线。进入保护模式后,将使用32位寻址模式,即采用32根地址线进行寻址,第21根(A20)至第32根地址线的选通控制将意味着寻址模式的切换。实模式下,当程序寻址超过0xFFFFF时,CPU将“回滚”至内存地址起始处寻址(注意,在只有20根地址线的条件下,0xFFFFF + 1 = 0x00000,最高位溢出)。例如,系统的段寄存器(如CS)的最大允许地址为0xFFFF,指令指针(IP)的最大允许段内偏移也为0xFFFF,两者确定的最大绝对地址为0x10FFEF,这将意味着程序中可产生的实模式下的寻址范围比1 MB多出将近64 KB(一些特殊寻址要求的程序就利用了这个特点)。这样,此处对A20地址线的启用相当于关闭CPU在实模式下寻址的“回滚”机制。在后续代码中也将看到利用此特点来验证A20地址线是否确实已经打开。
1.3.5 为保护模式下执行head.s做准备

CPU在保护模式下,int 0x00~int 0x1F被Intel保留作为内部(不可屏蔽)中断和异常中断。如果不对8259A进行重新编程,int 0x00~int 0x1F中断将被覆盖。例如,IRQ0(时钟中断)为8号(int 0x08)中断,但在保护模式下此中断号是Intel保留的“Double Fault”(双重故障)。因此,必须通过8259A编程将原来的IRQ0x00~IRQ0x0F对应的中断号重新分布,即在保护模式下,IRQ0x00~IRQ0x0F的中断号是int 0x20~int 0x2F。

CPU工作方式转变为保护模式,一个重要的特征就是要根据GDT决定后续执行哪里的程序

GDT(Global Descriptor Table,全局描述符表),在系统中唯一的存放段寄存器内容(段描述符)的数组,配合程序进行保护模式下的段寻址。它在操作系统的进程切换中具有重要意义,可理解为所有进程的总目录表,其中存放每一个任务(task)局部描述符表(LDT,Local Descriptor Table)地址和任务状态段(TSS,Task Structure Segment)地址,完成进程中各段的寻址、现场保护与现场恢复。
GDTR(Global Descriptor Table Register,GDT基地址寄存器),GDT可以存放在内存的任何位置。当程序通过段寄存器引用一个段描述符时,需要取得GDT的入口,GDTR标识的即为此入口。在操作系统对GDT的初始化完成后,可以用LGDT(Load GDT)指令将GDT基地址加载至GDTR。
IDT(Interrupt Descriptor Table,中断描述符表),保存保护模式下所有中断服务程序的入口地址,类似于实模式下的中断向量表。
IDTR(Interrupt Descriptor Table Register,IDT基地址寄存器),保存IDT的起始地址。

到这里为止,setup就执行完毕了,它为系统能够在保护模式下运行做了一系列的准备工作。但这些准备工作还不够,后续的准备工作将由head程序来完成。

1.3.5 head.s开始执行

在讲解head程序之前,我们先介绍一下从bootsect到main函数执行的整体技术策略。

在执行main函数之前,先要执行三个由汇编代码生成的程序,即bootsect、setup和head。之后,才执行由main函数开始的用C语言编写的操作系统内核程序。前面我们讲过,第一步,加载bootsect到0x07C00,然后复制到0x90000;第二步,加载setup到0x90200。值得注意的是,这两段程序是分别加载、分别执行的。 head程序与它们的加载方式有所不同。大致的过程是,先将head.s汇编成目标代码,将用C语言编写的内核程序编译成目标代码,然后链接成system模块。也就是说,system模块里面既有内核程序,又有head程序。两者是紧挨着的。要点是,head程序在前,内核程序在后,所以head程序名字为“head”。head程序在内存中占有25 KB + 184 B的空间。前面讲解过,system模块加载到内存后,setup将system模块复制到0x00000位置,由于head程序在system的前面,所以实际上,head程序就在0x00000这个位置

head程序除了做一些调用main的准备工作之外,还做了一件对内核程序在内存中的布局及内核程序的正常运行有重大意义的事,就是用程序自身的代码在程序自身所在的内存空间创建了内核分页机制,即在0x000000的位置创建了页目录表、页表、缓冲区、GDT、IDT,并将head程序已经执行过的代码所占内存空间覆盖。这意味着head程序自己将自己废弃,main函数即将开始执行。

标号_pg_dir标识内核分页机制完成后的内核起始位置,也就是物理内存的起始位置0x000000。head程序马上就要在此处建立页目录表,为分页机制做准备。这一点非常重要,是内核能够掌控用户进程的基础之一。

现在head程序正式开始执行,一切都是为适应保护模式做准备。其本质就是让CS的用法从实模式转变到保护模式。在实模式下,CS本身就是代码段基址。在保护模式下,CS本身不是代码段基址,而是代码段选择符。

1.4 总结

本章的内容主要分为两大部分。第一部分为加载操作系统;第二部分为32位保护、分页模式下的main函数的执行做准备。 从借助BIOS将bootsect.s文件加载到内存开始,相继加载了setup.s文件和system文件,从而完成操作系统程序的加载。 接下来设置IDT、GDT、页目录表、页表以及机器系统数据,为32位保护、分页模式下的main函数的执行做准备。 一切就绪后,跳转到main函数执行入口,开始执行main函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值