
简介本资源是南京大学《计算机系统基础》课程配套的LAB与PA系列实验完整源码实现面向高校计算机专业本科生及系统编程初学者旨在支撑底层原理理解与动手实践。包内共757个文件涵盖231个头文件定义数据结构与接口、192个C源文件实现核心实验逻辑、123个文本文件含实验说明、测试用例与评分脚本、34个图像资源用于可视化调试以及Makefile、Shell脚本、汇编文件等构建与自动化工具压缩包大小为39.52MB。已有448人学习下载资源结构高度还原南大课程体系包含bomblab、bufbomb、nemu等经典实验模块附带完整评分脚本score_.pa-、调试工具btest、dlc、hex2raw及参考实现lib-nemu-ref.a便于对照学习、自主验证与错误定位是深入掌握程序机器级表示、内存管理、链接加载与异常控制的优质实践材料。1. 这不是一份“作业答案”而是一套可复现、可调试、可延伸的系统级C语言实践骨架南京大学《计算机系统基础》课程中的 LAB实验与 PAProject Assignment并非传统意义上的编程练习——它们直指冯·诺依曼体系的核心矛盾程序员写的 C 代码如何在真实 x86-64 硬件上被编译、链接、加载、执行并最终与内存、寄存器、中断、异常、页表发生不可回避的交互。你看到的“基于C语言的南京大学计算机系统基础课程LAB与PA实验设计源码”本质是一组带约束条件的系统编程契约用纯 C极少内联汇编禁用标准库函数如printf/malloc在裸机或简化内核环境下亲手实现栈帧布局、函数调用约定、ELF 解析、虚拟地址映射、陷阱处理、设备驱动抽象等关键环节。它面向的是已掌握 C 基础语法、但尚未建立“程序即数据控制流状态”的系统视角的学习者也适用于想验证自己对 GCC 工具链、GDB 调试流程、Linux 内核模块加载机制理解深度的工程师。这套源码的价值不在于“跑通即止”而在于每一行movq %rsp, %rbp和每一页mmap()分配的内存都必须能被你用objdump -d反汇编出来、用gdb单步到寄存器变化、用/proc/pid/maps验证其布局——这才是“系统基础”的真实刻度。2. 从 Makefile 到链接脚本构建一个可控的裸机 C 执行环境2.1 为什么不能直接gcc main.c -o a.out理解 LAB/PA 的构建约束LAB 与 PA 实验强制要求脱离 glibc 运行时环境这意味着不允许调用printf、malloc、exit等依赖 libc 的函数启动入口不是main()而是自定义的_start符号程序必须显式管理栈、堆、全局偏移表GOT、过程链接表PLT内存布局需由开发者通过链接脚本精确控制而非由链接器默认分配。常见误操作是直接编译 C 文件生成动态链接可执行文件结果在 QEMU 或 Bochs 中启动失败报错Segmentation fault (core dumped)或Kernel panic: VFS: Unable to mount root fs。根本原因在于动态链接器ld-linux-x86-64.so.2未加载且.dynamic段缺失。LAB/PA 要求的是静态链接、无运行时依赖的 flat binary 或 ELF 可重定位对象。提示所有 LAB/PA 实验的Makefile开头必然包含CFLAGS -nostdlib -nostartfiles -static -fno-builtin -m64。其中-nostdlib禁用标准库-nostartfiles跳过默认启动文件如crt0.o-static强制静态链接-fno-builtin防止 GCC 将memcpy等优化为内建函数这些函数仍可能隐式调用 libc。2.2 编写最小可行链接脚本控制.text、.data、.bss的物理位置南京大学 PA1启动与内存管理要求实现一个能加载并跳转到用户程序的简易 bootloader。此时链接脚本link.ld是核心控制点。以下是一个典型 PA1 兼容的链接脚本片段SECTIONS { . 0x100000; /* 从 1MB 地址开始避开实模式 BIOS 区域 */ .text : { *(.text.start) /* 放置 _start 入口 */ *(.text) *(.rodata) } .data : { *(.data) *(.sdata) } .bss : { *(.bss) *(.sbss) . ALIGN(16); __bss_start .; *(COMMON) . ALIGN(16); __bss_end .; } /DISCARD/ : { *(.comment) *(.note.*) } }该脚本强制将代码段起始地址设为0x1000001MB确保 bootloader 加载后能正确跳转显式分离.text只读代码、.data初始化数据、.bss未初始化数据并标记__bss_start与__bss_end符号供 C 代码清零使用。若省略此脚本链接器会按默认布局如.text在0x400000生成 ELF导致 bootloader 加载后跳转地址错误CPU 执行非法指令。2.3 构建流程拆解从 C 源码到可加载二进制的完整命令链以 PA1 的loader.c为例其构建命令链如下摘自标准 Makefile# 1. 预处理展开宏、包含头文件生成 .i 文件 gcc -E -I./include -D__PA1__ loader.c -o loader.i # 2. 编译为汇编生成 .s 文件确认无 libc 调用 gcc -S -I./include -D__PA1__ -O2 -m64 -fno-pic -fno-stack-protector \ -nostdlib -nostartfiles -static -fno-builtin loader.c -o loader.s # 3. 汇编为目标文件生成 .o注意使用 -m64 和 -fno-pic as --64 -o loader.o loader.s # 4. 链接使用自定义链接脚本生成最终可执行文件 ld -T link.ld -o loader.bin loader.o # 5. 可选转换为扁平二进制供 bootloader 直接读取 objcopy -O binary loader.bin loader.img关键参数说明-fno-pic禁用位置无关代码因 LAB/PA 运行于固定地址空间-fno-stack-protector关闭栈保护避免插入__stack_chk_fail调用依赖 libcobjcopy -O binary剥离 ELF 头部仅保留原始字节流这是 bootloader 加载时实际读取的内容。验证构建结果是否合规执行readelf -h loader.bin # 应显示 Type: EXEC (Executable file)Machine: Advanced Micro Devices X86-64 nm loader.bin | grep U # 输出应为空表示无未定义符号即无 libc 依赖若nm输出含U printf或U malloc说明某处隐式调用了标准库函数需回溯 C 源码检查例如误用strlen而非手写循环。3. 栈帧与调用约定实战用 GDB 逐帧解析fibonacci()的寄存器流转3.1 x86-64 System V ABI 下的函数调用现场还原LAB2函数调用与栈管理要求学生手动绘制fibonacci(n)递归调用过程中 RSP、RBP、RIP 的变化并验证返回地址压栈/弹栈行为。这不能靠脑补必须用 GDB 在真实执行流中观测。以下是以fibonacci.c为例的调试步骤// fibonacci.c long fibonacci(long n) { if (n 1) return n; return fibonacci(n-1) fibonacci(n-2); } void _start() { long result fibonacci(5); // 此处放置断点观察 result 计算完成后的寄存器状态 __asm__ volatile (movq $0, %rax; int $0x80); // 系统调用退出模拟 }编译后启动调试gdb ./fibonacci.bin (gdb) set architecture i386:x86-64 (gdb) target remote | qemu-system-x86_64 -S -s -kernel ./fibonacci.bin (gdb) b *0x1000000x120 # 在 _start 入口下断点 (gdb) c (gdb) stepi # 单步进入 fibonacci 函数3.2 关键寄存器与内存布局的 GDB 观测指令集当fibonacci函数执行时执行以下命令获取核心证据# 查看当前栈顶及附近 16 字128 字节内容验证返回地址存储位置 (gdb) x/16gx $rsp # 查看 RBP 指向的栈帧基址及其前两个 quadword旧 RBP 和返回地址 (gdb) x/2gx $rbp # 查看函数参数 n 是否按 ABI 规定存于 %rdi 寄存器 (gdb) p $rdi # 查看局部变量如 n-1, n-2 的中间值是否分配在栈上-8(%rbp) 等 (gdb) x/4gx -32($rbp) # 反汇编当前函数确认 prologue/epilogue 指令序列 (gdb) disassemble fibonacci典型输出示例 0x0000000000100150 fibonacci: push %rbp 0x0000000000100151 fibonacci1: mov %rsp,%rbp 0x0000000000100154 fibonacci4: sub $0x10,%rsp 0x0000000000100158 fibonacci8: mov %rdi,-0x8(%rbp) # 参数 n 存入栈这证实了 System V ABI 规则第 1 个整数参数存于%rdi函数 prologue 必须push %rbp; mov %rsp, %rbp建立新栈帧且局部变量通过负偏移访问%rbp。注意若disassemble显示mov %rdi, %rax后直接ret无push %rbp说明 GCC 启用了尾递归优化-O2下常见此时栈帧被复用无法观测多层嵌套。解决方法是在编译时添加-O0 -fno-omit-frame-pointer强制生成标准栈帧。3.3 手动计算栈帧大小从汇编指令反推 C 变量布局观察fibonacci的汇编代码sub $0x10,%rsp表明该函数申请了 16 字节栈空间。结合 C 源码n是long8 字节为何分配 16 字节因为 x86-64 ABI 要求栈指针 16 字节对齐%rsp % 16 0。push %rbp占 8 字节sub $0x10再减 16 字节使%rsp保持对齐。因此-0x8(%rbp)存储n-0x10(%rbp)可用于临时变量如n-1的结果。这一细节在 LAB2 的“栈空间分析”题中是得分关键——仅写出sizeof(long)不够必须关联 ABI 对齐规则。4. ELF 解析与加载用 C 手写readelf -h的核心逻辑4.1 从二进制字节流中提取 ELF Header 的 52 字节结构体PA3程序加载与动态链接要求实现一个简易 ELF 解析器能读取e_type可执行/可重定位、e_machinex86-64、e_entry入口地址等字段。这需要精确理解 ELF Header 的内存布局。标准elf64.h头文件定义如下typedef struct { unsigned char e_ident[16]; // Magic class data version osabi abi_version uint16_t e_type; // ET_EXEC, ET_REL 等 uint16_t e_machine; // EM_X86_64 62 uint32_t e_version; // EV_CURRENT 1 uint64_t e_entry; // 程序入口虚拟地址 uint64_t e_phoff; // Program header table offset uint64_t e_shoff; // Section header table offset uint32_t e_flags; // Processor-specific flags uint16_t e_ehsize; // ELF header size 64 uint16_t e_phentsize; // Size of program header entry uint16_t e_phnum; // Number of program header entries uint16_t e_shentsize; // Size of section header entry uint16_t e_shnum; // Number of section header entries uint16_t e_shstrndx; // Section header string table index } Elf64_Ehdr;关键点e_ident[0]到e_ident[3]是魔数\x7fELFe_ident[4]为EI_CLASSELFCLASS642e_ident[5]为EI_DATAELFDATA2LSB1小端序。若解析出e_ident[4] ! 2则非 64 位 ELF应拒绝加载。4.2 实现parse_elf_header()安全读取与边界校验以下为 PA3 兼容的解析函数核心逻辑省略错误处理#include stdio.h #include stdint.h #include sys/mman.h #include fcntl.h #include unistd.h #define ELF_MAGIC \x7fELF int parse_elf_header(const char *filename) { int fd open(filename, O_RDONLY); if (fd 0) return -1; // 仅映射前 64 字节ELF Header 大小 void *map mmap(NULL, 64, PROT_READ, MAP_PRIVATE, fd, 0); if (map MAP_FAILED) { close(fd); return -1; } Elf64_Ehdr *ehdr (Elf64_Ehdr *)map; // 魔数校验 if (memcmp(ehdr-e_ident, ELF_MAGIC, 4) ! 0) { printf(Invalid ELF magic\n); goto fail; } // 64 位校验 if (ehdr-e_ident[4] ! 2) { printf(Not a 64-bit ELF\n); goto fail; } // 小端序校验 if (ehdr-e_ident[5] ! 1) { printf(Not little-endian ELF\n); goto fail; } // 机器类型校验x86-64 if (ehdr-e_machine ! 62) { printf(Not x86-64 ELF (e_machine%d)\n, ehdr-e_machine); goto fail; } printf(ELF Type: %d, Entry: 0x%lx, PhOff: 0x%lx\n, ehdr-e_type, ehdr-e_entry, ehdr-e_phoff); munmap(map, 64); close(fd); return 0; fail: munmap(map, 64); close(fd); return -1; }该函数严格遵循 PA3 要求不依赖libelf仅用 POSIX 系统调用校验链覆盖魔数、位宽、字节序、机器类型四大关键项mmap仅映射必要字节避免读取整个大文件。若跳过e_ident[5]校验可能在 ARM64 主机上误判 x86-64 ELF导致后续加载失败。4.3 验证解析器对比readelf与自研工具输出编译并测试gcc -o elf_parser elf_parser.c ./elf_parser ./lab2.bin readelf -h ./lab2.bin | grep -E (Type|Entry|Class|Data|Machine)正确输出应高度一致ELF Type: 2, Entry: 0x100000, PhOff: 0x40 Type: EXEC (Executable file) Entry address: 0x100000 Class: ELF64 Data: 2s complement, little endian Machine: Advanced Micro Devices X86-64若e_entry值与readelf不符说明链接脚本未生效或ld参数错误若e_type为REL可重定位而非EXEC则链接时遗漏-pie或未指定-Ttext0x100000。5. 内存映射与页表操作在 PA4 中实现mmap()的最小内核接口5.1 PA4 的核心挑战用 C 操作四级页表PML4 → PDP → PD → PTPA4虚拟内存管理要求在简易内核中实现mmap()系统调用其本质是修改当前进程的四级页表结构。x86-64 下虚拟地址0xffff800000000000开始的内核空间由内核页表管理而用户空间0x0000000000000000至0x00007fffffffffff由进程私有页表控制。mmap()需完成三件事在进程页表中分配新页目录PD和页表PT将物理页帧号PFN填入 PT 条目并设置Present1,UserAccess1,ReadWrite1更新 CR3 寄存器指向新页表根PML4。这无法用malloc()完成必须通过alloc_page()获取物理页并用__builtin_ia32_rdtscp()等内联汇编读写 CR3。5.2 关键数据结构四级页表条目的 C 语言定义为避免硬编码位移定义页表条目结构体typedef struct { uint64_t present : 1; // 0: not present uint64_t rw : 1; // 1: writable uint64_t user_access : 1; // 1: accessible from user mode uint64_t write_through : 1; // 1: write-through caching uint64_t cache_disable : 1; // 1: cache disabled uint64_t accessed : 1; // 1: accessed since last clear uint64_t dirty : 1; // 1: written since last clear uint64_t huge_page : 1; // 1: 2MB page (if in PD) uint64_t global : 1; // 1: global TLB entry uint64_t ignored : 3; // ignored by hardware uint64_t addr : 40; // physical address of next level table or page frame uint64_t reserved : 11; // must be zero uint64_t execute_disable : 1; // 1: instruction fetch disabled (NX bit) } pte_t; // PML4, PDP, PD 条目结构相同仅语义不同 typedef pte_t pml4e_t; typedef pte_t pdpe_t; typedef pte_t pde_t; typedef pte_t pte_t;addr字段占 40 位对应 4KB 对齐的物理地址低 12 位为 0因此addr 12得到真实物理地址。huge_page位在 PD 条目中置 1 表示 2MB 大页在 PT 中无效。5.3map_page()函数原子化更新页表的 C 实现以下是 PA4 中map_page()的核心逻辑简化版void map_page(uint64_t vaddr, uint64_t paddr, pml4e_t *pml4) { // 1. 计算各级索引x86-64 48-bit VA每级 9 位 int pml4_idx (vaddr 39) 0x1ff; int pdp_idx (vaddr 30) 0x1ff; int pd_idx (vaddr 21) 0x1ff; int pt_idx (vaddr 12) 0x1ff; // 2. 获取或分配 PML4 条目指向的 PDP pdpe_t *pdp get_or_alloc_pdp(pml4, pml4_idx); // 3. 获取或分配 PD 条目指向的 PT pde_t *pd get_or_alloc_pd(pdp, pdp_idx); // 4. 获取或分配 PT 条目指向的物理页 pte_t *pt get_or_alloc_pt(pd, pd_idx); // 5. 设置 PT 条目Present1, RW1, User1, NX0, addrPFN pt[pt_idx].present 1; pt[pt_idx].rw 1; pt[pt_idx].user_access 1; pt[pt_idx].execute_disable 0; pt[pt_idx].addr paddr 12; // PFN paddr / 4096 // 6. 刷新 TLB 中该地址的缓存 __builtin_ia32_invlpg((void*)vaddr); }get_or_alloc_pdp()等辅助函数需检查对应条目present位若为 0 则调用alloc_page()获取新物理页并清零该页memset(page, 0, 4096)再设置条目addr和present1。此过程必须保证原子性否则多核下页表不一致将导致#PF异常。提示PA4 测试用例常构造mmap(0, 4096, ...)请求匿名页此时paddr由alloc_page()返回。若map_page()中忘记清零新分配的页表页残留数据会导致后续mov %rax, (%rdi)写入随机地址引发不可预测崩溃。6. 调试与排错用 QEMU GDB 定位 PA 实验中最常见的三类故障6.1 故障类型一#GP(0)异常 —— 权限或地址越界现象QEMU 启动后立即卡死串口无输出GDB 中info registers显示RIP停在非法地址CS段选择子低 2 位非0b11用户态。原因最常见于 PA2 中pop %rax指令后RSP指向未映射内存或 PA4 中页表条目UserAccess0却在用户态执行。诊断步骤(gdb) info registers cs # 若 cs0x8内核态说明在内核态执行用户指令检查是否忘记切换到用户段 (gdb) x/4gx $rsp # 若输出全为 0 或乱码说明栈指针损坏回溯 push/pop 指令配对 (gdb) info proc mappings # 查看当前虚拟地址空间映射确认 $rsp 所在区域是否 rwx 且 UserAccess1解决方案在iretq前确保SS和RSP已加载有效用户栈地址页表条目必须设置user_access1mov %rax, %rsp后立即push %rax验证栈可写。6.2 故障类型二#PF异常 —— 页表缺失或权限错误现象程序执行到某条mov指令时触发Page FaultQEMU 日志打印EIP0x...GDB 中info registers显示CR2寄存器含故障线性地址。原因PA4 中mmap()未正确填充页表或fork()时未复制页表项。诊断步骤(gdb) p/x $cr2 # 获取故障虚拟地址 (gdb) p/x *(uint64_t*)0xffff800000000000 # 尝试读取内核页表根PML4验证是否已加载 (gdb) x/4gx 0xffff800000000000 # 若为 0说明 CR3 未更新或 PML4 未初始化关键检查点CR3寄存器值是否指向有效的 PML4 物理地址$cr2地址对应的 PML4/PDP/PD/PT 条目present位是否全为 1user_access位在用户态访问时是否为 1。6.3 故障类型三#DF异常 —— 栈溢出或中断嵌套过深现象递归调用如fibonacci(20)后系统重启QEMU 日志出现Double FaultGDB 中RSP值极小如0x1000。原因LAB2 中未限制递归深度或 PA1 中初始栈空间过小-0x1000分配不足。诊断步骤(gdb) p/x $rsp # 若低于 0x200000说明栈已撞到底部 (gdb) x/16gx $rsp # 查看栈底附近是否为 0确认是否被踩踏 (gdb) info stack # GDB 可能无法解析改用 x/100gx $rsp-0x100 观察栈增长方向解决方案在_start中显式分配足够栈空间sub $0x10000, %rsp递归函数添加深度计数器if (depth 100) return 0;启用CONFIG_DEBUG_STACK_USAGE内核选项若基于 Linux监控栈使用。提示南京大学 PA 实验的Makefile中CFLAGS常含-Wstack-protector但此警告仅提示潜在风险不阻止编译。真正防护需在运行时检测RSP是否接近栈底这正是 PA4mmap()实现中guard page机制的设计动机——在栈区末尾映射一个不可访问页使越界访问立即触发#PF而非静默破坏。本文还有配套的精品资源点击获取