重拾保护模式----整理一下笔记
标题可能起的有点大了。总之本文是对进入保护模式过程的一些针对性复习,主要是为了写代码做的准备。
实模式的一些特点
16 位数据总线、20 位地址总线,1MB 寻址能力。
寻址方式:Physical Address = Segment * 16 + Offset
实模式的进化
原因:自 80386 时代开始,Intel 的 CPU 进化到 32 位。寻址能力上升到 4GB。
解决方法:保护模式下的地址依然使用Segment:Offset的形式来表示,但其中段的含义发生了变化:段值的含义由原来的地址的一部分(这么称呼是因为实模式的段值直接参与了地址的计算)变为一个数据结构表(GDT)的其中一个表项(描述符)的指针。该结构包含一些属性,用于描述段的一些性质。
GDT 相关
每个 GDT 描述符占 8 字节,包括了段属性、段基地址、段界限三个部分。其中基址和界限根据名字就可理解,属性一共使用了 2 字节(里面有 4bit 是段界限的内容。由于历史原因,GDT 描述符的结构很混乱)来表示段的一些特征。(用电视分割线来标注代码了)

;从上到下对应地址从低到高
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh); 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节

上述代码利用 NASM 的宏定义了一个描述符的数据结构。
这里提一下 NASM 的语法,Descriptor 3中的 3 表示数据结构里有三个变量需要传入参数(用%1,%2,%3 来表示)。使用时,按照下列代码的格式就可以传入参数完整构造一个实例化的描述符结构。

LABEL_GDT: Descriptor 0, 0, 0
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32
;B8000h处为显存基地址
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW

上面的代码创建了三个描述符,对应定义了三个段。

mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
============分割线======================
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
============分割线======================
mov [gs:edi], ax

前两行将SelectorVideo移动到了 gs 中,最下面那一行代码的`[gs:edi]`实际上就等于[SelectorVideo:edi]。
一个选择子结构的 3-15 位是描述符的索引,根据定义可以看到是相对于一个描述符相对于 LABEL_GDT 的偏移量。选择子指向一个段描述符,正如上节“实模式的进化”中提到的那样。
上述的寻址计算过程结束后将得到一个线性地址。总体寻址过程可以描述为:`SEG:OFFSET <=> GDT.BASE + OFFSET`,其中`GDT.BASE`正是由`SEG`索引得到。

[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4 ;左移4位,代码段首地址
add eax, LABEL_SEG_CODE32 ;eax保存描述符地址
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah ;使用32位存放描述符地址
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
; END of [SECTION .s16]

上面一段代码为进入保护模式做了一些准备工作:
初始化保护模式代码段:首先把.s32代码段的物理地址存入eax,接着将这个地址拆分存入32位代码段描述符,这里只操作了DESC_CODE32的段基址,段界限和属性在前面已经定义过。后面的`xor eax, eax`意味着DESC_CODE32已经完成了赋值,现在该描述符已经可以描述保护模式下的地址了。
接下来是将GDT基地址复制到gdtptr这个结构中(gdtptr的数据结构设计和gdtr寄存器的访问方式是完全一致的),然后使用lgdt指令将指针送入gdtr寄存器,这步结束后,就可以通过寄存器很方便地访问GDT了。
A20
关于这部分的详细介绍在ucore第一章的笔记里。(https://bwonsamdi.github.io 专栏投稿貌似不能加站外链接)
对于实模式的地址,其最大寻址空间为1MB,但可表示的最大地址为FFFF:FFFF(即0x10ffef),比1MB多了0xffef,实模式在遇到这种情况时会采取回滚机制。
随着计算机的发展,地址线位数增加后,既要允许更大的地址空间访问,又要保证对原20根地址线的兼容,因此IBM使用键盘控制器来控制第20根地址线,具体控制过程不再描述。
进入保护模式
首先把CR0寄存器第0位置1,表示CPU切换到保护模式,此时cs的值仍为实模式的地址。
接下来将事先初始化后的32位代码段选择子复制到CS中,这个过程借助jmp指令实现。这条指令本身处于16位的代码段,但是跳转目的地是32位的地址,因此诸如SelectorCode32:0x11112222会被截断高位,只剩下0x2222。
在Linux中,对于上述问题的解决方案,采用了使用DB直接写入机器码强制执行的方法,在NASM中,支持利用jmp dword SelectorCode32:0的方式(加上dw)来解决。
特权级
Current Privilege Level
CPL是当前执行的程序或任务的特权级,存储在CS和SS的0位和1位上,通常情况下CPL等于代码所在段的特权级。当程序转移到不同特权级的代码段时,CPL将会改变。
一致代码段可以被相同或更低级的代码访问。访问与CPL不同的一致代码段时,CPL不会改变。
Descriptor Privilege Level
DPL表示段或门的特权级,存储在段描述符或门描述符的DPL字段中。当前代码段访问段或门时,DPL会与CPL、段或门选择子的RPL进行比较,根据段或门的类型,DPL会被区别对待
数据段:DPL规定了可以访问该段的最低特权级。若数据段的DPL为1,则CPL为0或1的程序可以访问此段。
没有调用门的非一致代码段:DPL规定了可以访问该段的特权级。
调用门:与数据段一致。
一致代码段/通过调用门访问的非一致代码段:DPL规定了可以访问该段的最高特权级。若段DPL为1,则CPL为2或3的低特权代码段可以访问该段。
TSS:和数据段一致。
Requested Privilege Level
RPL是段选择子的0位和1位,表示当前进程想要的请求权限。通过RPL和CPL可以确认访问请求的合法性。即便段的特权级足够,也要考虑RPL的级别,即访问合法性的判断取决于CPL和RPL中特权级最低的那个(数字最大)。
操作系统使用RPL避免低特权级应用程序访问高特权级段数据。当被调用过程(操作系统过程)从调用过程(应用程序)接收到选择子时,会把选择子的RPL设置成调用者的特权级,当操作系统使用该选择子访问对应段时,处理器会用已经被存到RPL的调用过程的特权级,而不是CPL进行检验。
特权级总结
调用门本质上是一个描述符,长8字节。一个门描述了由一个选择子和一个偏移指定的线性地址。
一般通过call/jmp加上远指针的方式访问调用门。这个远指针的段选择子用于指定调用门。
通过调用门进行程序控制流的段转移时,CPU会按以下顺序检查:
当前代码段的CPL
调用门描述符的DPL
调用门描述符的RPL
目的代码段描述符的DPL
目的代码段描述符的一致性标志C
同时,对于call和jmp,优先级检查规则也不同:
对于call指令
CPL<=调用门描述符DPL
RPL<=调用门描述符DPL
CPL>=目的代码段描述符DPL
对于jmp指令
CPL<=调用门描述符DPL
RPL<=调用门描述符DPL
如果目的代码段为一致代码段:CPL>=目的代码段描述符DPL
如果目的代码段为非一致代码段:CPL=目的代码段描述符DPL
4. 调用门的作用是使得一个代码段被不同特权级的程序访问。
5. 一致代码段:属于内核,允许用户访问的代码段。对于一致性代码来说,特权级高的程序不允许访问特权级低的数据(内核不能访问用户数据),特权级低的代码可以访问到特权级高的数据,但访问中**特权级不会改变**,访问内核的程序依然属于用户态。
6. 非一致性代码段:仅允许同级访问,内核态只能访问内核代码,用户态只能访问用户代码。
7. 通常低特权代码必须通过门来实现对高特权代码的访问。
8. RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。RPL的值由程序员自己来自由的设置,并不一定RPL>=CPL,
9. 当RPL < CPL时,实际起作用的就是CPL了。当选择子成功装入CS寄存器后,相应的选择子中的RPL就变成了CPL。
10. 在call调用门时,需要进行堆栈转移,堆栈转移需要借助TSS实现。
11. TSS是一个包含了多个字段的数据结构,其中包括了ring0-ring3的栈区指针,利用这个结构,可以将栈参数复制。
后记
关于特权级的部分,看得不是很详细,以后需要再特别写一篇文章来按位解析GDT、选择子、调用门、TSS这些数据结构,否则在理解一些机制的时候常常会晕头转向。这一部分的一些细节的地方就暂时搁置起来,目前距离脱离汇编进入C编程还有很长一段路要走。
参考
《自己动手写操作系统》
https://www.cnblogs.com/bamboos/archive/2009/03/26/1422041.html

专栏投稿属实难用,什么时候可以支持markdown啊......
另外github求一波关注......最近在做xv6的实验,不过暂时还没有放到远程仓库里。
近期的目标是xv6实验和linux kernel的实验
五月份开始学校就没课了,那个时候该专门琢磨考研了。要是大家能多投几个币,说不定我就有动力把实验都做完了呢

要是看到这里了,就投个票吧
