【Linux内核】---- 01 开机上电初始化过程
发布日期:2021-06-29 14:51:05 浏览次数:3 分类:技术文章

本文共 9036 字,大约阅读时间需要 30 分钟。

【Linux内核】---- 01 开机上电初始化过程

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

  • 第一步,启动BIOS,准备实模式下的中断向量表和中断服务程序
  • 第二步,从启动盘加载操作系经程序到内存,加载操作系统程序的工作就是利用第一步中准备的中断服务程序实现的
  • 第三步,为执行32位的main函数做过渡工作,初始化一系列的运行环境。

实模式的特性是一个20位的存储器地址空间(2^20 = 1048576,即IBM的存储器可被寻址),可以直接通过软件的方式访问BIOS以及周边硬件,没有硬件支持的分页机制和实时多任务的概念。

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

1.1.1 BIOS的启动原理

计算机的运行是离不开程序的,在加电的一瞬间,计算机的内存(RAM)中是空的。此时如果要运行软盘、硬盘中的操作系统,必须将软盘、硬盘中的操作系统程序加载到内存(RAM)中。

加载操作系统到内存的工作是由BIOS来实现的。

从硬件角度看,Intel 80x86系列CPU分别可以在16位实模式和32位保护模式下运行,Intel 设计为加电即进入16位实模式状态运行,同时在加电瞬间强行将段寄存器CS的值置为0xFFFF,IP的值置为0x0000,这样CS:IP就指向0xFFFF0这个地址位置,0xFFFF0指向了BIOS的地址范围

CS是代码段寄存器,IP是指令寄存器,记录将要执行的指令在代码段内的偏移地址。

16位实模式下为绝对地址,指令为16位,即IP ; 32位 保护模式下为线性地址,指令指针为32位,即EIP.

BIOS 程序的入口地址就是 0XFFFF0,在上电后,BIOS程序第一条指令就是从0xFFFF0 开始执行的(注:如果此处没有可执行的代码,则计算机会直接死机,即便可能其他地址存在可执行代码也跑不到。)

1.1.2 BIOS在内存中加载中断向量表和中断服务程序

BIOS程序是固化在计算机主板上的一块很小的ROM芯片中,不同的主机板所用的BIOS也有所不同。

前面我们讲了,上电瞬间,CPU强行重置CS:IP 为0xFFFF0,意味着BIOS程序开始执行。
接着BIOS 会做一些硬件上电自检,在屏幕上会显示显卡、内存的信息,
还有一项重要的工作是,

  • BIOS 在内存中建立中断向量表和中断服务程序

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

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

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

前面在初始化了中断向量表及对应的服务程序后,就要真正开始把软盘、硬盘中的操作系统程序加载到内存中。

对于Linux 0.11 操作系统而言,计算机将分三批逐次加载操作系统的代码。

  • 第一批 由BIOS中断 INT 0x19 把扇区bootsect的内容加载到内存;
  • 第二批 和 第三批 在bootsect 的指挥下,分别把其后的四个扇区和随后的240 个扇区的内容加载到内存。

1.2 第一部分,加载引导程序 bootsect

在计算机完成自检及配置好中断向量表后,会让CPU接收到一个 int 0x19 中断,CPU接收到这个中断后,会立即在中断向量表中找到 int 0x19 中断向量。

接下来,0x19 中断向量把CPU 指向其入口地址0x0E6F2,该中断服务程序的作用就是把软盘的第一个扇区中的程序(512B)加载到内存指定的位置,其功能是BIOS事先设计好的,代码是固定的,与Linux操作系统无关。

按照其规则,int 0x19 中断向量所指向的中断程序(即启动加载服务)将软驱0号磁头对应的盘面的0磁道1扇区的内容拷贝到内存0x07C00处

在这个扇区中的内容就是Linux 0.11 操作系统的引导程序,也就是bootsect ,其作用是陆续把软盘中的操作系统程序载入内存

至此, bootsect 已经加载入内存。

  • tips:
    BIOS 程序固化在主机板上的ROM中,是根据具体主板而不是根据具体的操作系统设计的,计算机可以安装任何操作系统。
    对操作系统而言,“约定”操作系统的设计者必须把最开始执行的程序“定位”在启动扇区(软盘中的0盘面0磁道1扇区),其余的程序可以依照操作系统的设计顺序加载在后续的扇区中。

1.3 第二部分,setup

1.3.1 bootsect 对内存的规划

现在BIOS 已经把bootsect (引导程序)载入内存了,它的作用就是把第二批和第三批程序陆续加载到内存

为了把程序加载到适当位置,bootsect 首先要规划内存。

在实模式下,寻址的最大范围是1MB,bootsect 规划内存代码如下:

// 代码路径:  /boot/bootsect.sSETUPLEN 	= 4			: nr of setup-sectors				// 要加载的setup 程序的扇区数BOOTSEG		= 0x07c0	: original address of boot-sector	// 启动扇区被BIOS加载的位置INITSEG		= 0x9000	: we move boot here-out of the way 	// 将要移动到的新位置SETUPSEG 	= 0x9020	: setup starts here					// 被加载到的内存位置SYSSEG		= 0x1000	: system loaded at 0x10000 (65536)	// 内核(Kernel)被加载的位置ENDSEG		= SYSSEG + SYSSIZE	: where to stop loading		// 内核(Kernel)末尾位置

设置这些位置就是为确保将要载入的内存的代码及数据各在其位,互不覆盖。

在这里插入图片描述

1.3.2 第一步 复制bootsect

接下来,bootsect 启动程序将它自身(全部的512B 内容) 从内存 0x07C00( BOOTSEG)处复制到 0x90000(INITSEG )处

在这里插入图片描述
代码如下:

// 代码路径:  boot/bootsect.smov ax , #BOOTSEG			// ax = 0x07C0mov ds , ax					// ds = ax = 0x07C0  源地址mov ax , #INITSEG			// ax = 0x9000mov es , ax					// es = ax = 0x9000  目的地址mov cx , #256				// cx = 256			 大小256 次sub si , si					sub di , direpmovw						// 一次移动一个 word = 二个 Byte , 256 个word = 512 Bytejmpi go, INITSEG			// CPU 跳转到 0x9000go: mov ax, cs	mov ds, ax	mov es, ax! put statk at 0x9ff00	mov ss ,ax	mov sp , #0xFF00		// arbitrary value >> 512! load the setup-sectors directly after the bootblock! Note that "es" is already set up前段代码用 CS 的0x9000 来把数据段寄存器(DS),附加寄存器(ES),栈寄存器(SS) 设置成与代码段寄存器CS 相同的位置,并将栈顶指针sp 指向偏移地址为0xFF00 处。

在jump 到0x07C00 后,操作系统就 不需要完全依赖BIOS ,可以按照自已的意志分配内存了。

这进而对SS 和 SP 进行设置,说明从现在开始,程序可以执行更加复杂的一些数据运算指令了。

DS/ES/FS/GS/SS: 数据段寄存器,存在于CPU中,其中 SS(Stack Segment)指向栈段,此区域将按栈机制进行管理。

SP (Stack Pointer): 栈顶指针寄存器,指向栈段的当前栈顶。

栈表示 Stack , 特指在C语言程序的运行时结构中,以“后进先出”机制运作的内存空间;

堆表示 Heap,特指用C语言库函数malloc 创建,free 释放的动态空间。

至此,bootsect 第一步操作已经完成了:规划内存并把自身从0x07C00的位置 复制到0x90000的位置。

1.3.3 第二步 将Setup 程序加载到内存中

加载Setup 程序,要借助BIOS提供的 int 0x13 中断向量所指向的中断服务程序。

int 0x13 中断可以将指定扇区的代码加载到内存的指定位置。 因此,我们需要事先将指定的扇区信息 和 加载的内存位置等信息传递给服务程序。
代码如下:

// 代码路径: /boot/bootsect.sload_setup:// 配置要传递的参数	mov dx , #0x0000		! drive 0 ,head 0	mov cx , #0x0002		! sector 2 , track 0	mov bx , #0x0200		! address = 512, in INITSEG	mov ax , #0x0200 + SETUPLEN  ! service 2, nr of sectors// 触发一个 0x13 的中断	int 0x13				! read it 	jnc ok_load_setup		! ok-continue	mov dx , #0x0000	mov ax , #0x0000		! reset the diskette	int 0x13	j load_setupok_load_setup:

能过上面的代码,将setup.s 对应的程序复制到 0x90000 的位置, 占用512 字节,一直到 0x90200 。可以看出,0x90200 紧挨着bootsect 的尾端。 所以 bootsect 和 setup 是连接在一起的。

在这里插入图片描述

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

此时 SS:SP 指向的位置 为 0x9FF00.

1.4 第三部分,system 模块

现在要加载第三批代码,仍然使用 BIOS提供的Int 0x13中断,将系统模块载入内存。

此次加载的扇区数为240个,是之前4个扇区的60 倍,且所需要的时间也是之前的几十倍,为了防止加载期间用户误认为是机器故障而执行不适当的操作,Linux 在此设计显示一行屏幕信息:“Loading system …” 以提示用户计算机正在加载系统。

需注意的是,此时操作系统main 函数还没有开始执行,在屏幕上显示一行字远没有用C语言代码写一句printf(“Loading System …”)调用那么容易,所有的工作需要用汇编代码实现。

总结第三部分工作如下:

bootsect 借着 BIOS 中断int 0x13 ,将240 个扇区的system 模块加载进内存,加载工作主要是由bootsect 调用read_int 子程序完成的,这个子程序将软盘第6 扇区开始的约240 个扇区的system 模块加载至内存的SYSSET(0x10000) 处往后的120KB 空间中。

在这里插入图片描述

到此为止,第三批程序已经加载完成,整个操作系统的代码已全部载入内存。bootsect 的主要工作已经全部做完了。

接下来确定一下根设备号,经过一系列的检,得知软盘为根设备,所以就 把根设备号保存在root_dev 中,

这个 根设备号作为机器系统数据之一。

在这里插入图片描述

至此,bootsect 程序所有的任务已经完成,下面要通过 执行 “jmpi 0, SETUPSEG” 这行语句跳转至 0x90200 处,就是前面讲过的第二批程序 — setup 程序加载的位置。

CS:IP 指向 setup 程序的第一条指令,意味着由setup 程序序接着 bootsect 程序继续执行。

此时,Setup 程序正式开始执行,其做的第一件事就是利用 BIOS 提供的中断服务程序从设备上提取内核运行所需的机器系统数据。

其中包括光标位置和显示页面等数据,并分别从中断向量0x41 和 0x46 向量所指向的内存地址处获取硬盘参数1 和硬盘参数表2,并把它们分别存放在0x9000:0x0080 和 0x9000: 0x0090 处。

这些机器数据被加载到内存的0x9000 ~ 0x901FC 位置,这些数据在后续 main 函数的执行时发挥重要作用

机器系统数据所占的内存空间为0x90000 ~ 0x901FD ,共510 个字节,即原来的bootsect 只有两个字节未被覆盖。

到此为止,操作系统的内核程序加载工作已完成。

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

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

在这里插入图片描述

将 CPU 的标志寄器(EFLAGS)中的中断允许标志(IF)置0 可以关闭中断。
这样,系统不会再响应此中断,直到下次main 函数中能够适应保护模式的中断服备体系被重建完毕后才会打开中断,而那时响应中断的服务程序将不再是BIOS提供的中断服务程序,取而代之的是系统自身提供的中断服务程序。

代码为 boot/setup.s —> 关中断指令(cli) ,开中断指令(sti)

关中断后,setup 程序将位于 0x10000 的内核程序拷贝到内存地址起始位置0x00000处,代码如下:

// boot/setup.sdo_move:	mov es, ax			! 段寄存器	add ax, #0x1000	cmp ax, #0x9000	jz end_move	mov ds, ax			! 代码段寄存器	sub di, di	sub si, si 	mov cx, #0x8000	rep	movsw	jmp do_move

在这里插入图片描述

在0x00000 位置原本存放由BIOS 建 立的中断向量表及BIOS 数据区,因此,复制后会将BIOS的这块内存完全覆盖掉。
此后,直到新的中断服务体务构建完毕之前,操作系统不会再响应并处理中断的能力。

这样做的好处在于:

  1. 废除BIOS的中断向量表,等价于废除了BIOS提供的实模式下的中断服务程序。
  2. 收回使用寿命刚结束的程序所占的内存空间。
  3. 让内核代码占据内存特理地址最开始的,最天然的,最有利的位置。

1.5.2 设置中断描述符表和全局描述符表

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

  • 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的起始地址。

  • 32位的中断机制和16位中断机制在原理上有比较大的差别
    最明显的是16位的中断机制用的是中断向量表,其起始信置在 0x00000处,这个位置是固定的
    32位的中断机制用的是中断描述符表 IDT, 位置是不固定的,可以由操作系统的设计者根据设计要求灵活安排,由IDTR 寄存器来锁定其位置 。

1.5.3 打开A20,实现32位寻址

打开A20后,意味着CPU可以进行32位寻址,最大寻址空间为4GB.

代码如下:

// boot/setup.scall empty_8042mov al , #0xD1			! command writeout #0x64 , al			call empty_8042mov al , #0xDF			! A20 onout #0x60, alcall empty_8042

在这里插入图片描述

实模式下CPU的寻址范围为 0 ~ 0xFFFFF,共 1MB 寻址空间,需要0 ~ 19 号共20 根地址线。

进入保护模式后,将使用32位寻址模式,即采用32 根地址线进行寻址,第21 ~ 32 根地址线的选通,将意味着寻址模式的切换。

1.5.4 为在保护模式下执行head.s 做准备

为了建立保护模式下的中断机制,setup 程序将对可编程中断控制器8259A 进行重新编程。

由两个8529A中断控制器级联可组成64级的向量优先级中断系统。

在保护模式下,int 0x00 ~ int 0x1F 被 Intel 保留作为内部(不可屏蔽)中断和异常中断。

如果不对8259A 进行重新编程,int 0x00 ~ int 0x1F 中断将被覆盖。

例如,IRQ1 (时钟中断) 为8号(int 0x08)中断,但在保护模式下此中断号是Intel 保留的“Double Fault (双重故障)”,因此必须通过对8259A 编程将原来的IRQ0x00 ~ IRQ0x0F 对应的中断号重新分布,即在保护模式下,IRQ 0x00~IRQ 0x0F 的中断号是 int 0x20 ~ 0x2F.

setup 程序通过将 CR0 寄存器的第0位置1, 即设定CPU处理器的工作方式为保护模式。

1.5.5 head.s 开始执行

在执行main 函数之前,先要执行三个由汇编代码生成的程序,即 bootsect、setup和head。 之后才执行由main函数开始的用C语言编写的操作系统内核程序。

前面我们讲过,

第一步:加载 bootsect 到 0x07C00,然后复制到 0x90000;
第二步:加载setup 到0x90200。

head.s 程序除了做一些调用main的准备工作之外,还做了一件对内核程序在内存中的布局及内核程序的正常运行有重大意义换事情,

用程序自身的代码在程序自身所在的内存空间创建了内核分页机制,即在 0x00000 的位置创建了页目录表,页表,缓冲区,GDT,IDT,并将head 程序已经执行的代码所占的内存空间覆盖,这意味着Head 程序自已将自已废弃,main 函数即开始执行

现在head 程序正式开始,一切就是为适应保护模式做准备。

其本质就是让CS的用法从实模式转变到保护模式。
在实模式下,CS 本身就是代码段基地址,在保护模式下,CS 本身并不是代码段基地址,而是代码 段选择符。

现在,head程序废除已有的GDT,并在内核中的新位置重新创建 全局描述符表。

其中第二项和第三 项分别为内核代码段描述符和内核数据段描述符,其段限长均被设置为16MB,并设置全局描述符表寄存器。

  • 为什么要废除原来的GDT而重新设置一套GDT呢?
    原来GDT所在的位置是设计代码在 setup.s 里面设置的,将来这个setup 模块所在的内存位置 会设计缓 冲区时被覆盖。
    如果不改变位置,GDT的内容将来肯定会被 缓冲区覆盖掉,从而影响系统的运行,这样一来,将来整个唯一安全的地方就是现在head.s所在的位置了。
    那么有没有可能在执行setup程序时直接把GDT的内容 拷贝到head.s 所在的位置呢?,肯定不能,如果先复制GDT的内容,后 移动system模块,它就会被后者覆盖掉;而如果先移动system模块,后复制GDT的内容,它又会把head.s 对应的程序覆盖掉,而这时head.s 还没有执行,所以无论如何,都要重新建立GDT。

head 程序执行最后一步,ret,跳入main函数程序执行

1.5.6 main函数的调用

用ret实现的调用操作系统的main函数,因为是ret调用的,所以就 不需要再调用ret返回了。

操作系统的设计者做了一个仿CALL的动作,手工编写压栈和跳转代码,模仿了CALL的全部动作,实现调用setup_paging函数.
注意,压栈的EIP值并不是调用setup_paging函数的下一行指令的地址,而是操作系统的main函数入口地址 _main。
这样,当setup_paging函数的执行到ret 时,从栈中将操作 系统的main函数的执行入口地址 _main自动出栈给EIP,
EIP 指向main 函数的入口地址,实现了用返回指令“调用”main 函数。

此时将压入的main函数的执行入口地址弹出给CS:EIP,这句话等价于开始执行main 函数程序。

学自《Linux内核设计的艺术》

转载地址:https://ciellee.blog.csdn.net/article/details/104615782 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【Linux内核】---- 02 从main到怠速
下一篇:20200208 新的一年,学习大方向

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年05月02日 10时15分48秒