首页 > 其他分享 >自制x86 Bootloader开发笔记(3)——— 进入长模式

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

时间:2023-11-11 17:11:39浏览次数:37  
标签:x86 自制 mov 模式 eax 地址 64 页表 Bootloader

前言

本项目是基于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

标签:x86,自制,mov,模式,eax,地址,64,页表,Bootloader
From: https://www.cnblogs.com/basic60/p/12563545.html

相关文章

  • sniperoj-pwn100-shellcode-x86-64
    来源本题来自ctfi-wiki,是基本ROP中shellcode的一道例题查看保护程序只开启了位置无关保护PIE并且有可写可执行的段反汇编发现程序中直接输出了buf的地址,所以PIE就不起作用了可以看到,后面read函数还调用了buf,很显然就是要利用buf这个段了然后在汇编代码发现调用read......
  • x86宕机日志解读1
    下面以一个典型的x86服务的宕机日志为例进行解读:[330931.874444]BUG:unabletohandlekernelpagingrequestatffffffffa22a4668[330931.874532]PGD3a23067P4D3a23067PUD3a24063PMD1ee9909067PTE0[330931.874618]Oops:0000[#1]SMPKASANNOPTI[330931.8746......
  • centOS6.5 无法使用yum源的问题 removing mirrorlist with no valid mirrors: /var/ca
     一次在临时服务器执行yum命令出现报错问题:removingmirrorlistwithnovalidmirrors:/var/cache/yum/x86_64/6/base/mirrorlist.txt ......1、修改fastestmirror.conf的配置参数sed-i"s|enabled=1|enabled=0|g"/etc/yum/pluginconf.d/fastestmirror.conf2、备份......
  • gcc 为什么在arm 开发板上 就是编译的arch64 , 在虚拟机中编译的就是 x86
     在开发板上,编译命令如下,并且程序也是可以运行的。  看看原因。    所以在板子上编译的是aarch64架构的。......
  • x86平台SIMD编程入门(5):提示与技巧
    1、提示与技巧访问内存的成本非常高,一次缓存未命中可能会耗费100~300个周期。L3缓存加载需要40~50个周期,L2缓存大约需要10个周期,即使L1缓存的访问速度也明显慢于寄存器。所以要尽量保持数据结构对SIMD友好,优先选择std::vector、CAtlArray、eastl::vector等容器,按照顺序读取数据......
  • x86平台SIMD编程入门(4):整型指令
    1、算术指令算术类型函数示例加_mm_add_epi32、_mm256_sub_epi16减_mm_sub_epi32、_mm256_sub_epi16乘_mm_mul_epi32、_mm_mullo_epi32除无水平加/减_mm_hadd_epi16、_mm256_hsub_epi32饱和加/减_mm_adds_epi8、_mm256_subs_epi16最大/最小值_......
  • x86平台SIMD编程入门(3):浮点指令
    1、算术指令算术类型函数示例备注加_mm_add_sd、_mm256_add_ps减_mm_sub_sd、_mm256_sub_ps乘_mm_mul_sd、_mm256_mul_ps除_mm_div_sd、_mm256_div_ps平方根_mm_sqrt_sd、_mm256_sqrt_ps倒数_mm_rcp_ss、_mm_rcp_ps、_mm256_rcp_ps快速计算......
  • x86平台SIMD编程入门(2):通用指令
    1、重解释转换虽然128位的XMM寄存器在硬件上只是256位YMM寄存器的下半部分,但在C++中它们是不同的类型。有一些intrinsic函数可以将它们重新解释为不同的类型,如下表所示,行代表源类型,列代表目标类型。__m128__m128d__m128i__m256__m256d__m256d__m128=_mm_castps_......
  • x86平台SIMD编程入门(1):SIMD基础知识
    1、简介SIMD(SingleInstruction,MultipleData)是一种并行计算技术,它通过向量寄存器存储多个数据元素,并使用单条指令同时对这些数据元素进行处理,从而提高了计算效率。SIMD已被广泛应用于需要大量数据并行计算的领域,包括图像处理、视频编码、信号处理、科学计算等。许多现代处理......
  • libpcre2-8.so.0()(64bit) is needed by zabbix-agent-6.4.0-release1.el7.x86_64
    报错:libpcre2-8.so.0()(64bit)isneededbyzabbix-agent-6.4.0-release1.el7.x86_64解决方法:[root@zabbix_server~]#yuminstallpcre2-......