正式加载内核
在前面的章节中,我们已经完成了从 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 1ELFDATA2LSB equ 1ET_EXEC equ 2EM_386 equ 3PT_LOAD equ 1PF_X equ 0x1PF_W equ 0x2PF_R equ 0x4
; ELF 头部偏移E_IDENT equ 0E_TYPE equ 16E_MACHINE equ 18E_VERSION equ 20E_ENTRY equ 24E_PHOFF equ 28E_PHENTSIZE equ 42E_PHNUM equ 44
; 程序头偏移P_TYPE equ 0P_OFFSET equ 4P_VADDR equ 8P_PADDR equ 12P_FILESZ equ 16P_MEMSZ equ 20P_FLAGS equ 24P_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从引导程序到内核的跳转过程
内核加载流程
完整的内核加载流程包括以下步骤:
- Loader 从磁盘读取内核 ELF 文件到内存缓冲区
- 解析 ELF 头部,验证文件格式
- 遍历程序头表,加载所有 PT_LOAD 类型的段
- 处理 BSS 段(清零未初始化数据区域)
- 获取内核入口点地址
- 设置内核运行环境(栈、GDT、IDT 等)
- 跳转到内核入口点执行
完整的 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", 0MSG_PROT_MODE db "Loader: 32-bit protected mode", 0MSG_PAGING db "Loader: Paging enabled", 0MSG_LOADING db "Loader: Loading kernel...", 0MSG_ELF_INVALID db "Error: Invalid ELF format!", 0MSG_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 0x100000PAGE_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 0x464C457FPT_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 语言内核示例:
// 简单的端口 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 = nasmCC = gccLD = ld
ASMFLAGS = -f elf32CFLAGS = -m32 -ffreestanding -fno-pie -fno-pic -nostdlib -nostdinc -Wall -WextraLDFLAGS = -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, 0x92or al, 2out 0x92, al
; 3. 加载 GDTlgdt [gdt_descriptor]
; 4. 设置 CR0.PE 位mov eax, cr0or eax, 1mov 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
- OSDev Wiki — 操作系统开发百科全书
- 南京大学:操作系统设计与实现 — 优质中文课程
- 《操作系统真象还原》— 郑钢,从零实现简易操作系统
- 《Linux 内核设计与实现》— Robert Love,深入 Linux 内核
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






