CPU的工作模式


实模式

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

实模式寄存器

由于 CPU 是根据指令完成相应的功能,举个例子:ADD AX,CX;这条指令完成加法操作,AX、CX 为 ADD 指令的操作数,可以理解为 ADD 函数的两个参数,其功能就是把 AX、CX 中的数据相加。指令的操作数,可以是寄存器、内存地址、常数,其实通常情况下是寄存器,AX、CX 就是 x86 CPU 中的寄存器。下面我们就去看看 x86 CPU 在实模式下的寄存器。表中每个寄存器都是 16 位的。

实模式下的寄存器

实模式下访问内存

虽然有了寄存器,但是数据和指令都是存放在内存中的。通常情况下,需要把数据装载进寄存器中才能操作,还要有获取指令的动作,这些都要访问内存才行,而我们知道访问内存靠的是地址值。

那问题来了,这个值是如何计算的呢?计算过程如下图。

实模式下内存访问方式

结合上图可以发现,所有的内存地址都是由段寄存器左移 4 位,再加上一个通用寄存器中的值或者常数形成地址,然后由这个地址去访问内存。这就是大名鼎鼎的分段内存管理模型。只不过这里要特别注意的是,代码段是由 CS 和 IP 确定的,而栈段是由 SS 和 SP 段确定的。

实模式中断

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

第一种情况是,中断控制器给 CPU 发送了一个电子信号,CPU 会对这个信号作出应答。随后中断控制器会将中断号发送给 CPU,这是硬件中断

第二种情况就是 CPU 执行了 INT 指令,这个指令后面会跟随一个常数,这个常数即是软中断号。这种情况是软件中断

无论是硬件中断还是软件中断,都是 CPU 响应外部事件的一种方式。为了实现中断,就需要在内存中放一个中断向量表,这个表的地址和长度由 CPU 的特定寄存器 IDTR 指向。实模式下,表中的一个条目由代码段地址和段内偏移组成,如下图所示。

中断向量表

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

下面我们写一个 DOS 下的 Hello World 应用程序,这是一个工作在实模式下的汇编代码程序,一共 16 位,具体代码如下:

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设置参数09H,AH是AX高8位,AL是AX低8位,其它类似
    INT 21h      ;执行DOS中断输出DS指向的DX指向的字符串hello
    MOV AX,4C00h ;给AX设置参数4C00h
    INT 21h      ;调用4C00h号功能,结束程序
code ENDS
END start

大多数是操作寄存器,其中 LEA 是取地址指令,MOV 是数据传输指令, INT 中断指令。

INT 21H 指令说明及使用方法

例如:需要键盘输入,并且回显。

MOV AH,01
INT 21H

通过这样两条指令,输入的字符就会被存储在AL中,AH的值需要查表取得,表在下面:

AH 功能 调用参数 返回参数
00 程序终止(同INT 20H) CS=程序段前缀
01 键盘输入并回显 AL=输入字符
02 显示输出 DL=输出字符
03 异步通迅输入 AL=输入数据
04 异步通迅输出 DL=输出数据
05 打印机输出 DL=输出字符
06 直接控制台I/O DL=FF(输入) DL=字符(输出) AL=输入字符
07 键盘输入(无回显) AL=输入字符
08 键盘输入(无回显)检测Ctrl-Break AL=输入字符
09 显示字符串DS:DX=串地址 ‘$’结束字符串
0A 键盘输入到缓冲区DS:DX=缓冲区首地址 (DS:DX)=缓冲区最大字符数 (DS:DX+1)=实际输入的字符数
0B 检验键盘状态 AL=00 有输入 AL=FF 无输入
0C 清除输入缓冲区并 请求指定的输入功能 AL=输入功能号(1,6,7,8,A)
0D 磁盘复位 清除文件缓冲区
0E 指定当前缺省的磁盘驱动器 DL=驱动器号 0=A,1=B,… AL=驱动器数
0F 打开文件 DS:DX=FCB首地址 AL=00 文件找到 AL=FF 文件未找到
10 关闭文件 DS:DX=FCB首地址 AL=00 目录修改成功 AL=FF 目录中未找到文件
11 查找第一个目录项DS:DX=FCB首地址 AL=00 找到 AL=FF 未找到
12 查找下一个目录项 DS:DX=FCB首地址(文件中带有*或?) AL=00 找到 AL=FF 未找到
13 删除文件 DS:DX=FCB首地址 AL=00 删除成功 AL=FF 未找到
14 顺序读 DS:DX=FCB首地址 AL=00 读成功 =01 文件结束,记录中无数据 =02 DTA空间不够 =03 文件结束,记录不完整
15 顺序写 DS:DX=FCB首地址 AL=00 写成功 =01 盘满 =02 DTA空间不够
16 建文件 DS:DX=FCB首地址 AL=00 建立成功 =FF 无磁盘空间
17 文件改名 DS:DX=FCB首地址 (DS:DX+1)=旧文件名 (DS:DX+17)=新文件名
19 取当前缺省磁盘驱动器 AL=缺省的驱动器号 0=A,1=B,2=C,…
1A 置DTA地址 DS:DX=DTA地址
1B 取缺省驱动器FAT信息 AL=每簇的扇区数 DS:BX=FAT标识字节 CX=物理扇区大小 DX=缺省驱动器的簇数
1C 取任一驱动器FAT信息 DL=驱动器号 同上
21 随机读 DS:DX=FCB首地址 AL=00 读成功 =01 文件结束 =02 缓冲区溢出 =03 缓冲区不满
22 随机写 DS:DX=FCB首地址 AL=00 写成功 =01 盘满 =02 缓冲区溢出
23 测定文件大小 DS:DX=FCB首地址 AL=00 成功(文件长度填入FCB) AL=FF 未找到
24 设置随机记录号 DS:DX=FCB首地址
25 设置中断向量 DS:DX=中断向量AL=中断类型号
26 建立程序段前缀 DX=新的程序段前缀
27 随机分块读 DS:DX=FCB首地址 CX=记录数 AL=00 读成功 =01 文件结束 =02 缓冲区太小,传输结束 =03 缓冲区不满
28 随机分块写 DS:DX=FCB首地址 CX=记录数 AL=00 写成功 =01 盘满 =02 缓冲区溢出
29 分析文件名 ES:DI=FCB首地址 DS:SI=ASCIIZ串 AL=控制分析标志 AL=00 标准文件 =01 多义文件 =02 非法盘符
2A 取日期 CX=年 DH:DL=月:日(二进制)
2B 设置日期 CX:DH:DL=年:月:日 AL=00 成功 =FF 无效
2C 取时间 CH:CL=时:分 DH:DL=秒:1/100秒
2D 设置时间 CH:CL=时:分 DH:DL=秒:1/100秒 AL=00 成功 =FF 无效
2E 置磁盘自动读写标志 AL=00 关闭标志 AL=01 打开标志
2F 取磁盘缓冲区的首址 ES:BX=缓冲区首址
30 取DOS版本号 AH=发行号,AL=版本
31 结束并驻留 AL=返回码 DX=驻留区大小
33 Ctrl-Break检测 AL=00 取状态 =01 置状态(DL) DL=00 关闭检测 =01 打开检测 DL=00 关闭Ctrl-Break检测 =01 打开Ctrl-Break检测
35 取中断向量 AL=中断类型 ES:BX=中断向量
36 取空闲磁盘空间 DL=驱动器号 0=缺省,1=A,2=B,… 成功:AX=每簇扇区数 BX=有效簇数 CX=每扇区字节数 DX=总簇数 失败:AX=FFFF
38 置/取国家信息 DS:DX=信息区首地址 BX=国家码(国际电话前缀码) AX=错误码
39 建立子目录(MKDIR) DS:DX=ASCIIZ串地址 AX=错误码
3A 删除子目录(RMDIR) DS:DX=ASCIIZ串地址 AX=错误码
3B 改变当前目录(CHDIR) DS:DX=ASCIIZ串地址 AX=错误码
3C 建立文件 DS:DX=ASCIIZ串地址 CX=文件属性 成功:AX=文件代号 错误:AX=错误码
3D 打开文件 DS:DX=ASCIIZ串地址 AL=0 读 =1 写 =3 读/写 成功:AX=文件代号 错误:AX=错误码
3E 关闭文件 BX=文件代号 失败:AX=错误码
3F 读文件或设备 DS:DX=数据缓冲区地址 BX=文件代号 CX=读取的字节数 读成功: AX=实际读入的字节数 AX=0 已到文件尾 读出错:AX=错误码
40 写文件或设备 DS:DX=数据缓冲区地址 BX=文件代号 CX=写入的字节数 写成功: AX=实际写入的字节数 写出错:AX=错误码
41 删除文件 DS:DX=ASCIIZ串地址 成功:AX=00
42 移动文件指针 BX=文件代号 CX:DX=位移量 AL=移动方式(0:从文件头绝对位移,1:从当前位置相对移动,2:从文件尾绝对位移) 成功:DX:AX=新文件指针位置 出错:AX=错误码
43 置/取文件属性 DS:DX=ASCIIZ串地址 AL=0 取文件属性AL=1置文件属性CX=文件属性 成功:CX=文件属性 失败:CX=错误码
44 设备文件I/O控制 BX=文件代号 AL=0 取状态 =1 置状态DX =2 读数据 =3 写数据 =6 取输入状态 =7 取输出状态 DX=设备信息
45 复制文件代号 BX=文件代号1 成功:AX=文件代号2 失败:AX=错误码
46 人工复制文件代号 BX=文件代号1 CX=文件代号2 失败:AX=错误码
47 取当前目录路径名 DL=驱动器号 DS:SI=ASCIIZ串地址 (DS:SI)=ASCIIZ串 失败:AX=出错码
48 分配内存空间 BX=申请内存容量 成功:AX=分配内存首地 失败:BX=最大可用内存
49 释放内容空间 ES=内存起始段地址 失败:AX=错误码
4A 调整已分配的存储块 ES=原内存起始地址 BX=再申请的容量 失败:BX=最大可用空间 AX=错误码
4B 装配/执行程序 DS:DX=ASCIIZ串地址 ES:BX=参数区首地址 AL=0 装入执行 AL=3 装入不执行 失败:AX=错误码
4C 带返回码结束 AL=返回码
4D 取返回代码 AX=返回代码
4E 查找第一个匹配文件 DS:DX=ASCIIZ串地址 CX=属性 AX=出错代码(02,18)
4F 查找下一个匹配文件 DS:DX=ASCIIZ串地址(文件名中带有?或*) AX=出错代码(18)
54 取盘自动读写标志 AL=当前标志值
56 文件改名 DS:DX=ASCIIZ串(旧) ES:DI=ASCIIZ串(新) AX=出错码(03,05,17)
57 置/取文件日期和时间 BX=文件代号AL=0 读取 AL=1设置(DX:CX) DX:CX=日期和时间 失败:AX=错误码
58 取/置分配策略码 AL=0 取码 AL=1 置码(BX) 成功:AX=策略码 失败:AX=错误码
59 取扩充错误码 AX=扩充错误码 BH=错误类型 BL=建议的操作 CH=错误场所
5A 建立临时文件 CX=文件属性 DS:DX=ASCIIZ串地址 成功:AX=文件代号 失败:AX=错误码
5B 建立新文件 CX=文件属性 DS:DX=ASCIIZ串地址 成功:AX=文件代号 失败:AX=错误码
5C 控制文件存取 AL=00封锁 =01开启 BX=文件代号 CX:DX=文件位移 SI:DI=文件长度 失败:AX=错误码
62 取程序段前缀 BX=PSP地址

保护模式

随着软件的规模不断增加,需要更高的计算量、更大的内存容量。内存一大,首先要解决的问题是寻址问题,因为 16 位的寄存器最多只能表示 216 个地址,所以 CPU 的寄存器和运算单元都要扩展成 32 位的。

保护模式寄存器

保护模式相比于实模式,增加了一些控制寄存器和段寄存器,扩展通用寄存器的位宽,所有的通用寄存器都是 32 位的,还可以单独使用低 16 位,这个低 16 位又可以拆分成两个 8 位寄存器,如下表。

保护模式下寄存器组成

保护模式特权级

为了区分哪些指令(如 in、out、cli)和哪些资源(如寄存器、I/O 端口、内存地址)可以被访问,CPU 实现了特权级。特权级分为 4 级,R0~R3,每个特权级执行指令的数量不同,R0 可以执行所有指令,R1、R2、R3 依次递减,它们只能执行上一级指令数量的子集。而内存的访问则是靠后面所说的段描述符和特权级相互配合去实现的。如下图:

CPU特权示意图

上面的圆环图,从外到内,既能体现权力的大小,又能体现各特权级对资源控制访问的多少,还能体现各特权级之间的包含关系。R0 拥有最大权力,可以访问低特权级的资源,反之则不行。

保护模式段描述符

目前为止,内存还是分段模型,要对内存进行保护,就可以转换成对段的保护。由于 CPU 的扩展导致了 32 位的段基地址和段内偏移,还有一些其它信息,所以 16 位的段寄存器肯定放不下。放不下就要找内存借空间,然后把描述一个段的信息封装成特定格式的段描述符,放在内存中,其格式如下:

保护模式的段描述符

一个段描述符有 64 位 8 字节数据,里面包含了段基地址、段长度、段权限、段类型(可以是系统段、代码段、数据段)、段是否可读写,可执行等。虽然数据分布有点乱,这是由于历史原因造成的。多个段描述符在内存中形成全局段描述符表,该表的基地址和长度由 CPU 和 GDTR 寄存器指示。如下图所示:

全局段描述符表

段寄存器中不再存放段基地址,而是具体段描述符的索引,访问一个内存地址时,段寄存器中的索引首先会结合 GDTR 寄存器找到内存中的段描述符,再根据其中的段信息判断能不能访问成功。

保护模式段选择子

如果你认为 CS、DS、ES、SS、FS、GS 这些段寄存器,里面存放的就是一个内存段的描述符索引,那你可就草率了,其实它们是由影子寄存器、段描述符索引、描述符表索引、权限级别组成的。如下图所示:

保护模式段选择子

上图中影子寄存器是靠硬件来操作的,对系统程序员不可见,是硬件为了减少性能损耗而设计的一个段描述符的高速缓存,不然每次内存访问都要去内存中查表,那性能损失是巨大的,影子寄存器也正好是 64 位,里面存放了 8 字节段描述符数据。

低三位之所以能放 TI 和 RPL,是因为段描述符 8 字节对齐,每个索引低 3 位都为 0,类似1000,10000,11000这样的。所以低3位可以用来做其他事,这里就用来放TI和RPL。由于我们不用关注 LDT,只需要使用 GDT 全局描述符表,所以 TI 永远设为 0。

通常情况下,CS 和 SS 中 RPL(Request Privilege Level) 就组成了 CPL(Current Privilege Level当前权限级别),所以常常是 RPL=CPL,进而 CPL 就表示发起访问者要以什么权限去访问目标段,当 CPL 大于目标段 DPL 时,则 CPU 禁止访问,只有 CPL 小于等于目标段 DPL(Descriptor Privilege Level) 时才能访问。

保护模式平坦模型

分段模型有很多缺陷,其实现代操作系统都会使用分页模型。但是 x86 CPU 并不能直接使用分页模型,而是要在分段模型的前提下,根据需要决定是否要开启分页。因为这是硬件的规定,程序员是无法改变的。但是我们可以简化设计,来使分段成为一种“虚设”,这就是保护模式的平坦模型。

CPU 32 位的寄存器最多只能产生 4GB 大小的地址,而一个段长度也只能是 4GB,所以我们把所有段的基地址设为 0,段的长度设为 0xFFFFF,段长度的粒度设为 4KB,这样所有的段都指向同一个(0~4GB-1)字节大小的地址空间。段长度需要和 G 位配合,若 G 位为 1 则段长度等于 0xfffff 个 4KB。

保护模式中断

实模式下 CPU 不需要做权限检查,所以它可以直接通过中断向量表中的值装载 CS:IP 寄存器就好了。而保护模式下的中断要权限检查,还有特权级的切换,所以就需要扩展中断向量表的信息,即每个中断用一个中断门描述符来表示,也可以简称为中断门,中断门描述符依然有自己的格式,如下图所示:

保护模式下中断门描述符

同样的,保护模式要实现中断,也必须在内存中有一个中断向量表,同样是由 IDTR 寄存器指向,只不过中断向量表中的条目变成了中断门描述符,如下图所示:

保护模式下段中断表

产生中断后,CPU按以下顺序处理中断:

  1. CPU 首先会检查中断号是否大于最后一个中断门描述符,x86 CPU 最大支持 256 个中断源(即中断号:0~255),然后检查描述符类型(是否是中断门或者陷阱门)、是否为系统描述符,是不是存在于内存中。
  2. 接着,检查中断门描述符中的段选择子指向的段描述符。
  3. 最后做权限检查,如果 CPL 小于等于中断门的 DPL,并且 CPL 大于等于中断门中的段选择子所指向的段描述符的 DPL,就指向段描述符的 DPL。进一步的,CPL 等于中断门中的段选择子指向段描述符的 DPL,则为同级权限不进行栈切换,否则进行栈切换。如果进行栈切换,还需要从 TSS 中加载具体权限的 SS、ESP,当然也要对 SS 中段选择子指向的段描述符进行检查。
  4. 做完这一系列检查之后,CPU 才会加载中断门描述符中目标代码段选择子到 CS 寄存器中,把目标代码段偏移加载到 EIP 寄存器中。

切换到保护模式

x86 CPU 在第一次加电和每次 reset 后,都会自动进入实模式,要想进入保护模式,就需要程序员写代码实现从实模式切换到保护模式。切换到保护模式的步骤如下:

第一步,准备全局段描述符表,代码如下。

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

第二步,加载设置 GDTR 寄存器,使之指向全局段描述符表。

lgdt [GDT_PTR]

第三步,设置 CR0 寄存器,开启保护模式。

;开启 PE
mov eax, cr0
bts eax, 0                      ; CR0.PE =1
mov cr0, eax         

第四步,进行长跳转,加载 CS 段寄存器,即段选择子。

jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移

为什么要进行长跳转,这是因为我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器还是实模式下的值,所以需要告诉 CPU 加载新的段信息。

接下来,CPU 发现了 CRO 寄存器第 0 位的值是 1,就会按 GDTR 的指示找到全局描述符表,然后根据索引值 8,把新的段描述符信息加载到 CS 影子寄存器,当然这里的前提是进行一系列合法的检查。到此为止,CPU 真正进入了保护模式,CPU 也有了 32 位的处理能力。

长模式

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

长模式寄存器

长模式相比于保护模式,增加了一些通用寄存器,并扩展通用寄存器的位宽,所有的通用寄存器都是 64 位,还可以单独使用低 32 位。这个低 32 位可以拆分成一个低 16 位寄存器,低 16 位又可以拆分成两个 8 位寄存器,如下表:

长模式下寄存器

长模式段描述符长模式段描述符

长模式依然具备保护模式绝大多数特性,如特权级和权限检查。相同的部分就不再重述了,这里只会说明长模式和保护模式下的差异。下面我们来看看长模式下段描述的格式,如下图所示:

长模式下段描述符

在长模式下,CPU 不再对段基址和段长度进行检查,只对 DPL 进行相关的检查,这个检查流程和保护模式下一样。当描述符中的 L=1,D/B=0 时,就是 64 位代码段,DPL 还是 0~3 的特权级。然后有多个段描述在内存中形成一个全局段描述符表,同样由 CPU 的 GDTR 寄存器指向。长模式弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU。

长模式中断

护模式下为了实现对中断进行权限检查,实现了中断门描述符,在中断门描述符中存放了对应的段选择子和其段内偏移,还有 DPL 权限,如果权限检查通过,则用对应的段选择子和其段内偏移装载 CS:EIP 寄存器。

长模式支持 64 位内存寻址,所以要对中断门描述符进行修改和扩展,下面我们就来看看长模式下的中断门描述符的格式,如下图所示:

长模式下段中断门描述符

结合上图,我们可以看出长模式下中断门描述符的格式变化。

  1. 首先为了支持 64 位寻址中断门描述符在原有基础上增加 8 字节,用于存放目标段偏移的高 32 位值。

  2. 其次,目标代码段选择子对应的代码段描述符必须是 64 位的代码段。

  3. 最后其中的 IST 是 64 位 TSS 中的 IST 指针。

长模式也同样在内存中有一个中断门描述符表,只不过表中的条目(如上图所示)是 16 字节大小,最多支持 256 个中断源,对中断的响应和相关权限的检查和保护模式一样,这里不再赘述。

切换到长模式

我们既可以从实模式直接切换到长模式,也可以从保护模式切换长模式。切换到长模式的步骤如下。

第一步,准备长模式全局段描述符表。

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

第二步,准备长模式下的 MMU 页表,这个是为了开启分页模式,切换到长模式必须要开启分页,想想看,长模式下已经不对段基址和段长度进行检查了,那么内存地址空间就得不到保护了。

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

mov eax, cr4
bts eax, 5   ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax

第三步,加载 GDTR 寄存器,使之指向全局段描述表:

lgdt [eGdtPtr]

第四步,开启长模式,要同时开启保护模式和分页模式,在实现长模式时定义了 MSR 寄存器,需要用专用的指令 rdmsr、wrmsr 进行读写,IA32_EFER 寄存器的地址为 0xC0000080,它的第 8 位决定了是否开启长模式。

;开启 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 

第五步,进行跳转,加载 CS 段寄存器,刷新其影子寄存器。

jmp 08:entry64 ;entry64为程序标号即64位偏移地址

切换到长模式和切换保护模式的流程差不多,只是需要准备的段描述符有所区别,还有就是要注意同时开启保护模式和分页模式。


Author: Andy Zhang
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Andy Zhang !
评论
  TOC