硬核C++实现64位操作系统 MULTIBOOT2引导 (1-1)
由于自己写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:
随后我们把页表定义一下,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++代码了