南大计算机系统基础实验:x86-32裸机开发与链接顺序陷阱

发布时间:2026/9/11 17:40:38
南大计算机系统基础实验:x86-32裸机开发与链接顺序陷阱 简介本资源是南京大学《计算机系统基础》课程配套的LAB与PA系列实验完整源码实现面向高校计算机专业学生及系统级编程学习者聚焦底层指令执行、内存管理、汇编与C混合编程等核心能力训练。压缩包共757个文件总计39.52MB涵盖192个C源文件含bomb、bufbomb、btest等经典实验模块、231个头文件支撑系统接口与数据结构、123个TXT文本含实验说明、测试用例与评分脚本、34个图像资源用于可视化调试以及Makefile、Shell脚本、汇编.S文件和可执行工具如dlc、hex2raw、ishow等关键构建与验证组件。已有448人学习下载资源结构高度还原南大课程实验体系包含全部PA1–PA4评分脚本、多版本bomb拆解、浮点/整数运算测试套件如test-float、add-longlong、movsx、排序与算法实现bubble-sort、quick-sort、select-sort及系统调用封装如game、makecookie便于读者逐层理解从机器级行为到高级语言映射的全过程。1. 这不是普通C项目南京大学《计算机系统基础》LAB/PA实验源码的真实运行逻辑如果你在Ubuntu 22.04上用make编译这个项目却卡在undefined reference to keyboard_read别急着查GCC版本——问题大概率出在lib-nemu-ref.a和libc.a的链接顺序上。这套源码不是教学演示代码而是真实支撑南京大学《计算机系统基础》课程7个PAProject Assignment和12个LABLaboratory的底层实验平台核心目标是让学生在x86-32裸机环境里亲手实现CPU指令译码、内存管理、中断处理和键盘驱动。它不依赖glibc所有系统调用都通过内联汇编直接对接NEMU模拟器32_keyboard_inline这类命名不是随意拼写而是明确指向32位实模式下键盘控制器8042芯片的端口操作封装。适合两类人一是刚学完《深入理解计算机系统》第4章想动手验证指令周期的学生二是需要复现PA3内存管理或PA5异常处理的助教——因为231个头文件里cpu/intr.h和memory/pmm.h的宏定义与南大课程讲义完全对齐。2. 实验平台构建从源码结构到可执行镜像的完整链路2.1 源码目录的隐含设计哲学为什么754个文件必须按此组织项目中122个TXT文件并非文档而是PA实验的测试用例输入输出规范如PA2/testcase/arith_add.txt每个文件包含三行第一行是待执行的汇编指令字符串第二行是寄存器初始状态格式eax0x1234 ebx0xabcd第三行是期望的寄存器终态。这种设计强制学生在cpu/exec.c中实现指令执行器时必须精确模拟标志位变化——比如add-longlong测试用例会触发OF溢出标志和CF进位标志的双重校验。而34个Image文件.bmp实际是PA4图形实验的帧缓冲区模板tools/bmp2bin.py脚本会将其转换为二进制数据嵌入kernel/graphics.c。关键在于S文件8个boot.S和entry.S采用ATT语法编写但trap.S里pushl %gs指令后紧跟movw $0x10, %ax; movw %ax, %gs这是为后续PA5的TSS切换预留的段寄存器初始化逻辑绝非冗余代码。提示不要用file *.S判断汇编语法——gcc -x assembler-with-cpp才能正确预处理#include cpu/reg.h中的寄存器宏定义。2.2 编译工具链的硬性约束为什么必须用i686-elf-gcc而非系统GCC该平台要求严格遵循IA-32实模式16位段地址16位偏移启动流程因此编译链必须满足三个条件目标架构-m32 -marchi386 -mno-80387禁用x87浮点以避免协处理器指令链接脚本linker.ld中.text段起始地址为0x100000且*(.multiboot)必须位于前8字节——这是GRUB多引导协议的强制要求库替换libc.a由tools/build-libc.sh生成其printf函数不调用write()系统调用而是直接向0xb8000显存地址写入ASCII属性字节验证方法# 检查bootloader是否符合Multiboot规范 readelf -e build/boot.o | grep -A2 Section Headers | grep -E (Name|Addr) # 输出应显示 .multiboot 头部在偏移0处且Addr0x0若使用系统GCC编译-fPIE默认开启会导致重定位错误而i686-elf-gcc的-ffreestanding参数确保不链接任何标准库符号——这正是add add add add-longlong重复出现的原因每个add对应一个独立的汇编测试桩用于隔离验证不同加法指令变体addb/addw/addl/addq。2.3 链接阶段的关键陷阱lib-nemu-ref.a与libc.a的符号解析冲突lib-nemu-ref.a是NEMU模拟器的引用实现库提供nemu_trap()等钩子函数libc.a则包含自研的memset/memcpy。当同时链接二者时若libc.a在命令行中位于lib-nemu-ref.a之前ld会优先解析memset符号导致PA3内存管理实验中pmm_init()调用的memset被替换成不支持页表清零的版本。正确顺序必须是i686-elf-gcc -T linker.ld -o build/kernel.bin \ build/boot.o build/kernel.o \ -L./lib -lnemu-ref -lc \ --entrystart -nostdlib此处-lnemu-ref在-lc之前确保nemu_trap等符号优先绑定。验证链接结果nm build/kernel.bin | grep -E (memset|nemu_trap|keyboard_read) # 正确输出应显示 memset 来自 libc.a类型 Tnemu_trap 来自 lib-nemu-ref.a类型 U3. LAB实验实战键盘驱动与内存管理模块的调试闭环3.132_keyboard_inline的硬件级实现从端口读写到中断处理32_keyboard_inline并非函数名而是cpu/intr.c中键盘中断服务程序ISR的标签前缀。其工作流程如下硬件触发按下键时8042芯片向CPU发送IRQ1中断请求中断响应CPU跳转至idt[0x21]指向的keyboard_intr入口端口读取执行inb(0x60)从键盘控制器数据端口读取扫描码状态同步调用keyboard_read()将扫描码转换为ASCII并存入环形缓冲区kb_buf关键代码段cpu/intr.c// keyboard_intr: 键盘中断处理主函数 void keyboard_intr() { uint8_t scancode inb(0x60); // 读取扫描码 if (scancode 0x80) { // 释放键事件忽略 return; } // 将扫描码映射为ASCII简化版实际需查表 char ch keyboard_scancode_to_ascii(scancode); // 环形缓冲区写入无锁因仅在中断上下文调用 kb_buf[kb_tail] ch; kb_tail (kb_tail 1) % KB_BUF_SIZE; }注意inb(0x60)必须配合outb(0x20, 0x20)向PIC发送EOIEnd of Interrupt信号否则中断被屏蔽。该操作在keyboard_intr末尾执行源码中位于pic_send_eoi(0x01)调用处。3.2 PA3内存管理物理内存页框分配器PMM的调试技巧PA3要求实现基于位图的物理内存管理核心难点在于pmm_alloc_pages()的原子性保障。源码中memory/pmm.c使用__sync_fetch_and_add()实现CAS操作但Ubuntu 22.04的i686-elf-gcc默认不启用-marchi686导致__sync_*函数无法生成xchgl指令。解决方案是在Makefile中添加CFLAGS -marchi686 -mtunei686调试时需验证页框分配是否连续# 在GDB中监控pmm_alloc_pages返回值 (gdb) break pmm_alloc_pages (gdb) run (gdb) print/x *(uint32_t*)0x100000 # 查看页框位图首字 # 若返回地址为0x200000检查位图第0位是否置1常见错误pmm_init()中mem_size参数传入的是总内存大小如0x800000但位图数组长度应为(mem_size 12) / 32每bit管1页每uint32_t管32页。源码中该计算位于memory/pmm.c:42若此处出错会导致pmm_alloc_pages()永远返回NULL。3.3 实验结果验证用tools/check-testcase.py自动化比对手动比对754个文件的输出不现实项目自带验证脚本tools/check-testcase.py可批量执行。其原理是读取testcase/arith_add.txt等文件提取指令和期望状态启动NEMU模拟器加载build/kernel.bin注入指令并捕获寄存器快照用difflib.SequenceMatcher比对实际与期望状态执行命令python3 tools/check-testcase.py \ --test-dir PA2/testcase \ --kernel build/kernel.bin \ --nemu ./nemu/build/nemu \ --timeout 5输出示例PASS: PA2/testcase/arith_add.txt (eax0x1234 ebx0xabcd - eax0xbe71) FAIL: PA2/testcase/logic_and.txt (expected: ecx0x1234, got: ecx0x0)失败时自动保存nemu.log其中[CPU] EAX0x00000000 EBX0x00000000等行即为寄存器状态快照可直接定位cpu/exec.c中and指令执行逻辑缺陷。4. 进阶调试GDB远程调试与内存布局可视化4.1 NEMUGDB双机调试精准定位PA5异常处理缺陷PA5要求实现除零异常#DE和页故障#PF的双重处理此时需用GDB连接NEMU的GDB stub。步骤如下启动NEMU并监听TCP端口./nemu/build/nemu -d -s 1234 # -d 启用调试模式-s 1234 开放GDB服务器在另一终端启动GDB并连接i686-elf-gdb build/kernel.bin (gdb) target remote :1234 (gdb) b cpu/intr.c:234 # 在do_exception入口设断点 (gdb) c触发异常在NEMU控制台输入div $0除零指令关键观察点cs寄存器值应为0x0008内核代码段选择子eip应指向idt[0]描述符的基地址源码中为0x80000若eip停在0x00000000说明IDT未正确加载需检查cpu/idt.c中idt_load()调用时机4.2 内存布局分析用readelf解构段地址冲突当实验出现Segmentation fault时90%源于段地址越界。用readelf提取关键信息readelf -S build/kernel.bin | grep -E (Name|Address|Offset|Size)重点关注三类段段名地址范围作用常见错误.text0x100000-0x10ffff内核代码被libc.a的.text覆盖导致指令错乱.data0x110000-0x110fff全局变量kb_buf等缓冲区未对齐到页边界.bss0x111000-0x111fff未初始化数据pmm_bitmap过大导致溢出到.rodata区域若.bssSize显示0x2000但实际需要0x10000需修改linker.ld中_bss_end定义并在pmm_init()中重新计算位图大小。4.3 Shell脚本自动化tools/run-pa.sh的参数定制技巧tools/run-pa.sh支持动态注入测试用例例如PA4图形实验需指定BMP路径./tools/run-pa.sh \ --pa PA4 \ --bmp tools/test.bmp \ --width 640 \ --height 480 \ --mode vga其中--mode vga会设置VGA_MODE宏使graphics.c启用VGA 16色模式若改为--mode vesa则启用VESA 256色模式此时需确保tools/bmp2bin.py已将BMP转换为256色索引格式。脚本内部通过sed -i s/CONFIG_VGA_MODE.*/CONFIG_VGA_MODE$MODE/ config.mk动态修改配置避免手动编辑。提示run-pa.sh的--debug参数会启动NEMU的-d模式并附加GDB但需提前在config.mk中设置GDB_PORT1234否则端口冲突导致调试失败。本文还有配套的精品资源点击获取