系统启动
CPU加电之后 初始化寄存器 从 CS:IP = 0xF000:FFF0 执行第一条指令 系统处于实模式 故 PC = 16 * CS + IP 此时物理地址为 0xFFFF0 20位地址总线 可用内存为 1MB 此时距离 1MB 只剩下 16个字节
16个字节够用吗?
0xffff0 物理地址处为跳转指令 会长跳转到 BIOS 代码真正开始的地方
BIOS 初始化
- 基本输入输出
- 系统设置信息
- 开机后自检
- 硬件自检POST
- 检测系统中内存或显卡等关键部位的存在和工作状态
- 查找并执行显卡等接口的初始化程序
- 系统初始化
- 检测配置即插即用设备
- 更新 ESCD 扩展系统配置数据
- 硬件自检POST
- 系统自启动等
初始化完成之后 会去读磁盘上的加载程序 读取后会将其加载到 0x7c00
多操作系统怎么办?
过去只有一个分区 可以直接去读 加载程序 但是现在普遍都有多分区 因此要加一个主引导记录 先去读0 盘 0 道 1扇区(CHS方法)主引导扇区(512字节 最后两字节必须为 0x55 0xaa)里的 MBR 主引导记录 再去找活动分区 最后才去读加载程序将其加载到 0x7c00
过程为 MBR->主分区 OBR 系统引导记录 / 子扩展分区 OBR
为什么是 0x7c00?
在 PC 5150 的 BIOS 开发中 运行的操作系统 为 32KB 的 要满足 MBR 不会过早的被覆盖 那就只能够将其加载到 32KB的末尾 同时 MBR 本身也是程序 有可能会使用到栈 故需要为栈分配一些空间 估计总共 1KB 内存就足够了 因此 将其放置在 32KB的末尾前1KB处 即 0x8000 - 0x400 = 0x7c00
系统启动规范
BIOS-MBR 主引导记录最多支持4个分区 一个分区占用 16字节 四个分区占用 64字节
BIOS-GPT 全局唯一标识分区表 不受4个分区的限制
PXE 网络启动标准 通过服务器下载内核镜像来加载
UEFI 统一可扩展固件接口 目标是在所有平台上一致的操作系统启动服务 会对引导记录进行可信性检查 只有通过可信性检查的才能运行
加载程序
当 BIOS 将加载程序从磁盘的主引导扇区加载到 0x7c00 之后 就跳转到 CS:IP 0x0000:7c00 处执行加载程序的指令 加载程序 又将操作系统的代码和数据从硬盘中加载到内存 最后跳转到 操作系统的起始地址
为什么BIOS不直接读操作系统内核镜像?
因为磁盘上有文件系统 机器出厂的时候 不能限制为某一种的文件系统 而 BIOS 不可能认识所有文件系统 因此有一个基本的约定 BIOS 不需要认识文件系统 直接从最开始的扇区 读取加载程序 再用加载程序来识别文件系统 再来读取操作系统的内核镜像 再加载到内存中 此外 BIOS 在设计的时候 就只能读取一个扇区 而一个 OS 可能不止是一个扇区 会增加BIOS 工作的难度
BIOS 系统调用
BIOS 以中断调用的方式 提供了基本的 I/O操作 仅能用于实模式 因为此时为实模式 存储中断程序数组的为中断向量表
- int 0x10: 字符显示
- int 0x13:磁盘扇区读写
- int 0x15: 检测内存大小 0xe820 0xe801(最大识别4GB内存) 0x88(最大识别64MB内存)
- int 0x16:键盘输入
BootLoader 到 OS
- 使能保护模式
- 开启 A20 地址线 若被禁止 CPU 将会采用 8086/8088 的地址回绕
- 加载 GDT 全局描述符表
- CR0 控制寄存器的 PE 位 置 1
- 从硬盘读取 kernel in ELF格式的 kernel 读到内存中指定的位置
- 跳到 OS 的入口处
段机制 和 页机制
段选择子 Segment Register 本质上就是个索引 指向描述符表的其中一项 TI 表示是在 GDT 还是 LDT
RPL 表示 请求特权级
段描述符 Segment descriptro 描述了起始地址 和 大小 通过段描述符 来找到代码段的起始地址和大小
在 Bootloader 中使用 lgdt 机器指令将 段描述符表 GDT Global Descriptor Table 全局描述符表 / LDT 本地描述符表 加载 到 GDTR 寄存器 / LDTR
由于后面还有个页机制 在段机制下 就将映射关系弄的简单一些 采用了 平坦模型 整个 4G 内存都是一个段
在页机制下 将起到分段的作用
系统中断、异常、调用
什么是中断?
CPU 暂停正在执行的程序转而去执行处理该事件的程序
为什么需要系统中断、异常、调用?中断不会使效率变低吗?
- 计算机运行时 内核是被信任的第三方
- 只有内核才能够执行特权指令
- 方便应用程序调用
中断会不会使效率变低是要取决于看中断的角度 中断虽然打断了当前操作的执行 但是却正是因为中断使得系统能够并发运行
系统本质上是一个死循环 但是死循环做不了什么大事 而系统运行的目的是为了等候某些事情的发生 系统是被动的 因此也可以说 操作系统是中断所驱动的
中断的分类
- 外部中断 也称之为硬件中断
- 可屏蔽中断 使用 INTR 信号线通知CPU
- 不可屏蔽中断 使用 NMI 信号线通知CPU
- 内部中断 又称软中断和异常
- int 8位立即数
- int3 调试断点指令
- into 中断溢出指令
- bound 数组索引越界
- ud2 未定义指令
除了 int 8位立即数以外 其它都可以称作是异常
中断、异常和系统调用区别
- 系统调用(system call):应用程序主动向操作系统发出的服务请求
- 异常(exception): 非法指令或其他原因导致当前指令执行失败后的处理请求
- 中断(handware interrupt):来自硬件设备的处理请求
每个中断或异常与一个中断服务例程(Interrupt Service Routine ISR) 相关联
Linux 系统调用并没有使用调用门 而是直接使用int 0x80中断 来完成系统调用
系统调用的过程
首先在Linux系统调用中 采用的是 int 0x80中断 来完成 因此在实现系统调用之前 要实现中断
在之前实模式下面 BIOS的中断是使用 中断向量表(Interrupt Vector Table IVT)来存储中断处理程序入口
而此时我们处于保护模式下 BIOS的中断已经不可用了 好在保护模式下也有一个用于存储中断处理程序入口的表
中断描述符表(Interrupt Descriptor Table IDT)里面存储了中断描述符又称门
门有四种 任务门 中断门 调用门 陷阱门 除了调用门其它门都能存储在IDT中
IDT 的起始地址和大小保存在中断描述符表寄存器 IDTR 通过机器指令 lidt 保存
中断门、陷阱门描述符格式
在实际实现中 进行分段操作的 不是在段机制 而是在页机制中完成的
因此 此处的段选择子是整个内核代码段 而目标代码段的偏移量 是整个 4G 的空间
系统调用的实现
由于系统调用在 Linux 是利用中断门的方式来实现的 每个系统调用对应一个中断向量号 其实就是一个中断描述符表的索引 在 IDT 中安装 0x80号中断对应的描述符 在该描述符表里注册系统调用的中断处理例程 建立系统调用子功能表 利用 eax寄存器 获取子功能号
系统调用的特别之处
- 它会进行堆栈的切换
- 特权级的转换
Linux中只用到了 0级特权级 和 3级特权级 因此 只在0级栈与3级栈中切换
系统调用的传递参数的方式可以使用栈或者寄存器 其中用寄存器来传递参数则比较简单
使用栈传递的话 是用户进程先将参数压入3级栈 然后内核将其读出来再压入0级栈即可
堆栈是怎么切换的?
CPU 会从 TSS 任务状态段 (Task Status Segment)获取ss0和esp0
定位内核态堆栈后 会将用户态堆栈的 ss3和esp3压入内核态堆栈
当发生中断且特权级有变化的时候 处理器会将用户栈 SS 和 EIP 压入内核栈中 以备返回时重新加载到栈段寄存器 SS 和栈指针 ESP 后在新栈压入 EFLAGS 寄存器 由于要切换到目标代码段 段间转移,要将 CS 和 EIP 保存到当前栈中备份 以便中断程序执行结束后能恢复到被中断的进程 有的异常会有错误码
EXT EXTernal event 外部事件 IDT 表示是否指向中断描述符表 TI 表示是指向 GDT 还是 LDT
特权级是怎么转换的?
3级特权级 进入 0级特权级 只需要进入中断即可
那么 操作系统一开始运行的时候处于 0级特权级 是怎么进入 3级特权级的?
通过 欺骗CPU 就是让CPU以为目前就处于中断处理程序中 然后通过 iret 退出中断 进入到 3级特权级
系统调用的开销
- 保护程序上下文
- 第一次调用要建立一个堆栈
- 验证参数 内核代码对用户进程不信任
- 内核态映射到用户态的地址空间 不同进程的切换 需要更新 cr3寄存器 存放页目录表物理地址