IA32体系结构
Published: 5/4/2024
80386 是Intel于1985年推出的一款32位微处理器,是Intel 80286的后继产品。80386是Intel首款支持32位操作系统的微处理器,同时也是Intel支持虚拟内存的微处理器。IA-32架构则是Intel基于I386提出的32位架构的微处理器架构的统称,并在历史进程中不断发展变化,但其大体内容仍然保持不变,下面以I386为例进行介绍。
执行模式
80386总共有3种执行模式,分别是:
-
实模式(Real Address mode): 机器初始化后会进入该模式,被看作是8086的拓展,带有新的指令。大多数情况下,都只会将实模式用于初始化。
-
保护模式(Protect mode):
-
V86模式(Virtual 8086 mode):
根据系统寄存器CR0
的PE
位(Protection Enable,bit 0)与EFLAGS中的VM
位(Virtual Machine,bit 15)来表示当前处于哪种模式。当PE=0时机器处于实模式,当PE=1时机器处于保护模式。当VM=1,并且PE=1时机器处于V86模式,如果PE=0,VM位无效,也就是说V86模式基于保护模式。
描述符
80386使用描述符来对资源进行管理。
386总共有3个和描述符相关的寄存器,分别是:GDTR
(全局描述符表寄存器),LDTR
(局部描述符表寄存器),IDTR
(中断描述符表寄存器)。
保护机制
80386具有一套完善的特权级与保护机制,对内存访问,I/O访问,中断处理等进行了权限管理,这一保护机制由硬件实现,并不会造成性能损失。
段保护
当段选择器被载入某一段寄存器中时,CPU会检查段描述符中保护域中的参数是否合法。
其中的TYPE
域用于区分不同的段以及其用法。DATA段与EXECUTABLE段描述符常被用户程序使用,TYPE最高位为1,SYSTEM段描述符TYPE最高位为0,被操作系统使用。 TYPE域其他位标注了该段的权限。需要注意一些规则:
- 只有可执行段能被载入CS寄存器,只有可写段能被载入SS段,只有可读段能被载入4个数据段的寄存器(至少包含哈)。
特权级
80386中对于对象特权级的取值范围为0-3,0表示最高特权级,使用特权级的对象主要包括:
-
存在DPL(Descriptor privilege level)位的描述符
-
存在RPL(requestor’s privilege level)位的选择器,用于表示发起该选择器的程序所处特权级
-
存在CPL(Current privilege level)的系统寄存器
如之前所说,CPU会自动比对CPL域与其他的特权级进行检查。数据访问操作合法当且仅当数据的DPL大于等于CPL与RPL的最大值。
控制流
80386的控制流主要通过指令与异常两部分改变,指令部分包括JMP
,CALL
,RET
,INT
,IRET
。
跳转指令主要分为FAR
与NEAR
两种模式,NEAR
模式只会在段内跳转,所以CPU只会对段地址范围与跳转地址合法性进行检查。FAR
模式会跨越段与段,所以CPU会在这种类型的跳转指令执行时进行特权级检查,该跳转指令合法当且仅当DPL=CPL或者目标代码段描述符中的Conforming位置位1,并且DPL小于等于CPL。(也就是说验证过了的或者更低特权的段可以跳转)
段描述符中Conforming bit置位1表示该段被验证过,主要用于高特权级使用低特权级所共享的代码。需要注意的是,当前往验证段时,CPL并不会改变。
如果前往未验证段并且该段的DPL表示的特权级高于CPL,则需要使用CALL GATE
描述符与CALL
指令进行跳转,不能使用JMP
指令。
Gate
Gate descriptor主要用于实现不同特权级之间控制流的切换,主要包括:Call gates
,Task gates
,Interrupt gates
,Trap gates
。
Call Gates
Call Gates主要用于定义程序的入口以及入口的特权级。 当使用Call gates进行跳转时,需要额外考虑其特权级。Call Gates的DPL决定了能前往该段的特权级,CPL必须小于等于Call Gates的DPL,并且当CPL小于该DPL时,必须只能是CALL指令。
栈
每个特权级都有自己独立的栈,并且确保高特权栈中为被验证低特权程序保留有足够的空间。
页面保护
80386同样使用页表进行页面保护,当页表项中U/S为0时,。当R/W为0时,页面只读,否则可读写;当P为0时,页面不存在。
IO
80386对于IO同样具有特权级保护机制。其中IO主要分为Port和Memory mapped两种方式。
系统寄存器中EFLAGS
中的IOPL
位表示使用IO指令的权限,只有当CPL <= IOPL时才能执行IO指令,并且IOPL只有在特权级0才能通过POPF改变。
TSS段中IO权限map则定义了使用IO port的权限。
需要注意的是,上面的机制只会在保护模式起作用,在实模式并不会对IO进行保护。
启动流程
BIOS
BIOS位于ROM中,在机器上电后由硬件负责载入RAM,首先跳转到0xFFF0处执行长跳转指令到BIOS的入口地址,然后执行机器自检与硬件初始化等工作;在这一过程结束后,BIOS会尝试寻找包含引导扇区的设备,通过检查设备第一个扇区的最后两个字节是否为0x55AA
来判断是否为引导扇区,如果找到就会将该扇区内容复制到0x7C00处并跳转到该地址执行启动代码。
值得注意的是,BIOS运行时机器会处于16位的保护模式。
主引导扇区
下面是对主引导扇区组成的详细介绍。
启动盘制作
寄存器
通用寄存器
I386拥有8个32位GPR,分别是EAX
,EBX
,ECX
,EDX
,ESI
,EDI
,EBP
,ESP
。这些寄存器可以分为两类,分别是数据寄存器(前四个)和指针寄存器。
4个数据寄存器可分为低16位的AX,BX,CX,DX,对于这四个16位寄存器,可以分别访问其低8位和高8位,分别为AL,AH,BL,BH,CL,CH,DL,DH。后面4个指针寄存器不可再分。
需要注意,虽然它们被称为通用寄存器,在实际代码中,它们都具有一定的规定用途(强制必须使用)。
段寄存器
段寄存器共有6个,分别是CS
,DS
,SS
,ES
,FS
,GS
。这些寄存器用于存储段地址,上图已经标注了每个段寄存器中所存放段的用途。
标志寄存器与指令寄存器
I386中的指令寄存器为32位的EIP
用于存放下一条指令的地址,标志寄存器为32位的EFLAGS
,其中的每一位都有特定的含义,下面是EFLAGS
的结构:
X86中的条件指令以及指令触发的异常都与标志寄存器有关,因此在编写汇编代码时,需要注意标志寄存器的状态。
系统寄存器
I386中的系统寄存器类型主要有:
-
标志寄存器
-
内存管理寄存器
-
控制寄存器
-
调试寄存器
-
测试寄存器
标志寄存器
标志寄存器存放了表示执行模式,中断与异常,IO权限等信息的标志位。
内存管理寄存器
主要包括了:
-
GDTR: 全局描述符表寄存器
-
LDTR: 局部描述符表寄存器
上面的两个寄存器指向了GDT和LDT的地址。
- IDTR: 中断描述符表寄存器
IDTR指向了中断处理函数的入口地址。
- TR: 任务寄存器
TR负责被CPU用于标识当前任务。
控制寄存器
主要包括了:CR0, CR2, CR3。
调试寄存器
主要提供了断点寄存器,能够不修改代码段就能够在指定地址设置断点。
系统指令
任务相关
-
LTR: 读TR
-
STR: 写TR
地址相关
-
LLDT: 读LDT
-
SLDT: 写LDT
-
LGDT: 读GDT
-
SGDT: 写GDT
验证指针
-
ARPL: 调整RPL
-
VERR: 验证读
-
VERW: 验证写
-
LAR : 读访问权限
-
LSL : 读段限制
IO相关
-
IN: 读端口
-
OUT: 写端口
-
INS: 读端口串
-
OUTS: 写端口串
中断相关
-
CLI: 关中断
-
STI: 开中断
-
LIDT : 读IDT
-
SIDT : 写IDT
控制相关
- HLT : 停CPU
指令格式
待补充。
内存管理
内存模型
I386支持4GB大小的物理内存空间,支持两类内存模型:
-
Flat Model: 最多4GB的线性地址空间
-
Segmented Model: 最多由16383个线性地址空间组成的段地址空间,其中每个线性地址空间最多4GB大小。
对于Segmented Model,地址主要由段选择器和段内偏移两部分组成。
数据类型
I386中的基本数据类型有字节,字,双字。需要注意I386手册中提出字与双字的地址并不需要对齐其数据大小,如果使用RUST等对数据对齐有一定要求的语言,需要注意这一点。
在I386中同样需要注意数据的大小端,I386是小端模式,对于某些文件系统或者网络协议需要注意处理好字节序,下面的图片很好地展示了I386的字与双字的布局。
除了上面的类型以外,I386还支持BCD(Binary Coded Decimal)类型,BCD类型是一种用二进制编码十进制数的方式,可以用来进行十进制数的运算。BCD码又分为未压缩BCD码和压缩BCD码。
地址转换
I386中的地址转换主要由逻辑地址(由16位段选择器和32位段内偏移两部分组成)转换为线性地址,线性地址转换为物理地址两部分组成。下图是I386中地址转换的示意图。
从上图也可以看到I386中逻辑地址,线性地址与物理地址的组成。
段翻译
段翻译负责将逻辑地址(段地址:段内偏移)翻译成线性地址,主要涉及到:
-
描述符
-
描述符表
-
段选择器
-
段寄存器
下图是该过程的具体表述:
从图中可以看到,根据段选择器从段描述符表中选取对应段描述符,得到其基地址并与段内偏移相加便得到了其线性地址。
描述符
描述符负责为CPU提供用于段地址翻译的数据。描述符通常由编译器与操作系统等系统软件创建,主要组成如下:
-
BASE
: 主要用于定位4GB线性地址中段的位置,CPU将3个BASE 组合成32位值 -
LIMIT
: 定义段大小,如果标志位中Granularity位为1,则单位为4KB,否则为1B -
TYPE
: 用于区分不同种类的描述符 -
DPL
: 描述符特权级,用于实现保护机制 -
Segment present bit
:用于表示描述符是否有效 -
Access bit
: 是否被访问过
描述符表
描述符可被存放于GDT
(全局描述符表)或者某个LDT
(局部描述符表)中。 描述符表只是存放于内存之中的8字节entry组成的数组,如下图所示:
段选择器
段选择器用于寻找对应的段描述符,其组成如下:
-
Index: 表示描述符在表中的下标
-
Table Indicator : 表明描述符表种类(0位GDT,1为当前LDT)
-
Requested Privilege level : 用于保护机制
段寄存器
页翻译
从前面可以得知,线性地址由DIR,PAGE,OFFSET三部分组成,页翻译使用两级页表将线性地址转化为物理地址。这一部分其实是可选的,当CR0寄存器中的PG位为1时才会开启,并且I386使用4K物理页框。 翻译时I386使用DIR域寻找一级页表中的页表项,使用PAGE寻找二级页表中的页表项进而得到其物理页,最后使用页内偏移得到其物理地址。
下面是页翻译的具体流程:
在I386中,每个页表占据一页物理内存,及4KB,并且每个页表项4B,故有1K个页表项。当前页表的物理地址被存放在CR3中,下面是页表项的组成:
Cache与TLB
异常与中断
上面是I386中的中断向量表,I386中的中断分为中断和异常两种,I386将中断视为异步异常,而将异常视为同步异常,中断是外部设备发起,而异常由CPU内部发起。
IO
I386中的IO主要有两种方式:
-
Port-Mapped IO: 通过端口号来访问IO设备(特定的IO指令)
-
Memory-Mapped IO: 通过内存地址来访问IO设备(普通的内存读写指令)
端口IO
80386总共支持最多16个外部设备,每个设备可拥有一个64K大小的地址空间。可被看作是8位寄存器,16位寄存器,32位寄存器的数据形式进行访问。需要注意地址对齐以便能够作为总线地址。
使用INT
与OUT
指令进行端口IO间的数据传输,使用INTS
与INS
指令进行数据流IO数据传输。
在使用INT
和OUT
时使用的GPR是EAS,AX与AL。而在使用INS
和OUTS
时使用的GPR是ES,ESI(地址由ES:EDI组成)。
内存映射IO
使用普通的内存读写指令进行IO设备的访问,并且这些地址和普通的内存地址一样,都会受到I386的保护。
TASK
80386支持多道程序。