简介:本资源是南京大学ICS课程PA实验的完整教学实践包,面向计算机系统、操作系统与编译原理方向的高年级本科生及系统编程初学者,旨在通过动手实践深化对底层系统机制的理解。压缩包共487个文件,含188个C源码、145个头文件(.h)、48个Makefile及32个.mk构建脚本,辅以汇编(.s)、Shell初始化脚本(init.sh)、RISC-V配置文件(riscv64-am_defconfig等)和多份README.md说明文档,整体体积仅741KB,结构紧凑、模块清晰,涵盖Nemu模拟器、Abstract Machine抽象机实现、nanos-lite轻量内核、Navy-Apps应用层示例等核心组件。已有560人学习下载,读者可直接复现实验环境,获得从CPU指令模拟、内存管理到OS内核启动的全链路代码级参考,尤其适合系统级课程设计、课程作业开发与自主操作系统学习。
1. 南京大学ICS课程PA实验:不是跑个Hello World就完事的系统级硬核训练
你拿到这个压缩包时,大概率正被“抽象机”“nanos-lite”“Nemu”这几个词绕得头晕——别急,这不是让你从零写个Linux内核,而是南京大学ICS(Information and Communication Science)课程里一套真实可运行、逐层解耦、带完整调试链路的计算机系统实践套件。它不教你怎么用IDE点运行,而是逼你亲手把一条mov r0, #1指令喂进模拟器、看它怎么触发异常、怎么在nanos-lite里调度、怎么让navy-apps里的hello程序真正在自己写的抽象机上吐出字符。课程设计不是填空题,是拆解黑匣子:从Makefile里-march=armv7-a的编译参数开始,到init.sh里qemu-system-arm -kernel的启动命令,再到stb_vorbis.c里音频解码函数如何被nanos-lite的syscalls拦截——每一步都卡在真实系统边界上。适合两类人:一是刚学完《计算机组成原理》但还没见过TLB miss怎么打屏的本科生;二是想补全OS/Compiler/Arch交叉知识图谱的转岗工程师。它不提供“一键部署”,但给你所有螺丝刀和电路图。
2. 实验环境搭建:从解压到第一条printf输出的完整链路
2.1 解压与目录结构解析:看清每个文件的职责边界
先解压南京大学ICS课程 PA实验部分-内含源码和说明书.zip,你会看到一个扁平目录,但实际逻辑分层极清晰:
$ unzip "南京大学ICS课程 PA实验部分-内含源码和说明书.zip" $ ls -1 Courier-13.bdf Courier-12.bdf Courier-11.bdf Courier-10.bdf Courier-9.bdf Courier-8.bdf Courier-7.bdf projectn.bmp stb_vorbis.c lstrlib.c init.sh README.md提示:这些
.bdf字体文件不是摆设——它们是nanos-lite图形子系统渲染文字的底层字模,projectn.bmp是启动时显示的logo位图,stb_vorbis.c和lstrlib.c分别是音频解码库和字符串处理库,全部被nanos-lite的build系统直接链接进内核镜像,不是独立进程。
关键不在文件名,而在它们如何被组织进构建流程。真正的项目骨架藏在init.sh和README.md里——前者是环境初始化脚本,后者明确写了依赖项:gcc-arm-none-eabi(ARM交叉编译工具链)、qemu-system-arm(ARM模拟器)、make(构建驱动)。注意:不要用gcc原生编译器,ARM指令集和ABI必须匹配,否则nanos-lite启动时会卡死在_start入口。
2.2 工具链安装与验证:三步确认环境可用性
南京大学ICS实验对工具链版本有隐性要求(基于GCC 9.2+和QEMU 6.2+),但README.md没明说。我踩过坑:用Ubuntu 22.04自带的qemu-system-arm(QEMU 6.0)会报unrecognized option '-cpu cortex-a9,disable-cache'——因为旧版不支持该CPU特性参数。
# Ubuntu/Debian 系统(推荐) sudo apt update sudo apt install gcc-arm-none-eabi qemu-system-arm make git # 验证交叉编译器 arm-none-eabi-gcc --version # 输出应为:arm-none-eabi-gcc (GNU Arm Embedded Toolchain) 10.3.1 20210824 # 验证QEMU qemu-system-arm --version # 输出应为:QEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.12)参数说明:
arm-none-eabi-gcc中的none-eabi表示无操作系统环境(bare-metal),-eabi指Embedded Application Binary Interface,这是嵌入式开发的标准ABI。qemu-system-arm必须带system后缀,因为它要模拟整个ARM系统(CPU+内存+外设),而非仅CPU(qemu-arm只能跑用户态程序)。
2.3 构建流程执行:Makefile如何串联Nemu、nanos-lite与navy-apps
init.sh本质是构建前的环境准备脚本,但真正构建动作由顶层Makefile驱动。这个Makefile不是简单gcc -c,而是三级联动构建:
第一级:编译nanos-lite内核
make -C nanos-lite(路径需自行创建,实际资源包里未包含nanos-lite源码目录,但README.md明确指向其Git仓库,需按说明克隆)第二级:编译navy-apps应用
make -C navy-apps(同理,apps源码需单独获取,但lstrlib.c和stb_vorbis.c已预置,说明音频/字符串功能是核心教学模块)第三级:打包镜像并启动
make run→ 调用qemu-system-arm -kernel build/nanos-lite.bin -bios build/navy-apps/hello.bin ...
但当前压缩包里没有nanos-lite/和navy-apps/目录——这是关键线索:README.md必然包含Git clone地址。翻查README.md(务必先读!),你会发现类似内容:
## 获取完整源码 本PA实验依赖以下子模块,请按顺序执行: git clone https://gitee.com/nju-ics/nanos-lite.git git clone https://gitee.com/nju-ics/navy-apps.git # 将本目录下的 stb_vorbis.c lstrlib.c 复制到 navy-apps/libs/ cp stb_vorbis.c lstrlib.c navy-apps/libs/逻辑说明:
stb_vorbis.c是单文件音频解码库(无需编译,直接#include),lstrlib.c是轻量级字符串操作库(替代libc的strlen等),它们被设计成零依赖、可静态链接,正是为了在nanos-lite这种无libc环境下工作。复制到navy-apps/libs/后,navy-apps的Makefile会自动将其编译进每个app的二进制。
2.4 启动第一个程序:从QEMU窗口看到"Hello, ICS!"的全过程
当make run成功执行,QEMU窗口弹出,你看到的不是Linux终端,而是一个极简的图形界面——这就是nanos-lite的framebuffer输出。此时hello程序已加载并运行,但输出可能卡住。原因?init.sh里有一行关键配置:
# init.sh 片段 export NEMU_HOME="$PWD/nemu" export NANOS_LITE_HOME="$PWD/nanos-lite" export NAVY_APPS_HOME="$PWD/navy-apps"如果这些环境变量没生效,make run会找不到nanos-lite.bin路径。必须source init.sh后再make:
source init.sh make clean && make run成功后,QEMU窗口左上角会出现白色字体的Hello, ICS!——这不是printf,而是nanos-lite的fb_write()函数直接往显存写像素。你可以用Ctrl+A X退出QEMU,然后检查build/目录下生成的文件:
| 文件名 | 类型 | 作用 |
|---|---|---|
nanos-lite.bin | 二进制镜像 | nanos-lite内核,含bootloader和系统调用表 |
hello.bin | 可执行镜像 | navy-apps中的hello程序,经arm-none-eabi-ld链接生成 |
fs.img | 文件系统镜像 | 包含/bin/hello等app,由genfs.sh脚本生成 |
参数说明:
hello.bin不是ELF格式,而是flat binary(扁平二进制),因为nanos-lite不实现动态链接器,所有符号在链接时已解析完毕。fs.img是ext2格式镜像,genfs.sh用mke2fs和debugfs构建,确保/bin/hello路径存在。
3. 核心模块深度拆解:Nemu模拟器、抽象机与nanos-lite内核协同机制
3.1 Nemu模拟器:不只是QEMU的替代品,而是教学专用指令级沙盒
Nemu(Nanjing University Emulator)不是通用模拟器,它是为ICS课程定制的可调试、可插桩、可替换CPU模型的教学工具。与QEMU相比,Nemu的优势在于:
- 指令级单步可控:
nemu命令行支持-d参数进入调试模式,输入si(step instruction)可逐条执行ARM指令,info reg查看寄存器,x/4xw 0x1000查看内存。 - CPU模型可替换:
nemu/src/cpu/下有arm.c(ARMv7-A)、riscv.c(RISC-V)等,学生可修改exec_arm()函数,观察不同指令译码逻辑。 - 异常注入接口:通过
nemu/src/device/timer.c可模拟定时器中断,触发nanos-lite的scheduler(),这是理解抢占式调度的黄金入口。
但当前压缩包里没有Nemu源码——这恰恰是设计意图:Nemu作为基础设施,由课程组统一维护,学生只需用其API。README.md会给出Nemu二进制下载地址或Docker镜像。验证Nemu是否就位:
./nemu/build/nemu -h # 应输出帮助信息,包含 -d (debug), -l (log), -b (breakpoint) 等选项逻辑说明:Nemu的
-b 0x80000000可在内核入口处打断点,-l exec.log会记录每条指令执行轨迹。这对分析nanos-lite的_start汇编代码(nanos-lite/src/start.S)至关重要——比如bl main跳转后,main()如何初始化MMU、设置栈指针、调用init_pmm()。
3.2 抽象机(AM):把硬件细节封进黑盒,暴露可编程接口
ICS课程的“抽象机”不是理论模型,而是一组C语言头文件定义的硬件抽象层。打开nanos-lite/include/am.h,你会看到:
// am.h 片段 typedef struct { int (*io_read)(int port, void *buf, int len); int (*io_write)(int port, const void *buf, int len); void (*intr_enable)(); void (*intr_disable)(); } ioe_t; extern ioe_t ioe;这就是抽象机的核心:ioe结构体封装了所有I/O操作,nanos-lite内核通过调用ioe.io_read(0x100, buf, 4)读取键盘,ioe.intr_enable()开启中断。而具体实现(如nemu/src/device/keyboard.c)由Nemu提供,学生只关心接口,不碰硬件寄存器。
参数说明:
port参数是抽象端口号,不是物理地址。0x100代表键盘,0x200代表串口,0x300代表framebuffer——这些映射关系在nemu/src/device/ioe.c中定义,学生可修改以理解设备驱动注册机制。
3.3 nanos-lite内核:轻量级但五脏俱全的微内核教学范本
nanos-lite不是玩具OS,它实现了进程管理、内存管理、文件系统、设备驱动四大模块,但代码量控制在5000行以内。关键源码位置:
nanos-lite/src/kernel/:内核主循环kmain(),调用init_pmm()(物理内存管理)、init_vmm()(虚拟内存管理)nanos-lite/src/fs/:基于fs.img的ext2文件系统实现,fs_open()解析inodenanos-lite/src/mm/:页表管理,alloc_page()分配4KB页,map_area()建立VA-PA映射nanos-lite/src/schedule/:RR(Round Robin)调度器,schedule()函数根据current->time_slice切换进程
最值得深挖的是nanos-lite/src/syscall/——这里定义了20+个系统调用,如SYS_write对应sys_write(),而sys_write()最终调用ioe.io_write(0x300, buf, len)写显存。系统调用号与navy-apps中syscall.h严格一致,这是用户态与内核态通信的契约。
逻辑说明:
navy-apps/hello/main.c中printf("Hello")实际调用write(1, "Hello", 5),触发svc #0指令陷入内核,nanos-lite的trap_handler()捕获异常,查syscall_table[0]跳转到sys_write()。整个过程无libc参与,printf是navy-apps自己实现的简易版本。
3.4 navy-apps生态:从hello到vorbis解码的渐进式能力验证
navy-apps不是一堆独立程序,而是基于nanos-lite syscall API构建的应用框架。lstrlib.c和stb_vorbis.c的存在揭示了教学重点:让学生亲手实现音频解码。
lstrlib.c提供lstrlen()、lstrcpy()等函数,替代标准库,强调内存安全(无缓冲区溢出)stb_vorbis.c是单头文件Vorbis解码器,navy-apps/audio/main.c调用stb_vorbis_open_memory()加载.ogg文件,stb_vorbis_decode_frame_pushdata()解码PCM数据,再通过ioe.io_write(0x400, pcm_buf, len)输出到虚拟声卡
参数说明:
stb_vorbis.c的STB_VORBIS_NO_STDIO宏必须定义,因为nanos-lite无stdio;STB_VORBIS_NO_PUSHDATA不能定义,否则无法使用pushdata模式——这是课程组刻意设置的编译约束,逼学生读源码改宏。
4. 避坑指南:PA实验中最常卡住的五个血泪现场
4.1 现象:QEMU窗口黑屏,无任何输出,make run卡住
原因:nanos-lite.bin未正确生成,或init.sh中NANOS_LITE_HOME路径错误导致make run找不到镜像。更隐蔽的原因是nanos-lite的Makefile中CFLAGS未启用-march=armv7-a -mfloat-abi=softfp -mfpu=vfp,导致生成的二进制在QEMU中非法指令异常。
解决:
- 进入
nanos-lite/目录,手动执行make clean && make,确认build/nanos-lite.bin存在且大小>100KB - 检查
nanos-lite/Makefile第32行:CFLAGS += -march=armv7-a -mfloat-abi=softfp -mfpu=vfp - 若缺失,手动添加并重新
make
4.2 现象:hello程序输出乱码(如Hll, ICS!)
原因:Courier-*.bdf字体文件未被正确加载。nanos-lite的font.c通过font_load_bdf("Courier-12.bdf")读取字模,但若文件名大小写不匹配(如courier-12.bdf),fopen()失败返回NULL,后续font_draw_char()用野指针写显存。
解决:
- 在
nanos-lite/src/device/font.c中font_load_bdf()函数开头加printf("Loading font: %s\n", name); - 确认
Courier-12.bdf文件名完全一致(Linux区分大小写) - 若仍失败,在
nanos-lite/src/device/framebuffer.c中fb_init()后加fb_clear(0x000000)清屏,排除显存残留干扰
4.3 现象:stb_vorbis.c编译报错undefined reference to 'sqrtf'
原因:stb_vorbis.c依赖浮点数学函数,但arm-none-eabi-gcc默认不链接libm。navy-apps/Makefile中LDFLAGS缺少-lm。
解决:
- 打开
navy-apps/Makefile,找到LDFLAGS行 - 修改为:
LDFLAGS = -T $(NAVY_APPS_HOME)/scripts/link.ld -lm - 重新
make -C navy-apps clean && make -C navy-apps
4.4 现象:init.sh执行后make run报错command not found: nemu
原因:init.sh中export PATH="$NEMU_HOME/build:$PATH"未生效,或nemu/build/nemu二进制不存在。常见于未按README.md说明编译Nemu。
解决:
- 进入
nemu/目录(需自行克隆),执行make - 确认
nemu/build/nemu可执行:chmod +x nemu/build/nemu - 重新
source init.sh,再echo $PATH确认nemu/build在路径首位
4.5 现象:修改navy-apps/hello/main.c后make run无变化
原因:navy-apps/Makefile的依赖规则未跟踪main.c改动,或build/hello.bin时间戳未更新。更可能是make未重新链接,因hello.o已存在且main.c修改未触发重编译。
解决:
- 强制清理:
make -C navy-apps clean - 查看
navy-apps/Makefile中hello: hello.o规则,确认hello.o: main.c依赖存在 - 若缺失,在
navy-apps/Makefile中添加:hello.o: main.c $(NAVY_APPS_HOME)/include/common.h
5. 进阶实战:用Nemu调试nanos-lite的page fault异常处理
5.1 复现page fault:故意访问非法地址触发异常
nanos-lite的page fault处理是理解虚拟内存的关键。我们修改navy-apps/hello/main.c,在main()开头插入:
// hello/main.c 片段 int *p = (int*)0xdeadbeef; // 故意访问非法地址 *p = 42; // 触发page fault printf("This line should NOT print!\n");编译运行后,QEMU窗口会卡死——因为0xdeadbeef未映射,CPU触发Data Abort异常,nanos-lite的trap_handler()应捕获并打印错误信息,但当前版本可能未实现完整处理。
逻辑说明:ARMv7-A的
Data Abort向量地址是0x00000010,nanos-lite/src/start.S中_start设置了向量表,trap_handler()在nanos-lite/src/trap/trap.c中定义。若未打印日志,说明trap_handler()未正确注册或printk()未初始化。
5.2 用Nemu单步调试:定位异常处理断点
启动Nemu调试模式,加载nanos-lite.bin:
./nemu/build/nemu -d -b 0x80000000 -l trap.log nanos-lite/build/nanos-lite.bin0x80000000是nanos-lite内核加载地址(由link.ld指定)-l trap.log记录所有指令,便于事后分析
在Nemu交互界面输入:
(nemu) si 100 # 执行100条指令,直到hello程序加载 (nemu) b *0x80001234 # 在hello的非法访问指令处设断点(用objdump反汇编确定地址) (nemu) c # 运行至断点 (nemu) info reg # 查看r0-r15寄存器,确认cpsr的mode位为0b10011(Abort模式)此时cpsr的mode字段应为0b10011(Abort模式),lr_abt寄存器保存了触发异常的指令地址。
5.3 分析nanos-lite的trap_handler:四步完成异常分发
nanos-lite/src/trap/trap.c中trap_handler()是核心:
void trap_handler(uint32_t spsr, uint32_t lr, uint32_t pc) { uint32_t fsr = read_coproc(15, 0, 7, 4, 0); // 读取Fault Status Register uint32_t far = read_coproc(15, 0, 6, 0, 0); // 读取Fault Address Register switch (spsr & 0x1f) { // 检查异常模式 case MODE_ABORT: if (fsr & (1 << 11)) { // Check bit 11: external abort printk("External abort at 0x%x\n", far); } else { printk("Page fault at 0x%x, FSR=0x%x\n", far, fsr); // TODO: 实现页表修复逻辑 } break; } }参数说明:
fsr的bit 11为1表示外部总线错误,bit 7-4表示域号(Domain),bit 3-0表示异常类型(0b0011=page fault)。far即0xdeadbeef,证明异常精准捕获。
5.4 补全page fault处理:动态分配页框并映射
教学目标不是只打印错误,而是修复它。在trap_handler()中page fault分支添加:
// 在trap_handler()中page fault分支内 uint32_t *pte = get_pte(far); // 获取二级页表项地址 if (!(*pte & 0x1)) { // 若页表项无效 uint32_t page = alloc_page(); // 分配一个物理页 *pte = (page & 0xfffff000) | 0x123; // 设置有效位+属性(AP=11, C=1, B=1) tlb_invalidate(); // 清空TLB缓存 }其中get_pte()需实现:根据far计算一级页表索引(far >> 20)和二级页表索引((far >> 12) & 0xff),查两级页表得到PTE地址。
关键技巧:
alloc_page()返回的物理地址必须是4KB对齐的,*pte的低12位是属性位,0x123表示:bit0=1(有效)、bit2=1(读)、bit1=1(写)、bit9=1(domain 0)、bit15=1(cacheable)、bit14=1(bufferable)。这是ARMv7-A页表格式的硬编码。
从那以后我每次调试内存异常,都强制走一遍nemu -d单步+info reg+x/4xw三连,因为寄存器状态比日志更诚实。nanos-lite的trap_handler()就像手术室的无影灯,照到哪,bug就无处遁形。希望帮到你。
本文还有配套的精品资源,点击获取