☰
HDU操作系统实验:Linux 0.11内核级实践指南
2026/10/8 8:38:11 网站建设 项目流程

简介:本资源是杭州电子科技大学(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_RR,task_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。现代as(GNU 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(加载到物理地址 0x90000)
Stage 3system内核主体(压缩后的kernel/代码)-s 0x100000(解压到 0x100000)

执行构建命令:

make clean # 清理旧镜像 make # 触发 tools/build # 成功后生成:Image(未压缩内核)和 system(压缩内核)

参数说明:tools/build的-a和-s参数定义了内存布局。若setup加载地址错误(如写成-a 0x80000),QEMU 启动时会卡在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 ]; // PAGES=256,每字节管 8 页(共 2048 页)

get_free_page()分配一页(4KB)时,会扫描memory_map找第一个 bit=0 的位置。调试时检查:

# 启动后在 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 v1,fs/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 of=fs/hdimage bs=1024 count=1 seek=1 # 再用 mkfs.minix 重建(HDU 提供 tools/mkfs.c,需 make tools)

血泪经验:seek=1表示跳过第一个块(引导扇区),从第二个块(逻辑块 1)开始写。若seek=0,会覆盖bootsect,QEMU 启动直接报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 项地址(n=0x80 对应int 0x80)
  • 0x800:DPL=0(内核级),Type=11(Trap Gate)
  • 0x008:段选择子(指向内核代码段)
  • addr:处理函数地址(如system_call)

若int 0x80不触发system_call,用 QEMU 的-d int参数查看中断日志:

qemu-system-i386 -fda boot/bootsect -hda fs/hdimage -d int -nographic # 输出包含:INT 0x80: vector=0x80, eip=0x00001234 # 若 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跳转失效。
解法:

  1. 检查boot/setup.s第 12 行:mov ax, #0x9000(标准 HDU 版本)
  2. 确认Makefile中BUILD命令是否含-a 0x90000(注意是0x90000,非0x9000)
  3. 若setup.s被修改为mov ax, #0x8000,则BUILD必须同步改为-a 0x80000

4.2 现象:fork()创建子进程后,父进程pid变为 0

原因:kernel/fork.c中copy_process()未正确设置子进程p->pid,且current->pid被copy_mem()覆盖(因task_struct在栈上分配,copy_mem()复制时越界)。
解法:

  1. 在copy_process()开头添加栈空间检查:
if (p->stack < current->stack || p->stack > current->stack + 4096) { panic("Stack overflow in fork"); }
  1. 确保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。
解法:

  1. 在kernel/init/main.c的init()函数中:
memset(current->comm, 0, sizeof(current->comm)); // 先清零 strcpy(current->comm, "init");
  1. 所有strcpy(p->comm, ...)调用前加memset(p->comm, 0, 16)

4.4 现象:write()系统调用返回 -1,errno=EBADF,但文件描述符fd=3明明已open()

原因:fs/open.c中sys_open()返回的fd未写入current->files->fd[fd],sys_write()查找fd时得到空指针。
解法:

  1. 检查sys_open()末尾是否有:
current->files->fd[fd] = f; return fd; // 必须 return fd,不能 return 0
  1. sys_write()中增加空指针检查:
if (!current->files->fd[fd]) { return -EBADF; }

4.5 现象:kill -9 1杀死 init 进程后,系统不 panic,反而新进程pid=1自动复活

原因:kernel/signal.c中do_signal()未处理SIGKILL对pid=1的特殊限制(Linux 规定 init 不可被 kill),sys_kill()直接执行send_sig()。
解法:

  1. 在sys_kill()开头添加:
if (pid == 1 && sig == SIGKILL) { return -EPERM; // 拒绝杀死 init }
  1. 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_start
00101234 T sys_fork
GDB 仅识别T(text)类型符号,D(data)类型需手动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 # 查看 eax=4(sys_write),eip=0x101234(system_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.s),next命令会跳过整个函数。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] pid=2 switched to RUNNING"

我的习惯:永远在schedule()和sys_call_table.c的每个系统调用入口加my_log(),而不是printk()。前者开销 < 100ns,后者可能 > 1ms。在调度实验中,这点时间差足以让counter计算失准,导致 RR 调度看起来像随机调度。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询