硬核C++实现64位操作系统 MULTIBOOT2引导 (1-1)

Aug 01, 2020

由于自己写MBR引导过于繁琐 原项目在https://github.com/dllexport/mos

现在已经改为multiboot2引导

只需要写少量汇编建立页表,开启64bit模式后即可跳入C函数

有了multiboot2,就不用自己在loader读磁盘kernel镜像了,也不用自己解析ELF找C函数的入口地址了,也不用自己去找ACPI入口了,而且E820内存信息也被multiboot2包办成tag了,方便快捷。

引导头部

我们要实现的是一个multiboot2的header

section .multiboot_header
align 8
header_start:
    dd 0xe85250d6                ; magic number (multiboot 2 spec)
    dd 0                         ; architecture 0
    dd header_end - header_start ; header length
    ; checksum
    dd 0x100000000 - (0xe85250d6 + 0 + (header_end - header_start))

    ; required end tag
    dw 0    ; type
    dw 0    ; flags
    dd 8    ; size
header_end:
boot.asm

随后我们把页表定义一下,align PAGE_SIZE把页表对齐到4K的地址上

section .data
global pml4
global pdpe
global pde
global pdpe_low
global pte
align PAGE_SIZE
pml4 equ $ - KERNEL_TEXT_BASE
    times 512 dq 0
pdpe equ $ - KERNEL_TEXT_BASE
    times 512 dq 0
pde equ $ - KERNEL_TEXT_BASE
    times 512 dq 0
pte equ $ - KERNEL_TEXT_BASE
    times 512 dq 0
pdpe_low equ $ - KERNEL_TEXT_BASE
    times 512 dq 0

由于我们的链接脚本把.data段全部安排在了0xFFFFFFFF80000000后面

其中pdpe_low用于做直接映射 即0x0虚拟地址映射到物理地址0x0

而pdpe用作内核地址的映射,例如

0xFFFFFFFF80000000-0xFFFFFFFF807fffff -> 0x0-0x7fffff

所以

equ $ - KERNEL_TEXT_BASE

等于是做一个虚拟地址到物理地址的转换

从编译好的镜像可以看到pml4地址为0X9000

pml4 实际地址取决于ld script

GDT表

先随便定义一个GDT用于进入64bit的long mode

align 16
GDT equ $ - KERNEL_TEXT_BASE
    dq 0x0000000000000000             ; Null Descriptor - should be present.
GDT.CODE equ $ - KERNEL_TEXT_BASE
    dq 0x00209A0000000000             ; 64-bit code descriptor (exec/read).
GDT.DATA equ $ - KERNEL_TEXT_BASE
    dq 0x0000920000000000             ; 64-bit data descriptor (read/write).
GDT.Pointer equ $ - KERNEL_TEXT_BASE
    dw $ - KERNEL_TEXT_BASE - GDT - 1
    dq GDT                            

IDT可以暂时不定义,等进入C函数后再写

boot的内核栈

待会要用到call指令,先安排一个栈空间

align PAGE_SIZE
STACK_END:
    times PAGE_SIZE * 4 db 0
global STACK_START
STACK_START:

STACK_START就是栈顶了

页表建立

 这里到了text段了,multiboot2初始化后会执行这里的代码

 注意现在仍然是32bit模式而且是没有开启分页的

我们先把64bit的页表搞起来

%define PAGE_PRESENT    (1 << 0)
%define PAGE_WRITE      (1 << 1)
%define PAGE_USER       (1 << 2)

section .text
bits 32
global _start
_start:

    mov eax, pdpe_low
    or  eax, (PAGE_PRESENT | PAGE_WRITE)
    mov dword [pml4], eax

    mov eax, pdpe
    or  eax, (PAGE_PRESENT | PAGE_WRITE)
    mov dword [pml4 + 0xff8], eax

    mov eax, pde
    or  eax, (PAGE_PRESENT | PAGE_WRITE)
    mov [pdpe_low], eax
    mov [pdpe + 0xff0], eax
    
    mov eax, pte
    or  eax, (PAGE_PRESENT | PAGE_WRITE)
    mov [pde], eax
    
    mov edx, pte         
    mov eax, (PAGE_PRESENT | PAGE_WRITE)    
.build2MTable:
    mov [edx], eax
    add eax, 0x1000
    add edx, 8
    cmp eax, 0x200000                 ; If we did all 2MiB, end.
    jb .build2MTable

4级页表逐级建立,

内核地址为0xffffffff00000000,它的最高9bit为

111111111

所以pml4索引为0x1ff,偏移为0xff8

同理pdpe对应的9bit为

111111110

它的索引为0x1f,偏移为0xff0

后面的索引都是0

最后建立的pte映射了2MB内存

当然现在还没开64bit模式 开启后可以看到下面的结果

开启64bit模式

页表弄完 下面切换模式

lea eax, [pml4] 
mov cr3, eax

把页表地址给CR3寄存器就行了,CR0保存的是物理地址

然后打开PAE

%define CONTROL_REGISTER4_PHYSICAL_ADDRESS_EXTENSION (1 << 5)
%define KERNEL_CR4 (CONTROL_REGISTER4_PHYSICAL_ADDRESS_EXTENSION)
mov eax, KERNEL_CR4
mov cr4, eax

打开长模式 需要写MSR_EFER寄存器

%define MSR_EFER 0xC0000080
%define MSR_EFER_LME (1 << 8)
%define MSR_EFER_SCE (1 << 0)

mov ecx, MSR_EFER
rdmsr
or eax, MSR_EFER_LME | MSR_EFER_SCE
wrmsr

这里LME用于打开长模式

SCE用于打开syscall sysret支持

因为后面实现系统调用不再使用中断的方式或者是sysenter sysexit这些指令

最后一步开启分页

%define CONTROL_REGISTER0_PROTECTED_MODE_ENABLED (1 << 0)
%define CONTROL_REGISTER0_EXTENSION_TYPE (1 << 4)
%define CONTROL_REGISTER0_PAGE (1 << 31)
%define KERNEL_CR0 (CONTROL_REGISTER0_PAGE | CONTROL_REGISTER0_PROTECTED_MODE_ENABLED | CONTROL_REGISTER0_EXTENSION_TYPE)

mov eax, KERNEL_CR0
mov cr0, eax

完事后加载gdt和跳转至64位代码

lgdt [GDT.Pointer]
jmp CODE_SEG:(_start64 - KERNEL_TEXT_BASE)

这里_start64处在高地址0xffffffff00000000上面,没法在32位下跳转64位的地址

由于做了直接映射 这里跳转低地址来解决问题

64bit模式·

现在已经进入了64bit模式

首先声明外部函数Kernel_Main 这是我们的C++入口函数

Kernel_Main是extern "C"声明的否则我们不知道导出的C++符号叫什么名字

section .text
bits 64
extern Kernel_Main
global _start64
_start64:
    mov ax, 0x0
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    mov rsp, STACK_START
    mov rbp, rsp

    mov rdi, rbx
    
    mov rax, Kernel_Main
    call rax

首先把段寄存器清理一下,虽然64位下这些寄存器没有什么卵用

然后设置好RSP RBP

注意rbx保存了multiboot2模块的信息,包括E820,内核ELF等等

从上面_start开始,我们都没有修改过rbx寄存器

这里把rbx作为Kernel_Main的第一个参数

根据X64调用约定 放在rdi上

然后直接CALL这个函数就进入了C++代码了

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.