按照Intel® 64 and IA-32 Architectures Software Developer’s Manual 的说法 X86 IA32 CPU有4种操作模式(Opearting Modes),其实更准确的说法是3种操作模式,1种准操作模式。搞出那么多的模式,主要是因为X86 CPU的历史包袱,为了向下兼容,让老的程序仍然可以在新的CPU上执行。这些模式分别是:
-
实模式 (Real-Address Mode):是指8086时代的工作模式,也就是通过 段寄存器«4 +偏移地址的寻址方式。
-
保护模式 (Protected Mode): 从80386开始引入了保护模式,它的寄存器从16位扩展到了32位,而且不再使用之前的段寄存器«4 +偏移地址的寻址方式,而是使用段描述符的方式描述基址和限长,并通过描述符中的属性设置实现对内存段的访问限制和数据 保护。
-
虚拟8086模式(Virtual-8086 Mode):这个就是所谓的准操作模式,在保护模式下,CPU支持运行8086的软件。
-
系统管理模式(System Management Mode ): 从80386以后引入的一个标准功能,最初应该是用于实现电源管理的功能或者用来让OEM做一些差异性的feature,它对OS或者其它应用是透明的。当有系统管理事件产生时,CPU上的SMI#(external system interrupt pin)就会被触发,CPU就会进入SMM mode并切换到一个单独的地址空间保存上下文然后执行相应的任务,当RSM从SMM返回时,CPU恢复之前的工作继续执行。
X64架构之前面提到的所有4种模式的同时还引入IA-32e模式:
- IA-32e 模式:这个模式下有两个子模式,兼容模式和64bit长模式(long mode)。在兼容模式,保护模式下的软件可以直接在IA-32e下运行,长模式支持64位的线性地址而且支持物理地址大于64G。
接下来我就以UEFI 的实现为例介绍一下如何从实模式切换到保护模式,以及从保护模式切换到长模式(long mode)的过程。因为UEFI出于简化的目的保护模式没有开启分页,而是使用平坦模式(flat model);long mode也是用的是虚拟地址和物理地址1:1映射。
实模式切换到保护模式
X86 reset的时候,CPU处于实模式下,第一条指令会到BIOS code中,BIOS首先要做的就要切换到保护模式。保护模式还会使用段寄存器(CS,DS,FS,GS,SS),但是它们改名为段选择子了,也不再保存段基址而是保存指向全局描述符中(GDT)的段索引信息。最终从全局描述符中获得的是一个个的段描述符(分为数据段,指令段,系统段描述符等) 在平坦模型下所有的段选择子都使用同样的段描述符,只用同样的段基址0x00000000,和段限制0xFFFFFFFF。 按照spec规定进入保护模式,我们只需要把CR0 PE(Protection Enable)位设起来就表示是保护模式了。但是再此之前我们先要把段描述符表准备好放在内存的某个地方,把表格的地址和长度通过LGDT填到GDTR寄存器中。 UEFI BIOS 实模式保护模式切换的参考实现如下所示:
;
; Load the GDT table in GdtDesc
;
mov esi, OFFSET GdtDesc
db 66h
lgdt fword ptr cs:[si]
;
; Transition to 16 bit protected mode
;
mov eax, cr0 ; Get control register 0
or eax, 00000003h ; Set PE bit (bit #0) & MP bit (bit #1)
mov cr0, eax ; Activate protected mode
;
; Now we're in 16 bit protected mode
; Set up the selectors for 32 bit protected mode entry
;
mov ax, SYS_DATA_SEL
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
;
; Transition to Flat 32 bit protected mode
; The jump to a far pointer causes the transition to 32 bit mode
;
mov esi, offset ProtectedModeEntryLinearAddress
jmp fword ptr cs:[si]
PUBLIC BootGdtTable
;
; GDT[0]: 0x00: Null entry, never used.
;
NULL_SEL equ $ - GDT_BASE ; Selector [0]
GDT_BASE:
BootGdtTable DD 0
DD 0
;
; Linear data segment descriptor
;
LINEAR_SEL equ $ - GDT_BASE ; Selector [0x8]
DW 0FFFFh ; limit 0xFFFF
DW 0 ; base 0
DB 0
DB 092h ; present, ring 0, data, expand-up, writable
DB 0CFh ; page-granular, 32-bit
DB 0
;
; Linear code segment descriptor
;
LINEAR_CODE_SEL equ $ - GDT_BASE ; Selector [0x10]
DW 0FFFFh ; limit 0xFFFF
DW 0 ; base 0
DB 0
DB 09Bh ; present, ring 0, data, expand-up, not-writable
DB 0CFh ; page-granular, 32-bit
DB 0
;
; System data segment descriptor
;
SYS_DATA_SEL equ $ - GDT_BASE ; Selector [0x18]
DW 0FFFFh ; limit 0xFFFF
DW 0 ; base 0
DB 0
DB 093h ; present, ring 0, data, expand-up, not-writable
DB 0CFh ; page-granular, 32-bit
DB 0
;
; System code segment descriptor
;
SYS_CODE_SEL equ $ - GDT_BASE ; Selector [0x20]
DW 0FFFFh ; limit 0xFFFF
DW 0 ; base 0
DB 0
DB 09Ah ; present, ring 0, data, expand-up, writable
DB 0CFh ; page-granular, 32-bit
DB 0
;
; Spare segment descriptor
;
SYS16_CODE_SEL equ $ - GDT_BASE ; Selector [0x28]
DW 0FFFFh ; limit 0xFFFF
DW 0 ; base 0
DB 0Fh
DB 09Bh ; present, ring 0, code, expand-up, writable
DB 00h ; byte-granular, 16-bit
DB 0
;
; Spare segment descriptor
;
SYS16_DATA_SEL equ $ - GDT_BASE ; Selector [0x30]
DW 0FFFFh ; limit 0xFFFF
DW 0 ; base 0
DB 0
DB 093h ; present, ring 0, data, expand-up, not-writable
DB 00h ; byte-granular, 16-bit
DB 0
;
; Spare segment descriptor
;
SPARE5_SEL equ $ - GDT_BASE ; Selector [0x38]
DW 0 ; limit 0xFFFF
DW 0 ; base 0
DB 0
DB 0 ; present, ring 0, data, expand-up, writable
DB 0 ; page-granular, 32-bit
DB 0
GDT_SIZE EQU $ - BootGDTtable ; Size, in bytes
;
; GDT Descriptor
;
GdtDesc: ; GDT descriptor
DW GDT_SIZE - 1 ; GDT limit
DD OFFSET BootGdtTable ; GDT base address
在此之后段寄存器都已经指向了全局描述符表中,当我们访问一个逻辑地址到线性地址的转换,如下图所示:因为平坦模式段寄存器的基地址是0,所以其实线性地址就是offset的地址。
保护模式切换到长模式:
当UEFI BIOS从PEI切换到DXE阶段的时候也会从保护模式切换到长模式,寻址空间从32位变成64位,最大的物理地址取决于实际的地址线的大小。跟保护模式不同的是,保护模式可以不开启分页模式而只是使用flat mode,但是长模式则是必须要开启分页模式。如下表所示enable long mode,我们需要把CR4.PAE设置为Enabled, PDPE.PS=1,另外长模式还增加一层新的页表转换机制被称为Page Map Level 4(PML4). 通过PDPE.PS和PDE.PS的组合,长模式支持4Kbyte,2Mbyte,1Gbyte三种模式的分页基址。在长模式下,CR3被扩展为64 bit而且用来指向PML4的基地址,因为地址扩展了,所以PML4可以放在任何地方,当然需要是4KB对齐 为了简化实现,UEFI BIOS 使用的是1GByte paging。在这种模式下虚拟地址被分成4个部分,其中bit39:47被用来索引PML4 table中entry,这个entry会指向PDPE的基地址,bit30:38用来索引PDPE中的某一个entry,这个entry中是页物理地址的基址。最后我们用这个页物理地址的基址加上bit0:29 page offset就得到了最终的物理地址。 PML4 Entry Format: PDPE Entry Format:
UEFI BIOS 保护模式切换到长模式的参考实现如下所示:
if (FeaturePcdGet (PcdDxeIplBuildPageTables)) {
//
// Create page table and save PageMapLevel4 to CR3
//
PageTables = CreateIdentityMappingPageTables ((EFI_PHYSICAL_ADDRESS) (UINTN) BaseOfStack, STACK_SIZE);
} else {
//
// Set NX for stack feature also require PcdDxeIplBuildPageTables be TRUE
// for the DxeIpl and the DxeCore are both X64.
//
ASSERT (PcdGetBool (PcdSetNxForStack) == FALSE);
ASSERT (PcdGetBool (PcdCpuStackGuard) == FALSE);
}
//
// End of PEI phase signal
//
Status = PeiServicesInstallPpi (&gEndOfPeiSignalPpi);
ASSERT_EFI_ERROR (Status);
if (FeaturePcdGet (PcdDxeIplBuildPageTables)) {
AsmWriteCr3 (PageTables);
}
切换到系统管理模式
从模式切换图上可以看出,我们可以从任何模式切换到SMM mode,也可以从SMM mode返回到切换之前的模式。SMM最初发明主要是为了APM的电源管理的功能,现在APM已经不用了,但是SMM却保留了下来被用来做一些像安全相关的功能或者实现RAS的处理程序。 进入SMM mode是通过触发SMI才可以, SMI 全称是SYSTEM MANAGEMENT INTERRUPT 系统管理中断。SMM的代码工作在SMM RAM,默认情况下第一个SMI产生的时候X86 CPU会跳转到3000:8000的地方去执行指令。这是一个1M以下的地址,空间太小,随着SMM code 越来越大,后来的CPU又提出了T-SEG,可以把SMM RAM放到4G以下的某个地址并且可以达到8M-32M或者更大。SMMBASE 从3000:8000切换到T-SEG的过程叫做rebase,它的原理是利用进入SMM mode时CPU的上下文内容会保存在SMM RAM中,其中就包括SMMBASE, 我们在SMI hanlder中把SMBASE改成T-SEG,这样RSM返回之后,下一次SMI产生时,SMBASE就会是在T-SEG。实现原理如下: UEFI 实现了一个SMM core的基础架构,可以放让开发人员非常容易的实现SMM driver注册一个SMI 的callback,这样就可以实现所需要的功能。UEFI的具体实现可以参考PiSmmCpuDxeSmm
//
// Load image for relocation
//
CopyMem (U8Ptr, gcSmmInitTemplate, gcSmmInitSize);
//
// Retrieve the local APIC ID of current processor
//
ApicId = GetApicId ();
//
// Relocate SM bases for all APs
// This is APs' 1st SMI - rebase will be done here, and APs' default SMI handler will be overridden by gcSmmInitTemplate
//
mIsBsp = FALSE;
BspIndex = (UINTN)-1;
for (Index = 0; Index < mNumberOfCpus; Index++) {
mRebased[Index] = FALSE;
if (ApicId != (UINT32)gSmmCpuPrivate->ProcessorInfo[Index].ProcessorId) {
SendSmiIpi ((UINT32)gSmmCpuPrivate->ProcessorInfo[Index].ProcessorId);
//
// Wait for this AP to finish its 1st SMI
//
while (!mRebased[Index]);
} else {
//
// BSP will be Relocated later
//
BspIndex = Index;
}
}
//
// Relocate BSP's SMM base
//
ASSERT (BspIndex != (UINTN)-1);
mIsBsp = TRUE;
SendSmiIpi (ApicId);
//
// Wait for the BSP to finish its 1st SMI
//
while (!mRebased[BspIndex]);
//
// Restore contents at address 0x38000
//
CopyMem (CpuStatePtr, &BakBuf2, sizeof (BakBuf2));
CopyMem (U8Ptr, BakBuf, sizeof (BakBuf));
}
Refer:
1. Intel® 64 and IA-32 Architectures Software Developer’s Manual
2. AMD64 Architecture Programmer’s Manual Volume 2: System Programming