进入保护模式五部曲
本节对应代码:进入保护模式-代码详解
参考:《操作系统真相还原》《x86汇编:从实模式到保护模式》
五部曲概览
打开 A20 地址线
加载 GDT
将 CR0 寄存器的 PE 位设置为 1
关闭中断
刷新流水线
以上五个步骤没有顺序,但为了保险起见,最好将操作 CR0 放在最后一步。
打开 A20 地址线
8086/8088 CPU 的地址总线为 20 位,寻址空间为 1MB 。当地址超过 1MB 时,地址就会发生回绕,即重新从 0 算起,相当于对 1MB 求模。这种缺陷被当时很多程序员利用,成为了一个编程技巧,并写入了程序(一旦用得多,就必须考虑兼容)。后来的 80286 CPU的地址总线扩展到 24 位,寻址空间达到 16MB 。考虑到 80286 需要兼容 8086 实模式,即在实模式下必须只使用 20 位地址线,以确保能够发生回绕。为实现这一目的,CPU 在 A20 地址线(第21根地址线)处设置了一个开关(A20Gate),在实模式下关闭,则最大地址空间为 1MB,能够发生回绕;在保护模式下打开,寻址空间扩展到 24 位。
32位 CPU 类似,只有打开 A20G ...
内存段与段寄存器保护
修改段寄存器时的保护
处理器在变更段寄存器以及描述符高速缓存器时,会检查代入值的合法性,若不合法,则抛出异常。将段选择子送入段寄存器时,CPU 的固件会自动确认选择子和该选择子对应的段描述符的正确性。
对选择子的检查内容包括:
选择子的索引是否超界 ,即对应的段描述符是否在 GDT 范围内。要求 索引号×8+7 <= 边界 。若超过边界,则产生异常中断 13,同时段寄存器的原值不变。
选择子的索引是否为 0 。在全局描述符表 & 段选择子概述一文中说过,GDT 的第 0 个描述符不可用。对于 DS,ES,FS,GS 这四个段寄存器,可以向其中加载索引为 0 的段选择子,CS,SS 段寄存器则不可。
虽然能够加载索引为 0 的段选择子,但访问内存时就会出错并抛出中断。CPU 使用这种特殊的设计来保证系统安全。
对段描述符的检查内容包括:
结合 S 位判断 TYPE 字段的有效性 。比如 0000 就是无效值。
检查描述符类型是否和段寄存器用途匹配 。段的类别检查规则如下:
大概原则可以总结为:
1)只有可执行的段才能加载进 CS;
2)只有可读写的段才能 ...
GDT&段描述符&选择子概述
为了方便程序的浮动装配(重定位),处理器访问内存时采用了 [段地址+偏移地址] 的策略,这是 IA-32 的基因。在保护模式下,段有了新的作用——权限管理的单位,操作系统将一定权限赋予给某些段 ,当段内指令访问内存或进行其他操作时,CPU 会根据段的权限来检查其行为的可行性 ,如果其行为越界,则会阻止并抛出异常。那么这些权限是如何记录的呢?请见下文。
段描述符
段的各类权限信息和其地址都被记录在 段描述符 中,其结构如下:
段基址共 32 位,段界限共 20 位。可见它们都是“断开”的,这是因为需要兼容 80286 的 16 位保护模式,详细原因可见下一点。这种散乱分布不利于 CPU 获取段基址和段界限,所以段描述符中的内容会被整理好后存入描述符高速缓存器,CPU 直接从缓存器中获取段基址和段界限 。注意,对于数据段和代码段来说,段界限决定了偏移量的最大值;对于栈段而言,段界限决定了偏移量的最小值 ,其细节差异在内存保护与寄存器保护中有详细阐述。实际段界限等于 (段界限+1)*粒度-1 (减1是因为偏移从0开始)。段基址可选在任何地方,但最好与 16 位对齐。以段基址为起点开始偏 ...
32位保护模式概览
概览
基本工作模式
线性地址
寄存器扩展
寻址扩展
模式反转
指令扩展
全局描述符表、特权级、分页等
基本工作模式
Intel 32 位处理器架构简称 IA-32,尽管 8086 是 16 位的处理器,但它也是 32 位架构内的一部分。16 位到 32 位,不单单是地址线和数据线的扩展 ,实际上还有更多的部分,包括高速缓存、流水线、浮点处理部件、多处理器(核)管理、多媒体扩展、乱序执行、分支预测、虚拟化、温度和电源管理等。
80286 首次提出保护模式的概念。80286 的地址线扩充到 24 位,能够访问 16 MB的内存,但即使这样 ,其仍然遵守内存分段模型,即使用 [段地址+偏址] 方式来访问内存,这是整个 IA-32 的基因 。在保护模式下,段寄存器中存放的不再是段地址,而是段选择子,段选择子映射到段描述符,段描述符中记录了段基址和段界限以及各种权限。 同时,访问内存也不再需要将段地址向左移动四位,而是直接将段地址和偏移地址相加 。这样一来,段地址就无须位于 16 字节对齐的地方,而可以在 16 MB内存的任何角落。然而,80286 的偏移地址仍被限制在 64KB ,对段长度 ...
处理器微架构
流水线
CPU 流水线技术 (pipeline) 是一种将指令分解为多步,并让不同指令的各步操作重叠,从而实现几条指令并行处理 ,以加速程序运行过程的技术。采用流水线技术后,并没有加速单条指令的执行,每条指令的操作步骤一个也不能少,只是多条指令的不同操作步骤同时执行 ,因而从总体上加快了指令流速度,缩短了程序执行时间。
比如,假设执行一条指令需要经过如下步骤:1)取指令;2)译码;3)执行。如果按 串行 方式来运行指令,如下:
可见,每执行一个指令,就需要 3 个时钟。要知道,完成各个操作的单元是相互独立的、并行的 ,译码时,取指令单元就处于等待中;执行时,取指令单元和译码单元就处于空闲。所以,要想加快 CPU 执行速度,就不能让这些单元处于空闲,要让它们忙起来。流水线工作方式就是让这些单元并行,如下:
以上是一个简单的三级流水线,而奔腾 CPU 可是达到了惊人的 32 级流水线,这是怎么做到的呢?很简单,就是 不断地细分这些操作,让更多的微操作并行处理 。显然,流水线级数越多,每级所花的时间越短,时钟周期就越短,指令速度越快,指令平均执行时间也就越短。
实际上,现代处理器的流水 ...
硬盘基础及其读写
硬盘组成概述
盘片
盘面
磁头
磁道
扇区
柱面
盘片、盘面和磁头
硬盘中一般会有多个盘片组成,每个盘片包含两个面,称为盘面,每个盘面都对应地有一个读/写磁头。受到硬盘整体体积和生产成本的限制,盘片数量都受到限制,一般都在5片以内。盘面的编号和磁头编号相对应,从上到下从零开始,如最上边的盘片有 0 面和 1 面,再下一个盘片就编号为 2 面和 3 面。由于每个盘面都有自己的磁头,因此,盘面数等于总的磁头数。如下图:
早期的硬盘不工作的时候,磁头停靠在靠近主轴接触盘片的表面,即线速度最小的地方,这里是一个不存放任何数据的特殊区域,称为启停区或着陆区,启停区以外就是数据区。
在硬盘的最外圈,离主轴最远的磁道称为 0 磁道,硬盘数据的存放是从最外圈 0 磁道开始的 。即硬盘数据从最外圈开始,而停止时磁头又是在最内圈启停区。
0磁道非常重要,我们知道,系统的引导程序就在 0 柱面 0 磁道 1 扇区的前 446 Bytes,后 64 字节是分区表,最后两字节是结束标志 0x55 和 0xaa。0磁道属于隐藏磁道,这个磁道的 63 个扇区属于隐藏扇区。操作系统的所有命令,除了 FDIS ...
bochs虚拟机的使用
bochs简介
bochs 是一个虚拟机(模拟器),能够完整的模拟一台计算机。详细来讲 bochs 是 X86 硬件平台的开源模拟器,完全可以通过软件来给我们提供各种所需的硬件资源。和 bochs 类似的虚拟机软件还有我们常用的 VMware、Virtuabox,但区别也是明显的。bochs 是完全依靠软件来模拟整个环境的:从启动到重启包括 PC 的外设键盘、鼠标、磁盘以及网卡等,全部都是由软件来模拟的,而其余软件则不然(部分依赖于硬件)。也就是说,bochs 可以从头到尾模拟整个硬件环境。它可以从PC机刚启动的那一刹那起就开始模拟。同时,bochs 带有强大的调试功,能够直接单步调试二进制文件,我们可以看到二进制代码在硬件上运行的每一步!后面咋们的操作系统都将由 bochs 运行 。
bochs安装与配置
本自制操作系统笔记系列都在 Ubuntu 下进行,关于 Bochs 在 Windows 下的使用,可参考 程序加载器 一文末尾。
前往 bochs官网 下载 2.7 版本:bochs-2.7.tar.gz 。必须为 2.7 版本,后面的 bochs 配置仅对此版本有效。
...
详解函数调用过程和约定\extern c
本文涉及汇编知识,没有基础的朋友请移步汇编入门 。
本文参考:为什么用0xcc初始化内存 ,C/C++函数调用约定与函数名称修饰规则
函数执行流
graph LR
开辟栈帧 --> 分配栈内存 --> 保存现场 --> 代码逻辑 --> 恢复现场 --> 恢复栈帧
栈帧本质上是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)
我们使用 VS 反汇编以下代码:
1234567891011int add(int a, int b){ return a + b;}int main(){ int a = 1; int b = 2; int c = add(1, 2); return 0;}
得到如下汇编代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667int mai ...
make clean的必要性&自动找寻头文件
本人初学 makefile,对此问题有认识不足之处,烦请在评论区指出错误或补充您的观点,谢谢。
什么时候必须 make clean ?
当依赖文件中没有显式写出头文件时,如果仅改动了头文件,那么必须 make clean 。否则不会重新编译,如下:
123456789101112131415161718192021222324252627#######################mekefilevpath %.c ./srccalc.exe: main.c add.o gcc ./src/main.c add.o -o calc.exeadd.o: add.c gcc -c ./src/add.c -o add.o.PHONY: cleanclean: rm -rf *.o#######################main.c#include "../header/add.h"int main() { int a=0; int b=1; int c=add(a,b); return 0;}########## ...
详解前缀树(字典树)
前缀树又叫字典树,通常用来高效地查询字符串,比如查询库中是否有以某个子字符串为前缀的字符串,某个字符串出现的次数等。前缀树是 N 叉树的一种特殊形式,每一个节点会有多个子节点,通往不同子节点的 路径上 有着不同的字符,子节点中包含两种信息:pass(经过此字符的次数),end(以此字符结尾的次数)。说多了没用,直接上图:
0-‘a’,1-’b’,2-‘c’,每个节点对应的下标即代表相应字母,除 root 外的其他节点中包含着该字母的信息。
细心的同学应该发现,root 的 end 域只可能为 0,因为这代表着插入了一个空字符串,这是不被允许的。所以我们似乎可以利用这个 end 域做些坏事。容易知道,root 的 pass 域代表着此前缀树中一共有多少个字符串(相同字符串会被重复计数),那么,我如果想知道一共有多少种字符串呢?此时,就要利用 root 的 end 域来记录了。详细注释已在代码给出,不在赘述。代码如下(仅支持小写字母):
1234567891011121314151617181920212223242526272829303132333435363738394041424 ...