第一讲: 程序的运行过程

程序的编译过程

compile_process

  1. 预处理:加入头文件,替换宏 gcc HelloWorld.c -o HelloWorld.i
  2. 编译:包含预处理,将C程序转成成汇编程序 gcc HelloWorld.c -S -c Helloworld.s
  3. 汇编:包含预处理和汇编,将汇编程序转换成可链接的二进制程序 gcc HelloWorld.c -c HelloWorld.o
  4. 链接:将可链接的二进制程序和其他的库链接在一起,形成可执行的程序文件 gcc HelloWorld.c -o HelloWorld

程序装载执行

冯诺依曼体系结构五大组件

  • 装载数据和程序的输入设备
  • 记住程序和数据的存储器
  • 完成数据加工的运算器
  • 控制程序执行的控制器
  • 显示处理结果的输出设备

HelloWorld汇编代码解释

objdump -d hello_world.o compile_code

上图分为四列:

  • 第一列为地址
  • 第二列为数据
  • 第三列为汇编命令
  • 第四列为注释

将上图中代码装入计算机中,状态如下图:

compile_code_status

第二讲:实现一个最小内核

OS引导流程

os_load

BIOS固件负责检测和初始化CPU、内存及主办平台,然后加载引导设备(如磁盘)的第一个扇区地址的数据 到0x7c00地址开始的内存空间,街道跳转到0x7c00处执行指令,即GRUB指导程序

引导汇编代码

C作为通用的高级语言不能直接操作特定的硬件,而且C语言的函数调用、传参都 需要栈 所以需要汇编代码来出席这些C语言的工作环境

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
;彭东 @ 2021.01.09
MBT_HDR_FLAGS EQU 0x00010003
MBT_HDR_MAGIC EQU 0x1BADB002 ;多引导协议头魔数
MBT_HDR2_MAGIC EQU 0xe85250d6 ;第二版多引导协议头魔数
global _start ;导出_start符号
extern main ;导入外部的main函数符号
[section .start.text] ;定义.start.text代码节
[bits 32] ;汇编成32位代码
_start:
jmp _entry
ALIGN 8
mbt_hdr:
dd MBT_HDR_MAGIC
dd MBT_HDR_FLAGS
dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
dd mbt_hdr
dd _start
dd 0
dd 0
dd _entry
;以上是GRUB所需要的头
ALIGN 8
mbt2_hdr:
DD MBT_HDR2_MAGIC
DD 0
DD mbt2_hdr_end - mbt2_hdr
DD -(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))
DW 2, 0
DD 24
DD mbt2_hdr
DD _start
DD 0
DD 0
DW 3, 0
DD 12
DD _entry
DD 0
DW 0, 0
DD 8
mbt2_hdr_end:
;以上是GRUB2所需要的头
;包含两个头是为了同时兼容GRUB、GRUB2
ALIGN 8
_entry:
;关中断
cli
;关不可屏蔽中断
in al, 0x70
or al, 0x80
out 0x70,al
;重新加载GDT
lgdt [GDT_PTR]
jmp dword 0x8 :_32bits_mode
_32bits_mode:
;下面初始化C语言可能会用到的寄存器
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
xor edi,edi
xor esi,esi
xor ebp,ebp
xor esp,esp
;初始化栈,C语言需要栈才能工作
mov esp,0x9000
;调用C语言函数main
call main
;让CPU停止执行指令
halt_step:
halt
jmp halt_step
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009e000000ffff
k16da_dsc: dq 0x000092000000ffff
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START
  • 1~40行:GRUB多引导协议头
  • 44~52行:关掉中断,设定CPU的工作模式
  • 54~73行:初始化CPU的寄存器和C语言的运行环境
  • 78~87行:设置CPU工作模式所需要的数据

主函数

1
2
3
4
5
6
7
//main.c
#include "vgastr.h"
void main()
{
  printf("Hello OS!");
  return;
} 

控制计算机屏幕

显卡有多种形式

  • 集显:集成在主办
  • 核显:CPU芯片内
  • 独显:独立存在,同时PCIE接口连接

显卡的字符模式将屏幕分成24行,每行80个字符,把字符映射到以0xb8000开始的内存中 一个字符对应两个字节,一个字节是字符的ASCII码,另外一个字节为字符的颜色值

byte_mode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// vgsstr.c
void _strwrite(char* string) 
{
    char* p_string = (char*)(oxb8000); //显存开始的地址
    while (*string) 
    {
        *p_string = *string++;
        p_string += 2;
    }
    return;
}

void printf(char* fmt, ...) 
{
    _strwrite(fmt);
    return;
}

编译和安装

编译

compile_process

安装

上述编译流程会得到HelloOS.bin文件,但序言GRUB能够找到它 具体配置在grub.cfg的文件中

1
2
3
4
5
6
7
menuentry 'HelloOS' {
     insmod part_msdos #GRUB加载分区模块识别分区
     insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
     set root='hd0,msdos4' #注意boot目录挂载的分区(df /boot 查看)
     multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载HelloOS.bin
     boot #GRUB启动HelloOS.bin
}

重启系统长按shit键然后选择HelloOS即可

第三讲:内核结构设计

内核功能

  • 管理CPU:CPU是执行程序的,而内核吧运行时的程序抽象成 进程,所以称之为进程管理
  • 管理内存
  • 管理硬盘
  • 管理显卡
  • 管理各种I/O设备

宏内核结构

宏内核就是所有诸如管理进程等功能的代码进过编译链接形成一个大的可执行程序

这个大程序里有实现这个功能的所有代码,向应用软件提供一些接口(即系统API) macro-core

宏内核的优点是组件都在内核中合一相互调用性能极高;但缺点也很明显,高度耦合,没有模块化

微内核结构

微内核仅仅只有进程调度、处理中断、内存空间映射和进程间通信等功能

实际功能如进程管理、内存管理、文件管理等做成一个个服务进程

微内核与应用进程和服务进程通过消息通信

应用程序要请求相关服务,就向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,接着服务进程会完成相关的服务。

我们的选择

大致将内核分为三个大层

  • 内核接口层
  • 内核功能层
  • 内核硬件层

core-structure

内核接口层

  1. 定义了一套UNIX接口的子集
  2. 检查参数的合法性

内核功能层

主要完成各种事件功能

  1. 进程管理:实现进程的创建、销毁、调度
  2. 内存管理:内存池分为页面内存池和任意大小的内存池
  3. 中断管理:把中断回调函数安插在相关的数据结构中,发生相关的中断就会调用回调函数
  4. 设备管理:把驱动程序模块、驱动程序本身和驱动程序创建的设备组织在一起

内核硬件层

  1. 初始化:初始化少量的设备、CPU、内存、中断的控制,内核用户管理的数据结构
  2. CPU控制:提供CPU模式设定、开关中断、读写CPU特定寄存器等功能
  3. 中断处理:保存上下文、调用中断回调函数、操作中断控制器
  4. 物理内存管理:分配和释放大块内存、内存空间映射、操作MMU和Cache
  5. 平台其他相关功能

第四讲:业界成熟内核架构

Linux

linux-core

Linux使用的宏内核架构,模块之间的通信主要是函数调用

Darwin

macos-core

Darwin有两个内核层

  • Mach层:微内核,然提供十分简单的进程、线程、IPC 通信、虚拟内存设备驱动相关的功能服务
  • BSD层:提供强大的安全特性,完善的网络服务,各种文件系统的支持,同时对 Mach 的进程、线程、IPC、虚拟内核组件进行细化、扩展延伸

Windows NT

windows-core

每个执行体互相独立,只对外提供相应的接口,其它执行体要通过内核模式可调用接口和其它执行体通信或者请求其完成相应的功能服务

评论区拾遗

内核相当于所有的功能都耦合在一起,放在内核内 微内核是把大多数功能解耦出来,放在用户态,使用IPC在用户态调用服务进程 混合结构其实与微内核相似,只不过解耦出来的这些功能依然放在内核里,动态加载和卸载

第五讲:执行程序的三种模式

CPU的工作模式有三种

  • 实模式
  • 保护模式
  • 长模式

实模式

实模式又称实地址模式,一方面是运行真实的指令,另一方面内存地址是真实的

寄存器

通常情况下指令的操作数就是寄存器,下图为x86 实模式下的寄存器

physical_register

内存

指令和数据都放在内存中,内存的地址值计算过程如下图

physical_memory

内存地址是由段寄存器左移4位,再加上一个统统寄存器的值形成地址,然后由这个地址去访问内存 (这个即是分段内存管理模式)

代码段是CS和IP确定的,栈段是由SS和SP确定的

DOS实模式汇编代码程序实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
data SEGMENT ;定义一个数据段存放Hello World!
    hello  DB 'Hello World!$' ;注意要以$结束
data ENDS
code SEGMENT ;定义一个代码段存放程序指令
    ASSUME CS:CODE,DS:DATA ;告诉汇编程序,DS指向数据段CS指向代码段
start:
    MOV AX,data  ;data段首地址赋值给AX                
    MOV DS,AX    ;AX赋值给DS,使DS指向data段
    LEA DX,hello ;使DX指向hello首地址
    MOV AH,09h   ;AH设置参数09HAH是AX高8位AL是AX低8位,其它类似
    INT 21h      ;执行DOS中断输出DS指向的DX指向的字符串hello
    MOV AX,4C00h ;AH设置参数4C00h
    INT 21h      ;调用4C00h号功能,结束程序
code ENDS
END start

中断

中断即终止当前程序,转而跳转到另一个特定的地址上,去运行特定的代码。

在实模式中即:先保存CS和IP寄存器,然后装载新的CS和IP寄存器

interrupt-vector-table 为了实现中断,就需要在内存中放一个中断向量表(中断信号和对应响应程序的首地址)

这个表的地址和长度由IDTR寄存器指向,在实模式中,一个条目由代码段地址和端内偏移组成

根据中断信合和IDTR寄存器的信息,CPU能够计算出中断向量的条目,进而装载CS(段基地址)、IP寄存器(段内偏移),最终响应中断

保护模式

protected-register

相比实模式,保护模式增加了一些控制寄存器和段寄存器,扩展通用寄存器的位宽

特权级

protected-privilege-level

从内到外,权力逐步提升

段描述符

protected-segment-descriptor

内存是分段模型,对内存的保护可以转化成对段的保护

段描述符是一个64位的数据,包含了段基地址、段长度、段权限、段类型和读写状态等

由于信息的扩展,16位的寄存器放不下,段描述符放在内存中

多个段描述符在内存中形成全局段描述符表,该表的基地址和长度由GDTR寄存器指示

protected-segment-descriptor-table

段寄存器不再放段基地址,而是段在段描述符表的索引

平坦模型

x86 CPU不能直接使用分页模型,通过简化设计,来时分段称为一个"虚设",这个称之为保护模式的平坦模型

将段的基地址设为0,段长度设为0xFFFFF(2 ** 20 = 1M),段的粒度为4KB 在此模式下不同的段可以重叠、交叉和包含

中断

保护模式需要检查权限,所以需要扩展中断向量表的信息 每个中断用一个中断门描述符来表示,格式如下

protected-interrupt

中断向量表的条目也变成了中断门描述符 protected-interrupt-table

产生中断后

  1. 检查中断号是否在有效区间(0~255)
  2. 检查描述符类型
  3. 权限检查(如果权限等级不同会进行栈切换)
  4. 加载目标代码偏移段到EIP寄存器

切换

由实模式切换到保护模式步骤如下:

  1. 准备全局段描述符表
1
2
3
4
5
6
7
8
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
GDT_END:
GDT_PTR:
GDTLEN  dw GDT_END-GDT_START-1
GDTBASE  dd GDT_START
  1. 设置GDTR寄存器,使之指向全局段描述符表
1
lgdt [GDT_PTR]
  1. 设置CR0寄存器,开启保护模式
1
2
3
4
;开启 PE
mov eax, cr0
bts eax, 0                      ; CR0.PE =1
mov cr0, eax         
  1. 进行长跳转,加载CS段寄存器
1
jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移

长模式

长模式又称AMD64,它是CPU在现有基础上有了64位的处理能力

寄存器

所有通用寄存器都是64位,可以单独使用低32位,低32位可以查封成一个低16位,低16位可以拆分成两个8位寄存器

amd64-register

段描述符

amd64-segment-descriptor

中断

amd64-interrupt

切换

  1. 准备长模式全局段描述符表
1
2
3
4
5
6
7
8
ex64_GDT:
null_dsc:  dq 0
;第一个段描述符CPU硬件规定必须为0
c64_dsc:dq 0x0020980000000000  ;64位代码段
d64_dsc:dq 0x0000920000000000  ;64位数据段
eGdtLen   equ $ - null_dsc  ;GDT长度
eGdtPtr:dw eGdtLen - 1  ;GDT界限
     dq ex64_GDT
  1. 准备长模式下的MMU页表

长模式下内存地址空间的保护交给了 MMU,MMU 依赖页表对地址进行转换,页表有特定的格式存放在内存中,其地址由 CPU 的 CR3 寄存器指向

1
2
3
4
5
mov eax, cr4
bts eax, 5   ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax
  1. 加载GDTR寄存器,是指指向全局段描述符
1
lgdt [eGdtPtr]
  1. 开启长模式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
;开启 64位长模式
mov ecx, IA32_EFER
rdmsr
bts eax, 8  ;IA32_EFER.LME =1
wrmsr
;开启 保护模式和分页模式
mov eax, cr0
bts eax, 0    ;CR0.PE =1
bts eax, 31
mov cr0, eax 
  1. 进行跳转,加载CS段寄存器
1
jmp 08:entry64 ;entry64为程序标号即64位偏移地址

第六讲:地址转换

虚拟地址

虚拟地址由链接器产生,链接器的主要工作是吧多个代码模块组装在一起,并解决模块之间的引用 ,即处理程序代码间的地址引用,形成程序运行的静态内存空间视图

物理地址

物理地址会被地址译码器变成电信号,放在地址总线上,地址总线电子信号的各种组合就可以选择到内存的存储单元

虚实转换

虚实地址转换是通过MMU(内存管理单元)实现的

convert-virtual-address

地址转换关系表本身存放在内存中,如果一个虚拟对称对应一个物理地址,转换表就会把内存耗尽

于是引出了分页模型,虚拟地址空间和物理地址空间都分成了同等大小的页

page-mode

MMU

MMU负责接受虚拟地址值和地址关系转换表,然后输出物理地址

页表

页表即虚拟页到物理页的映射关系

为了洁身空间,页表值存放物理页面的地址,MMU以虚拟地址为索引去查表返回物理页地址

页表是分级的,分为三部分

  • 一个顶级页目录
  • 多个中继页目录
  • 页表

page-mode2

一个虚拟地址从左到右分为四个位段

  • 第一个位段索引顶级页目录,得到中继页目录
  • 第二个位段索引中级页目录,得到页目录
  • 第三个位段索引也目录,得到物理页地址
  • 第四个位段用作该物理页的偏移去访问物理内存

保护模式下的分页

保护模式下只有32位地址空间,32位虚拟地址经过分段机制后得到线性地址, 通常使用平坦模式,所以线性地址和虚拟地址是相同的

4KB页

该分页方式下32位虚拟地址被分为三个位段: 页目录索引、页表索引、页内偏移

page-mode3

CR3寄存器、页目录项和页表项都是32位,所以低12位可以另做它用,形成了页面相关属性,如 是否存在,是否可读写、是否已访问等待

page-mode4 page-mode5 page-mode6

4MB页

该分页方式下,32位虚拟地址被分为2段:页表索引、页内偏移

共1024个条目,每个条目指向一个物理页4MB,正好为4GB地址空间

page-mode7

CR3还是32位寄存器,指向一个4KB大小的页表,仍然要4KB地址对齐,其中包含1024个页表项

长模式下的分页

长模式下扩展为64位

4KB页

long-mode1

64位虚拟地址被分为6段

  • 保留位段: 24位
  • 顶级页目录索引段:9位
  • 页目录指针段: 9位
  • 页目录段:9位
  • 页表段: 9位
  • 页内偏移:12位

顶级页目录、页目录指针、页目录、页表都各占4KB, 各512个条目,每个条目8B即64位

因为x86 CPU并没有实现全64位的地址总线,而是只实现了48位,但寄存器却是64位的, 当第47位是1的时候48~63为1,反之为0

2MB页

该方式下分为5个分段

  • 保留位段: 16位
  • 顶级页目录索引:9位
  • 页目录指针索引:9位
  • 页目录索引:9位
  • 页内偏移:21位

顶级页目录、页目录指针、页目录都各占4KB, 各512个条目,每个条目8B即64位 页表项被放弃,页目录项直接指定了2MB大小的物理页面

开启MMU

  1. 使CPU进入保护模式或长模式
  2. 准备页表数据
  3. 将顶级页目录的物理地址赋值给CR3寄存器
1
2
mov eax, PGAE_TLB_BADR ;页表物理地址
mov cr3, eax
  1. 将CPU的CR0的PE为设置为0,这样就开启了 MMU
1
2
3
4
5
;开启保护模式和分页模式
mov eax, cr0
bts eax, 0    ;CR0.PE = 1
bts eax, 31   ;CR0.P = 1
mov cr0, eax

第七讲:Cache与内存

内存

控制内存读写和刷新的是内存控制器,内存控制器集成在北桥芯片中

由于制造工艺升级,现在北桥芯片被集成到了CPU芯片,这样大大提升了CPU访问内存的性能

Cache

通过程序的局部性原理可以知道CPU大多数时间在访问相同或者相近的地址

那么可以用一块小而快的存储器放在CPU和内存之间,来缓解CPU和内存的之间的性能差距

这个就称之为Cache

Cache中存放了部分内存数据,CPU在访问内存时要先访问Cache

现在的x86 CPU是将Cache集成在CPU内

cache

Cache主要由高速的静态存储器、地址转换模块和行替换模块组成

Cache会把存储器分成若干行,每行32字节或64字节,和内存交换数据最小单位为一行

为了方便管理,多个行又会组成一组

除了正常数据外,行中还有一些标志位,如脏位、回写位等

Cache的大致工作流程如下:

  1. CPU发出的地址由Cache的地址转换模块分成3段:组号、行号和行内偏移
  2. Cache会根据组号、行号查找高速静态存储器中对应的行。如果找到即命中,用行内偏移读取并返回数据给CPU; 否则就分配一个新行并访问内存,把内存中对应的数据加载到Cache行并返回给CPU. 写入操作分为回写和直写,回写就是写入对应的Cache行即可,直写写入Cache行的同时会写入内存
  3. 如果容量不足,就要进入行替换逻辑,即找出一个Cache行写回内存,腾出空间。

Cache引入的问题

x86_cache

上图是简单的双核心CPU,有三级Cache,第一级Cache是指令和数据分开的, 第二级是独立于CPU核心的,第三集是所有CPU核心共享的。

Cache一致性问题主要包括以下三个:

  1. 一个CPU中指令Cache和数据Cache一致性的问题
  2. 多个CPU各自的2级Cache一致性问题
  3. 3级Cache与网卡、显存等设备存储之间的一致性问题

Cache的MESI协议

MESI协议定义了四种基本状态

  1. Modified 当前Cache内容有效,但已和内存不一致,且不存在其他核心的Cache中

  2. Exclusive 当前Cache内容有效,和内存一致,但不存在其他核心的Cache中

  3. Shared

当前Cache内容有效,和内存一致,且在其他核心的Cache中也一直

  1. Invalid 其他情况,当前Cache无效