您的当前位置:首页正文

从开机到分段分页都做了什么?

2024-12-01 来源:个人技术集锦

引言

当我们要使用一个操作系统的时候,我们要做的第一步就是打开电源.

当我们打开电源之后第一个运行的软件就是BIOS,于是会产生以下三个问题.

BIOS (基本输入输出系统)

起始结束大小用途
FFFF0FFFFF16BBIOS 入口地址,此地址属于BIOS 代码,当操作系统刚开始加载时, CPU 默认 CS:IP 值为ffff:0000,通过此部分是16字节的跳转指令, jmp f000:e05b 跳转到入口
F0000FFFEF64KB-16B系统BIOS范围是F0000~FFFFF共64KB,最上面16字节为入口地址
C8000EFFFF160KB映射硬件适配器的ROM或内存映射式I/O
C0000C7FFF32KB显示适配器BIOS
B8000BFFFF32KB用于文本模式显示适配器
B0000B7FFF32KB用于黑白显示适配器
A0000AFFFF64KB用于彩色显示适配器
9FC009FFFF1KB扩展BIOS数据区
7E009FBFF622080B可用区域
7C007DFF512BMBR被BIOS加载到此处,共512字节
5007BFF30464B可用区域
4004FF256BBIOS数据区
0003FF1KBInterrupt Vector Table(中断向量表)

顶部的 0xF0000-0xFFFFF 这 64KB 是内存是 ROM(只读存储器) ,这里面存放的是 BIOS 的代码, BIOS 主要工作是检测和初始化硬件,怎么初始化的?硬件自己提供了一些初始化的功能调用, BIOS 直接调用就好.其次 BIOS 还建立的中断向量表,这样就可以通过 “int 中断号” 来实现相关的硬件调用了,当然 BIOS 建立的这些功能就是对硬件的 IO 操作,也就是输入输出,但由于就 64KB 大小的空间,不可能实现所有硬件的 IO 操作,所以只实现了一些能保证计算机能运行的那些硬件的基本IO操作,这就是 BIOS 称为基本输入输出系统的原因.

如何进入 BIOS ?

因为 BIOS 在 ROM 中,所以不能更改(也没有更改的必要),因此我们只需要知道他做了什么事情就可以了,在上述检测进行完之后,最后一项工作是校验启动盘中位于 0 盘 0 道 1 扇区的内容(即 MBR ),当检测无误之后, BIOS 会通过 jmp 0:0x7c00 跳转到 mbr 中.

MBR (主引导记录)

简单介绍

  1. MBR 只能是 512 字节,而且最后两字节为0x55,0xaa(魔数).

  2. 我们需要在启动操作系统前,将硬盘的 0 盘 0 道 1 扇区填充为 MBR 程序的内容(通过 dd 命令),然后给计算机配置此硬盘为启动盘,这样计算机启动时,就能够自动从 BIOS 到 MBR 了.

MBR 主要做了什么?

通过 ROM 中默认的 BIOS 程序,我们成功进入了 MBR 程序,在 MBR 中我们主要干了下面几件事情.

  1. 从磁盘的第二扇区读取 loader (内核加载器),当然,内核加载器的位置可以随意放在磁盘的不同位置,我选择放在第二扇区,将读取的内容存到 0x900 这个内存地址中,之后当 mbr jmp 到了 0x900 是,就会执行 0x900 这块地址的指令,也就是 loader 中的内容.
  2. 因为按照规定 MBR 的大小必须是 512 字节,而且最后两个字节必须是魔数0x55,0xaa,因此在代码的最后如果不足 512 字节,还需要用类似与 times 510-( − - $) db 0 这样的指令凑满 512 字节.

loader 主要做了什么?

当 mbr 跳转到 loader 之后,就到来 loader 大显身手的时候了,在 loader 程序中我们主要做这几件事情.

  1. 我们通过上文提到的 BIOS 中断获取整个计算机中的物理内存
  2. 跳转进入保护模式,因此需要在 loader.S 中定义代码段,数据段,显示段,还要定义页表(我采用二级页表的形式).
  3. 加载内核

这里牵扯到了新的概念,保护模式是什么,为什么要有保护模式?

为什么要有保护模式?

保护模式有什么特点?

  1. 保护模式赋予不同的进程不同的特权等级,操作系统为最高的 0 级,用户进程为 3 级,将用户资源和操作系统资源隔离,更加安全.
  2. CPU 和操作系统通过分段机制,根据段描述符(8字节,是描述段的结构,信息包括段基质,段界限,段类型,段是否可读,段的方向(由低到高还是由高到低)等等)
  3. 在 CPU 发展到 32 位后,地址总线和数据总线扩展到来 32 位,通用寄存器的大小也扩展为 32 位,这样能访问的内存空间编程了 4G ,可以不需要段基址了,不过兼容性依旧保存了段基址+偏移地址的访问方式来访问最终的物理地址,这也就是传说中的平坦模式.这里有个概念需要明确一下,是什么模式以处理器是多少位并没有关系,即使是 32 位系统,在刚开机时都是实模式,只有在经过 loader 的一系列操作之后,才会变为保护模式.

分段机制概述

上文提到了段描述符,一个段描述符保存一个段的信息,有一个专门的数据结构保存着多个段描述符,称为描述符表, 80386/80486 CPU 共有3 种描述符表:全局描述符表( GDT ),局部描述符表( LDT )和中断描述符表( IDT ).描述符表由描述符顺序排列组成,占一定的内存.

段描述符是一个 8 字节 64 位的结构.

  • 低 32 位的 16-31 位和高 32 位的 0-7 位及 24-31 位共同描述段基址的 32 位,因为要兼容之前的处理器,因此,当要把段基址扩展到 32 位,把段界限扩展为 20 位时,只能继续往后面添加,所以段界限和段基址会分散在不同的地方.

  • 当段界限和段基址都被拆分之后,如果每次访问都需要去拼接段界限和段基址未免太过于繁琐,而且还要多余访问一次内存,性能也会受到影响,因此,当今的 CPU 会将段信息缓存到段描述符缓存寄存器中,赐婚村寄存器中保存的内容是段描述符的内容,它是经过 CPU 整理后的,段界限和段基址已经被拼合到一起,CPU 下次会直接从段描述符缓存寄存器中取段数据.

  • S 代表一个段是系统段还是数据段,在 CPU 眼里,凡是硬件使用到的东西称为系统,凡是软件使用到的东西称为数据,所以代码段,数据段,栈段等也属于 S 中所代表的的数据段.

  • Type 指定段的类型,一共四位.只有S决定了,Type才有它的意义.下图是Type在系统段和数据段里不同的意义.

    1. 表中的 A 为代表的是 Accessed 位,这是由 CPU 来设置的,每当该段被 CPU 访问之后,CPU 就将此位置职位1,创建一个新描述符时,应该将此位置设置为0.

    2. C 表示一致性代码,一致性代码段是指如果自己是转移的目标段,并且自己是一致性代码段.自己的特权级一定要高于当前特权级,转移后的特权级不与自己的 DPL 为主,而是与转移前的低特权级一致,也就是听从,依从转移前的低特权级. C 为 1 时则表示该段是一致性代码段, C 为 0 时则表示该段为非一致性代码段.

    3. R 表示可读,R 为 1 表示可读,为0不可读,这个属性用来限制代码段的访问.

    4. X 表示该段是否可以执行,如果 X 为 1,说明是代码段.可以执行,如果为 0 ,说明是数据段不可以执行.

    5. W 表示是否可写,W 为 1 表示可写,通常用于数据段, W 为 0 表示不可写,通常用于代码段.

  • DPL 字段,表示描述符特权级,这是保护模式提供的安全解决方案,将计算机世界按权利分为不同等级,每一种等级称为一种特权级.

  • P 字段,表示段是否存在在内存中,P 为 1表示段在内存中,P 为 0 表示不在,但是这是在未开启分页时的解决方案,目前的保护模式有分页功能,所以按照也的单位来将内存换入换出.

  • ACL 字段没有什么实际意义.

  • L 字段用于设置目前环境是 32 位还是 64 位,32 位设置为0,64位设置为1.

    1. 对于代码段来说,此位 是 D 位,当 D 为 0,表示指令中的有效地址和操作数是 16 位,指令的有效地址用 IP 寄存器,当 D 为 1, 表示指令中的有效地址和操作数是 32 位,指令有效地址用 EIP 寄存器.
    2. 对于栈段来说,此处 为 B 位,用来指定操作数大小,此操作数涉及到栈指针寄存器的选择及栈的地址上限.若 B 为 0 ,使用的是 sp 寄存器,也就是栈的起始地址是 16 位寄存器的最大寻址范围, 0xFFFF.若 B 为 1,使用的是 esp 寄存器,也就是栈的起始地址是 32 位寄存器的最大寻址范围, 0xFFFFFFFF .
  • G 字段,用于制定段界限的单位大小,配合段界限使用, G 为 0,表示段界限单位为 1 字节,G 为 1 ,表示段界限的单位为 4KB.

全局描述符表 GDT

一个段描述符只用来定义(描述)一个内存段.代码段要占用一个段描述符,数据段和栈段等,多个内存段要各自占一个段描述符,放在全局描述符表中,全局描述符表示共用的,多个程序都可以在这个表定义自己的段描述符.我们进入保护模式的其中一个步骤之一就是加载全局描述符表,让 CPU 知道全局描述符表的位置,在操作内存的时候, CPU 就会根据描述符的信息检查这操作是否有效.

段选择子

A20 地址线

CR0 的 PE 位

进入保护模式的最后一个步骤是,打开 CR0 的 PE 位, CR0 是控制寄存器.控制寄存器是 CPU 的窗口,它既可以展示 CPU 的内部状态,也可以控制 CPU 的运行机制. CR0 的第0位, PE 位,就是保护模式的开关,我们打开 PE位,就是告诉 CPU 接下来我们要进入保护模式.

进入保护模式

由上面可以知道,进入保护模式的步骤如下:

1. 打开 A20 地址线
2. 加载 GDT
3. 将 CR0 的 PE 位置为 1

值得注意的是:jmp dword SELECTOR_CODE:p_mode_start,这个指令是用来刷新流水线的,因为在进入保护模式之前,p_mode_start后面的指令也会被放上流水线,指令会按照16位译码,其实本来应该按照32位译码才能正常执行,所以我们需要清除流水线上的这些指令,保证这些指令按32位译码,这样才能正常地运行下去.

获取物理内存容量

在实现了分段之后,我们紧接着就要实现分页模式和虚拟内存基址了,在这之前,我们得先做一个小任务就是获取内存容量是多少?

在 Linux 中有多种方法获取内存容量,其函数在本质上是通过调用 BIOS 中断 0x15 实现的,分别是 BIOS 中断 0x15 的 3 个子功能,子功能号要存放到寄存器 EAX 或 AX 中,如下。

  • EAX=0xE820:遍历主机上全部内存。
  • AX=0xE801: 分别检测低 15MB 和 16MB~4GB 的内存,最大支持 4GB。
  • AH=0x88:最多检测出 64MB 内存,实际内存超过此容量也按照 64MB 返回。

BIOS 0x15 中断提供了丰富的功能,具体要调用的功能,需要在寄存器 ax 中指定。其中 0xE8xx 系列的子功能较为强大, 0x15 中断的子功能 0xE820 和 0xE801 都可以用来获取内存,区别是 0xE820 返回的是内存布局,信息量相对多一些,操作也相对复杂。而 0xE801 直接返回的是内存容量,操作适中.子功能 0x88 也能获取内存容量,这是最简单的用法,功能也最薄弱。

一般来说就使用 0xE820 子功能就好了.

分页模式

经过了一系列操作,终于实现了分段,操作系统也进入了保护模式,分段模式解决了一些实模式留下的问题,但是分段模式还有一个缺陷没有解决.

  • 由于分段模式是以进程为单位分配内存的,本来剩余的内存空间是足以分配给进程的,但由于这些剩余的内存片并不连续,我们就不能分配这些内存给对应的进程了.

注意:分段是分页的基础,段页式的内存布局映射如下

分页实现

为了节省分页的开销,我们采取的每个内存页的大小为 4KB,这样在 32 位下,内存块的数量就是 1MB 左右,但是单纯使用页大小的为 4KB 的一级页表也有一定的问题,因为一个页表项是 4 字节, 1M 个内存块就是 4MB 的开销,这样算下来,一个进程光页表就要占据 4MB 的开销,而且很多时候根本也用不到这么多的内存映射,所以这是一笔很大的内存开销,因此,我们使用二级页表的形式去实现分页功能,另外,目前 Linux 已经发展到了5级页表.

  • 页表表项结构

  • P :存在位.为 1 表示页表或者页位于内存中.否则,表示不在内存中,必须先予以创建或者从磁盘调入内存后方可使用.
  • R/W :读写标志.为 1 表示页面可以被读写,为 0 表示只读.当处理器运行在 0,1,2 特权级时,此位不起作用.页目录中的这个位对其所映射的所有页面起作用.
  • U/S :用户/超级用户标志.为 1 时,允许所有特权级别的程序访问;为 0 时,仅允许特权级为0 ,1 ,2 的程序访问.页目录中的这个位对其所映射的所有页面起作用.
  • PWT : Page 级的 Write-Through 标志位.为 1 时使用 Write-Through 的Cache类型;为0时使用Write-Back的Cache类型.当 CR0.CD = 1 时( Cache 被 Disable 掉),此标志被忽略.这里我将此位清零.
  • PCD : Page 级的 Cache Disable 标志位.为 1 时,物理页面是不能被 Cache 的;为 0 时允许 Cache .当 CR0.CD = 1 时,此标志被忽略.我将此位清零.
  • A : 访问位.该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位.这个位可以被操作系统用来监视页的使用频率.
  • D : 脏位.该位由处理器固件设置,用来指示此表项所指向的页是否写过数据.
  • PAT :意为页属性表位,能够在页面一级的粒度上设置内存属性.比较复杂,将此位置 0 即可.
  • PS : Page Size 位.为 0 时,页的大小是 4KB ,为 1 时,页的大小是 4MB 或者 2MB
    G:全局位.如果页是全局的,那么它将在高速缓存中一直保存.当CR4.PGE=1 时,可以设置此位为1,指示 Page 是全局 Page ,在 CR3 被更新时, TLB 内的全局 Page 不会被刷新.
    AVL :被处理器忽略,软件可以使用.
  1. 实现分页机制,分段是前提
  2. 需要提前实现好页目录表和页表
  3. 页目录表的基地址写入控制寄存器 CR3
  4. 控制寄存器 CR0 的 PG 位设置为 1 ,表示开启分页基址

二级页表的地址映射

  • 线性地址,分段得到的地址,再到页表中找到对应的页表项,再到物理地址,映射过程(二级分页)
  1. 虚拟地址高10位*4,作为页目录表内的偏移地址,加上目录表的物理地址(CR3寄存器含有页目录表基地址),就能得到页目录的物理地址.读取页目录表的内容,可以得到页表的物理地址
  2. 虚拟地址的中间10位*4,作为页表内的偏移地址,加上步骤1的页表物理地址,将得到页表项的物理地址.读取该页表项的内容,可以得到分配的物理页的地址.
  3. 虚拟地址高10位和中间10位分别是页目录表和页表的索引值,所以需要乘以4.低12位不是索引值,其范围是0-0xfff,作为页内偏移.步骤2的物理地址加上此偏移,得到最终的物理地址.

页表总结:

如果使用二级页表,理论上,每次访问内存需要经过三次访问内存才能访问到真正的物理单元.

TLB

进入分页模式

由上文可得,进入分页模式需要三个步骤

  1. 准备好页目录和页表
  2. 将页目录的地址加载到 CR3 控制寄存器
  3. 将 CR0 控制寄存器的 PG 位打开

下面解释一下各个步骤的意义:

  1. 将第 770-1023 的页目录项设置为第 2-255 个页表的地址,按书上的说法是与之后建立用户进程相关,咱不在这里讨论.

如何从 loader 跳转到内核?

下一步我们要进入内核的编程,之前的 mbr.S 和 loader.S 都是使用的汇编语言,在我们使用汇编语言的时候,我们用的是 nasm 去将汇编语言转换为二进制文件,而现在到了内核,我们需要用 C 语言了,那么 C 语言是如何给变成二进制文件的呢?又是怎么和通过 nasm 汇编成的代码一起运行的呢?因此在我们写内核之前,我们需要一些前置知识.

对于高级语言如何变成二进制文件,我之前写过一篇帖子,,如果你对 C 语言编译成可执行文件的过程很熟悉,就可以不用看了.

  • 符号指的是函数或者变量
  • 可执行文件是由几个目标文件组成, kernel 内核有多个代码文件,生成了 kernel.bin
  • 编排地址就是对程序中的代码安排对应的地址

汇编和链接后的文件是纯二进制文件?

在实现 mbr 和 loader 的时候,我们使用 nasm 汇编器直接生成纯二进制文件,当时的我们是这样调用程序的

所以到了内核这一步,我们需要处理的就不是纯的二进制文件了,而是 ELF 文件.

现在,内核被加载到内存后, loader 还要通过分析其 elf 结构将其展开到新的位置,所以说,内核在内存中有两份拷贝,一份是 elf格式的原文件 kernel.bin,另一份是 loader 解析 elf格式的 kernel.bin 后在内存中生成的内核映像(也就是将程序中的各种段 segment 复制到内存后的程序体),这个映像才是真正运行的内核.

什么是 ELF 文件?

Linux下的可执行文件格式为ELF,即Executable and Linkable Format,可执行链接格式.与ELF相关的文件类型有三种,是我们需要区分一下的.

ELF的布局在链接阶段和运行阶段并不太一样,主要是因为节最终会合并成段,ELF 格式的文件头包含了程序头表(program header table)和节头表(section header table)和 ELF 表.

程序头表

程序头表中存储的是一种记录段信息的数据结构,每个成员称为条目(entry),条目对应着段的描述信息.

// 在 /usr/include/elf.h
typedef struct
{
  Elf32_Word	p_type;			/* Segment type */
  Elf32_Off	p_offset;		/* Segment file offset */
  Elf32_Addr	p_vaddr;		/* Segment virtual address */
  Elf32_Addr	p_paddr;		/* Segment physical address */
  Elf32_Word	p_filesz;		/* Segment size in file */
  Elf32_Word	p_memsz;		/* Segment size in memory */
  Elf32_Word	p_flags;		/* Segment flags */
  Elf32_Word	p_align;		/* Segment alignment */
} Elf32_Phdr;
节头表

多个节经过链接之后就被合并成一个段,因为 CPU 内存存储的是有序的段,节头表就不使用了.

ELF 头
// 在 /usr/include/elf.h
typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf32_Half	e_type;			/* Object file type */
  Elf32_Half	e_machine;		/* Architecture */
  Elf32_Word	e_version;		/* Object file version */
  Elf32_Addr	e_entry;		/* Entry point virtual address */
  Elf32_Off	e_phoff;		/* Program header table file offset */
  Elf32_Off	e_shoff;		/* Section header table file offset */
  Elf32_Word	e_flags;		/* Processor-specific flags */
  Elf32_Half	e_ehsize;		/* ELF header size in bytes */
  Elf32_Half	e_phentsize;		/* Program header table entry size */
  Elf32_Half	e_phnum;		/* Program header table entry count */
  Elf32_Half	e_shentsize;		/* Section header table entry size */
  Elf32_Half	e_shnum;		/* Section header table entry count */
  Elf32_Half	e_shstrndx;		/* Section header string table index */
} Elf32_Ehdr;

一个固定大小的数据结构来描述程序头表和节头表的大小及位置信息,位于文件最开始的部分,可以说是用来描述各种"头"的"头".

关于 ELF 文件的具体细节实在是太多了,我这里建议去看《程序员的自我修养》和《深入理解计算机系统》两本书,我这里就不多说了.

如何加载 ELF 文件?

我们学习加载 ELF 文件到内存,主要是为了加载内核到内存,在上文说道,在此操作系统中内核有两份拷贝,源文件在 0x70000 处,之后会被真正的内核映像覆盖掉,至于如何将源文件加载到内核,实际上这里与从硬盘读取loader到内存的方法基本一样,区别在于是32位操作数和寻址方式.具体代码请参考 loader.S 中的 rd_disk_m_32 函数.

其实这里在这里,加载内核到内存与打开分页模式这两步其实是顺序可以互换,在代码中是先加载内核,后打开分页,这样处理会简单一点,我在叙述的时候把为了描述清楚,把分页和分段放在了一起讲

在加载完了内核源文件,我们就需要初始化内核了,主要就是解析 ELF 头和程序头,具体步骤如下:

  1. 得到程序头的大小
  2. 得到第一个程序头的偏移量
  3. 得到程序头的个数
    • 开始复制段
  4. 判断段类型是否被葫芦哦,是的话不复制,跳到第 7 步,否则继续
  5. 得到段在文件的偏移量,段的大小,在内存的地址
  6. 将段复制到内存里.
  7. 判断是否全部段都复制好了,不是就跳到下一个程序头,跳到 4 ,否则复制完成.
enter_kernel:
	call kernel_init          ;返回地址压栈4字节
	mov esp,0xc009f000        ;栈转移到可用区域  0xc0001000起始为内核映像,大小为 60KB,栈不会破坏
	jmp kernel_entry_addr     ;进入内核的入口虚拟地址 0xc0001500

总结一下:

mbr

loader

loader 建立分段,分页机制等,并读取内核所在的磁盘区域,把内核加载到内存,然后跳转到内核入口处,结束自己.

参考资料
  • 《从实模式到保护模式》
  • 《操作系统真相还原》
  • 《程序员的自我修养》
  • https://www.cnblogs.com/thougr/p/12158456.html
  • https://www.cnblogs.com/thougr/p/11874962.html
  • https://www.cnblogs.com/thougr/p/12203650.html
  • https:///qq_33620667/article/details/60145621
  • https://www.cnblogs.com/nullecho/p/10266467.html
显示全文