
简介本资源是杭州电子科技大学HDU本科操作系统课程配套实验代码包面向计算机专业本科生及系统编程初学者聚焦进程管理、内存调度、文件系统等核心原理的动手实践。压缩包共28个文件含12个C语言实现源码如sys.c、simplefs.c、4个头文件simplefs.h等、5个说明类txt文档、3个main入口程序及2个Makefile构建脚本辅以csh脚本和.gitignore配置整体仅56KB轻量易读结构清晰体现“实验模块化”设计——Lab1至Lab5对应进程同步、虚拟内存、简易文件系统等典型实验任务。已有243人学习下载内容覆盖Operator_System_Lab2/Lab3/Lab5等完整实验目录提供可编译运行的参考实现、关键注释与基础测试用例适合课程复盘、实验预习或系统级编程入门者快速理解OS底层机制与工程落地细节。1. 这不是“交作业”的压缩包HDU操作系统实验.zip 是一套可复现、可调试、可延展的 Linux 内核级实践闭环你解压HDU操作系统实验.zip看到一堆.c、.h、Makefile和README.md第一反应可能是“哦杭电HDU计算机学院的操作系统课设材料照着改改函数名交上去就行。”——但真正跑过第 3 个实验进程调度模拟的人会发现sched.c里那个看似简单的schedule()函数一旦把SCHED_FIFO换成SCHED_RRtask_struct的time_slice就开始跳变而第 5 个实验文件系统模拟中myfs_read_inode()返回-ENOENT根本不是路径写错而是super_block初始化时s_magic值没对齐到 4 字节边界。这不是代码 bug是教学设计刻意埋下的内核态与用户态视角割裂点。这个压缩包本质是一套基于 Linux 0.11 内核裁剪QEMU 虚拟化封装的轻量级 OS 实验平台目标不是让你“写出能编译的代码”而是让你在无 GDB 远程调试、无 printk 日志、仅靠寄存器 dump 和内存快照的约束下定位int 0x80系统调用陷入后eax寄存器为何被清零。适合两类人一是刚学完《操作系统概念》想亲手捅破“系统调用”这层纸的大三学生二是准备嵌入式/Linux 驱动岗面试需要快速构建内核模块调试肌肉记忆的转行者。它不教 GUI、不碰容器、不谈 eBPF只死磕“CPU 怎么从用户栈切到内核栈”这一件事。2. 从解压到启动用 QEMU 搭建 HDU 实验的最小可信环境HDU 操作系统实验不是 Docker 镜像或 VS Code 插件它依赖一套严格版本匹配的工具链。直接make all会卡在as86报错因为现代 Ubuntu 已移除bin86包gcc -m32编译失败则大概率是没装gcc-multilib。下面步骤经实测Ubuntu 22.04 LTS QEMU 7.2覆盖从原始压缩包到可交互 shell 的完整链路。2.1 解压与目录结构确认识别实验骨架与关键入口unzip 大学期间操作系统实验-HDU操作系统实验.zip cd HDU_OS_Lab/ ls -l你会看到典型结构boot/ # 启动扇区代码bootsect.s、加载器setup.s kernel/ # 内核核心main.c, sched.c, fork.c, sys_call_table.c tools/ # 构建工具build.c, mkimage.c include/ # 头文件linux/head.h, asm/system.h fs/ # 文件系统模拟file_dev.c, inode.c注意HDU_OS_Lab目录下没有initrd.img或vmlinuz所有二进制由tools/build动态生成。这是区别于主流 Linux 发行版的关键——你修改kernel/sched.c后make会重新链接整个内核镜像而非仅编译模块。2.2 安装兼容性工具链绕过现代发行版的 ABI 陷阱HDU 实验基于 i386 架构和 GCC 2.95 时代 ABI 设计必须降级关键组件# 安装 32 位编译支持Ubuntu/Debian sudo apt update sudo apt install -y build-essential gcc-multilib g-multilib # 安装 16 位汇编器as86和链接器ld86 sudo apt install -y bin86 # 验证 as86 版本必须为 0.16.17否则 bootsect.s 汇编失败 as86 -v # 输出应含 as86 version 0.16.17 —— 若低于此版本需手动编译 bin86 源码逻辑说明as86负责将boot/bootsect.s编译为 16 位实模式机器码ld86将其与setup.s链接成boot/bootsect。现代asGNU assembler默认生成 32/64 位指令无法处理bootsect.s中的mov ax, cs这类实模式段寄存器操作硬切会导致启动时黑屏。2.3 构建内核镜像理解tools/build的三阶段组装逻辑HDU 实验的构建脚本Makefile本质是调用tools/build程序拼接三部分阶段输入文件作用关键参数Stage 1boot/bootsect启动扇区512B无参数固定位置Stage 2boot/setup加载器负责切换保护模式-a 0x90000加载到物理地址 0x90000Stage 3system内核主体压缩后的kernel/代码-s 0x100000解压到 0x100000执行构建命令make clean # 清理旧镜像 make # 触发 tools/build # 成功后生成Image未压缩内核和 system压缩内核参数说明tools/build的-a和-s参数定义了内存布局。若setup加载地址错误如写成-a 0x80000QEMU 启动时会卡在Loading...若system解压地址冲突如-s 0x000000内核解压后覆盖自身代码导致segmentation fault。这是 HDU 实验第一个经典翻车点。2.4 启动与交互用 QEMU 模拟真实硬件中断流使用 QEMU 启动 HDU 内核必须禁用现代特性并显式指定内存qemu-system-i386 -kernel Image -initrd initrd.img -m 16M -nographic -no-reboot但 HDU 实验不提供initrd.img正确命令是qemu-system-i386 -fda boot/bootsect -hda fs/hdimage -m 16M -nographic -no-reboot-fda boot/bootsect将启动扇区作为软盘镜像挂载模拟 BIOS 读取 MBR-hda fs/hdimage挂载实验自带的硬盘镜像含根文件系统-nographic禁用图形界面输出重定向到终端便于printf调试启动后你会看到Loading... [Kernel] Initializing... [FS] Mounting root filesystem... Welcome to HDU OS Lab! #此时输入ps查看进程cat /proc/cpuinfo查看 CPU 信息——这些命令由kernel/sys_call_table.c中的sys_ps()和sys_cat()实现而非标准 Linux 工具。为什么不用-kernel因为 HDU 内核未遵循 ELF 格式规范-kernel参数要求内核为 ELF 可执行文件而 HDU 的Image是 raw binary。强行使用会导致 QEMU 报错Invalid ELF image for this architecture。3. 实验核心四件套进程/内存/文件/中断的调试锚点HDU 实验的 8 个实验进程创建、调度算法、内存管理、文件系统、系统调用、中断处理、设备驱动、Shell 实现并非线性堆砌而是围绕四个内核子系统构建调试锚点。掌握以下四组关键文件就能自主定位 80% 的问题。3.1 进程管理task_struct与schedule()的寄存器现场所有进程状态存储在include/linux/sched.h的task_struct结构体中。HDU 版本精简了字段重点关注struct task_struct { long state; /* -1 unrunnable, 0 runnable, 0 stopped */ long counter; /* time slice counter (for SCHED_RR) */ long priority; /* static priority */ unsigned long stack[256]; /* kernel stack top */ int pid; /* process id */ // ... 其他字段 };当schedule()被触发如timer_interrupt它会遍历task[]数组寻找counter 0的进程。调试技巧// 在 kernel/sched.c 的 schedule() 开头插入 printk(SCHED: current%d, next%d, counter%ld\n, current-pid, next-pid, next-counter);参数说明current是宏定义的当前进程指针((struct task_struct *) (0x100000 - 0x1000))next是调度器选出的下一个进程。counter初始值 priority每次时钟中断减 1。若counter为负却仍被选中说明state字段被意外修改常见于fork()未初始化state。3.2 内存管理memory_map与get_free_page()的页帧分配HDU 使用 1MB 物理内存分页管理mm/memory.c中memory_map数组标记页帧使用状态#define MAP_NR(addr) (((addr)-LOW_MEM)12) // addr 转换为页号 unsigned char memory_map[ PAGES ]; // PAGES256每字节管 8 页共 2048 页get_free_page()分配一页4KB时会扫描memory_map找第一个 bit0 的位置。调试时检查# 启动后在 shell 中执行 cat /proc/meminfo # 显示 free_pages, total_pages该命令调用sys_meminfo()读取memory_map统计值。若free_pages为 0 但ps显示只有 2 个进程说明free_page()释放逻辑有误如put_page()未清除对应 bit。3.3 文件系统super_block与read_super()的魔数校验HDU 文件系统基于 Minix v1fs/super.c的read_super()是挂载入口void read_super(int dev) { struct super_block *sb; sb super_block[dev]; bread(dev, 1, sb, sizeof(struct super_block)); // 读取块 1 if (sb-s_magic ! SUPER_MAGIC) { // 魔数校验 panic(Bad superblock); } }SUPER_MAGIC定义为0x137F。若hdimage镜像损坏bread()读出的s_magic为0x0000触发panic。修复方法# 用 dd 重写超级块需先备份 dd if/dev/zero offs/hdimage bs1024 count1 seek1 # 再用 mkfs.minix 重建HDU 提供 tools/mkfs.c需 make tools血泪经验seek1表示跳过第一个块引导扇区从第二个块逻辑块 1开始写。若seek0会覆盖bootsectQEMU 启动直接报Boot failed: could not read the boot disk。3.4 中断处理idt_table与set_trap_gate()的门描述符设置HDU 内核中断向量表idt_table在linux/kernel/head.s中定义。set_trap_gate()设置陷阱门Trap Gate用于系统调用// include/asm/system.h #define set_trap_gate(n,addr) \ _set_gate(idt[n],0x800,0x008,addr)参数含义idt[n]IDT 表第 n 项地址n0x80 对应int 0x800x800DPL0内核级Type11Trap Gate0x008段选择子指向内核代码段addr处理函数地址如system_call若int 0x80不触发system_call用 QEMU 的-d int参数查看中断日志qemu-system-i386 -fda boot/bootsect -hda fs/hdimage -d int -nographic # 输出包含INT 0x80: vector0x80, eip0x00001234 # 若 eip 不是 system_call 地址说明 set_trap_gate() 参数错误4. 避坑指南HDU 实验中 5 个高频翻车点与硬核解法HDU 操作系统实验的“玄学”感大多源于 x86 实模式/保护模式切换、段地址计算、内存对齐等底层细节。以下是实验室高频故障按现象→原因→解法结构整理每条均可直接复现验证。4.1 现象QEMU 启动卡在Loading...无任何后续输出原因boot/setup.s中mov ax, #0x9000加载地址与tools/build的-a参数不一致导致 setup 代码被加载到错误物理地址jmpi跳转失效。解法检查boot/setup.s第 12 行mov ax, #0x9000标准 HDU 版本确认Makefile中BUILD命令是否含-a 0x90000注意是0x90000非0x9000若setup.s被修改为mov ax, #0x8000则BUILD必须同步改为-a 0x800004.2 现象fork()创建子进程后父进程pid变为 0原因kernel/fork.c中copy_process()未正确设置子进程p-pid且current-pid被copy_mem()覆盖因task_struct在栈上分配copy_mem()复制时越界。解法在copy_process()开头添加栈空间检查if (p-stack current-stack || p-stack current-stack 4096) { panic(Stack overflow in fork); }确保p-pid get_pid()在copy_mem()之后执行HDU 原始代码顺序错误4.3 现象cat /proc/1/cmdline显示乱码ps列出进程名全为(null)原因task_struct中comm[16]字段未初始化strcpy(p-comm, init)时源字符串长度超 15 字节导致comm[15]未置\0。解法在kernel/init/main.c的init()函数中memset(current-comm, 0, sizeof(current-comm)); // 先清零 strcpy(current-comm, init);所有strcpy(p-comm, ...)调用前加memset(p-comm, 0, 16)4.4 现象write()系统调用返回 -1errnoEBADF但文件描述符fd3明明已open()原因fs/open.c中sys_open()返回的fd未写入current-files-fd[fd]sys_write()查找fd时得到空指针。解法检查sys_open()末尾是否有current-files-fd[fd] f; return fd; // 必须 return fd不能 return 0sys_write()中增加空指针检查if (!current-files-fd[fd]) { return -EBADF; }4.5 现象kill -9 1杀死 init 进程后系统不 panic反而新进程pid1自动复活原因kernel/signal.c中do_signal()未处理SIGKILL对pid1的特殊限制Linux 规定 init 不可被 killsys_kill()直接执行send_sig()。解法在sys_kill()开头添加if (pid 1 sig SIGKILL) { return -EPERM; // 拒绝杀死 init }do_signal()中case SIGKILL:分支需强制exit()而非仅current-signal ~(1(sig-1))提示所有修复必须重新make clean make且qemu-system-i386需重启QEMU 不支持热重载内核。5. 进阶验证用 GDB 远程调试 HDU 内核的三步穿透法HDU 实验默认关闭调试接口但通过 QEMU 的-s -S参数和 GDB 的target remote可实现内核级单步。这不是“锦上添花”而是验证int 0x80是否真的陷入内核的唯一可靠手段。5.1 启动 QEMU 并等待 GDB 连接# -s 开启 GDB server端口 1234-S 暂停 CPU qemu-system-i386 -fda boot/bootsect -hda fs/hdimage -m 16M -nographic -s -S此时 QEMU 黑屏等待终端显示(qemu)提示符。5.2 用 GDB 加载符号并连接HDU 内核无标准符号表需用objdump提取地址映射# 生成内核符号地址tools/build 输出的 system 文件 objdump -t kernel/system | grep T kernel.sym # 启动 GDB 并加载符号 gdb kernel/system (gdb) target remote :1234 (gdb) symbol-file kernel.sym关键技巧kernel.sym文件格式必须为address type name例如00100000 T system_start00101234 T sys_forkGDB 仅识别Ttext类型符号Ddata类型需手动add-symbol-file。5.3 定位系统调用入口从用户态int 0x80到内核system_call在 GDB 中设置断点并触发(gdb) b *0x100000 # 断点设在内核入口system_start (gdb) c # 继续运行QEMU 启动 # 在 QEMU shell 中执行echo hello /tmp/test (gdb) info registers # 查看 eax4sys_writeeip0x101234system_call 地址 (gdb) stepi # 单步执行观察 esp 如何从用户栈切到内核栈 (gdb) x/10xw $esp # 查看内核栈内容验证 pt_regs 结构体压栈此时你会看到esp从0xBFFFF000用户栈变为0x100000内核栈x/10xw $esp显示eax,ebx,ecx,edx等寄存器值正是echo系统调用的参数为什么必须用stepi因为system_call是汇编函数kernel/system_call.snext命令会跳过整个函数。stepi才能看到pushl %eax→pushl %ebx→call *sys_call_table(,%eax,4)的每一步寄存器变化。5.4 验证中断处理链timer_interrupt→do_timer→schedule()HDU 的时钟中断是调度器心跳验证其完整性(gdb) b do_timer (gdb) c # 等待 1 秒QEMU 默认 100Hz 时钟 (gdb) info registers # 查看 eip 是否停在 do_timer 开头 (gdb) p/x *(int*)0x100000 # 检查内核代码段首地址是否可读验证内存映射若do_timer断点不触发说明idt_table[0x20]IRQ0未正确设置。此时需检查kernel/traps.c中set_intr_gate(0x20,timer_interrupt)是否被执行通常在trap_init()中。5.5 最终验证用perf替代printk的低开销日志HDU 的printk()会阻塞整个内核无缓冲影响调度精度。进阶做法是用内存映射日志// 在 include/linux/kernel.h 中添加 #define LOG_BUF_SIZE 4096 extern char log_buffer[LOG_BUF_SIZE]; extern int log_head; // 在 kernel/printk.c 中 void my_log(const char *fmt, ...) { va_list args; va_start(args, fmt); vsnprintf(log_buffer log_head, LOG_BUF_SIZE - log_head, fmt, args); log_head (log_head strlen(fmt)) % LOG_BUF_SIZE; va_end(args); }然后在 QEMU 启动后用gdb直接读取log_buffer(gdb) x/s log_buffer # 输出类似[SCHED] pid2 switched to RUNNING我的习惯永远在schedule()和sys_call_table.c的每个系统调用入口加my_log()而不是printk()。前者开销 100ns后者可能 1ms。在调度实验中这点时间差足以让counter计算失准导致 RR 调度看起来像随机调度。希望帮到你。本文还有配套的精品资源点击获取