mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1615 字
4 分钟
正式加载内核
2021-05-07

正式加载内核#

在前面的章节中,我们已经完成了从 MBR 到 Loader 的引导链,成功进入保护模式并开启了分页虚拟内存。现在,我们来到了最激动人心的一步:加载并运行真正的操作系统内核。本章将详细介绍 ELF 可执行文件格式,以及如何从汇编引导程序跳转到 C 语言内核入口。

为什么选择 ELF 格式#

ELF(Executable and Linkable Format)是 Unix/Linux 系统中最主流的可执行文件格式,具有以下优势:

第一,ELF 格式支持灵活的内存布局。内核代码段、数据段、BSS 段可以被加载到不同的内存地址,满足操作系统内核的布局需求。

第二,ELF 格式支持符号表和调试信息。在内核开发过程中,调试信息对于定位问题非常重要。

第三,ELF 格式有成熟完善的工具链支持。GCC、LD、OBJDUMP 等工具都能完美处理 ELF 文件。

第四,ELF 格式文档齐全,结构清晰。理解 ELF 格式对于深入理解操作系统原理大有裨益。

ELF 文件格式解析#

ELF 文件总体结构#

ELF 文件由四部分组成:ELF 头部(ELF Header)、程序头表(Program Header Table)、节区(Sections)和节区头表(Section Header Table)。

ELF 文件结构:
┌─────────────────────────┐
│ ELF Header │ <- 文件开头,描述整个文件
├─────────────────────────┤
│ Program Header Table │ <- 描述可加载段(对运行时重要)
├─────────────────────────┤
│ │
│ Section 1 │ <- .text 代码段
│ │
├─────────────────────────┤
│ │
│ Section 2 │ <- .data 数据段
│ │
├─────────────────────────┤
│ ... │
├─────────────────────────┤
│ │
│ Section Header Table │ <- 描述各个节区(对链接时重要)
│ │
└─────────────────────────┘

对于加载内核而言,主要关注 ELF 头部和程序头表,因为它们包含了运行时需要的所有信息。

ELF 头部(ELF Header)#

ELF 头部位于文件开头,描述了整个文件的基本信息。在 32 位系统中,ELF 头部的大小为 52 字节;在 64 位系统中,大小为 64 字节。

ELF 头部数据结构#

// 32 位 ELF 头部
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF 标识信息
uint16_t e_type; // 文件类型
uint16_t e_machine; // 目标架构
uint32_t e_version; // ELF 版本
uint32_t e_entry; // 程序入口点地址
uint32_t e_phoff; // 程序头表偏移
uint32_t e_shoff; // 节区头表偏移
uint32_t e_flags; // 处理器特定标志
uint16_t e_ehsize; // ELF 头部大小
uint16_t e_phentsize; // 程序头表条目大小
uint16_t e_phnum; // 程序头表条目数量
uint16_t e_shentsize; // 节区头表条目大小
uint16_t e_shnum; // 节区头表条目数量
uint16_t e_shstrndx; // 节区名称字符串表索引
} Elf32_Ehdr;
// 64 位 ELF 头部
typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF 标识信息
uint16_t e_type; // 文件类型
uint16_t e_machine; // 目标架构
uint32_t e_version; // ELF 版本
uint64_t e_entry; // 程序入口点地址
uint64_t e_phoff; // 程序头表偏移
uint64_t e_shoff; // 节区头表偏移
uint32_t e_flags; // 处理器特定标志
uint16_t e_ehsize; // ELF 头部大小
uint16_t e_phentsize; // 程序头表条目大小
uint16_t e_phnum; // 程序头表条目数量
uint16_t e_shentsize; // 节区头表条目大小
uint16_t e_shnum; // 节区头表条目数量
uint16_t e_shstrndx; // 节区名称字符串表索引
} Elf64_Ehdr;

e_ident 字段详解#

e_ident 数组包含了 ELF 文件的魔数和基本标识信息:

// e_ident 索引定义
#define EI_MAG0 0 // 魔数第一个字节:0x7F
#define EI_MAG1 1 // 魔数第二个字节:'E'
#define EI_MAG2 2 // 魔数第三个字节:'L'
#define EI_MAG3 3 // 魔数第四个字节:'F'
#define EI_CLASS 4 // 文件类别(32位/64位)
#define EI_DATA 5 // 数据编码(大端/小端)
#define EI_VERSION 6 // ELF 版本
#define EI_OSABI 7 // 操作系统/ABI 标识
#define EI_ABIVERSION 8 // ABI 版本
#define EI_PAD 9 // 填充字节起始位置
// EI_CLASS 值
#define ELFCLASSNONE 0 // 无效
#define ELFCLASS32 1 // 32 位
#define ELFCLASS64 2 // 64 位
// EI_DATA 值
#define ELFDATANONE 0 // 无效
#define ELFDATA2LSB 1 // 小端序
#define ELFDATA2MSB 2 // 大端序

e_type 字段#

文件类型字段用于区分不同用途的 ELF 文件:

#define ET_NONE 0 // 无类型
#define ET_REL 1 // 可重定位文件(.o 文件)
#define ET_EXEC 2 // 可执行文件
#define ET_DYN 3 // 共享目标文件(.so 文件)
#define ET_CORE 4 // 核心转储文件

对于内核文件,通常是 ET_EXEC 类型。

e_machine 字段#

目标架构字段标识文件运行的目标平台:

#define EM_NONE 0 // 无机器类型
#define EM_386 3 // Intel 80386
#define EM_X86_64 62 // AMD x86-64
#define EM_ARM 40 // ARM
#define EM_AARCH64 183 // ARM 64 位

程序头表(Program Header Table)#

程序头表描述了 ELF 文件中的段(Segment),这些段在加载时会被映射到内存中。程序头表是一个数组,每个元素描述一个段。

程序头数据结构#

// 32 位程序头
typedef struct {
uint32_t p_type; // 段类型
uint32_t p_offset; // 段在文件中的偏移
uint32_t p_vaddr; // 段在内存中的虚拟地址
uint32_t p_paddr; // 段的物理地址(通常等于 p_vaddr)
uint32_t p_filesz; // 段在文件中的大小
uint32_t p_memsz; // 段在内存中的大小
uint32_t p_flags; // 段的标志
uint32_t p_align; // 段的对齐方式
} Elf32_Phdr;
// 64 位程序头
typedef struct {
uint32_t p_type; // 段类型
uint32_t p_flags; // 段的标志
uint64_t p_offset; // 段在文件中的偏移
uint64_t p_vaddr; // 段在内存中的虚拟地址
uint64_t p_paddr; // 段的物理地址
uint64_t p_filesz; // 段在文件中的大小
uint64_t p_memsz; // 段在内存中的大小
uint64_t p_align; // 段的对齐方式
} Elf64_Phdr;

p_type 字段#

段类型决定了段的处理方式:

#define PT_NULL 0 // 未使用
#define PT_LOAD 1 // 可加载段(必须加载到内存)
#define PT_DYNAMIC 2 // 动态链接信息
#define PT_INTERP 3 // 解释器路径
#define PT_NOTE 4 // 注释信息
#define PT_SHLIB 5 // 保留
#define PT_PHDR 6 // 程序头表自身
#define PT_GNU_STACK 0x6474e551 // GNU 栈属性

对于加载内核,主要关注 PT_LOAD 类型的段,这些段包含了实际的代码和数据。

p_flags 字段#

段的权限标志:

#define PF_X 0x1 // 可执行
#define PF_W 0x2 // 可写
#define PF_R 0x4 // 可读

这些标志将转换为页表项的权限位。

节区头表(Section Header Table)#

节区头表主要用于链接阶段,描述了 ELF 文件中的各个节区(Section)。虽然在运行时加载不是必需的,但对于理解和调试内核非常有用。

// 32 位节区头
typedef struct {
uint32_t sh_name; // 节区名称(在字符串表中的索引)
uint32_t sh_type; // 节区类型
uint32_t sh_flags; // 节区标志
uint32_t sh_addr; // 节区在内存中的地址
uint32_t sh_offset; // 节区在文件中的偏移
uint32_t sh_size; // 节区大小
uint32_t sh_link; // 链接到另一个节区的索引
uint32_t sh_info; // 附加信息
uint32_t sh_addralign; // 对齐方式
uint32_t sh_entsize; // 固定大小条目的表项大小
} Elf32_Shdr;

常见的节区类型:

#define SHT_NULL 0 // 无效节区
#define SHT_PROGBITS 1 // 程序定义的内容(如代码、数据)
#define SHT_SYMTAB 2 // 符号表
#define SHT_STRTAB 3 // 字符串表
#define SHT_RELA 4 // 重定位表(带加数)
#define SHT_HASH 5 // 符号哈希表
#define SHT_DYNAMIC 6 // 动态链接信息
#define SHT_NOTE 7 // 注释
#define SHT_NOBITS 8 // 无内容(如 BSS 段)

ELF 解析代码示例#

下面是在 Loader 中解析 ELF 文件的汇编代码:

; -----------------------------------------------------------------------
; elf_loader.S — ELF 内核加载器
; NASM syntax, 32-bit protected mode
; -----------------------------------------------------------------------
%include "../common/bootmacros.inc"
; ELF 常量定义
ELF_MAGIC equ 0x464C457F ; "\x7FELF"
ELFCLASS32 equ 1
ELFDATA2LSB equ 1
ET_EXEC equ 2
EM_386 equ 3
PT_LOAD equ 1
PF_X equ 0x1
PF_W equ 0x2
PF_R equ 0x4
; ELF 头部偏移
E_IDENT equ 0
E_TYPE equ 16
E_MACHINE equ 18
E_VERSION equ 20
E_ENTRY equ 24
E_PHOFF equ 28
E_PHENTSIZE equ 42
E_PHNUM equ 44
; 程序头偏移
P_TYPE equ 0
P_OFFSET equ 4
P_VADDR equ 8
P_PADDR equ 12
P_FILESZ equ 16
P_MEMSZ equ 20
P_FLAGS equ 24
P_ALIGN equ 28
; -----------------------------------------------------------------------
; load_elf_kernel
; 输入: esi = ELF 文件在内存中的起始地址
; 输出: eax = 内核入口地址,加载成功返回入口点,失败返回 0
; -----------------------------------------------------------------------
load_elf_kernel:
push ebx
push ecx
push edx
push edi
push ebp
; 保存 ELF 起始地址
mov ebp, esi
; 验证 ELF 魔数
mov eax, [esi + E_IDENT]
cmp eax, ELF_MAGIC
jne .invalid_elf
; 验证类别(32位)
cmp byte [esi + E_IDENT + 4], ELFCLASS32
jne .invalid_elf
; 验证字节序(小端)
cmp byte [esi + E_IDENT + 5], ELFDATA2LSB
jne .invalid_elf
; 验证文件类型(可执行)
mov ax, [esi + E_TYPE]
cmp ax, ET_EXEC
jne .invalid_elf
; 验证架构(i386)
mov ax, [esi + E_MACHINE]
cmp ax, EM_386
jne .invalid_elf
; 获取程序头表信息
mov ebx, [esi + E_PHOFF] ; 程序头表偏移
add ebx, esi ; 程序头表地址
mov cx, [esi + E_PHNUM] ; 程序头数量
mov dx, [esi + E_PHENTSIZE] ; 程序头条目大小
.phdr_loop:
test cx, cx
jz .load_done
; 检查段类型
mov eax, [ebx + P_TYPE]
cmp eax, PT_LOAD
jne .next_phdr
; 这是一个可加载段,需要复制到内存
push ecx
push ebx
; 获取源地址(文件中的偏移 + ELF 起始)
mov esi, [ebx + P_OFFSET]
add esi, ebp
; 获取目标地址
mov edi, [ebx + P_PADDR]
; 获取复制大小
mov ecx, [ebx + P_FILESZ]
; 复制段内容
cld
rep movsb
; 如果内存大小 > 文件大小,需要填充 0(BSS)
mov ecx, [ebx + P_MEMSZ]
sub ecx, [ebx + P_FILESZ]
jle .no_bss
xor eax, eax
rep stosb
.no_bss:
pop ebx
pop ecx
.next_phdr:
; 移动到下一个程序头
add ebx, edx
dec cx
jmp .phdr_loop
.load_done:
; 返回入口点地址
mov eax, [ebp + E_ENTRY]
jmp .exit
.invalid_elf:
xor eax, eax ; 返回 0 表示失败
.exit:
pop ebp
pop edi
pop edx
pop ecx
pop ebx
ret

从引导程序到内核的跳转过程#

内核加载流程#

完整的内核加载流程包括以下步骤:

  1. Loader 从磁盘读取内核 ELF 文件到内存缓冲区
  2. 解析 ELF 头部,验证文件格式
  3. 遍历程序头表,加载所有 PT_LOAD 类型的段
  4. 处理 BSS 段(清零未初始化数据区域)
  5. 获取内核入口点地址
  6. 设置内核运行环境(栈、GDT、IDT 等)
  7. 跳转到内核入口点执行
graph TD A[Loader 启动] --> B[进入保护模式] B --> C[开启分页] C --> D[读取内核 ELF 到缓冲区] D --> E{验证 ELF 格式} E -->|失败| F[报错并停止] E -->|成功| G[遍历程序头表] G --> H{PT_LOAD 段?} H -->|是| I[复制段到目标内存] I --> J[处理 BSS] J --> H H -->|否| K[下一个程序头] K --> H H -->|遍历完成| L[获取入口点] L --> M[设置内核环境] M --> N[跳转到内核入口] N --> O[内核开始执行]

完整的 Loader 代码#

下面是整合了 ELF 加载功能的完整 Loader 代码:

; -----------------------------------------------------------------------
; loader.S — 操作系统加载器
; 职责:进入保护模式、开启分页、加载 ELF 内核、跳转到内核入口
; -----------------------------------------------------------------------
[bits 16]
org 0x8000
%include "../common/bootmacros.inc"
_start:
jmp loader_main
; 数据区
KERNEL_SECTORS equ 64 ; 内核占用的扇区数(约32KB)
KERNEL_LOAD_ADDR equ 0x100000 ; 内核临时加载地址
MSG_REAL_MODE db "Loader: 16-bit real mode", 0
MSG_PROT_MODE db "Loader: 32-bit protected mode", 0
MSG_PAGING db "Loader: Paging enabled", 0
MSG_LOADING db "Loader: Loading kernel...", 0
MSG_ELF_INVALID db "Error: Invalid ELF format!", 0
MSG_JUMP_KERNEL db "Loader: Jumping to kernel...", 0
; -----------------------------------------------------------------------
; 主程序入口
; -----------------------------------------------------------------------
loader_main:
; 初始化段寄存器
cli
xor ax, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; 设置栈
SET_STACK_AT_MBR
; 清屏并打印信息
call cls
mov bx, MSG_REAL_MODE
call print_bios
; 启用 A20 地址线
in al, 0x92
or al, 0000_0010b
out 0x92, al
; 加载 GDT
lgdt [gdt_descriptor]
; 开启保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 远跳转到保护模式代码
jmp SELECTOR_CODE32:pm_entry
; -----------------------------------------------------------------------
; 实模式辅助函数
; -----------------------------------------------------------------------
print_bios:
pusha
.loop:
mov al, [bx]
test al, al
jz .done
mov ah, 0x0e
int 0x10
inc bx
jmp .loop
.done:
popa
ret
cls:
mov ax, 0x0600
mov bx, 0x0700
mov cx, 0
mov dx, 184Fh
int 0x10
mov ah, 0x02
xor dx, dx
xor bh, bh
int 0x10
ret
; -----------------------------------------------------------------------
; 保护模式代码
; -----------------------------------------------------------------------
[bits 32]
%include "../common/lib/pm_utils.S"
pm_entry:
; 初始化段寄存器
mov ax, SELECTOR_DATA32
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov ax, SELECTOR_VIDEO
mov gs, ax
; 设置栈
SET_STACK_AT_PM
; 清屏
call clear_screen
; 打印保护模式信息
push 0x0F
push MSG_PROT_MODE
call println
add esp, 8
; 初始化并开启分页
call setup_paging
push 0x0F
push MSG_PAGING
call println
add esp, 8
; 加载内核
push 0x0F
push MSG_LOADING
call println
add esp, 8
call load_kernel
; 检查加载结果
test eax, eax
jz .elf_error
; 保存入口点
mov [kernel_entry], eax
push 0x0F
push MSG_JUMP_KERNEL
call println
add esp, 8
; 准备跳转到内核
; 清空流水线
jmp .jump_to_kernel
.elf_error:
push 0x0C ; 红色
push MSG_ELF_INVALID
call println
add esp, 8
cli
.halt:
hlt
jmp .halt
.jump_to_kernel:
; 设置内核栈
mov esp, 0x90000
; 跳转到内核入口点
; 此时:
; - 分页已开启
; - 段寄存器已设置
; - 栈已就绪
jmp [kernel_entry]
; -----------------------------------------------------------------------
; 分页设置
; -----------------------------------------------------------------------
PAGE_DIR_ADDR equ 0x100000
PAGE_TABLE_ADDR equ 0x101000
setup_paging:
pusha
; 清零页目录和第一个页表
mov edi, PAGE_DIR_ADDR
mov ecx, 2048 ; 2 页(页目录 + 页表)
xor eax, eax
rep stosd
; 设置页目录项
; PDE[0] 指向第一个页表,映射低 4MB
mov edi, PAGE_DIR_ADDR
mov eax, PAGE_TABLE_ADDR
or eax, 0x03 ; P=1, R/W=1
mov [edi], eax
; PDE[768] 也指向第一个页表(高 1GB 内核空间映射)
mov [edi + 768 * 4], eax
; 设置页表项(恒等映射低 4MB)
mov edi, PAGE_TABLE_ADDR
mov eax, 0x03 ; P=1, R/W=1
mov ecx, 1024
.fill_pte:
mov [edi], eax
add eax, 4096
add edi, 4
loop .fill_pte
; 加载 CR3
mov eax, PAGE_DIR_ADDR
mov cr3, eax
; 开启分页
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
popa
ret
; -----------------------------------------------------------------------
; 内核加载
; -----------------------------------------------------------------------
load_kernel:
; 读取内核 ELF 文件到临时缓冲区
mov eax, 2 ; 起始扇区(跳过 MBR 和 Loader)
mov ebx, KERNEL_LOAD_ADDR ; 目标地址
mov ecx, KERNEL_SECTORS ; 扇区数
call read_disk_ata
; 解析并加载 ELF
mov esi, KERNEL_LOAD_ADDR
call load_elf_kernel
ret
; -----------------------------------------------------------------------
; ELF 加载器
; -----------------------------------------------------------------------
ELF_MAGIC equ 0x464C457F
PT_LOAD equ 1
load_elf_kernel:
push ebx
push ecx
push edx
push edi
push ebp
mov ebp, esi
; 验证魔数
cmp dword [esi], ELF_MAGIC
jne .invalid
; 验证 32 位
cmp byte [esi + 4], 1
jne .invalid
; 验证小端序
cmp byte [esi + 5], 1
jne .invalid
; 验证可执行文件
cmp word [esi + 16], 2
jne .invalid
; 验证 i386 架构
cmp word [esi + 18], 3
jne .invalid
; 获取程序头表信息
movzx ecx, word [esi + 44] ; p_phnum
movzx edx, word [esi + 42] ; p_phentsize
mov ebx, [esi + 28] ; p_phoff
add ebx, esi ; 程序头表地址
.phdr_loop:
test ecx, ecx
jz .done
; 检查是否为 PT_LOAD
cmp dword [ebx], PT_LOAD
jne .next_phdr
; 复制段
push ecx
push ebx
mov esi, [ebx + 4] ; p_offset
add esi, ebp
mov edi, [ebx + 12] ; p_paddr
mov ecx, [ebx + 16] ; p_filesz
cld
rep movsb
; 处理 BSS
mov ecx, [ebx + 20] ; p_memsz
sub ecx, [ebx + 16] ; p_filesz
jle .no_bss
xor eax, eax
rep stosb
.no_bss:
pop ebx
pop ecx
.next_phdr:
add ebx, edx
dec ecx
jmp .phdr_loop
.done:
; 返回入口点
mov eax, [ebp + 24]
jmp .exit
.invalid:
xor eax, eax
.exit:
pop ebp
pop edi
pop edx
pop ecx
pop ebx
ret
; -----------------------------------------------------------------------
; 数据区
; -----------------------------------------------------------------------
kernel_entry: dd 0
; GDT 定义
DEFINE_STANDARD_GDT

内核入口点与初始化流程#

最小内核示例#

当 Loader 成功加载并跳转到内核入口点后,内核开始执行。下面是一个最小化的 C 语言内核示例:

kernel/main.c
// 简单的端口 I/O 函数
static inline void outb(uint16_t port, uint8_t val) {
asm volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
}
static inline uint8_t inb(uint16_t port) {
uint8_t ret;
asm volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
return ret;
}
// VGA 文本模式缓冲区
#define VGA_BUFFER 0xB8000
#define VGA_WIDTH 80
#define VGA_HEIGHT 25
// VGA 颜色枚举
typedef enum {
VGA_BLACK = 0,
VGA_BLUE = 1,
VGA_GREEN = 2,
VGA_CYAN = 3,
VGA_RED = 4,
VGA_MAGENTA = 5,
VGA_BROWN = 6,
VGA_LIGHT_GREY = 7,
VGA_DARK_GREY = 8,
VGA_LIGHT_BLUE = 9,
VGA_LIGHT_GREEN = 10,
VGA_LIGHT_CYAN = 11,
VGA_LIGHT_RED = 12,
VGA_LIGHT_MAGENTA = 13,
VGA_LIGHT_BROWN = 14,
VGA_WHITE = 15,
} vga_color;
// 创建 VGA 颜色属性字节
static inline uint8_t vga_entry_color(vga_color fg, vga_color bg) {
return fg | bg << 4;
}
// 创建 VGA 字符项
static inline uint16_t vga_entry(unsigned char uc, uint8_t color) {
return (uint16_t) uc | (uint16_t) color << 8;
}
// 字符串长度
size_t strlen(const char* str) {
size_t len = 0;
while (str[len])
len++;
return len;
}
// 全局终端状态
static size_t terminal_row;
static size_t terminal_column;
static uint8_t terminal_color;
static uint16_t* terminal_buffer;
// 初始化终端
void terminal_initialize(void) {
terminal_row = 0;
terminal_column = 0;
terminal_color = vga_entry_color(VGA_LIGHT_GREY, VGA_BLACK);
terminal_buffer = (uint16_t*) VGA_BUFFER;
// 清屏
for (size_t y = 0; y < VGA_HEIGHT; y++) {
for (size_t x = 0; x < VGA_WIDTH; x++) {
const size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = vga_entry(' ', terminal_color);
}
}
}
// 设置终端颜色
void terminal_setcolor(uint8_t color) {
terminal_color = color;
}
// 在指定位置输出字符
void terminal_putentryat(char c, uint8_t color, size_t x, size_t y) {
const size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = vga_entry(c, color);
}
// 滚动终端
void terminal_scroll(void) {
// 将所有行上移一行
for (size_t y = 0; y < VGA_HEIGHT - 1; y++) {
for (size_t x = 0; x < VGA_WIDTH; x++) {
const size_t src_index = (y + 1) * VGA_WIDTH + x;
const size_t dst_index = y * VGA_WIDTH + x;
terminal_buffer[dst_index] = terminal_buffer[src_index];
}
}
// 清空最后一行
for (size_t x = 0; x < VGA_WIDTH; x++) {
const size_t index = (VGA_HEIGHT - 1) * VGA_WIDTH + x;
terminal_buffer[index] = vga_entry(' ', terminal_color);
}
}
// 输出字符
void terminal_putchar(char c) {
if (c == '\n') {
terminal_column = 0;
if (++terminal_row == VGA_HEIGHT) {
terminal_scroll();
terminal_row = VGA_HEIGHT - 1;
}
return;
}
terminal_putentryat(c, terminal_color, terminal_column, terminal_row);
if (++terminal_column == VGA_WIDTH) {
terminal_column = 0;
if (++terminal_row == VGA_HEIGHT) {
terminal_scroll();
terminal_row = VGA_HEIGHT - 1;
}
}
}
// 输出字符串
void terminal_write(const char* data, size_t size) {
for (size_t i = 0; i < size; i++)
terminal_putchar(data[i]);
}
// 输出字符串
void terminal_writestring(const char* data) {
terminal_write(data, strlen(data));
}
// 内核入口点
void kernel_main(void) {
// 初始化终端
terminal_initialize();
// 输出欢迎信息
terminal_writestring("Welcome to MyOS!\n");
terminal_writestring("Kernel loaded successfully.\n");
terminal_writestring("Virtual memory and paging are enabled.\n");
// 设置不同颜色
terminal_setcolor(vga_entry_color(VGA_LIGHT_GREEN, VGA_BLACK));
terminal_writestring("\nSystem ready.\n");
// 主循环
while (1) {
asm volatile ("hlt");
}
}

内核链接脚本#

为了让内核代码正确加载到指定地址,需要编写链接脚本:

/* kernel.ld - 内核链接脚本 */
ENTRY(kernel_main)
SECTIONS
{
/* 内核加载到 1MB 以上 */
. = 1M;
/* 代码段 */
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
/* 只读数据段 */
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
/* 数据段 */
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
/* BSS 段(未初始化数据) */
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
}
}

构建系统#

下面是用于编译内核的 Makefile:

# Makefile - 内核构建脚本
ASM = nasm
CC = gcc
LD = ld
ASMFLAGS = -f elf32
CFLAGS = -m32 -ffreestanding -fno-pie -fno-pic -nostdlib -nostdinc -Wall -Wextra
LDFLAGS = -m elf_i386 -T kernel.ld
ASM_SOURCES = $(wildcard *.asm)
C_SOURCES = $(wildcard *.c)
ASM_OBJECTS = $(ASM_SOURCES:.asm=.o)
C_OBJECTS = $(C_SOURCES:.c=.o)
OBJECTS = $(ASM_OBJECTS) $(C_OBJECTS)
all: kernel.bin
%.o: %.asm
$(ASM) $(ASMFLAGS) $< -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
kernel.bin: $(OBJECTS)
$(LD) $(LDFLAGS) -o $@ $(OBJECTS)
clean:
rm -f *.o kernel.bin
.PHONY: all clean

实模式到保护模式的切换#

虽然在第 3 章已经介绍过实模式到保护模式的切换,但这里从内核加载的角度再次总结关键步骤:

切换步骤#

; 1. 禁用中断
cli
; 2. 启用 A20 地址线(方法一:使用 FAST A20)
in al, 0x92
or al, 2
out 0x92, al
; 3. 加载 GDT
lgdt [gdt_descriptor]
; 4. 设置 CR0.PE 位
mov eax, cr0
or eax, 1
mov cr0, eax
; 5. 远跳转刷新流水线
jmp CODE_SELECTOR:protected_mode_entry
; 6. 在保护模式中初始化段寄存器
[bits 32]
protected_mode_entry:
mov ax, DATA_SELECTOR
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; 设置栈
mov esp, 0x90000

关键注意事项#

在从实模式切换到保护模式时,有几个关键点需要注意:

第一,必须先加载 GDT 再设置 CR0.PE 位,否则 CPU 无法正确加载段描述符。

第二,设置 PE 位后必须立即执行远跳转,刷新流水线中的 16 位指令预取队列。

第三,进入保护模式后要尽快初始化所有段寄存器,避免使用实模式下的段值。

第四,在开启分页之前,需要确保页表已正确设置,且代码段和数据段在页表中有正确的映射。

; 完整的模式切换和分页开启流程
switch_to_protected_mode_with_paging:
; 1. 禁用中断
cli
; 2. 启用 A20
call enable_a20
; 3. 加载临时 GDT(在低内存)
lgdt [temp_gdt_descriptor]
; 4. 开启保护模式
mov eax, cr0
or al, 1
mov cr0, eax
; 5. 远跳转
jmp 0x08:protected_mode_init
protected_mode_init:
; 6. 初始化段寄存器
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x90000
; 7. 设置页表
call setup_identity_page_tables
; 8. 加载 CR3
mov eax, PAGE_DIR_ADDR
mov cr3, eax
; 9. 开启分页
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
; 10. 加载最终 GDT(在高内存)
lgdt [final_gdt_descriptor]
ret

小结#

本章详细介绍了 ELF 可执行文件格式的解析方法,以及从汇编 Loader 跳转到 C 语言内核的完整流程。通过理解 ELF 文件结构,能够正确加载内核的代码段、数据段,并处理好 BSS 段的初始化。从实模式到保护模式的切换,再到分页的开启,每一步都是操作系统启动过程中不可或缺的环节。

至此,我们已经完成了操作系统启动的全部基础工作。接下来,内核将开始真正的系统初始化:设置中断处理、初始化内存管理、加载驱动程序、启动调度器,最终为用户提供服务。

参考#

ELF Format Specification
OSDev Wiki - ELF
Intel 64 and IA-32 Architectures Software Developer’s Manual
JamesM’s kernel development tutorials
The little book about OS development

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

正式加载内核
https://blog.souloss.com/posts/os/loading-the-kernel/
作者
Souloss
发布于
2021-05-07
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时