资讯动态

MIPS_OS实验从启动到多线程调度:交叉编译与调试全解析

发布时间:2026/10/17 18:34:53 来源:尧图企业网站定制
简介BUAA_MIPS_OS 是北京航空航天大学操作系统课程的小型操作系统实验源码合集覆盖实验一至实验六面向正在学习 MIPS 架构与内核实现的学生及自学者。实验按启动引导、中断异常、内存管理、进程管理、文件系统、设备驱动递进展开有助于建立从硬件启动到系统调用的完整认知。资源为 ZIP 压缩包共 123 个文件大小 1.3MB文件类型以 C 语言源码49 个、头文件26 个、汇编源码10 个为主并包含构建脚本、链接脚本、镜像文件与格式转换工具方便直接编译与调试。目前已有 2036 人学习使用。整套代码保留了各阶段工程结构内核对中断向量设置、页表与缺页处理、进程切换、文件读写及设备控制等关键模块均可逐行阅读配合模拟器逐步验证适合课程设计、考研复习或自主实验参考。1. MIPS_OS小操作系统实验代码lab1~lab6别一上来就编译MIPS_OS 这套小操作系统实验代码把 lab1~lab6 串成了一条从裸机启动到多线程抢占调度的完整链路但它不是按编号顺序一路 make 就能跑通的资源前三个 lab 的关键在交叉工具链和链接脚本后三个 lab 的坑几乎全在中断现场和栈上。我第一次拿到它时直接在本机默认 gcc 环境里编译结果入口地址、寄存器约定、模拟器端序三处一起翻车浪费了大半个周末。它能解决的问题很具体让你看见操作系统内核里启动、内存、异常、系统调用和调度各自的分工适合正在补操作系统课、或者想用 MIPS 模拟器把内核基础机制完整复现一遍的人。下面按我实际踩通的顺序讲。2. 先读懂六份代码的依赖关系从启动到调度不是直线推进2.1 资源包的核心结构与“哪份代码先被CPU执行”拿到压缩包别急着解压编译先把目录里几类文件分清楚。小操作系统实验最常见的组织方式是按 boot、mm、trap、syscall、schedule 模块拆源文件lab1~lab6 并不是六个互不相干的小工程而是同一套代码在不同阶段的递进版本。lab2 在 lab1 的启动框架上加内存管理lab4 在 lab3 的中断框架上加系统调用lab5 再往系统调用上挂调度器lab6 处理的是并发同步。这个依赖关系决定了你后面调试时始终要回答一个问题当前这个 lab 比上一个 lab 多执行了哪条路径。拆包后先做的是一张文件地图模块常见文件命名对应实验阶段核心职责bootstart.S / head.Slab1设置栈、清 BSS、装好异常跳板mmmm.c / mem.clab2物理内存初始化和分配traptrap.c / int.clab3异常分发和时钟中断处理syscallsyscall.c / sys.clab4系统调用表与派发逻辑scheduleschedule.c / thread.clab5上下文切换与时间片调度syncsync.c / lock.clab6锁、信号量等同步原语这张表不用背关键是记住先执行的一定是链接脚本里 ENTRY 指定的文件通常是 start.S 或 head.S。它把 sp 指向内核栈顶、关闭中断、清 BSS然后把异常入口的跳板装好最后才调用 main.c 里的内核初始化函数。如果你连哪个文件先跑都没确认后面看内存管理和调度代码会一直找不到方向。实验包有时是每个 lab 一份完整代码副本有时是同一个工程里用分支或标签推进。区分方法很简单——看 lab2 目录是否包含 lab1 的全部源文件。如果是完整副本改代码时不要直接在 lab5 目录里改 lab1 的启动文件否则后面合版本时会乱如果是单工程多标签那每次切 lab 前先看 diff避免把上一阶段的调试残留带进下一阶段。2.2 lab1~lab2入口地址、内存布局与页表初始化lab1 的目标是把 CPU 拉进可控状态并让屏幕上出现第一行输出。MIPS 平台上的常见做法是先写 Status 寄存器关掉所有中断再设置栈指针最后操作串口寄存器打印字符串。MIPS 的地址空间分成几个固定段实验代码通常放在 KSEG00x80000000 之后这一段经过缓存映射比直接访问物理地址快也省得在启动阶段配 TLB。0x80000000 这个数字会在链接脚本、启动代码和调试断点里反复出现建议一上来就记进笔记。lab2 在启动框架上加入内存探测与物理内存管理。教育版实验很少启用完整 TLB 和反向页表常见做法是维护一个 mem_map 数组或空闲链表记录哪些物理页空闲、哪些被内核占用提供 init_mem()、alloc_page()、free_page() 这类接口。看这部分代码时重点看两件事一是内存上限从哪里来二是分配算法是首次适应还是循环适应。这两个选择直接影响 lab6 里用户进程能否稳定申请到连续空间。如果内存上限写死成一个常量而模拟器配备的物理内存更大后面加载用户程序时就会莫名其妙失败。KSEG0 的直接映射关系也要在这里建立起来内核态看到的地址等于物理地址加 0x80000000。很多实验的 mm.c 里会看到 virt_to_phys 和 phys_to_virt 两个小函数它们在 lab4 之后的用户页表、lab5 的线程栈分配里到处出现不建议跳过。如果实验在 lab2 就引入 TLB 初始化你会看到 EntryLo、EntryHi、Index 这些 CP0 寄存器的赋值教育版里往往只建一个把全部物理内存直接映射的页表代码看起来是一大段循环写 TLB。这种情况不必深究硬件页表遍历细节抓住两步就行先关中断清空 TLB再按虚拟地址等于物理地址加 0x80000000 的规则填页表项。lab6 用户进程如果出现取指异常问题基本就藏在这里。2.3 lab3~lab6中断、系统调用与多线程调度的耦合方式lab3 是整条链路的转折点CPU 的控制权开始被动转移。MIPS 的异常入口固定在几个特定地址普通异常入口是 0x80000180内核需要提前把跳板指令放到那里再写一个异常处理函数读取 CP0 的 Cause 寄存器判断是时钟中断、系统调用还是其他异常。时钟在这里的地位很容易被低估lab4 的陷入、lab5 的抢占、lab6 的睡眠唤醒全部依赖一个能周期性触发的中断源。初始化时钟时先设 Compare 寄存器为当前 Count 加一个间隔值再开中断使能处理函数每次响应后要把 Compare 再加一个间隔值否则只会有一次时钟中断。这是我在 lab3 里踩过的最不值钱的坑。lab4 在异常分发的基础上加 syscall 指令处理。用户态程序把系统调用号放进寄存器执行 syscallCPU 跳进异常入口内核保存现场后根据号派发。这一步的坑在于现场保存不完整用户态寄存器在返回时被改写现象是用户程序第一次调用正常、第二次开始乱跳。第 5 章会专门讲怎么排查。lab5 把上下文切换挂在时钟中断里。每个线程一个栈中断到来时把当前线程的寄存器压进自己的栈再弹出下一个线程的寄存器。这里有一个很反直觉的点调度器本身不是单独“运行”出来的它是被中断打断后顺势切换的。理解了这个lab6 的锁就不会写成用户态忙等而是在关中断和原子指令之间做选择。整个 lab1~lab6 的主线其实就是“中断怎么进来、现场怎么保存、控制权怎么交出去”这条线。lab6 的信号量实现也可以沿着这个思路读信号量计数在临界区内加减等待队列挂在时钟中断的唤醒路径上。很多实现把 schedule() 直接写在信号量等待分支里而不是等下一个时钟中断这会让睡眠唤醒立即生效也让 lab5 的调度时机从固定 tick 变成可触发式。读到时不用惊讶它仍然是中断驱动这条主线上的延展。如果你时间有限可以先只读三个文件start.S、trap.c、schedule.c。start.S 告诉你内核从哪里开始trap.c 告诉你中断怎么进来schedule.c 告诉你控制权怎么转移。其他文件都是围绕这三个文件服务先抓住主链再回看细节比从头到尾通读快得多。3. 把六个lab跑起来的完整路径交叉编译、链接脚本与GDB断点调试3.1 工具链选型为什么必须用mips交叉编译器实验代码只能在 MIPS 指令集的模拟器里跑不能用本机 gcc 直接编。实验包通常要求安装交叉编译器和 QEMU 系统模拟器。常见组合是 mipsel-linux-gnu-gcc 加 qemu-system-mipsel小端另一组常见的是 mips-linux-gnu-gcc 加 qemu-system-mips大端。端序不匹配时编译可能能过但启动后取指就失败属于最隐蔽的坑。建议拿到资源包后先看 Makefile 里的 CROSS 前缀再决定装哪个工具链不要凭文件名猜。# 以常见 Linux 发行版为例安装小端 MIPS 工具链与系统模拟器 sudo apt-get update sudo apt-get install gcc-mipsel-linux-gnu qemu-system-mips装完先做一次自检输入 mipsel-linux-gnu-gcc --version 能打印版本说明编译器可用再输入 qemu-system-mipsel --version 确认模拟器在。两件事都通过后再碰代码能省掉大量定位环境问题的时间。编译参数是第二个坑实验内核通常不用标准库也不希望编译器生成 gp 相对寻址。我最常用的参数组合是-G0关闭 GP 相对寻址避免全局变量访问依赖 gp 寄存器初始化顺序。-mno-abicalls关闭 MIPS ABI 的辅助调用减少意外引入的库函数。-fno-pic关闭位置无关代码保证链接脚本里的绝对地址生效。-marchmips32按 mips32 指令集生成代码和 QEMU 默认模拟的 CPU 兼容。-msoft-float不生成浮点指令模拟器没开 FPU 时不会触发非法指令。这些参数不是越多越好。有些实验包本身已经配置好多加了反而报错如果你拿到的是带 Makefile 的版本先编译一次报错再逐个加不要一上来全堆上去。我最常看到的新手翻车是把 -marchmips32 写成 mips64导致生成的指令 QEMU 模型不支持。3.2 Makefile 与链接脚本把内核放进预期的内存段实验包的 Makefile 大致是这个套路CROSS 前缀在前面选好后基本不用改CROSS : mipsel-linux-gnu- CC : $(CROSS)gcc LD : $(CROSS)ld CFLAGS : -Wall -G0 -mno-abicalls -fno-pic -marchmips32 -msoft-float all: vmlinux vmlinux: start.o main.o mm.o trap.o syscall.o schedule.o $(LD) -T kernel.ld -o $ $^ %.o: %.S $(CC) $(CFLAGS) -c $ -o $ %.o: %.c $(CC) $(CFLAGS) -c $ -o $这段逻辑不复杂先编译汇编和 C 源文件再把所有 .o 用 kernel.ld 链接成 ELF 内核。注意链接时没有标准启动文件 crt0也没有 libc默认入口就是链接脚本里 ENTRY 指定的位置所以 ENTRY 写什么实际上决定了 CPU 从哪一条指令开始。链接脚本是启动能否成功的关键。下面是一份常见的内核布局OUTPUT_ARCH(mips) ENTRY(_start) SECTIONS { . 0x80020000; .text : { *(.text*) } .data : { *(.data*) } .bss : { *(COMMON) *(.bss*) } }0x80020000 是 KSEG0 内一段常见入口既能避开 0x80000000 到 0x80000180 的异常向量区又能留在缓存映射段内。ENTRY(_start) 要求 start.S 里确实导出 _start 符号如果符号名拼错链接器不会报错只会把入口定到第一个节的头部位置启动时往往取到函数头几条指令PC 直接跑飞。生成 vmlinux 后我习惯用 mipsel-linux-gnu-readelf -h vmlinux 看 Entry point address 是不是 0x80020000顺便看 Data 字段是不是 little endian一次把两个隐患全验掉。3.3 用GDB卡住异常入口和调度切换点模拟器最大的好处是可以把内核冻结在任何指令上。常见做法是让 QEMU 启动后停在第一条指令再用 GDB 连上去。先起模拟器qemu-system-mipsel -M mipssim -kernel vmlinux -nographic -S -s-S 表示启动后不立即执行-s 表示在 1234 端口开放 GDB 调试 stub。-nographic 把模拟串口重定向到当前终端没有图形界面也能看到内核输出。另一个终端里启动 GDBmipsel-linux-gnu-gdb vmlinux target remote localhost:1234 break _start break *0x80000180 continue两个断点分别卡在入口处和普通异常入口处。入口断点确认内核加载成功异常入口断点用来验证时钟中断是否真的触发。如果 GDB 连不上先确认 QEMU 是否还在带 -s 的运行状态如果断点无效先确认 vmlinux 是不是用 -g 编译的符号表没打进去的话 GDB 里只能看到裸地址。这个排查顺序比反复重编代码快得多。提示QEMU 的机器型号参数和实验代码假设的 CPU 不一致时启动流程同样走不到 _start。先对照实验代码里 CPU 特性说明选机型再用 GDB 验证不要调半天发现是模拟器选错。4. 中断、系统调用和调度三个核心机制的参数与实现关键4.1 MIPS异常入口与现场保护顺序MIPS CPU 的异常入口非常少普通异常入口是 0x80000180复位入口是 0xbfc00000。内核启动代码通常会在 0x80000180 放一条跳转指令跳到真正的异常处理函数。处理函数的第一件事是保存现场顺序很有讲究先关中断把 Status、Cause、EPC 三个 CP0 寄存器保存下来再保存通用寄存器。理由很简单如果先保存 CP0 再关中断时钟中断嵌套进来会把 Cause 改成新值等外层恢复时异常类型已经被污染。下面是一段常见异常入口开头只保留最核心的几步exc_entry: mfc0 t0, $12 # CP0 Status mfc0 t1, $13 # CP0 Cause mfc0 t2, $14 # CP0 EPC andi t0, t0, 0xfffe # 清除 Status 的全局中断使能位 mtc0 t0, $12 addiu sp, sp, -128 sw t0, 0(sp) # 保存 Status sw t1, 4(sp) # 保存 Cause sw t2, 8(sp) # 保存 EPCCP0 寄存器编号在不同模拟器里一致但更稳妥的写法是用头文件里定义的 CP0_STATUS、CP0_CAUSE、CP0_EPC 宏避免数字看串。这段的关键是 EPC 必须保存lab4 返回用户态时靠它恢复 PClab5 上下文切换时靠它知道线程被抢占在哪条指令。调试时如果 EPC 永远是同一个值说明中断没有真正发生去查 Compare 和 Count 的初始化。4.2 syscall表用户态陷入内核的参数约定与派发逻辑MIPS o32 ABI 约定很固定$v0 放系统调用号$a0~$a3 放前四个参数系统调用完成后返回值写在 $v0。用户态写一个系统调用通常长这样li $v0, 4 # 系统调用号sys_write li $a0, 1 # fd la $a1, msg # buf 地址 li $a2, 12 # len syscall内核侧的派发逻辑本质是一张表。常见实现是int do_syscall(int nr, int a0, int a1, int a2) { switch (nr) { case SYS_write: return k_write(a0, (const char *)a1, a2); case SYS_exit: do_exit(a0); break; case SYS_getpid: return current-pid; default: return -ENOSYS; } }这段代码看起来简单真正的难点在从异常现场到 do_syscall 的参数搬运$v0 要变成 nr$a0~$a2 要按顺序变成参数而且不能让编译器优化掉现场里的寄存器。我一般会用一个结构体保存全部现场再在 dispatch 时按偏移取。另一个高频错误是 EPC 没加 4返回用户态前要把 EPC 指向 syscall 指令的下一条否则用户程序每次返回都重复执行同一条 syscall。判断方法很直接让用户程序连续调用三次同样的 syscall第二次返回值就开始异常那就是 EPC 没加 4。4.3 lab5/lab6的调度与同步参数时间片、优先级、临界区看调度器代码前先把这几个参数找出来参数典型位置常见取值修改后观察时钟中断间隔timer_init 或 trap.c10ms~20ms切换频率时间片schedule.c 顶部宏5~10个tick两个线程各跑多少输出线程栈大小thread_create4KB~8KB深递归或大数组是否溢出优先级thread_create 参数数字越小越高调度顺序时间片这个参数对实验观察影响最直接。设太小时串口日志里全是切换记录两个线程的计数都涨不快设太大又跟没有调度一样。我一般先把时间片设成 5~10 tick观察两个线程的计数输出是否交错出现再往小调。调完注意同步修改注释里的说明不然几天后回来看代码会以为宏就是写死的。lab6 的同步原语在 MIPS 上没有统一的原子操作实现最省事的是关中断自旋锁void spin_lock(volatile int *l) { int saved disable_irq_save(); if (*l) { while (*l) ; } *l 1; enable_irq_restore(saved); }逻辑说明先关中断再检查锁标志没有竞争就置位后恢复中断。关闭中断期间不能做任何耗时操作否则时钟中断进不来串口会卡死。如果实验要求用户态进程同步锁的获取和释放要走 lab4 的 syscall 分发不能只在库函数里做个标志位——用户态两个进程各自有独立地址空间标志位根本不可见。5. 六个lab的常见问题与排查现象、原因与解决5.1 交叉编译器与模拟器端序不匹配启动即崩现象make 正常通过QEMU 也能加载 vmlinux但串口没有任何输出CPU 停在复位向量反复取指GDB 里看到 PC 在 0xbfc00000 附近转圈。原因编译目标端序和模拟器机器端序不一致。常见是小端模拟器 qemu-system-mipsel 配了大端编译器 mips-linux-gnu-gcc或者反过来。编译器能生成 ELF但第一个取指周期读到的字节顺序完全错位。解决先别改代码用 mipsel-linux-gnu-readelf -h vmlinux 看 ELF 头部 Data 字段是 little endian 还是 big endian再对照 QEMU 机器名。端序必须一致不一致时换工具链前缀重编而不是在 Makefile 里加奇怪参数硬凑。5.2 链接脚本入口地址错误代码跑飞现象GDB 单步能走进 _start但执行十几条指令后 PC 跳到 0x00000000 或者一个随机的数据地址寄存器也变成乱值。原因链接脚本的 ENTRY 和实际启动符号不一致或者 .text 起始地址覆盖了异常向量区。链接器不负责校验逻辑地址是否有意义它只按脚本布局。解决检查 kernel.ld 里 . 0x80020000 与 ENTRY(_start) 是否和 start.S 的导出符号一致。用 nm vmlinux | grep _start 确认符号存在再用 readelf -h 确认 Entry point address。如果实验代码把入口设在 0x80000000要确认那里没有和异常向量区冲突冲突时的解决办法是把入口挪到 0x80020000并在异常跳板处做重定位。5.3 串口没有任何输出但CPU看起来在跑现象GDB 能 continue断点也能停但终端上一直空白。有时不是完全没输出而是启动打印打到一半停住。原因常见三种——串口基地址与模拟器不匹配、发送前没轮询状态位、QEMU 输出没有被 -nographic 重定向到当前终端。实验代码如果依赖某个库函数初始化串口那个库函数可能在裸机环境下不工作。解决把串口初始化简化为直接操作寄存器发送前轮询 busy 位而不是依赖标准库 stdout。常见做法是先定位实验代码里 uart_putc 的基地址再对照模拟器机型确认是否一致。排查时在 uart_putc 里下断点如果断点不停说明内核还没走到输出函数问题在启动路径而不是串口。5.4 lab4之后用户程序第一次正常第二次乱跳现象用户程序执行第一次系统调用返回值和预期一致紧接着第二次调用参数错乱甚至直接跳到非法地址触发第二次异常。原因最经典的是 EPC 没有加 4CPU 在执行完 syscall 返回用户态后立刻又执行同一条 syscall其次是现场保存漏了 ra 和 s0~s7 寄存器恢复现场时用户程序自己维护的调用现场被破坏。解决在异常处理返回前把存入栈中的 EPC 值加 4 再恢复。写一个小测试程序连续调用三次同样的 syscall每次返回值正常且指针按顺序递增基本可以确定 EPC 处理正确。如果返回值正常但内存内容错乱重点检查寄存器保存函数里 s 寄存器有没有完整入栈。5.5 lab5线程调度卡死在一个循环现象两个线程各自打印计数运行一小段时间后只有一个线程还在输出另一个像消失了一样有时是全部卡住串口不再有新输出。原因线程切换时栈指针没有保存到线程控制块或者恢复时从错误的 TCB 里取 sp另一种可能是临界区里关中断时间太长时钟中断丢了调度没法发生。解决在上下文切换代码里把 sp 保存进 current-thread_sp恢复时再从目标 TCB 加载顺手把 ra 和 s 寄存器也一起保存。配合 GDB 在 schedule() 入口下断点观察 current 指针是否在两个线程之间交替如果只在一个线程里反复停说明 TCB 链表断了。临界区缩短到只做标志位赋值不打印、不分配内存。6. 把开机日志做成分级输出验证lab4~lab6行为的最省事办法实验跑到 lab4 之后单靠 printf 很快分不清日志来自内核态还是用户态也看不出调度切换顺序。这套 lab1~lab6 资源包下载后我建议第一件事不是跑代码而是先把分级日志挂上再开始对拍每个实验行为。做法是给实验代码加一个分级日志模块按模块控制输出量#define LOG_BOOT 0 #define LOG_SYSC 1 #define LOG_SCHED 2 #define LOG_LEVEL LOG_SCHED void klog(int level, const char *fmt, ...) { if (level LOG_LEVEL) { /* 简单格式化后逐个字符走 uart_putc */ } }用法很直接lab1~lab3 阶段只用 LOG_BOOT把 LOG_LEVEL 设成 0启动路径上的每个里程碑打一条比如“栈初始化完成”“异常跳板已安装”“内存管理器就绪”验证内核到底走到哪一步。到 lab4 调 syscall 时把 LOG_LEVEL 提到 LOG_SYSC在 do_syscall 入口打印系统调用号和前三个参数在返回前打印返回值对照用户程序预期就能快速定位是派发错还是 EPC 错。到 lab5 调度时再提到 LOG_SCHED在 schedule() 入口打印当前线程 pid、旧 sp、新 sp两个线程的切换轨迹一眼就能对上。这个方法比纯单步调试快在两点一是串口日志是持续输出的不需要每次操作都盯住 GDB二是分级开关让你不用在源码里反复增删打印语句只改一个宏。验证调度公平性时可以让每个线程各跑一个大循环累计到固定次数后打印自己的计数两个线程的最终计数接近说明时间片分配没有大问题差距过大就调时间片或检查优先级参数。我从那以后每次换实验小包都强制走一遍“先验端序、再验入口、最后把日志分级打开”的流程很多看着像玄学的调度卡死最后都是日志里一句 old sp 恢复错了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑