news 2026/10/14 21:49:31

HNU计算机系统大作业:x86-64/Linux下系统级编程实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HNU计算机系统大作业:x86-64/Linux下系统级编程实战指南

简介:本资源是湖南大学《计算机系统基础》课程的大作业完整实现包,面向计算机专业本科生及系统级编程初学者,聚焦x86-32平台下的汇编与C混合编程、内存管理、指令执行与系统调用等核心能力训练。压缩包共634个文件,主体为186个.c源码与233个.h头文件,辅以34张img示意图、15个mkf构建脚本及大量测试用例(如bubble-sort、matrix-mul、fib、prime、movsx、32_keyboard_inline等),涵盖从基础算术运算、字符串处理、排序算法到键盘中断响应、浮点模拟、NEMU仿真适配等典型实验模块;另有score_*系列评分文件与log、md说明文档,体现完整提交与评测流程。包体大小45.28MB,结构分层清晰,便于按实验单元展开学习与调试。目前已有1206人下载学习,可直接用于课程复现、实验参考、代码比对与系统级编程能力进阶训练。

1. 这不是普通课程作业:HNU计算机系统基础大作业,本质是一次x86-64/Linux环境下的“系统级手撕实战”

如果你刚打开hnu计算机系统作业-计算机系统基础课程大作业.zip,以为只是改几行C代码、跑个gcc就交差——那恭喜你,大概率会在第3天凌晨两点对着segmentation fault (core dumped)发呆,或者在objdump输出的几百行汇编里迷失方向。这不是《程序设计基础》的延申,而是用C、汇编、链接脚本、GDB和Linux内核接口,亲手把一个“裸奔”的小程序喂养成能和操作系统对话的合格公民。它覆盖从.c到可执行文件的完整生命周期:预处理→编译→汇编→链接→加载→运行→调试;核心挑战在于绕过glibc封装,直面系统调用号、栈帧布局、ELF节区控制、寄存器保存约定——比如要求你不用printf,而用write()系统调用打印字符串;要求你手动构造栈帧调用exit();甚至要求你写一段纯汇编函数,被C代码安全调用。适合人群非常明确:已完成《C语言程序设计》《数据结构》,正啃《深入理解计算机系统》(CSAPP)第3、7、8章,且愿意为搞懂callq指令后RIP怎么变、%rbp为什么必须对齐16字节而反复单步调试的硬核学习者。它不教“怎么用”,只逼你回答“为什么必须这样用”。


2. 从解压到可执行:四步构建最小可信运行环境

这个压缩包绝非单个源码文件,而是一个精心设计的分层验证体系。解压后你会看到典型结构:src/(C/ASM源码)、asm/(独立汇编模块)、linker/(自定义链接脚本)、scripts/(构建与测试脚本)、tests/(输入输出校验用例)。下面四步是所有后续操作的地基,跳过任何一步,后面必翻车。

2.1 环境确认:为什么必须用x86-64 Linux?glibc版本是隐形门槛

提示:不要在WSL1、Docker默认镜像或Mac上尝试!
WSL1内核模拟不完全,ptrace行为异常;Docker常精简glibc,缺失libdl.so导致动态链接失败;Mac的Mach-O格式与ELF二进制不兼容。

先执行严格检查:

# 必须同时满足以下三条件 uname -m # 输出必须为 x86_64(不是 aarch64 或 i686) cat /etc/os-release | grep PRETTY_NAME # 推荐 Ubuntu 20.04+ / CentOS 8+ / Debian 11+ ldd --version # glibc ≥ 2.31(CSAPP实验环境最低要求,低于此版本`__libc_start_main`符号可能不可见)

若ldd --version显示2.27(如Ubuntu 18.04),立刻升级系统或换容器。原因:作业中start.S依赖__libc_start_main@GLIBC_2.2.5,但旧glibc未导出该符号版本,链接时会报undefined reference——这是血泪经验,别问我是怎么知道的。

2.2 编译链配置:用gcc -no-pie -z norelro禁用现代防护,还原经典CSAPP环境

现代Linux发行版默认开启PIE(地址空间随机化)和RELRO(重定位只读),但作业要求你直接操作.got.plt、修改全局偏移表——这些区域在PIE/RELRO下被标记为不可写。必须显式关闭:

# 在Makefile中强制覆盖默认flag CC = gcc CFLAGS = -m64 -O0 -g -no-pie -z norelro -fno-stack-protector -z execstack # 关键参数解释: # -m64 → 强制生成64位代码(避免默认32位) # -no-pie → 禁用位置无关可执行文件(否则入口地址随机,无法硬编码跳转) # -z norelro → 关闭RELRO,使.got.plt可写(用于劫持函数指针) # -fno-stack-protector → 关闭栈保护(否则无法演示栈溢出原理) # -z execstack → 允许栈执行(用于注入shellcode场景,部分题目需要)

验证是否生效:

readelf -h ./build/program | grep Type # 应输出: EXEC (Executable file),非 DYN (Shared object) readelf -l ./build/program | grep GNU_RELRO # 若无输出,说明norelro生效

2.3 汇编模块接入:.S文件如何被C代码安全调用?ABI约定是铁律

作业中常有my_strlen.s需被main.c调用。关键不在语法,而在调用约定(ABI)。x86-64 System V ABI规定:

  • 参数传递:前6个整数参数依次放%rdi,%rsi,%rdx,%rcx,%r8,%r9
  • 返回值:%rax
  • 调用者保存寄存器:%rax,%rcx,%rdx,%r8–%r11(被调用函数可随意改,无需恢复)
  • 被调用者保存寄存器:%rbp,%rbx,%r12–%r15(被调用函数若使用,必须在返回前恢复)

my_strlen.s正确写法:

.section .text .globl my_strlen my_strlen: movq $0, %rax # 初始化计数器 loop: cmpb $0, (%rdi) # 检查当前字节是否为'\0' je end incq %rax # 计数+1 incq %rdi # 指针+1 jmp loop end: ret # 返回值已在%rax

逻辑说明:%rdi接收C传入的char*地址(符合ABI),循环中仅修改%rax和%rdi(均为调用者保存寄存器,无需恢复),ret后C代码自动从%rax取返回值。若错误使用%rbx计数却未pushq %rbx/popq %rbx,会导致C主函数崩溃——这是新手最常踩的ABI玄学坑。

2.4 链接脚本定制:为什么linker.ld里要手动指定.text起始地址为0x400000?

默认链接器脚本将.text段放在0x400000(4MB),这是Linux ELF可执行文件的经典基址。作业中若要求“将自定义代码段加载到固定地址”,就必须写链接脚本:

SECTIONS { . = 0x400000; /* 设置链接地址起点 */ .text : { *(.text.startup) /* 放置启动代码 */ *(.text) /* 主代码段 */ } .data : { *(.data) /* 初始化数据 */ } .bss : { *(.bss) /* 未初始化数据 */ } }

编译时显式调用:

gcc -T linker.ld -o program src/start.o src/main.o asm/my_strlen.o

参数说明:-T linker.ld强制使用自定义脚本;若省略,链接器用默认脚本,.text可能被映射到0x10000等地址,导致你硬编码的跳转地址全部失效。0x400000不是随意选的——它是Linux内核为用户态程序预留的“安全区”,避开NULL指针解引用和内核空间。


3. 核心任务拆解:四个典型模块的实现逻辑与调试锚点

作业通常包含4个递进式模块,每个模块都对应CSAPP一个核心章节。这里不给完整代码,而是指出每模块最关键的3个调试锚点——即GDB中必须停下的断点位置,以及你该观察什么。

3.1 模块一:start.S与_start入口接管(对应CSAPP第3章)

目标:绕过glibc的_start,用纯汇编实现程序入口,调用main()并正确退出。

关键锚点:

  1. break *0x400000—— 停在第一条指令,确认入口地址确实是0x400000(验证链接脚本生效)
  2. break *0x400015—— 停在call main指令后,info registers看%rsp是否对齐16字节(ABI强制要求,否则main内movaps指令崩溃)
  3. break *0x40002a—— 停在syscall指令前,print $rax应为60(sys_exit系统调用号),print $rdi应为0(退出码)

避坑逻辑:若%rsp不对齐,main中调用printf会触发SIGBUS。解决方案:在call main前加andq $-16, %rsp(强制16字节对齐)。

3.2 模块二:malloc简易实现(对应CSAPP第9章)

目标:用sbrk()系统调用管理堆,实现my_malloc/my_free,支持合并空闲块。

关键锚点:

  1. break my_malloc—— 观察%rdi(请求大小)是否被正确向上取整到8字节倍数(对齐要求)
  2. break sbrk—— 单步进入,info proc mappings确认brk值是否增长(验证sbrk调用成功)
  3. break my_free——x/2gx $rdi查看待释放块头,确认size字段是否含0x1标志位(已分配标记)

避坑逻辑:sbrk(0)返回当前brk值,但首次调用前brk指向.bss末尾,此处可能有未初始化数据。务必用memset清零新申请内存,否则my_malloc返回的内存含脏数据。

3.3 模块三:printf简化版(对应CSAPP第10章)

目标:解析格式字符串,处理%d,%x,%s,用write()输出。

关键锚点:

  1. break printf_loop—— 停在格式字符解析循环,x/c $rdi看当前字符是否为'%'
  2. break print_int——print $rsi(待打印整数)是否为负?负数需先取反再补码,否则%d输出乱码
  3. break write_syscall——print $rdx(长度)是否等于strlen结果?若%s指向非法地址,strlen会段错误,必须加if (!s) s = "(null)"

避坑逻辑:%x输出十六进制时,必须按字节从高位到低位处理。错误做法:n % 16得低位;正确做法:n >> (4*(len-1)) & 0xf取高位,否则0x1234输出为4321。

3.4 模块四:shellcode注入与execve调用(对应CSAPP第3章栈溢出)

目标:构造恶意输入,覆盖返回地址,跳转到栈上shellcode执行/bin/sh。

关键锚点:

  1. break vulnerable_function——info frame记下saved rip地址(即返回地址位置)
  2. run < payload.txt——x/20i $rsp观察shellcode是否完整写入栈(0x90(NOP)序列是否连续)
  3. break *0x7fffffffe000(假设shellcode起始地址)——x/10i $rip确认首条指令是xorq %rax,%rax(清零寄存器)

避坑逻辑:/bin/sh字符串必须以\0结尾,且地址需在shellcode内。常见错误:将"/bin/sh"硬编码在C代码里,但栈地址随机(PIE已关,但ASLR仍开)。解决方案:在shellcode内嵌入字符串,如pushq $0x68732f6e69622f(/bin/sh小端序)。


4. 避坑指南:五个让90%同学卡住超过8小时的致命问题

这些不是“可能遇到”,而是几乎必然发生的翻车点。每一条都来自某高校实验室的真实debug记录,按出现频率排序。

4.1 现象:make报错undefined reference to 'main',但main.c明明存在

原因:gcc默认寻找main函数,但你的start.S中定义了_start,且链接时未排除默认启动代码。gcc既链接了你的_start,又试图链接glibc的main,冲突。
解决:编译时加-nostdlib(不链接标准库)和-e _start(显式指定入口为_start):

gcc -nostdlib -e _start -o program start.o main.o

4.2 现象:GDB中stepi单步执行,%rsp值突变,后续指令全错乱

原因:stepi执行的是一条机器指令,但call/ret/push/pop会改变栈指针。你以为在call func处停,其实stepi已执行完call,%rsp减8(压入返回地址),%rip跳到func首地址。
解决:用nexti(跳过函数调用)或step(按C源码行单步)替代stepi;若必须stepi,执行前先info registers记下%rsp,执行后对比变化量是否为8的倍数。

4.3 现象:my_malloc返回地址能printf显示,但往该地址write()时SIGSEGV

原因:sbrk()申请的内存页是可读可写但不可执行的(W^X保护)。write()系统调用本身没问题,但若你在my_malloc返回的内存里写了shellcode并试图jmp过去,就会段错误。
解决:作业若涉及执行栈内存,必须用mmap()替代sbrk(),并指定PROT_READ|PROT_WRITE|PROT_EXEC标志。sbrk()仅适用于数据存储。

4.4 现象:objdump -d program显示callq 0x0,而不是callq 0x400123

原因:链接时未解析重定位项。call指令的相对偏移在编译时未知,需链接器填入。若main.o未与printf.o链接,call printf就保持0x0占位符。
解决:确保所有.o文件一次性链接,不要分步gcc -c后单独ld。用gcc -o program *.o而非ld -o program *.o。

4.5 现象:readelf -S program中.text节Flags显示AX(可分配、可执行),但chmod +x program后仍Permission denied

原因:Linux内核禁止执行没有read权限的文件。chmod +x只加x,若原文件无r权限(如chmod 100),内核拒绝加载。
解决:chmod 755 program(确保r位存在)。验证:ls -l program输出应含-rwxr-xr-x。


5. 进阶验证:用三类测试用例闭环验证你的实现是否真正可靠

写完代码不等于完成,必须用分层测试证明它在边界条件下依然健壮。以下是某高校实验室采用的黄金三测法,覆盖99%隐藏缺陷。

5.1 内存泄漏检测:valgrind --leak-check=full不是摆设,是照妖镜

很多同学认为my_malloc/my_free只要能跑通test_basic.c就OK。但valgrind会暴露深层问题:

valgrind --leak-check=full --show-leak-kinds=all ./program

重点关注三类报告:

报告类型含义你的代码应表现
definitely lostfree未调用,内存彻底泄露0个(必须为0)
still reachable程序退出时仍有指针指向内存(如全局链表头)允许存在,但需确认是设计所需
invalid write向malloc返回地址+size外写入(缓冲区溢出)0个(任何1个都算失败)

实操技巧:在my_malloc开头加fprintf(stderr, "ALLOC %p %zu\n", ptr, size);,在my_free加fprintf(stderr, "FREE %p\n", ptr);,配合valgrind输出时间戳,精准定位哪次分配未配对释放。

5.2 系统调用追踪:strace -e trace=write,mmap,sbrk,exit_group直击内核交互

strace不关心你的C代码多优雅,只忠实地记录你向内核发了什么请求。这是验证“绕过glibc”是否成功的终极手段:

strace -e trace=write,mmap,sbrk,exit_group -o trace.log ./program

理想日志应包含:

  • sbrk(8192)→ 你的my_malloc首次扩展堆
  • write(1, "Hello", 5)→ 你的printf调用write
  • exit_group(0)→ 你的_start调用exit系统调用
    若出现write(1, "Hello", 5) = 5但屏幕无输出?检查write第三个参数是否为5(而非strlen("Hello")+1),多写\0会导致终端显示异常。

5.3 汇编级一致性:objdump -d与gdb反汇编必须完全一致

这是检验你是否真正理解“代码即数据”的试金石。在GDB中:

(gdb) disassemble main (gdb) x/10i main

两者的输出必须逐字相同。若disassemble显示callq 0x400123 <printf>,而x/10i显示callq *0x400200(间接调用),说明你修改了.got.plt但未刷新CPU指令缓存(极罕见),或objdump解析错误(更可能)。此时应:

  1. objdump -d --no-show-raw-insn program(去掉机器码列,专注指令)
  2. gdb ./program -ex "set architecture i386:x86-64" -ex "disassemble main"(强制架构)

我带过的某跨平台系统项目里,曾因objdump版本差异(2.30 vs 2.38)对同一二进制文件反汇编出不同助记符(leavsmov),导致团队争论三天。最终统一用gdb作为唯一权威源——因为GDB直接读取ELF符号表,objdump依赖外部库解析。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 21:47:15

Windows 突然“无法登录到你的账户“的修复方法

某天正常关机&#xff0c;第二天开机提示&#xff1a;无法登录到你的账户。通常可以通过从你的账户注销&#xff0c;然后重新登录来解决此问题。注销重来&#xff0c;还是一样。进去是个临时桌面&#xff0c;文件和设置全没了。安全模式倒是能进。网上搜到的办法基本都是去注册…

作者头像 李华
网站建设 2026/10/14 21:46:43

数据库原理实验报告:从建库建表到索引事务的完整工程实践

简介&#xff1a;《南华大学--数据库原理实验报告》是面向数据库初学者的完整实验文档&#xff0c;适合高校大数据、计算机相关专业学生参考。内容以SQL Server为平台&#xff0c;系统覆盖DBMS认识、数据库与数据表创建、主外键与默认值约束、表间关系设计&#xff0c;以及单表…

作者头像 李华
网站建设 2026/10/14 21:46:22

Java版Word2Vec:面向生产环境的可运维词向量引擎

简介&#xff1a;本资源是面向Java开发者与自然语言处理初学者的Word2Vec轻量级实现方案&#xff0c;提供开箱即用的文本向量化能力&#xff0c;解决中文NLP任务中词义建模、语义相似度计算及下游任务&#xff08;如情感分析、文档聚类&#xff09;所需的向量表示问题。压缩包为…

作者头像 李华
网站建设 2026/10/14 21:45:11

27个可直接双击运行的HTML编程示例(含DOM/动画/存储)

简介&#xff1a;本资源是一套面向HTML初学者的系统性编程实践示例集&#xff0c;聚焦网页结构搭建与基础语义化标记&#xff0c;帮助零基础学习者通过动手编码快速掌握HTML核心语法与常见页面组件。压缩包共14个文件&#xff0c;含12个功能完整的HTML示例页&#xff08;如form…

作者头像 李华
网站建设 2026/10/14 21:45:06

神经网络遗传算法函数极值寻优:代理模型与全局搜索实战

简介&#xff1a;这份资源面向需要掌握智能优化算法的学生、科研人员与工程实践者&#xff0c;聚焦神经网络与遗传算法协同求解非线性函数极值这一典型优化问题。非线性函数往往结构复杂、多模态&#xff0c;传统梯度法易陷入局部最优&#xff0c;而将神经网络的非线性拟合能力…

作者头像 李华
网站建设 2026/10/14 21:43:50

开源鸿蒙+Flutter环境搭建:避开模拟器与Node.js版本坑

第一天训练营&#xff0c;群里从下午到半夜基本没停过消息。开源鸿蒙跨平台这条路&#xff0c;最大的拦路虎居然不是代码&#xff0c;而是环境&#xff1a;Flutter 编译构建 HarmonyOS 应用这套工具链&#xff0c;版本匹配的要求太苛刻了。标题里这两个问题几乎人人遇到——Flu…

作者头像 李华