简介:本资源是华中科技大学计算机学院《操作系统》课程设计的完整报告文档,面向高校计算机及相关专业本科生,聚焦Linux系统级开发实践能力培养。报告覆盖Linux环境搭建(Ubuntu 11.10 + VMware + root权限配置)、系统调用添加、字符设备驱动开发、/proc文件系统分析、GTK+图形界面编程等核心实验模块,内容详实、步骤完整,含代码实现、错误处理逻辑与运行结果截图说明。资源为单个Word文档(.doc),大小1.22MB,结构清晰,含课程设计目的、9大实验任务分解、详细实现过程及源码片段,便于对照学习与复现。目前已有403人学习下载,适合操作系统原理课设参考、Linux内核实践入门及系统编程能力提升者直接使用。
1. 这不是一份普通课程报告:它是一份可复现、可验证、能跑通的OS内核级实践手记
“华中科技大学计算机学院操作系统课程设计报告.doc”——光看标题,你可能以为这只是某届学生交上去就归档的Word文档。但实际翻过几十份同类材料后你会发现:真正能体现操作系统底层能力的课程设计,从来不是文字堆砌,而是在真实x86_64裸机环境里调度进程、接管中断、手写页表、让printf输出到VGA显存的完整闭环。这份报告背后对应的,是一个最小可行内核(microkernel)雏形:从实模式跳转到保护模式,加载GDT,初始化IDT,响应键盘中断,实现简易shell命令解析,甚至完成进程切换的上下文保存与恢复。它不依赖Linux内核或任何现有OS服务,所有代码运行在ring 0,所有内存布局由自己定义。适合刚学完《现代操作系统》前六章、正卡在“理论懂了但不知道第一行汇编该写在哪”的本科生;也适合想补全系统级工程手感的嵌入式/驱动方向从业者——因为这里没有抽象层兜底,每个段错误、页故障、GPF都是你亲手写的描述符没对齐、IDT门类型设错、栈指针越界的真实回响。这不是模拟器玩具,是能烧进QEMU镜像、能接真实USB键盘、能让你在黑屏上敲出ps并看到两个任务在跑的硬核现场。
2. 从零构建可启动内核:汇编引导 + C语言主干的最小可信链
操作系统课程设计的第一道生死线,永远是“如何让CPU执行我的代码”。很多同学卡在bootloader阶段:用GRUB加载?太黑盒;用NASM写512字节MBR?又太脆弱。我们采用更可控、更贴近教学目标的方案:用NASM写实模式引导扇区 → 跳转至保护模式C函数入口 → 建立基础运行时环境。这个链条必须每一步都可验证、可调试、可单步跟踪,否则后续所有进程/内存管理全是空中楼阁。
2.1 实模式引导:512字节里的三件事
引导扇区(boot.S)必须完成三件不可妥协的事:关闭中断、加载GDT、跳入保护模式。常见错误是直接跳jmp 0x08:main却忘了设置CS段选择子为0x08(即GDT中第1项,索引1<<3),导致跳转失败后CPU锁死。以下是精简但完整的引导逻辑:
; boot.S - NASM syntax, built with: nasm -f bin -o boot.bin boot.S bits 16 org 0x7c00 start: cli ; 关中断,避免IRQ干扰GDT加载 xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 ; 栈顶设在引导区末尾 ; 加载GDT(gdt_descriptor定义见下) lgdt [gdt_descriptor] ; 开启A20线(关键!否则高地址内存无法访问) in al, 0x92 or al, 2 out 0x92, al ; 切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 远跳转:刷新CS并进入32位模式 jmp 0x08:protected_mode bits 32 protected_mode: mov ax, 0x10 ; 数据段选择子(GDT第2项) mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov esp, 0x90000 ; 切换到高端栈(避免覆盖引导区) ; 调用C语言main函数(需extern声明) call main ; 死循环,防止返回后执行垃圾指令 hang: hlt jmp hang ; GDT定义:Null Descriptor + Code Segment + Data Segment gdt_start: dq 0x0 ; Null descriptor code_seg: dw 0xFFFF ; Limit (low) dw 0x0 ; Base (low) db 0x0 ; Base (mid) db 10011010b ; Access byte: P=1, DPL=0, S=1, TYPE=1010 (code, exec, read, conforming=0) db 11001111b ; Flags+Limit (high): G=1, D/B=1, L=0, AVL=0, Limit(high)=0xF db 0x0 ; Base (high) data_seg: dw 0xFFFF dw 0x0 db 0x0 db 10010010b ; Access: P=1, DPL=0, S=1, TYPE=0010 (data, write, expand-down=0) db 11001111b db 0x0 gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 ; GDT limit (size - 1) dd gdt_start ; GDT base address times 510-($-$$) db 0 ; 填充至510字节 dw 0xaa55 ; 引导签名逻辑说明:这段汇编严格遵循x86启动规范。
lgdt加载GDT前必须确保DS=0,否则地址计算错误;in/out 0x92开启A20是访问1MB以上内存的前提,缺失则后续所有高端内存分配(如页表、内核堆)全部失效;远跳转jmp 0x08:protected_mode中的0x08必须与GDT中code_seg的索引完全对应((index << 3) | RPL),否则触发#GP异常且无调试输出。
参数说明:dw 0xFFFF是段限长(64KB),教学场景够用;db 10011010b是代码段访问字节,重点在bit 3(Executable)、bit 1(Readable)必须为1;db 11001111b中bit 5(G)为1表示粒度为4KB,这是保护模式下页式内存管理的基础。
2.2 C语言运行时初始化:从裸机到可用堆栈
进入保护模式后,C函数main()需要一个干净的C运行时环境:全局变量清零、.bss段初始化、堆栈可用、能调用memset/memcpy等基础函数。我们不链接libc,而是手写最小启动文件(cstart.c):
// cstart.c void memset(void *dst, int c, unsigned int n) { unsigned char *p = dst; while (n--) *p++ = c; } void memcpy(void *dst, const void *src, unsigned int n) { const unsigned char *s = src; unsigned char *d = dst; while (n--) *d++ = *s++; } // .bss段起始与结束地址(由链接脚本定义) extern char _bss_start[]; extern char _bss_end[]; void bss_init() { memset(_bss_start, 0, _bss_end - _bss_start); } // 全局构造器调用(若需C++风格初始化) void __attribute__((constructor)) init_runtime() { bss_init(); } // main函数原型(由boot.S调用) void main() { // 此处可安全使用memset/memcpy // 初始化VGA显存、IDT、内存管理模块... while(1); }逻辑说明:
.bss段存放未初始化全局变量,链接时仅记录起止地址,运行时必须手动清零,否则残留垃圾值会导致不可预测行为(如某个链表头指针非NULL却指向非法地址)。memset和memcpy必须手写,因为标准库依赖动态链接和系统调用,而此时连中断都没启用。
参数说明:_bss_start和_bss_end是链接脚本(linker.ld)中定义的符号,例如:SECTIONS { . = 0x100000; /* 内核加载地址 */ _text_start = .; .text : { *(.text) } _text_end = .; .rodata : { *(.rodata) } .data : { *(.data) } _data_end = .; _bss_start = .; .bss : { *(.bss) } _bss_end = .; }编译时需用
ld -T linker.ld -o kernel.bin boot.o cstart.o kernel.o确保符号正确解析。
3. 内存管理实战:两级页表搭建与物理页帧分配器
没有内存管理,操作系统只是个高级计算器。课程设计中常被忽略的致命环节是:页表结构是否真正映射了所有必需内存区域?页目录/页表项的标志位(P、RW、US、PS)是否按需设置?物理页帧分配器能否抗住连续malloc/free压力?我们采用x86标准两级页表(Page Directory + Page Table),不启用PAE或PSE,确保与QEMU默认配置兼容,并手写位图式物理页帧分配器(Bitmap-based PFN Allocator)。
3.1 页表初始化:从空页目录到内核空间全覆盖
内核启动时需建立初始页表,将0~8MB物理内存(含内核代码、数据、栈、页表自身)映射到虚拟地址0xC0000000~0xC0800000(内核空间起始)。关键点在于:页目录项(PDE)和页表项(PTE)必须同时设置Present=1、ReadWrite=1、UserSupervisor=0(ring 0 only),否则访问时触发#PF异常。
// mm.c #define PAGE_SIZE 0x1000 #define PAGE_DIR_ENTRIES 1024 #define PAGE_TABLE_ENTRIES 1024 // 全局页目录(PD),位于物理地址0x100000(1MB) uint32_t *page_dir = (uint32_t*)0x100000; // 页表数组(每个PT 4KB,共1024个,占4MB) uint32_t *page_tables[PAGE_DIR_ENTRIES]; void init_paging() { // 1. 清零页目录 memset(page_dir, 0, PAGE_SIZE); // 2. 为每个页表分配物理页,并填入页目录 for (int i = 0; i < PAGE_DIR_ENTRIES; i++) { uint32_t pt_phys = alloc_page_frame(); // 返回物理地址 if (!pt_phys) panic("OOM in page table alloc"); // 设置PDE:Present|RW|US = 0x07,物理地址低12位清零 page_dir[i] = (pt_phys & 0xFFFFF000) | 0x07; // 3. 清零对应页表 uint32_t *pt_virt = (uint32_t*)(0x100000 + i * PAGE_SIZE); // 简单线性映射 memset(pt_virt, 0, PAGE_SIZE); page_tables[i] = pt_virt; } // 4. 映射内核空间:虚拟地址0xC0000000 ~ 0xC0800000 -> 物理0x00000000 ~ 0x00800000 // 即:将前8MB物理内存映射到内核虚拟空间 for (uint32_t phys = 0; phys < 0x00800000; phys += PAGE_SIZE) { uint32_t virt = 0xC0000000 + phys; uint32_t pd_index = virt >> 22; // 高10位 uint32_t pt_index = (virt >> 12) & 0x3FF; // 中10位 page_tables[pd_index][pt_index] = (phys & 0xFFFFF000) | 0x07; } // 5. 加载CR3寄存器 asm volatile("mov %0, %%cr3" :: "r"(page_dir)); // 6. 开启分页 uint32_t cr0; asm volatile("mov %%cr0, %0" : "=r"(cr0)); cr0 |= 0x80000000; // Set PG bit asm volatile("mov %0, %%cr0" :: "r"(cr0)); }逻辑说明:
alloc_page_frame()必须返回未被使用的物理页帧地址(如0x100000起始的高端内存),不能返回已占用的引导区(0x7C00)或内核代码区;页表项0x07= Present(1) + ReadWrite(1) + UserSupervisor(0) + Reserved(0),缺一不可;虚拟地址0xC0000000是约定俗成的内核空间起始,其高10位0xC00对应页目录索引,必须确保该PDE已指向有效页表。
参数说明:PAGE_DIR_ENTRIES=1024对应4GB虚拟空间划分为1024个4MB块;0x00800000= 8MB,覆盖内核代码+数据+初始栈+页表自身所需空间;cr0 |= 0x80000000开启分页,此指令后所有内存访问均经MMU转换,若页表有误立即#PF。
3.2 物理页帧分配器:位图管理与碎片规避
alloc_page_frame()不能简单递增计数器(会快速耗尽),也不能用链表(初始化开销大)。位图法(Bitmap)最适配教学场景:用1bit标记1个4KB页帧,128MB物理内存仅需4KB位图,且支持O(1)查找。
// pmm.c #define MAX_PHYS_MEM 0x8000000 // 128MB #define NUM_FRAMES (MAX_PHYS_MEM / PAGE_SIZE) // 32768 frames #define BITMAP_SIZE ((NUM_FRAMES + 7) / 8) // 4096 bytes uint8_t frame_bitmap[BITMAP_SIZE]; // 全局位图,初始全0(空闲) // 初始化:标记前1MB为已用(引导区+内核+页表) void init_frame_bitmap() { memset(frame_bitmap, 0, BITMAP_SIZE); // 标记0x00000000 ~ 0x000FFFFF(1MB)为已用 for (int i = 0; i < 0x100000 / PAGE_SIZE; i++) { set_bit(i); } // 标记页表所在物理页(0x100000 ~ 0x103FFF)为已用 for (int i = 0x100000 / PAGE_SIZE; i < 0x104000 / PAGE_SIZE; i++) { set_bit(i); } } static inline void set_bit(int n) { frame_bitmap[n/8] |= (1 << (n%8)); } static inline void clear_bit(int n) { frame_bitmap[n/8] &= ~(1 << (n%8)); } static inline int test_bit(int n) { return frame_bitmap[n/8] & (1 << (n%8)); } // 分配一个空闲页帧,返回物理地址 uint32_t alloc_page_frame() { for (int i = 0; i < NUM_FRAMES; i++) { if (!test_bit(i)) { set_bit(i); return i * PAGE_SIZE; } } return 0; // OOM } // 释放页帧 void free_page_frame(uint32_t phys) { int idx = phys / PAGE_SIZE; if (idx < NUM_FRAMES) clear_bit(idx); }逻辑说明:位图初始化必须精确排除所有已占用区域:实模式引导区(0x7C00)、GDT/IDT表(通常放0x10000)、内核二进制(0x100000起)、页目录/页表(0x100000起连续4MB)。遗漏任一区域,
alloc_page_frame()可能返回正在运行的代码页,导致内核崩溃。
参数说明:MAX_PHYS_MEM=0x8000000(128MB)是QEMU常用内存上限,足够课程设计;NUM_FRAMES=32768,位图大小4096字节,可静态分配在内核BSS段;set_bit/clear_bit使用位运算而非函数调用,避免栈溢出风险。
4. 进程管理落地:TSS切换、上下文保存与简易调度器
“实现多任务”是课程设计核心目标,但多数人只做到fork()系统调用,却没真正理解CPU如何在两个任务间切换、栈指针如何重定向、特权级如何降升、TSS为何不可替代。我们采用硬件TSS(Task State Segment)配合软件调度器,确保每次iret都能正确恢复目标任务的SS/ESP/CS/EIP。
4.1 TSS结构定义与初始化:ring 0与ring 3的桥梁
x86要求每个任务有独立TSS,存储ring 0栈指针(SS0/ESP0)、I/O位图基址等。课程设计中只需关注SS0/ESP0(处理ring 3中断时切栈)和LDT(若需用户段)。TSS必须放在GDT中,且TSS描述符DPL=0。
// tss.h struct tss_entry { uint32_t prev_tss; // 上一个TSS链接(0表示无) uint32_t ss0; // ring 0栈段 uint32_t esp0; // ring 0栈指针(关键!) uint32_t ss1; // ring 1栈(不用) uint32_t esp1; // ring 1栈(不用) uint32_t ss2; // ring 2栈(不用) uint32_t esp2; // ring 2栈(不用) uint32_t cr3; // 页目录地址(若需切换地址空间) uint32_t eip; // 任务被中断时的EIP(由CPU自动填) uint32_t eflags; // 任务被中断时的EFLAGS uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi; uint32_t es, cs, ss, ds, fs, gs; uint32_t ldtr; // 局部描述符表寄存器 uint16_t trap; // 调试陷阱(0) uint16_t iomap_base; // I/O位图偏移(0x68,表示无I/O权限) } __attribute__((packed)); extern struct tss_entry tss; extern uint32_t tss_size; // tss.c struct tss_entry tss; uint32_t tss_size = sizeof(tss); void init_tss() { // 初始化TSS内容 tss.ss0 = 0x10; // ring 0数据段选择子(GDT第2项) tss.esp0 = 0x90000; // ring 0栈顶(高端内存) // 其他字段清零 memset(&tss + 1, 0, sizeof(tss) - sizeof(uint32_t)); // 将TSS加载到GDT(假设GDT第3项为TSS) uint32_t *gdt = (uint32_t*)0x10000; // GDT物理地址 gdt[3] = (uint32_t)&tss & 0xFFFFF000; // Base low gdt[3] |= ((uint32_t)&tss & 0xFFF) << 16; // Base mid gdt[3] |= 0x0040EC00; // Access: P=1, DPL=0, S=0, TYPE=1100 (TSS), B=0 gdt[3] |= (sizeof(tss) - 1) & 0xFFFF0000; // Limit high // 加载TR寄存器 asm volatile("ltr %0" :: "r"(0x18)); // 0x18 = 3<<3 | RPL=0 }逻辑说明:
esp0必须指向一个独立、足够大、且不会被其他任务覆盖的栈空间(如0x90000),否则ring 3中断时CPU自动切栈会破坏当前任务数据;TSS描述符0x0040EC00中,0xEC= 11101100b 表示Present=1, DPL=0, S=0(系统段), TYPE=1100(Busy 32-bit TSS);ltr指令加载TSS到TR寄存器,此后iret到ring 3时CPU自动从TSS读取SS0/ESP0。
参数说明:0x18是TSS在GDT中的选择子(索引3 << 3),必须与GDT中实际位置一致;sizeof(tss)必须准确,否则GDT中Limit字段错误导致#GP。
4.2 进程控制块与轮转调度器:抢占式切换的最小实现
进程控制块(PCB)需包含上下文寄存器、状态、优先级。调度器在定时器中断中触发,保存当前上下文到PCB,加载下一个PCB的上下文,通过iret完成切换。
// process.h #define MAX_PROCESSES 4 struct pcb { uint32_t esp; // 任务栈指针(ring 3栈顶) uint32_t eip; // 下一条指令地址 uint32_t state; // READY/RUNNING/BLOCKED uint32_t pid; uint32_t *stack; // 指向ring 3栈底(用于分配新栈) }; extern struct pcb processes[MAX_PROCESSES]; extern int current_pid; extern int next_pid; // scheduler.c void schedule() { // 1. 保存当前任务上下文到其PCB(由中断处理程序完成) // 2. 找下一个READY状态任务 int old_pid = current_pid; do { next_pid = (next_pid + 1) % MAX_PROCESSES; } while (processes[next_pid].state != READY); current_pid = next_pid; processes[old_pid].state = READY; processes[current_pid].state = RUNNING; // 3. 加载新任务的ESP(ring 3栈),EIP由iret从栈中弹出 asm volatile( "mov %0, %%esp\n\t" "iret" :: "r"(processes[current_pid].esp) : "esp" ); } // 创建新进程:复制父进程栈,设置子进程EIP int fork() { int child_pid = -1; for (int i = 0; i < MAX_PROCESSES; i++) { if (processes[i].state == 0) { child_pid = i; break; } } if (child_pid == -1) return -1; // 复制父进程栈(简化版,仅复制一页) uint32_t *child_stack = kmalloc(PAGE_SIZE); memcpy(child_stack, processes[current_pid].stack, PAGE_SIZE); // 设置子进程PCB processes[child_pid].stack = child_stack; processes[child_pid].esp = (uint32_t)child_stack + PAGE_SIZE - 16; // 栈顶 processes[child_pid].eip = processes[current_pid].eip; // 继承EIP processes[child_pid].state = READY; processes[child_pid].pid = child_pid; return child_pid; }逻辑说明:
schedule()中iret是切换核心——它从当前栈顶弹出EIP:CS:EFLAGS:ESP:SS,从而跳转到新任务的指令流并切换栈;fork()必须复制整个ring 3栈,因为子进程需拥有独立的局部变量、函数调用帧;kmalloc()需基于物理页帧分配器实现,确保返回的地址可被新任务访问。
参数说明:MAX_PROCESSES=4是教学安全上限,避免栈溢出;PAGE_SIZE-16预留栈空间存iret所需的5个寄存器;current_pid和next_pid是全局变量,需在中断处理中保护(禁用中断或使用原子操作)。
5. 避坑指南:课程设计中最常踩的5个血泪现场
做操作系统课程设计,80%的时间花在debug,20%花在功能开发。以下5个坑是历届学生反复栽倒的高频雷区,现象、原因、解法全部来自真实调试记录,拒绝玄学。
5.1 现象:QEMU启动后黑屏,CPU占用100%,无任何输出
原因:实模式引导扇区跳转保护模式后,jmp 0x08:protected_mode的0x08与GDT中code_seg索引不匹配,导致#GP异常。由于IDT未初始化,CPU无法处理异常,直接锁死。
解决:在boot.S中jmp前插入hlt指令,用GDB远程调试(qemu-system-x86_64 -s -S -kernel kernel.bin),在gdb中target remote :1234,si单步到jmp,检查cs寄存器值是否为0x08。若非0x08,检查GDT定义中code_seg是否为第一个有效描述符(索引0),且lgdt加载的地址是否正确。
5.2 现象:printf输出乱码或只显示首字符
原因:VGA显存(0xB8000)写入时未同步更新cursor_pos变量,或写入0xB8000 + cursor_pos*2时cursor_pos超界(>2000),导致覆盖显存外内存。
解决:在vga_write_char()中增加边界检查:
if (cursor_pos >= 2000) cursor_pos = 0; // 80x25=2000字符 uint16_t *vga = (uint16_t*)0xB8000; vga[cursor_pos++] = (attr << 8) | ch;并在printf每输出一个字符后调用vga_scroll()检测是否需换行。
5.3 现象:fork()后子进程执行printf崩溃,GDB显示#PF异常
原因:子进程继承了父进程的页表映射,但fork()未为子进程分配新的页表项(PTE),导致子进程写栈时触发页故障(Page Fault),而IDT中#PF处理程序未注册或处理逻辑错误。
解决:fork()中必须为子进程栈分配新物理页,并在页表中建立映射:
uint32_t child_phys = alloc_page_frame(); uint32_t child_virt = (uint32_t)child_stack; uint32_t pd_index = child_virt >> 22; uint32_t pt_index = (child_virt >> 12) & 0x3FF; page_tables[pd_index][pt_index] = (child_phys & 0xFFFFF000) | 0x07;5.4 现象:定时器中断(IRQ0)触发后,系统卡死,不再响应键盘
原因:IDT中timer中断门的DPL=3(允许ring 3调用),但中断处理程序在ring 0执行,CPU尝试从ring 3栈切到ring 0栈时,TSS中esp0未正确设置或指向非法地址。
解决:确认init_tss()中tss.esp0指向高端内存(如0x90000),且该地址未被其他模块占用;在IDT初始化时,timer门描述符必须设为DPL=0(0x008E0000而非0x00FE0000),强制ring 0调用。
5.5 现象:free_page_frame()释放后,再次alloc_page_frame()返回同一地址,但写入时触发#PF
原因:页表项(PTE)未清零。释放物理页帧后,对应PTE仍指向旧物理地址且Present=1,导致新任务访问该虚拟地址时MMU查到旧映射,引发冲突。
解决:free_page_frame()必须同步清零所有映射该物理页的PTE:
for (int pd = 0; pd < PAGE_DIR_ENTRIES; pd++) { for (int pt = 0; pt < PAGE_TABLE_ENTRIES; pt++) { if ((page_tables[pd][pt] & 0xFFFFF000) == (phys & 0xFFFFF000)) { page_tables[pd][pt] = 0; // Clear PTE } } }6. 验证你的内核是否真正“活”着:四层可观测性建设
写完代码不等于跑通,跑通不等于稳定。我坚持在课程设计中加入四层验证机制,每一层都对应一个可执行命令或可观察现象,确保内核不是“看起来在跑”,而是“确凿无疑在运行”。
6.1 第一层:启动日志自检(Boot Log Sanity Check)
在main()开头插入硬编码日志,直接写VGA显存,不依赖printf:
void log_boot(const char* s) { static int pos = 0; const uint16_t attr = 0x0F; // 白字黑底 uint16_t *vga = (uint16_t*)0xB8000; while (*s) { if (pos >= 2000) pos = 0; vga[pos++] = (attr << 8) | *s++; } } void main() { log_boot("KERNEL: INIT START"); init_gdt(); log_boot("GDT: OK"); init_idt(); log_boot("IDT: OK"); init_paging(); log_boot("PAGING: OK"); init_tss(); log_boot("TSS: OK"); // ... 启动调度器 log_boot("SCHED: RUNNING"); }验证价值:如果能看到
SCHED: RUNNING,证明从实模式到保护模式、GDT/IDT/分页/TSS全部初始化成功,且调度器已启动。这是最底层的“心跳信号”,比任何printf都可靠。
6.2 第二层:内存映射可视化(Memory Map Visualization)
实现memmap命令,打印当前页表映射状态。关键不是功能,而是验证页表是否按预期工作:
// shell.c void cmd_memmap() { kprintf("PD[0] -> PT @ 0x%08x\n", page_dir[0] & 0xFFFFF000); for (int i = 0; i < 10; i++) { // 打印前10个PTE uint32_t pte = page_tables[0][i]; if (pte & 1) { // Present kprintf(" PT[0][%d]: 0x%08x -> 0x%08x\n", i, i*PAGE_SIZE, pte & 0xFFFFF000); } } }验证价值:执行
memmap应看到0x00000000->0x00000000,0x00001000->0x00001000等一一对应的映射,证明页表项正确生成;若出现0x00000000->0x12345000,说明alloc_page_frame()返回了错误地址,需检查位图初始化。
6.3 第三层:进程快照(Process Snapshot)
ps命令必须显示每个进程的PID、STATE、ESP、EIP,且EIP值应在合理范围内(如0xC0100000附近):
void cmd_ps() { kprintf("PID\tSTATE\tESP\t\tEIP\n"); for (int i = 0; i < MAX_PROCESSES; i++) { if (processes[i].state) { kprintf("%d\t%s\t0x%08x\t0x%08x\n", processes[i].pid, processes[i].state == RUNNING ? "RUN" : processes[i].state == READY ? "READY" : "BLOCK", processes[i].esp, processes[i].eip); } } }验证价值:若
ps显示两个RUNNING进程,说明
本文还有配套的精品资源,点击获取