外观
操作系统(10) - 跳转高址
约 1800 字大约 6 分钟
2025-7-12
请注意:这一章是重构章,因此主要以修改记录的方式来呈现。
为什么要跳转到高地址?
我们即将进入用户模式。为了不让用户意外访问内核地址(会触发 #GPF),就要给内核搬个家。
分离虚拟地址与物理地址
首先,我们之前内核的写法是虚拟地址、物理地址不分的。我们必须区分二者,才能适应高地址映射。
核心代码是一个转换函数:
typedef uint64_t PhysicalAddress;
static inline void* phys2virt(PhysicalAddress phys){
return (void*) phys; // 暂时为恒等映射
}
incenterOS
src
kernel
paging.c
pmm_alloc.c
kernel
kmalloc.c
src/kernel /paging.c
PageTable kernel_pml4; // 内核页表根
PageTable alloc_table(){
PageTable page = (PageTable) alloc_page();
if (!page) {
PhysicalAddress alloc_table(){
PhysicalAddress page_phys = alloc_page();
if (!page_phys) {
raise_err("[ERROR] Cannot alloc new page!");
}
PageTable page = (PageTable) phys2virt(page_phys);
memset(page, 0, PAGE_SIZE);
return page;
return page_phys;
}
PageTable get_or_alloc_table_of(PageTableEntry* entry, uint64_t flags) {
if (!(entry->present)) {
void *table = alloc_table();
entry->value = ((uint64_t)table) | flags;
PhysicalAddress table_phys = alloc_table();
entry->value = table_phys | flags;
}
return (uint64_t*)(entry->value & ~(PAGE_SIZE-1));
return (uint64_t*) phys2virt(entry->value & ~(PAGE_SIZE-1));
}
...
void paging_set_root(){
kernel_pml4 = alloc_table();
kernel_pml4 = phys2virt(alloc_table());
if (!kernel_pml4){
raise_err("[ERROR] Cannot alloc PML4 table.");
}
src/kernel /pmm_alloc.c
void* alloc_page(){
PhysicalAddress alloc_page(){
for (size_t i = last_pos; i < MAX_PAGE_COUNT; i++){
if (!test_bit(page_bitmap, i)){
set_bit(page_bitmap, i);
last_pos = i + 1;
return (void*) (i * PAGE_SIZE);
return i * PAGE_SIZE;
}
}
return NULL;
return 0;
}
void free_page(size_t addr){
void free_page(PhysicalAddress addr){
...
}
src/kernel/kmalloc.c
void *addr = alloc_page();
void *addr = phys2virt(alloc_page());
在 Bootloader 设置页表
内核自己把自己挪到高地址是非常困难的。因此,我们要将页表的构建前移到 bootloader。
请注意:CR3 要在最后设置。
incenterOS
src
bootloader
paging_adapter.c
bootloader.c
paging.c
paging.h
kernel
kernel.c
paging.c
paging.h
shared
bootinfo.h
src/bootloader/paging_adapter.c
#include "output.h"
#include "err.h"
#include "efi.h"
void raise_err(char *err){
for (; *err; err++){
PutChar((CHAR16){*err, 0});
}
Err(L"");
}
EFI_PHYSICAL_ADDRESS alloc_page(){
EFI_STATUS status;
EFI_PHYSICAL_ADDRESS Memory;
status = gBS->AllocatePages(
AllocateAnyPages, // 分配任意可用页
EfiLoaderData,
1,
&Memory // 返回分配的物理地址
);
if (EFI_ERROR(status)) {
return 0;
}
return Memory;
}
src/bootloader/bootloader.c
void init_paging(BootInfo boot_info);
EFI_STATUS EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
...
PutStr(L"[BOOT] Initalizing paging...\r\n");
UINT64 MemSize = GetMemSize();
paging_set_root();
...
BI.mem.phys_mem_size = GetMemSize();
BI.pml4_phys = pml4_phys;
PutStr(L"[BOOT] Mapping virtual address...\r\n");
init_paging(BI);
PutStr(L"[BOOT] Exiting boot...\r\n");
UINTN MapKey;
GetMemMap(&BI.mem.mem_map, &BI.mem.count, &BI.mem.desc_size, &MapKey);
ExitBootDevices(ImageHandle, MapKey);
__asm__ volatile("mov %0, %%cr3" :: "r"(pml4_phys)); // 调用函数的话接下来可能会出事
...
}
#define MB (1024 * 1024)
#define APIC_DEFAULT_BASE 0xFEE00000
#define IOAPIC_DEFAULT_BASE 0xFEC00000
void init_paging(BootInfo boot_info){
// 内核
map_pages(0, boot_info.mem.phys_mem_size, 0, PRESENT | WRITABLE); // 恒等
// 已经包括栈
// 帧缓冲区
uint64_t fb_base = (uint64_t)(boot_info.graphics.framebuffer);
uint64_t fb_size = boot_info.graphics.size_bytes; // BI 新增项目
map_pages(fb_base, fb_size, fb_base, PRESENT | WRITABLE);
// APIC MMIO
map_pages(APIC_DEFAULT_BASE, 0x1000, APIC_DEFAULT_BASE, PRESENT | WRITABLE);
map_pages(IOAPIC_DEFAULT_BASE, 0x1000, IOAPIC_DEFAULT_BASE, PRESENT | WRITABLE);
// initrd
map_pages(boot_info.initrd.start,
boot_info.initrd.size,
boot_info.initrd.start,
PRESENT | WRITABLE);
}
src/bootloader/paging.c
#define BOOTLOADER
#include "../shared/paging.c"
#undef BOOTLOADER
src/bootloader/paging.h
#pragma once
#define BOOTLOADER
#include "../shared/paging.h"
#undef BOOTLOADER
src/kernel /kernel.c
void init_paging();
void kernel_main(){
...
paging_load_root(boot_info.pml4_phys);
InitGOPFrom(boot_info.graphics);
ClearScreen();
set_scaling(1, 2);
print("[KERNEL] Successfully loaded page table!\n");
__asm__ volatile ("cli");
...
print("[KERNEL] Initializing paging...\n");
paging_set_root();
init_paging();
print("[KERNEL] Loading page table...\n");
set_cr3();
...
}
src/kernel /paging.c
**DELETED ALL**
#define KERNEL
#include "../shared/paging.c"
src/kernel /paging.h
**DELETED ALL**
#pragma once
#define KERNEL
#include "../shared/paging.h"
#undef KERNEL
src/shared/bootinfo.h
...
typedef struct {
EFI_MEMORY_DESCRIPTOR *mem_map;
uint64_t count;
uint64_t desc_size;
uint64_t phys_mem_size;
} MemMapInfo;
...
typedef struct {
StackInfo stack;
MemMapInfo mem;
InitrdInfo initrd;
uint64_t pml4_phys;
} BootInfo;
映射数据区域到高地址
以下,我们先映射数据区域到高地址。以下是映射的地址。为了内核访问物理内存方便,我们会把整块物理内存映射到高地址。
src/shared/addr.h
#pragma once
#define KB (1ull << 10)
#define MB (1ull << 20)
#define GB (1ull << 30)
#define TB (1ull << 40)
#define APIC_DEFAULT_BASE 0xFEE00000
#define IOAPIC_DEFAULT_BASE 0xFEC00000
#define APIC_VIRT_BASE 0xFFFFFFFFFEE00000
#define IOAPIC_VIRT_BASE 0xFFFFFFFFFEC00000
#define FB_VIRT_BASE 0xFFFFFFFFC0000000
#define STACK_VIRT_BASE 0xFFFFFFFF80000000
#define INITRAMFS_POOL_VIRT 0xFFFFFFF800000000
#define HIGH_ADDR 0xFFFF800000000000
#define PHYS_MAP_MAX (8 * TB)
#define HIGH_ADDR_END (HIGH_ADDR + PHYS_MAP_MAX)
此时,phys2virt
就会变成下面这样:
#ifdef BOOTLOADER
#define PHYS_BASE 0
#endif
#ifdef KERNEL
#define PHYS_BASE HIGH_ADDR
#endif
#ifdef PHYS_BASE
static inline void* phys2virt(PhysicalAddress phys){
return (void*)phys + PHYS_BASE;
}
#endif
我们把地址映射与迁移相关的代码全部写入 High Address Loader:
src/bootloader/hloader.c
#include "hloader.h"
#include "paging.h"
#include "../shared/addr.h"
#define MB (1024 * 1024)
void InitPaging(BootInfo boot_info){
// 全内存(主要是内核,供内核使用)
uint64_t mem_size = boot_info.mem.phys_mem_size;
if (mem_size > PHYS_MAP_MAX){
mem_size = PHYS_MAP_MAX;
}
map_pages(HIGH_ADDR, mem_size, 0, PRESENT | WRITABLE);
map_pages(0, 8*MB, 0, PRESENT | WRITABLE); // 恒等,此处只映射内核以确保其他数据映射完整
// 内核栈
map_pages(STACK_VIRT_BASE, boot_info.stack.size, boot_info.stack.base_addr, PRESENT | WRITABLE);
// 帧缓冲区
uint64_t fb_base = (uint64_t)(boot_info.graphics.framebuffer);
uint64_t fb_size = boot_info.graphics.size_bytes;
map_pages(FB_VIRT_BASE, fb_size, fb_base, PRESENT | WRITABLE | UNCACHEABLE);
// APIC MMIO
map_pages(APIC_VIRT_BASE, 0x1000, APIC_DEFAULT_BASE, PRESENT | WRITABLE | UNCACHEABLE);
map_pages(IOAPIC_VIRT_BASE, 0x1000, IOAPIC_DEFAULT_BASE, PRESENT | WRITABLE | UNCACHEABLE);
}
BootInfo* TranslateBI(BootInfo *BI){
BI->mem.mem_map = (void*)BI->mem.mem_map + HIGH_ADDR;
BI->initrd.start = (void*)BI->initrd.start + HIGH_ADDR;
BI->graphics.framebuffer = (void*)FB_VIRT_BASE;
BI->stack.base_addr = STACK_VIRT_BASE;
return (void*)BI + HIGH_ADDR;
// 必须转成 (void*),因为 ptr + idx 相当于 &ptr[idx]
}
incenterOS
src
bootloader
bootloader.c
kernel
apic.c
apic.h
initramfs.c
kernel.c
shared
bootinfo.h
src/bootloader/bootloader.c
#include "output.h"
#include "gop.h"
#include "../shared/graphics/logo.h"
#include "hloader.h"
#include "../shared/addr.h"
#include "paging.h"
EFI_SYSTEM_TABLE *ST;
EFI_BOOT_SERVICES *BS;
BootInfo BI;
// 规避和栈有关的乱七八糟的问题
KERNEL_ENTRY KernelEntry;
void *BIVirt;
EFI_STATUS EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
PutStr(L"[BOOT] Loading kernel...\r\n");
void *KernelBuffer = ReadFile(Volume, L"\\kernel.elf", NULL);
KERNEL_ENTRY KernelEntry = LoadKernel(KernelBuffer);
KernelEntry = LoadKernel(KernelBuffer);
SystemTable->BootServices->FreePool(KernelBuffer);
...
PutStr(L"[BOOT] Initalizing paging...\r\n");
paging_set_root();
PutStr(L"[BOOT] Initalizing boot info...\r\n");
BI.magic = MAGIC;
...
BI.graphics.pitch = Pitch;
BI.stack.base_addr = KernelStackBase;
BI.stack.size = KernelStackSize;
BI.mem.phys_mem_size = GetMemSize();
BI.initrd.start = (EFI_PHYSICAL_ADDRESS)InitRDStart;
BI.initrd.start = InitRDStart;
BI.initrd.size = InitRDSize;
BI.pml4_phys = pml4_phys;
init_paging(BI);
InitPaging(BI);
// 临时映射 bootloader
uint64_t rip;
__asm__ volatile ("leaq (%%rip), %0" : "=r"(rip));
PutStr(L"[BOOT] RIP="); PrintHex(rip); PutStr(L"\r\n");
map_pages(rip &~0xFFF, 2*MB, rip &~0xFFF, WRITABLE | PRESENT);
PutStr(L"[BOOT] Exiting boot...\r\n");
UINTN MapKey;
GetMemMap(&BI.mem.mem_map, &BI.mem.count, &BI.mem.desc_size, &MapKey);
ExitBootDevices(ImageHandle, MapKey);
BIVirt = TranslateBI(&BI);
__asm__ volatile ("mov %0, %%cr3" :: "r"(pml4_phys));
__asm__ volatile (
// "r" 的参数将在内联汇编之前放入寄存器,因此可以使用局部变量
// 但是为了避免奇奇怪怪的问题我们还是用全局变量
"mov %[stack], %%rsp\n"
"mov %[info], %%rdi\n"
"jmp *%[entry]\n"
:
: [stack] "r"(KernelStackTop)
[info] "r"(&BI),
: [stack] "r"(BI.stack.base_addr + BI.stack.size),
// 注意:x86 是满递减栈;rsp 指向有效内存的下一个地址
[info] "r"(BIVirt),
[entry] "r"(KernelEntry)
);
return EFI_SUCCESS;
}
// 后面的都删掉
src/kernel /apic.c
#define APIC_BASE_MSR 0x1B
#define APIC_SVR 0xF0
#define APIC_ENABLE 0x100
#define APIC_LVT_LINT0 (APIC_DEFAULT_BASE + 0x350)
#define APIC_LVT_LINT0 (APIC_VIRT_BASE + 0x350)
src/kernel /apic.h
#pragma once
#include <stdint.h>
#define APIC_DEFAULT_BASE 0xFEE00000
#define IOAPIC_DEFAULT_BASE 0xFEC00000
#include "../shared/addr.h"
#define APIC_EOI 0xB0
#define IOAPIC_REGSEL 0xFEC00000
#define IOAPIC_WINDOW 0xFEC00010
#define IOAPIC_REGSEL IOAPIC_VIRT_BASE
#define IOAPIC_WINDOW (IOAPIC_VIRT_BASE + 0x10)
static inline void apic_write(uint32_t reg, uint32_t val) {
((volatile uint32_t*)APIC_DEFAULT_BASE)[reg / 4] = val;
((volatile uint32_t*)APIC_VIRT_BASE)[reg / 4] = val;
}
static inline uint32_t apic_read(uint32_t reg) {
return ((volatile uint32_t*)APIC_DEFAULT_BASE)[reg / 4];
return ((volatile uint32_t*)APIC_VIRT_BASE)[reg / 4];
}
src/kernel /initramfs.c
#include "print.h"
#include "pool.h"
#include "hash.h"
#include "../shared/addr.h"
#define INITRAMFS_POOL_VIRT 0xFFFFFFFF80000000
#define INITRAMFS_POOL_MAX (1 << 30) // 1 GB
#define INITRAMFS_POOL_INIT_SIZE (1 << 24) // 16MB
...
src/kernel /kernel.c
void kernel_main(){
...
print("[KERNEL] Loading initrd...\n");
cpio_init((void*)boot_info.initrd.start, boot_info.initrd.size);
cpio_init(boot_info.initrd.start, boot_info.initrd.size);
print("[KERNEL]\n");
FSItem *file_info = initramfs_find(NULL, "/dir/ciallo.txt");
}
src/shared/bootinfo.h
...
typedef struct {
uint64_t start;
void *start;
uint64_t size;
} InitrdInfo;
在高地址运行内核
经过了前面的准备工作,我们就可以很轻松地把内核挪到高地址了。只需改写链接器和加载代码即可。
src/kernel /linker.ld
ENTRY(kernel_entry)
SECTIONS {
. = 0x100000; // [!code --]
. = 0xFFFF800000100000; // [!code ++]
.text : { *(.text*) }
.data : { *(.data*) }
.bss : { *(.bss*) }
}
src/bootloader/loadkernel.c
KERNEL_ENTRY LoadKernel(void *KernelBuffer) {
...
for (...){
...
void *src = KernelBuffer + ph->p_offset;
void *dest = (void*)(UINTN) ph->p_vaddr;
void *dest = (void*)(UINTN) ph->p_vaddr - HIGH_ADDR;
EFI_PHYSICAL_ADDRESS addr = ph->p_vaddr & ~PageMask;
EFI_PHYSICAL_ADDRESS addr = (UINTN)dest & ~PageMask;
UINTN num_pages = (ph->p_memsz + PageSize-1) / PageSize;
TryAllocPagesAt(addr, num_pages);
...
}
...
return (KERNEL_ENTRY)(UINTN)(ehdr->e_entry); // 链接器决定的高地址
}