自制x86 Bootloader开发笔记(3)——— 进入长模式

发布时间 2023-11-11 17:04:48作者: basic60

前言

本项目是基于IA32架构架构(32位Intel架构)的,而IA32架构有以下的操作模式:

  • 实模式、保护模式、虚拟8086模式和系统管理模式。这些模式被称为 传统模式

实模式是计算机刚启动时的模式,在实模式下可以随意访问可用的内存地址,实模式比较简单直接,但是随着操作系统的发展,实模式这种直接操作物理内存的方式已经不适合现代操作系统了,试想一下内核数据和用户进程的数据如果都在同一个地址空间,而双方又可以互访随意访问所有的数据,那整个系统将会变得危险起来,因为这时候用户进程的一个错误操作导致内核数据被修改,就可能使得整个系统崩溃。因此,保护模式 就应运而生,保护模式提供了虚拟内存,内存分段、分页等新特性,使得每个进程有自己的虚拟内存,其真实使用的物理地址不会被非法地修改,增加了系统的稳定性。

而为了支持64位的程序,传统保护模式经过扩展就发展出了 长模式。长模式(Long Mode)又叫IA-32e(Intel Architecture 32bit extension, Intel对64位技术最初的称呼)模式,在长模式下,软件可以使用以下两个子模式:

  1. 64-bit mode, 支持64位操作系统和64位的应用程序。
  2. 兼容模式,支持传统保护模式下的软件运行,使得他们可以在64位操作系统中和64位的软件共存。

进入长模式

Intel开发者手册中的这张图很好地描述了进入长模式的过程:

图片名称

我们一开始位于实模式,从图中可以看出想要转到IA32-e模式,需要设置PE位LME位CR0.PG位。PE位是控制寄存器CR0中的一个标志位,表示开启保护功能,而在开启保护功能之前我们必须先开启分段的功能。CR0.PG表示是CR0寄存器中的分页标志位,将其置为1表示开启分页,当然在开启之前我们需要先设置好页表。LME表示MSR寄存器的充模式标志位,将其置为1表示开启长模式。

设置分段

想要进入保护模式就必须开启分段,实际上我们Bootloader的内存模型并不是很需要分段,分页就足够了。但是在IA32架构下,分段是必须的(而分页才是可选的),因此我们必须要设置分段。在介绍如何开启分段之前,我们先介绍一下内存分段是如何工作的。

图片名称

图3-1的左半部分描述了分段的实现,在开启分段和分页的情况下,程序所使用的内存地址并不是物理地址,而需要通过分段和分页的处理之后,才能得到最终的物理地址。我们把程序使用的地址称为逻辑地址,逻辑地址由段选择子偏移组成,段选择子指向了段描述符,段描述符中有目标内存段的基地址,通过基地址和偏移地址的组合我们就得到了线性地址。

段选择子是是16位的,它指向了一个段描述符,段选择子的结构如下:

图片名称

RPL 表示特权级。TI 表示指向的段是GDT中的还是LDT中的,GDT是全局描述符表,整个系统一张,LDT是局部描述符表, 每个任务可以有一张,我们只需要设置GDT即可。Index则表示指向的是GDT或这LDT中的第几个段描述符。

为了降低地址翻译的之间和编码的复杂度,通常把段选择自放在CS,SS,DS,ES,FS,GS这些段寄存器中。段寄存器由可见部分隐藏部分这两部分组成,可见部分就是段选择子,而隐藏部分就是段描述符缓存(包括段基地址,段长度限制等信息),有了段寄存器的缓存,CPU在进行地址翻译时就不用每次都去读取段描述符的数据。如果想要刷新段寄存器的缓存,可以使用MOV等指令更新段寄存器的值,或者使用long jmp, long call等指令来更新段寄存器(CS)的值。

段描述符的结构如图3-8所示:

图片名称

可以看见段描述符包含了段的基地址和长度限制等信息,如果我们的目标是进入保护模式,需要对段描述符进行设置。但是在IA32-e模式下,分段“基本上”是被关闭的,说基本上关闭因为分段机制并没有完全关闭,Intel开发者手册3.2.4节原话:

In IA-32e mode of Intel 64 architecture, the effects of segmentation depend on whether the processor is running in compatibility mode or 64-bit mode. In compatibility mode, segmentation functions just as it does using legacy 16-bit or 32-bit protected mode semantics.
In 64-bit mode, segmentation is generally (but not completely) disabled, creating a flat 64-bit linear-address space. The processor treats the segment base of CS, DS, ES, SS as zero, creating a linear address that is equal to the effective address. The FS and GS segments are exceptions. These segment registers (which hold the segment base) can be used as additional base registers in linear address calculations. They facilitate addressing local data and certain operating system data structures.

可见在64-bit模式下,CPU直接使用了一个64位的平坦线性地址,CS,DS,ES,SS等段寄存器直接以段基地址为0进行处理。不过我们仍然需要对代码段进行设置,因为在做特权级检查代码段的段描述符仍然被使用。

Intel Manual V3 5.2.1:
Code segments continue to exist in 64-bit mode even though, for address calculations, the segment base is treated as zero. Some code-segment (CS) descriptor content (the base address and limit fields) is ignored; the remaining fields function normally (except for the readable bit in the type field).

长模式下的段描述符中关于基地址和段长限制等比特位被忽略:

图片名称

各个field的含义如下:

field 含义
A 是否被被访问,设置成0即可,CPU访问该段时会将其置1
R 可读
C 1:一致性代码段,0:非一致性代码段
DPL 特权级
P Present,该段是否在内存中
AVL 是否被系统软件可用
L 64位标记
D 默认操作符大小,如果L为0,当前为兼容模式,D为0表示16位,1表示32位。L位1表示64bit模式,D设为0
G 段长限制粒度,0: 字节粒度,1: 4kb粒度

我们直接采用硬编码的方式来编写段描述符:

gdt64:
.Null:
    dq 0                                                 ; gdt中第一项必须为空描述符
.Code:
    dq 0x00209A0000000000         ; 64-bit code descriptor (exec/read).
.pointer:
    dw $ - gdt64 - 1                             ; 16-bit Size (Limit) of GDT.
    dd gdt64

设置分页

再次回到图3-1,在分段之后,分页机制通过设置好的页目录和页表,将线性地址翻译为真正的物理地址。而在开启之前,自然需要设置好页目录和页面。在正式介绍分页模式之前,首先介绍一下分页模式。

图片名称

从图中可以看出,有不分页,32-bit Paging, PAE Paging和4-level Paging这几种模式。

当CR0.PG = 1 and CR4.PAE = 0时处理器采用32-bit Paging模式。32-bit Paging模式采用二级页表的结构,将32位的线性地址翻译成40位的物理地址。当CR0.PG = 1, CR4.PAE = 1, IA32_EFER.LME = 0是处理器采用PAE Paging模式, PAE Paging模式采用三级页表的模式,将32位的线性地址翻译成52位的物理地址。当CR0.PG = 1, CR4.PAE = 1, IA32_EFER.LME = 1时处理器使用4-level paging模式,4-level Paging采用四级分页,将48位的线性地址翻译成52位的物理地址,最多支持256TB的线性地址空间(CPU位于长模式时,支持64位的指令集,但并不等同于支持64位的线性地址,4级页表只支持到48位,对于高位地址的比特位填0处理)。

这里不展开32-bit Paging和PAE Paging,因为从图2-3中看出,进入IA32-e模式需要将PE,PG,LME位都置为1,而这正是4-level paging模式的开启条件,所以我们详细描述一下四级页表的设置方式。四级页表支持4KB的页表,2MB的页表和1GB的页表,我们选择4KB的页表。线性地址在使用4KB页表的情况下翻译成物理地址的过程如下图所示:

图片名称

可以看出,48位的线性地址被分成了PML4, Directory Ptr, Directory, Table, Offset 这五个部分。其中Offset是12位,表示在页内的偏移,12个比特位正好覆盖了4KB。PML4, Directory Ptr, Directory, Table长度都是9位, 这几个部分虽然名字不同,但是它们的作用是一样的,就是表示了一个偏移,通过这个偏移取得对应表的表项,表项指向下一级的页表。方便描述起见,将PML4, Directory Ptr, Directory, Table对应的表称为四级页表,三级页表,二级页表和一级页表。每个页表是一个数组,翻译过程可以如下描述:

三级页表 = 四级页表[PML4] 
二级页表 = 三级页表[Directory Ptr]
一级页表 = 二级页表[Directory]
页面 = 一级页表[Table]
物理地址 = 页面[Offset]

可见页表中的项除了一级页表指向了物理页,其他都指向了下一级的页表,而我们需要做的就是对页表中的每一项进行设置,页表项的格式如下图:

图片名称

可以看出,各级页表项的格式都一样,其中高位存放下一级页表或者页面的地址的高位地址,低12位则存储一些其它信息(这样做完全没有问题,因为页表要求是4KB对齐的,这意味着页表的地址低12位一定都是0,所以页表项对于页表的低12位我们不用进行设置,可以利用这些空间存储一些其他的信息)。其中页表项各个比特位的含义如下:

BIt Position 含义
0 (P) Present, 是否在内存中
1 (R/W) Read/write
2 (U/S) 1:一致性代码段,0:非一致性代码段
3 (PWT) PWT位,间接决定页面缓存类型
4 (PCD) PCD位,间接决定页面缓存类型
5 (A) Accessed, 被访问
6 (D) Dirty
7 (PAT) PAT位,间接决定页面缓存类型
8 (G) Global,当CR4.PGE为1时并且Global为1时该页面为全局页面。重新装入CR3不会使全局页面的TLB项无效。
11:9 Ignored

在填充页表项时,我们只需要将P位和R/W位设置为1即可。具体代码如下:

%define PAGE_TABLE 0x40000

fill_page_table:
    mov edi, PAGE_TABLE   ; page talbe start at 0x40000, occupy 20KB memroy and map the first 26MB
    push edi
    mov ecx, 0x10000
    xor eax, eax
    cld
    rep stosd          ; zero out 64KB memory
    pop edi

    lea eax, [es:edi + 0x1000]     
    or eax, 3 
    mov [es:edi], eax

    lea eax, [es:edi + 0x2000]
	or eax, 3 
    mov [es:edi + 0x1000], eax

    mov ebx, 0x3000
    mov edx, 0x2000
    mov ecx, 52
    .loop_p4:
        lea eax, [es:edi + ebx]        
        or eax, 3
        mov [es:edi + edx], eax

        add ebx, 0x1000
        add edx, 8
        dec ecx
        cmp ecx, 0
        jne .loop_p4

    push edi               
    lea edi, [es:edi + 0x3000]
    mov eax, 3 
    .loop_page_table:
        mov [es:edi], eax
        add eax, 0x1000
        add edi, 8
        cmp eax, 0x1a00000       
        jb .loop_page_table
    pop edi

我们首先在0x40000处建立临时页表首先映射26MB供之后加载内核的工作使用。在设置完页表之后,就可以正式进入长模式了:

enter_long_mode:
    call fill_page_table
    call enable_paging
    lgdt [gdt64.pointer]

    jmp CODE_SEG:long_mode_entry
    jmp $

enable_paging:
    ; enable pae and pge
    mov eax, 10100000b
    mov cr4, eax

    mov eax, PAGE_TABLE
    mov cr3, eax

    ; set the long mode bit in the EFER MSR (model specific register)
    mov ecx, 0xC0000080
    rdmsr
    or eax, 0x00000100 
    wrmsr

    ; enable paging and protection
    mov eax, cr0
    or eax, 0x80000001
    mov cr0, eax
    ret

[BITS 64]
long_mode_entry:
    jmp 0x8000
    jmp $

enter_long_mode函数包含三个阶段fill_page_table,enable_paginglgdtjmp,第一个阶段是填充页表。第二个阶段是开启分页,其中通过CR4开启了分页,将4级页表的地址加载到CR3寄存器实现了页表切换,然后通过对 EFER MSR进行写入设置长模式位,最后修改CR0寄存器开启分页和保护正式进入长模式。lgdt加载全局描述符,jmp命令刷新段寄存器,并且跳转到长模式下64位代码的入口,正式进入长模式。


项目地址: https://github.com/basic60/ARCUS

引用

  1. https://os.phil-opp.com/entering-longmode/
  2. https://software.intel.com/en-us/articles/intel-sdm ,英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A
  3. https://stackoverflow.com/questions/49811461/why-segmentation-cannot-be-completely-disable