X86 CPU的工作模式

Peter 于 2020-11-17 发布

按照Intel® 64 and IA-32 Architectures Software Developer’s Manual 的说法 X86 IA32 CPU有4种操作模式(Opearting Modes),其实更准确的说法是3种操作模式,1种准操作模式。搞出那么多的模式,主要是因为X86 CPU的历史包袱,为了向下兼容,让老的程序仍然可以在新的CPU上执行。这些模式分别是:

X86 CPU Operation Mode

  1. 实模式 (Real-Address Mode):是指8086时代的工作模式,也就是通过 段寄存器«4 +偏移地址的寻址方式。

  2. 保护模式 (Protected Mode): 从80386开始引入了保护模式,它的寄存器从16位扩展到了32位,而且不再使用之前的段寄存器«4 +偏移地址的寻址方式,而是使用段描述符的方式描述基址和限长,并通过描述符中的属性设置实现对内存段的访问限制和数据 保护。

  3. 虚拟8086模式(Virtual-8086 Mode):这个就是所谓的准操作模式,在保护模式下,CPU支持运行8086的软件。

  4. 系统管理模式(System Management Mode ): 从80386以后引入的一个标准功能,最初应该是用于实现电源管理的功能或者用来让OEM做一些差异性的feature,它对OS或者其它应用是透明的。当有系统管理事件产生时,CPU上的SMI#(external system interrupt pin)就会被触发,CPU就会进入SMM mode并切换到一个单独的地址空间保存上下文然后执行相应的任务,当RSM从SMM返回时,CPU恢复之前的工作继续执行。

X64架构之前面提到的所有4种模式的同时还引入IA-32e模式:

  1. 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)的段索引信息。最终从全局描述符中获得的是一个个的段描述符(分为数据段,指令段,系统段描述符等) CsDesc 在平坦模型下所有的段选择子都使用同样的段描述符,只用同样的段基址0x00000000,和段限制0xFFFFFFFF。 Flat 按照spec规定进入保护模式,我们只需要把CR0 PE(Protection Enable)位设起来就表示是保护模式了。但是再此之前我们先要把段描述符表准备好放在内存的某个地方,把表格的地址和长度通过LGDT填到GDTR寄存器中。 Lgdt CR0 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的地址。 AddTran0

保护模式切换到长模式:

当UEFI BIOS从PEI切换到DXE阶段的时候也会从保护模式切换到长模式,寻址空间从32位变成64位,最大的物理地址取决于实际的地址线的大小。跟保护模式不同的是,保护模式可以不开启分页模式而只是使用flat mode,但是长模式则是必须要开启分页模式。如下表所示enable long mode,我们需要把CR4.PAE设置为Enabled, PDPE.PS=1,另外长模式还增加一层新的页表转换机制被称为Page Map Level 4(PML4). Spal 通过PDPE.PS和PDE.PS的组合,长模式支持4Kbyte,2Mbyte,1Gbyte三种模式的分页基址。在长模式下,CR3被扩展为64 bit而且用来指向PML4的基地址,因为地址扩展了,所以PML4可以放在任何地方,当然需要是4KB对齐 Pml4 为了简化实现,UEFI BIOS 使用的是1GByte paging。在这种模式下虚拟地址被分成4个部分,其中bit39:47被用来索引PML4 table中entry,这个entry会指向PDPE的基地址,bit30:38用来索引PDPE中的某一个entry,这个entry中是页物理地址的基址。最后我们用这个页物理地址的基址加上bit0:29 page offset就得到了最终的物理地址。 AddTran1 PML4 Entry Format: Pml4e PDPE Entry Format: Pdpee

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。实现原理如下: Reloc SmbaseReg 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

3. Flat32.asm

4. DxeLoadFunc.c

5. PiSmmCpuDxeSmm.c

6. SMM Core Architecture