底层剖析assert断言
本文前置内容:assert与if ,C语言中的#和##
本节对应分支:assert
关于 assert 断言函数的意义和用法,请参见assert与if ,本文不再赘述。在我们自制的 OS 中,会实现两种 assert 函数,一种为内核服务,另一种为用户进程服务。本节实现内核 assert 函数。
内核 assert 函数有以下几个要点需要注意:
1) 一旦内核 assert 函数被调用,就说明此时发生了严重的错误,系统可能面临崩溃的危险,所以应该立即停止运行。如何让系统停止运行呢?你可能会想到在 assert 函数末尾加上一个 while(1) 。没错,这也是我们的做法,但这还不够!还记得吗,操作系统是由中断来驱动并发的,即使当前代码正在 while(1) 中循环,只要发生中断,执行流依旧会转移到中断程序 。如果中断目的是任务调度,那么执行流转移到任务后,因为操作系统已经出现问题,所以任务的执行也是不可靠的。因此,我们还需要关闭中断 。视频演示如下:
(function(){var player = new DPlayer({"container":document.getElem ...
特权级全面剖析(LDT/TSS/GATE)
本文参考:《x86汇编:从实模式到保护模式》《操作系统:真相还原》,SegmentationAndPrivilege ,80386 Programmer’s Reference Manual
特权级概述
我们知道,实模式是单任务系统,对于各个段的隔离,仅依靠分段机制来维护,可以说毫无安全可言。而在保护模式下,通过将内存分为大小不一的段,并用描述符指定各个段的类型与权限,就可以在程序运行时由处理器硬件实施访问保护。但这仍然无法有效保护操作系统,比如,如果恶意程序通过某种方式知道了 GDT 的位置,它就能向段寄存器加载操作系统的数据段描述符,或者在 GDT 中增加一个指向操作系统数据区的描述符,以此来修改操作系统的私有数据。再者,多任务系统对任务之间的隔离与保护,以及任务与操作系统之间的隔离与保护都提出了复杂的要求,基本的段保护机制已经无法胜任。因此,操作系统引入了特权级的概念。
特权级分为 0、1、2、3 四级,数字越小,权力越大 。0 级特权级是操作系统拥有的权力;系统程序(如虚拟机, 驱动程序)分别位于 1、2 特权级;用户程序则位于第 3 特权级。
需要注意的是,我们将数值 ...
加入中断-代码剖析
本文前置内容:中断详解 ,结构体对齐
本节对应分支:interrupt
概述
本节我们为操作系统加入中断,初始化中断描述符表,并为 0~0x2f 中断添加对应的中断处理程序。当前的思路是,在 interrupt.s 中定义实际中断例程的入口函数( 通过入口函数转移到实际中断例程 ),并利用汇编宏技术得到所有入口函数的地址,形成入口函数的地址数组 interrupt_entry_table ;然后在 idt.c 中引入该数组,进而我们能够很方便地向中断描述符中填写入口函数的地址。现在读者可能不明白这个思路的具体含义,别急,下面做具体阐述。
代码解析
interrupt.s
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051 ...
中断/IDT超详解
本文前置内容:特权级全面剖析
文章参考:中断的作用 ,《真相还原》,Bochs源码分析 ,《X86汇编:从实模式到保护模式》
什么是中断?
定义:中断是指计算机运行过程中,出现某些情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
中断是 CPU 对系统发生的某个事件作出的一种反应。引起中断的事件称为中断源 ;中断源向 CPU 提出处理的请求称为中断请求 ;发生中断时被打断程序的暂停点成为断点 ;CPU 暂停现行程序而转为响应中断请求的过程称为中断响应 ;处理中断源的程序称为中断处理程序 ;CPU执行有关的中断处理程序称为中断处理 ;而返回断点的过程称为中断返回 。
中断的意义
操作系统由事件驱动,而事件是以中断的形式来通知操作系统的,所以操作系统是由中断来驱动的。
中断机制是现代计算机系统中的基础设施之一,它在系统中起着通信网络作用(相当于信号),以协调系统对各种外部事件的响应和处理。
中断使得计算机系统具备应对对处理突发事件的能力,提高了CPU的工作效率 。如果没有中断系统,CPU 就只能按照原来的程序编写 ...
实现系统打印函数/除法溢出
概述
实现 put_char() 函数,这是最基础的系统级打印函数,其他打印函数都基于此函数 。
实现 put_str() 函数,该函数以 put_char() 为基础,极大地方便了字符串的打印。
实现 put_int() 函数,该函数以 put_str() 为基础,支持有符号 32 位整型的打印,同时支持十进制与十六进制格式打印 。
后续文章将利用以上函数实现 printf() 可变参打印函数。
注意,前三者是系统级打印函数,也就是所谓的系统调用,和普通的库函数(如printf)要区分开。
函数原型如下 :
1234enum radix{HEX=16,DEC=10};put_char(char, unsigned char); //参数1:字符; 参数2:字符属性put_str(char*, unsigned char); //参数1:字符串; 参数2:字符属性put_int(int, unsigned char, enum radix); //参数1:数字; 参数2:字符属性; 参数3:进制
实现打印函数之前,我们还需要了解一些显存 ...
加载内核-代码详解
前置内容:浅析C语言和汇编混合编程,makefile入门
本节对应分支:load-kernel
概览
让我们看看目录结构:
相比 open-page 分支,本分支新增了两个文件,一个是 /kernel/main.c (如上),另一个是 /src/guide.s :
loader.s 作了改动,下面是分支 load-kernel 相对于 open-page 的修改:
显然,load-kernel 从硬盘中读取内核并加载到 KERNEL_ADDR 地址处,最后跳转进入内核,loader 使命到此结束 。
loader 的使命虽然结束了,但里面的 GDT 我们可还要用呢,后面注意不能把 loader 覆盖,即使要覆盖,也必须先转移 GDT。
为什么需要引导文件?
容易知道,main.c 就是内核。按之前编写 mbr.s 和 loader.s 的经验,我们可能会想到直接将 main.c 编译成 main.bin 文件,然后将 main.bin 直接加载到内存 KERNEL_ADDR 处,接着再跳转进入内核,这不就大功告成了吗?那么为啥还得先进入 guide.s ,然后再调用内核 ...
浅析C和汇编混合编程/ABI规则
本节说明:本节内容与编译和链接相关,该部分内容繁杂,不是一篇博客就能说明的,且本文仅为后续文章加载内核作铺垫,关于这方面详细的内容请阅读《装载,链接与库》。如有错误,请在评论区提出,谢谢。
本文前置内容:函数调用约定
本节对应代码:加载内核-代码详解
C和汇编相互调用
编写源文件
给出如下两个文件:
12345678//文件说明:cprint.cextern void asm_print(char *, int);void c_print(char* str){ int len = 0; while (str[len++]); asm_print(str, len);}
1234567891011121314151617181920212223242526272829303132;文件说明:asm_print.s[bits 32]section .datastr: db "asm_print say hi youyifeng!",0xa,0x00 ;0x0a是换行符,0x00是字符串结束符,不加的话会把后面字符陆续输出,直到遇到空白字符;w ...
开启分页-代码详解
阅读开启分页机制是本节的前置要求。
本节代码对应分支 open-page 。
boot.inc
在进入保护模式的基础上,boot.inc 增添了如下内容:
123456789;========页目录地址和页表起始地址===========PAGE_DIR_POS equ 0x00100000 ;目录表起始位置为1MB处PAGE_TABLE_POS equ PAGE_DIR_POS + 4096 ;页表起始位置;===========页表相关属性===================PG_P equ 1bPG_RW_R equ 00bPG_RW_W equ 10bPG_US_S equ 000bPG_US_U equ 100b
和 GDT 相同,页目录也可以放置在内存中的任何地方 ,这里我们直接将其放在 0x100000 处。
为了使内存紧凑,这里让页表紧挨着页目录。注意,这不是必须的!页目录表大小为 4KB,所以页表地址在页目录地址的基础上加 4096 (0x1000)。
以下为内存映像图:
loader.s ...
开启分页机制
本文参考:为什么要分页 ,《操作系统真相还原》《x86汇编:从实模式到保护模式》《操作系统之哲学原理》《装载、链接与库》
本节对应代码:开启分页-代码详解 。
为什么分页?
分页机制 最早 是为了解决内存碎片利用的问题。举例如下:
在图 1 中,内存被完美利用,还多出 15MB 可用内存。来到图 2,进程 B 运行完毕,从内存中移除,则原来的 20MB 内存变为空闲,则现在一共有 35MB 可用内存。而后进程 D 想要运行,但其需要的内存大小为 20MB+3KB,没有空闲内存段装得下。没办法,即使一共有 35MB 可用内存,由于必须要连续的空闲内存 ,进程 D 就只有等待进程 A 或 进程 C 加载完毕,腾出空间后才能载入内存运行。
显然,等待是不能等待的,谁知道这些进程什么时候运行完呢。细心的你可能会发现,以上问题并不在于内存不够,而在于无法利用“断开”的内存 。当运行许多进程后,可用内存就会参差不齐,形成大量内存碎片,虽然总可用内存数量很客观,但却完全无法利用。为了解决这个问题,便提出了分页的概念。分页和虚拟内存是两个密不可分的概念 。接下来让我们看看这两个概念是如何解决以上 ...
进入保护模式-代码详解
MBR --> Loader --> Kernel
本节代码只涉及 MBR 和 Loader 部分,暂未考虑内核代码。同时,为规范操作,我们使用 [加载器-用户程序] 方式将 Loader 从硬盘载入内存。这种方式非常漂亮,同时能让你理解重定位的本质,详细请阅读程序加载器-重定位 (本节前置要求,务必阅读)。
本节代码对应分支 protected-mode 。
配置文件
本节的 MBR 可以直接引用 程序加载器 一文中的 MBR 代码,并在文件头引入配置文件:
1234%include "loader.inc"SECTION mbr align=16 vstart=0x7c00 ;....................以下省略....................
其中 loader.inc 文件内容如下:
123;文件说明:loader.incBASE_ADDR equ 0x900 ;最好不超过0xFFFF,原因在下文解释START_SECTOR equ 2 ...