GuoXin Li's Blog

OS-Learning: Linux Homebrew

字数统计: 7.5k阅读时长: 27 min
2021/05/12 Share

OS-Learning: Linux Homebrew

准备工作

程序编译过程

img

gcc helloworld.c -E -o helloworld.i 预处理:加入头文件,替换宏。

gcc helloworld.c -s -c helloworld.s 编译:包含预处理,将 C 程序转换成汇编程序

gcc helloworld.c -c helloworld.o 汇编:将汇编程序转换成可链接的二进制程序

gcc helloworld.c -o helloworld 链接:包含了以上的所有操作,直接将可链接的二进制程序和其他别的库链接在一起,形成可执行的程序文件。

PC OS 引导流程

img

  • PC 机的 BIOS 固件是固化在 PC 机主板上的 ROM芯片中的,掉电也能保存

  • PC 机上电后的第一条指令就是 BIOS 固件中的,它负责 检测和初始化 CPU、内存以及主板平台,然后是夹在引导设备(大概率是硬盘)中的第一个扇区数据,到 0x7c00地址开始的内存空间,接着再跳转到 0x7c00处执行指令,这里为 GRUB 引导程序。

  • UEFI 模式与上有差异:www.uefi.org

初始汇编代码

  • 定义 GRUB 多协议头,即一定格式的数据,兼容 GRUB1和GRUB2
  • 关闭中断,设定 CPU 的工作模式
  • 初始化 CPU 的寄存器和 C 语言的运行环境
  • GDT_START开始处,为 CPU 工作模式所需要的数据
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
88
;彭东 @ 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
;这里是调用了外部的 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

main 函数

1
2
3
4
void main(){
printf("Hello OS!"); //这里的 printf 也是需要自己实现的
return;
}

控制计算机的屏幕

调用函数将我们想要现实的字符串里的每个字符依次写入到 0xb8000 地址开始的显存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//彭东 @ 2021.01.09
void _strwrite(char* string)
{
char* p_strdst = (char*)(0xb8000);//指向显存的开始地址
while (*string)
{
*p_strdst = *string++;
p_strdst += 2; //+2 是为了跳过字符的颜色信息的空间
}
return;
}

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

make 工具

1
2
3
4
5
6
7
8
9
10

CC = gcc #定义一个宏CC 等于gcc
CFLAGS = -c #定义一个宏 CFLAGS 等于-c
OBJS_FILE = file.c file1.c file2.c file3.c file4.c #定义一个宏
.PHONY : all everything #定义两个伪目标all、everything
all:everything #伪目标all依赖于伪目标everything
everything :$( OBJS_FILE) #伪目标everything依赖于OBJS_FILE,而OBJS_FILE是宏会被
#替换成file.c file1.c file2.c file3.c file4.c
%.o : %.c
$(CC) $(CFLAGS) -o $@ $<
  • .PHONY 在 makefile 中表示定义伪目标。所谓伪目标,就是它不代表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令。但是伪目标可以依赖于另一个伪目标或者文件,例如:all 依赖于 everything,everything 最终依赖于 file.c file1.c file2.c file3.c file4.c。

  • 针对这些依赖关系,分别会执行:$(CC) $(CFLAGS) -o $@ $< 命令,当然最终会转换为:gcc –c –o xxxx.o xxxx.c,这里的“xxxx”表示一个具体的文件名。

Hello OS 整个编译过程

img

安装 Hello OS

  • 经过上述流程将会得到 Hello OS.bin 文件

  • 设置 GRUB 找到 bin 文件

  • GRUB 启动时会加载 grub.cfg 文件,设置启动项:

    1
    2
    3
    4
    5
    6
    7
    8

    menuentry 'HelloOS' {
    insmod part_msdos #GRUB加载分区模块识别分区
    insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
    set root='hd0,msdos4' #注意boot目录挂载的分区,这是我机器上的情况
    multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载,如果挂载点是 /boot 的话,需要省略掉/boot 然后挂载 HelloOS.bin
    boot #GRUB启动HelloOS.bin
    }

    Linux 系统下可以通过 df 来看 /boot目录挂载的分区。

    1
    2
    文件系统          1K-块    已用     可用      已用% 挂载点
    /dev/sda4 48752308 8087584 38158536 18% /

    “sda4”就是硬盘的第四个分区,但是 GRUB 的 menuentry 中不能写 sda4,而是要写“hd0,msdos4”,这是 GRUB 的命名方式,hd0 表示第一块硬盘,结合起来就是第一块硬盘的第四个分区

设计蓝图

内核逻辑

  • 管理 CPU,由于 CPU 是执行程序的,内核把运行时的程序抽象成进程,所以又称之为进程管理。
  • 管理内存,内核进行对于内存的分配和释放
  • 管理硬盘, 内核把用户数据抽象成文件,即文件管理,方便用户进行查找和读写,所以形成了文件系统。
  • 管理显卡, 对于 GUI的支持,对显卡的管理成为内核中的图形系统。
  • 管理 I/O 设备,对于数据输出设备的 I/O 管理器。

  • 管理硬件的驱动程序,内核想要管理和控制不同的硬件,需要对应的代码,即驱动程序。

宏内核结构

宏内核就是把以上诸如管理进程的代码、管理内存的代码、管理各种 I/O 设备的代码、文件系统的代码、图形系统代码以及其它功能模块的代码,把这些所有的代码经过编译,最后链接在一起,形成一个大的可执行程序。

img

微内核结构

内核功能尽可能少:仅仅只有进程调度、处理中断、内存空间映射、进程间通信等功能。

这样的内核是不能完成什么实际功能的,开发者们把实际的进程管理、内存管理、设备管理、文件管理等服务功能,做成一个个服务进程。和用户应用进程一样,只是它们很特殊,宏内核提供的功能,在微内核架构里由这些服务进程专门负责完成。

微内核定义了一种良好的进程间通信的机制——消息。应用程序要请求相关服务,就向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,接着服务进程会完成相关的服务。服务进程的编程模型就是循环处理来自其它进程的消息,完成相关的服务功能。

img

内核的三个大层

  • 内核接口层

    定义一套 UNIX 接口的子集。

    检查接口的参数是否合法,出错返回错误信息。

  • 内核功能层

    完成各种实际功能,如进程管理,内存管理,中断管理,设备管理。

  • 内核硬件层

    硬件初始化,加载内存中最先需要运行的代码,如CPU、内存、中断等的控制。

    img

Linux 内核

img

Linux 是宏内核架构。

Darwin-XNU 内核

Darwin 是由苹果公司在 2000 年开发的一个开源代码的操作系统。

Darwin mac OS 和 iOS 操作系统的核心。

Darwin 使用了一种微内核(Mach)和响应的固件来支持不同的处理器平台,并提供操作系统原始的基础服务,上册功能性系统服务和工具则是整合了 BSD 系统所提供的。苹果公司为其开发了大量的库、框架和服务,但它们都工作在用户态且闭源。

Darwin 架构:

img

内核层为用户转换层以下的部分,它有两个内核层——Mach 层与 BSD 层。

Mach 内核(微内核)是卡耐基梅隆大学开发的经典微内核,意在提供最基本的操作系统服务。

Mach 提供简单的进程、线程、IPC通信、虚拟内存设备驱动相关的功能服务。

BSD 则是伯克利大学开发的类 UNIX 操作系统,提供一整套操作系统服务,它们工作在用户态且闭源。

BSD 提供强大的安全特性,完善的网络服务,各种文件系统的支持,同时对 Mach 的进程、线程、IPC 、虚拟内核组建进行细化、拓展延伸。

应用 Darwin 系统的服务

在调用 Darwin 系统 API 时,会传入一个API 号码,此号码用以索引 Mach 陷入中断服务表中的函数,若 API 号码小于0,则表明请求 Mach 内核服务,API 若大于0,表明请求 BSD 内核服务。

Mach 中还有一个重要组建 Libkern,一个库文件,提供了底层的操作函数,同时支持 C++ 运行环境。

Windows NT 内核

微软基于 MS- DOS 内核实现了 windows 3.1、windows 95/98/ME ,不稳定,容易死机。

WIndows NT 为1993年微软推出的面形工作站、网络服务器和大型计算机的网络操作系统,也可作为 PC 操作系统。 NT 为 New Technology。

普通用户第一次接触基于 NT 内核的是 Windows 2000,存在对于用户的硬件和应用程序的兼容性的问题。

随着硬件厂商和应用厂商对于程序的升级,这个兼容性的问题被缓解了,此后的 Windows XP 取得了巨大的成功。

Windows NT 内核:

img

微软自己在 HAL 层上是定义了一个小内核,小内核之下是硬件抽象层 HAL,这个 HAL 存在的好处是:不同的硬件平台只要提供对应的 HAL 就可以移植系统了。小内核之上是各种内核组件,微软称之为内核执行体,它们完成进程、内存、配置、I/O 文件缓存、电源与即插即用、安全等相关的服务。

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

设备驱动和文件系统由 I/O 管理器统一管理,驱动程序可以堆叠形成 I/O驱动栈,功能请求被封装成 I/O包,在栈中一层层流动处理。

Windows 引以为傲的图形子系统也在内核中。

NT 内核中各层次分明,各个执行体相互独立,“高内聚,低耦合”。

Linux 性能良好,结构异常复杂,不利于问题的排查和功能的拓展,而 Darwin- XNU 和 Windows 结构良好,层面分明,利于功能拓展,不容易产生问题且性能稳定。

Windows NT 为混合内核。

硬件

CPU 工作模式

CPU 的工作模式有实模式、保护模式、长模式

实模式

又称地址模式,一方面是运行真正的指令,对指令的动作不作区分,直接执行指令的真实功能,另一方面是发往内存的地址是真实的,对任何地址不加限制地发往内存。

实模式寄存器

x86 CPU 在实模式下的寄存器。

img

实模式下访问内存

通常情况下,需要把数据装载进寄存器中才能操作,还要获取指令的动作,这些操作都需要访问内存,而访问内存靠的是地址值。而地址值的计算方法:

img

所有的内存地址都是由段寄存器左移 4 位,再加上一个通用寄存器中的值或者常数形成地址,然后由这个地址去访问内存。这就是大名鼎鼎的分段内存管理模型。

实模式中断

中断即终止执行当前程序,转而跳转到另一个特定的地址上,去运行特定的代码。在实模式下它的实现过程是先保存 CS 和 IP寄存器,然后装载新的 CS 和 IP 寄存器。

中断的产生:

  • 硬件中断:中断控制器 CPU 发送一个电子信号,CPU 会对这个信号作出应答,随后中断控制器会将中断号发送给 CPU 。

  • 软件中断,INT 指令中断:CPU 执行一个 INT 指令,这个指令后面会跟随一个常数,这个常数即为软中断号。

实现中断,需要内存中的一个中断向量表,表地址和长度由 CPU 特定寄存器 IDTR 指向,实模式下,表中的一个条目由代码地址和段内偏移组成:

img

有了中断号以后, CPU 就能根据 IDTR 寄存器中的信息,计算出中断向量中的条目,进而装载 CS(装入代码段基地址)、IP(装入代码段内偏移)寄存器,最终响应中断。

保护模式

由于 CPU 的特性,CPU 对任何指令不加区分地执行;CPU 对访问内存的地址不加限制。

随多道程序的出现,内存需求量的不断增加,CPU 实现了保护模式以支持需求的增加。

保护模式包含特权级,对指令及其访问的资源进行控制,对内存段与段之间的访问进行严格检查,没有权限的绝不放行,对中断的响应也要进行严格的权限检查,扩展了 CPU 寄存器位宽,使之能够寻址 32 位的内存地址空间和处理 32 位的数据,从而 CPU 的性能大大提高。

长模式

长模式又名 AMD64,因为这个标准是 AMD 公司最早定义的,它使 CPU 在现有的基础上有了 64 位的处理能力,既能完成 64 位的数据运算,也能寻址 64 位的地址空间。这在大型计算机上犹为重要,因为它们的物理内存通常有几百 GB。

长模式 弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU。

程序中的地址如何转换

内存相关的这几个核心问题:

  1. 谁来保证程序 A 跟程序 B 没有内存地址的冲突?换句话说,就是程序 A、B 各自放在什么内存地址,这个问题是由 A、B 程序协商,还是由操作系统决定。2. 怎样保证程序 A 跟程序 B 不会互相读写各自的内存空间?这个问题相对简单,用保护模式就能解决。3. 如何解决内存容量问题?程序 A 和程序 B,在不断开发迭代中程序代码占用的空间会越来越大,导致内存装不下。4. 还要考虑一个扩展后的复杂情况,如果不只程序 A、B,还可能有程序 C、D、E、F、G……它们分别由不同的公司开发,而每台计算机的内存容量不同。这时候,又对我们的内存方案有怎样的影响呢?要想完美地解决以上最核心的 4 个问题,一个较好的方案是:让所有的程序都各自享有一个从 0 开始到最大地址的空间,这个地址空间是独立的,是该程序私有的,其它程序既看不到,也不能访问该地址空间,这个地址空间和其它程序无关,和具体的计算机也无关。

虚拟地址

虚拟地址是逻辑上存在的一个数据值。Hello World 的例子:使用 objdump 工具反汇编为二进制文件会得到如下代码片段:

1
2
3
4
5
6
7
8
9

00000000000004e8 <_init>:
4e8: 48 83 ec 08 sub $0x8,%rsp
4ec: 48 8b 05 f5 0a 20 00 mov 0x200af5(%rip),%rax # 200fe8 <__gmon_start__>
4f3: 48 85 c0 test %rax,%rax
4f6: 74 02 je 4fa <_init+0x12>
4f8: ff d0 callq *%rax
4fa: 48 83 c4 08 add $0x8,%rsp
4fe: c3 retq

第三列中的 mov 0x200af5… 指令中的数据就是虚拟地址。

所有应用程序的开始部分都是如上这样的,因为每个应用程序的虚拟地址空间都是相同且独立的。

这个地址的产生是由链接器产生的,软件开发经过编译步骤后,就需要链接成可执行文件才可以运行,而链接器的主要工作就是把多个代码模块组装到一起,解决模块之间的应用,即处理程序代码间的地址引用,形成程序运行的静态内存空间视图。

上述的地址是虚拟而统一的,根据操作系统的不同,这个虚拟地址空间的定义可能不同,应用软件开发人员无需关心,由开发工具链给自动处理。

物理地址

虚拟地址只是逻辑上存在的地址,无法用于硬件电路,程序装进内存中想要执行就需要和内存打交道,从内存中取得指令和数据,而内存只认一种地址,即物理地址。

物理地址在即地址总线上的信号,无力地址在逻辑上也是一个数据,只是这个数据会被地址译码器等电子器件变成电子信号,放在地址总线上,地址总线电子信号的各种组合就可以选择到内存的存储单元。

虚拟地址到物理地址的转换

虚拟地址必须转换成物理地址,这样程序才能正常执行。即,输入虚拟地址,输出物理地址。

软件的方式太低效,单纯的硬件实现没有灵活性,最终通过软硬件结合的方式进行实现,它就是 MMU(内存管理单元)。MMU 可以接受软件给出的地址对应关系数据,进行地址转换。

img

MMU 通过地址关系转换表,将 0x80000~0x84000 的虚拟地址空间转换成 0x10000~0x14000 的物理地址空间,而地址关系转换表本身则是放物理内存中的。问题来了,32 位地址空间下,4GB 虚拟地址的地址关系转换表就会把整个 32 位物理地址空间用完,这显然不行。

保护模式下分段方式,地址关系转换表中存放:一个虚拟段基址对应一个物理段基址,这样看似可以,但是因为段长度各不相同,所以依然不可取。

系统设计者最后采用一个折中的方案,即把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。根据软件配置不同,这个页的大小可以设置为 4KB、2MB、4MB、1GB,这样就进入了现代内存管理模式——分页模型。

img

一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放虚拟页地址对应的物理页地址就行了。

MMU

MMU 即内存管理单元,使用硬件电路逻辑实现的一个地址转换器件。它负责接收虚拟地址和地址关系转换表,以及输出物理地址。

根据实现方式的不同,MMU 可以是独立的芯片,也可以是集成在其它芯片内部的,比如集成在 CPU 内部,x86、ARM 系列的 CPU 就是将 MMU 集成在 CPU 核心中的。

SUN 公司的 CPU 将独立的 MMU芯片卡在总线上。x86 CPU 要想开启 MMU,就必须先开启保护模式或者长模式,实模式下是不能开启 MMU 的。

由于保护模式的内存模型是分段模型,它不适合于MMU的分页模型,要使用保护模式的平坦模式,这样就绕过了分段模型。平潭模型和长模式下忽略段基址和段长度是异曲同工的。

CPU 地址转换图:
img

程序代码中的虚拟地址经过 CPU 的分段机制产生了线性地址,平坦模式和长模式下线性地址和虚拟地址是相等的。

如果不开启 MMU,在保护模式下可以关闭 MMU,这个线性地址就是物理地址。因为长模式下的分段 弱化了地址空间的隔离,所以开启 MMU 是必须要做的,开启 MMU 才能访问内存地址空间。

MMU页表

地址关系转换表有一个更加专业的名字——页表,它描述了虚拟地址到物理地址的转换关系,可以说是虚拟页到物理页的映射关系。

为了增加灵活性和节约物理内存空间(因为页表是放在物理内存中的),所以页表中并不存放虚拟地址和物理地址的对应关系,只存放物理页面的地址,MMU 以虚拟地址为索引去查表返回物理页面地址,而且页表是分级的,总体分为三个部分:一个顶级页目录,多个中级页目录,最后才是页表,逻辑结构图如下.

img

虚拟地址被分成从左到右四个位段:

第一位索引顶级目录中的一项 ——> 中级页目录,第二位段去索引中页级目录中的一项 ——> 一个页目录,第三个位段去索引页目录中的项 ——> 物理页地址,最后用第四位段作该物理页内的偏移去访问物理内存。

保护模式下的分页

分页模式的灵活性、通用性、安全性是现代操作系统内存管理的基石,更是事实上的标准内存管理模型,现代商用操作系统必须都以此为基础实现虚拟内存功能模块。

保护模式下的分页

保护模式下只有32位地址空间,最多4GB - 1 大小的空间。

保护模式下的分页大小通常有两种,一种是 4KB 大小的页,一种是 4MB 大小的页。分页大小的不同会导致虚拟地址位段的分隔和页目录的层级不同,但虚拟页和物理页的大小始终是相同的。

保护模式下的分页 —— 4KB 页

该分页方式下,32 位虚拟地址被分为三个位段:页目录索引、页表索引、页内偏移,只有一级页目录,其中包含 1024 个条目 ,每个条目指向一个页表,每个页表中有 1024 个条目。其中一个条目就指向一个物理页,每个物理页 4KB。这正好是 4GB 地址空间。如下图所示。

img

CR3 为 CPU 的一个32位寄存器,MMU 就是根据这个寄存器找到页目录。

img

页目录项、页表项都是4字节32位,1024项正好是4KB(一个页),因此它们的地址始终都是4KB对齐的,所以低12位才可以另作他用,形成页面的相关属性,如是否存在、是否可读可写、是用户页还是内核页、是否已写入、是否已访问等。

保护模式下的分页——4MB页

该分页方式下,32 位虚拟地址被分为两个位段:页表索引、页内偏移,只有一级页目录,其中包含 1024 个条目。其中一个条目指向一个物理页,每个物理页 4MB,正好为 4GB 地址空间,如下图所示。

img

CR3 32位寄存器,不再只想顶级页目录,而是指向一个4KB大小的页表,这个页表依然要4KB地址对齐,其中包含1024个页表项:

img

4MB 大小的页面下,页表项还是 4 字节 32 位,但只需要用高 10 位来保存物理页面的基地址就可以。因为每个物理页面都是 4MB,所以低 22 位始终为 0,为了兼容 4MB 页表项低 8 位和 4KB 页表项一样,只不过第 7 位变成了 PS 位,且必须为 1,而 PAT 位移到了 12 位。

长模式下的分页

如果开启长模式,则必须开启分页模式,因为长模式弱化了分页模式,而分段模型也有很多不足,不适应现在操作系统和应用软件的发展。
同时,长模式也拓展了 CPU 的位宽,使得 CPU 能使用64位的超大内存地址空间。所以长模式下的虚拟地址必须等于线性地址且为64位。
长模式下的分页通常也有两种,4KB大小的页和2MB大小的页。

长模式下的分页——4KB

该分页方式下,64位虚拟地址被分为6个位段,分别是:保留位段,顶级页目录索引、页目录指针索引、页目录索引、页表索引、页内便宜、顶级页目录、页目录指针、页目录、页表各占有4KB大小,其中各有512个条目,每个条目8字节,64位大小。
img

CR3 变为64 位的64位的CPU寄存器,它指向一个顶级页目录,里面的顶级页目录项指向目录指针,依次类推。
需要注意的是,虚拟地址 48 到 63 这 16 位是根据第 47 位来决定的,47 位为 1,它们就为 1,反之为 0,这是因为 x86 CPU 并没有实现全 64 位的地址总线,而是只实现了 48 位,但是 CPU 的寄存器却是 64 位的。
这种最高有效位填充的方式,即使后面拓展CPU的地址总线也不会有任何影响。

img

长模式下的4KB分页下,由一个顶层目录、二级中间层目录和一层页表组成了 64 位地址翻译过程。

长模式下的分页——2MB
在这种分页方式下,64 位虚拟地址被分为 5 个位段 :保留位段、顶级页目录索引、页目录指针索引、页目录索引,页内偏移,顶级页目录、页目录指针、页目录各占有 4KB 大小,其中各有 512 个条目,每个条目 8 字节 64 位大小。

img

长模式下 2MB 和 4KB 分页的区别是,2MB 分页下是页目录项直接指向了 2MB 大小的物理页面,放弃了页表项,然后把虚拟地址的低 21 位作为页内偏移,21 位正好索引 2MB 大小的地址空间。
2MB 分页模式下的 CR3、顶级页目录项、页目录指针项、页目录项的格式,格式如下图:

img

上图中没有了页表项,取而代之的是,页目录项中直接存放了 2MB 物理页基地址。由于物理页始终 2MB 对齐,所以其地址的低 21 位为 0,用于存放页面属性位。

开启MMU
要使用分页模式就必须先开启MMU,但开启MMU的前提是CPU进入保护模式或者长模式,开启CPU这两种模式的方法:

  • 使 CPU 进入保护模式或者长模式

  • 准备好页表数据,这包含顶级页目录,中间层页目录,页表,物理内存中会生成这些数据

  • 顶级页目录的物理内存地址赋值给 CR3 寄存器

    1
    2
    mov eax, PAGE_TLB_BADR ;页表物理地址
    mov cr3, eax
  • 设置 CPU 的 CR0 的 PE 为 1 即可开启 MMU

    1
    2
    3
    4
    5
    6

    ;开启 保护模式和分页模式
    mov eax, cr0
    bts eax, 0 ;CR0.PE =1
    bts eax, 31 ;CR0.P = 1
    mov cr0, eax

MMU地址转换失败

MMU 主要功能即根据页表数据把虚拟地址转换为物理地址,失败的情况:页表项中的数据为空,用户程序访问了超级管理者的页面,向只读页面中写入了数据,这些都会导致 MMU 地址转换失败。

MMU 地址转换失败的操作:

  • MMU 停止转换地址
  • MMU 把转换失败的虚拟地址写入 CPU 的 CR2 寄存器
  • MMU 触发 CPU 的14号中断,使 CPU 停止执行当前指令
  • CPU 开始执行14号中断的处理代码,代码会检查原因,处理好页表数据返回
  • CPU 中断返回继续执行 MMU 地址转换失败时的指令

总结

多道程序同时运行有很多问题,内存需要隔离和保护,从而提出了虚拟地址与物理地址分离,让应用程序从实际物理内存中解耦出来。

MMU(内存管理单元),增加了转换的灵活性,实现方式是硬件执行转换过程,但又依赖于软件提供的地址转换表。

操作系统是如何对应用程序的地址空间进行隔离的?

多个进程隔离应该是操作系统进行任务切换时会改写CPU的页表基地址寄存器为当前被运行进程的页表基地址。对于每个进程而言,它会误认为(被操作系统欺骗)自己独有占用的地址空间,因此它访问地址是不会考虑任何问题的,但这个地址是虚拟地址,待被 MMU 翻译后会得到对应的页表,而这个页表由操作系统管理,不同的进程拥有不同的页表,因此产生了进程地址空间隔离,但是多个进程也是可以共享某个页表,这也是进程通信(IPC)的根本手段。

CATALOG
  1. 1. OS-Learning: Linux Homebrew
  • 准备工作
    1. 1. PC OS 引导流程
    2. 2. 控制计算机的屏幕
    3. 3. Hello OS 整个编译过程
  • 设计蓝图
    1. 1. 内核逻辑
    2. 2. 宏内核结构
    3. 3. 微内核结构
    4. 4. 内核的三个大层
  • Linux 内核
  • Darwin-XNU 内核
    1. 1. Windows NT 内核
  • 硬件
    1. 1. CPU 工作模式
    2. 2. 程序中的地址如何转换