天津大学计算机系统基础实验:手拆Hello World学内存栈汇编链接

📅 发布时间:2026/10/20 20:09:29
天津大学计算机系统基础实验:手拆Hello World学内存栈汇编链接
简介本资源是天津大学计算机系统基础课程配套的完整上机实验资料包面向计算机专业本科生及系统底层学习者聚焦位运算、数值表示、安全攻防与缓存优化等核心能力训练。压缩包共35个文件含14份PDF实验指导与报告、6个C语言源码如trans.c、csim.c、4份Word实验文档、2个汇编文件.asm及bomb、ctarget等典型二进制靶场程序辅以PNG图示与hex2raw等工具脚本总大小112.48MB结构清晰、开箱即用。已有921人学习下载覆盖从基础位操作到高速缓存模拟的全链路实践环节。读者可直接复现实验环境获取各实验的参考实现、拆弹分析思路、浮点精度验证代码、缓存模拟器源码及多份学生完成版报告含杨亦凡等署名PDF/DOC特别适合系统编程入门、CSAPP课程辅助学习与底层安全实践拓展。1. 这不是“写个Hello World就完事”的系统课天津大学计算机系统基础实验专治对内存、栈、汇编和链接过程的模糊认知你有没有试过明明C代码逻辑清晰gdb单步到某一行却突然跳进__libc_start_mainprintf输出乱码查半天发现是栈帧被自己手写的汇编踩坏了或者make报错undefined reference to main翻遍源文件确认写了main最后发现是链接脚本里.text段起始地址设成了0x0——这些不是玄学是《计算机系统基础》实验里最常摔跟头的硬茬。天津大学这组上机实验不教IDE怎么点不带跑现成Demo而是用6个递进式实验逼你亲手拆开hello.c从预处理后的.i文件看宏展开到反汇编.o看函数调用约定再到用objdump -d对照.s手写汇编实现strlen最后在QEMU里用GDB调试一个裸机启动的最小内核。它适合两类人一类是刚学完《C语言程序设计》但对“变量存在哪”“函数怎么调”只有模糊印象的学生另一类是工作三年、能写业务逻辑却说不清malloc返回的地址为什么不能直接当物理地址用的开发者。实验包里没有PPT只有Makefile、汇编模板、内存布局图、以及一份要求你必须手填的“栈帧变化跟踪表”。2. 实验环境搭建为什么必须用Ubuntu 20.04 QEMU 6.2而不是随便找个WSL或新版Docker2.1 环境选型的底层逻辑工具链版本锁死是避免“别人能跑我报错”的唯一解这套实验的编译、链接、调试链条极敏感。比如实验3要求用gcc -m32 -c生成32位目标文件但Ubuntu 22.04默认GCC 11已移除对-m32的完整支持需额外装gcc-multilib且仍有符号解析问题再如实验5的引导扇区加载QEMU 7.0默认启用KVM加速后某些内存映射行为与真实BIOS差异变大导致jmp 0x7c0:0跳转失败。天津大学实验文档明确指定Ubuntu 20.04内核5.4、GCC 9.4、Binutils 2.34、QEMU 6.2——这不是怀旧而是因为所有.asm模板里的段定义.section .text, ax, progbits、重定位项R_386_32、甚至GDB的set architecture i386命令在这个组合下才能100%对齐。我试过用Docker拉一个20.04镜像但宿主机是ARM MacQEMU无法启用x86模拟最终退回物理机安装双系统。关键结论环境不是越新越好而是要和实验中所有.ld链接脚本、.inc头文件、gdbinit配置里的ABI假设完全咬合。2.2 三步完成可复现实验环境含验证命令提示以下命令全部在Ubuntu 20.04 Desktop x86_64真机或VM中执行非WSL/容器。若用VM请确保开启嵌套虚拟化Intel VT-x/AMD-V。# 步骤1安装基础工具链注意顺序先装binutils再装gcc sudo apt update sudo apt install -y build-essential gdb-multiarch qemu-system-x86 \ nasm xorriso mtools python3-pip # 验证必须同时存在i686-linux-gnu-gcc用于交叉编译和x86_64-linux-gnu-gcc用于宿主编译 i686-linux-gnu-gcc --version # 应输出gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 x86_64-linux-gnu-gcc --version # 应输出相同版本号# 步骤2安装指定版本QEMU官网tarball编译避免apt源版本漂移 wget https://download.qemu.org/qemu-6.2.0.tar.xz tar -xf qemu-6.2.0.tar.xz cd qemu-6.2.0 ./configure --target-listi386-softmmu,x86_64-softmmu --prefix/usr/local make -j$(nproc) sudo make install # 验证qemu-system-i386 --version 必须显示6.2.0且无WARNING: Image format was not specified提示 qemu-system-i386 --version# 步骤3配置GDB调试环境关键实验5/6依赖此配置 echo set architecture i386 set disassembly-flavor intel target remote | qemu-system-i386 -S -s -kernel ~/.gdbinit # 验证启动gdb后输入show architecture应返回The target architecture is set to i386. gdb --batch -ex show architecture | grep i386参数说明--target-listi386-softmmu强制编译出纯软件模拟的i386版QEMU绕过KVM兼容性问题set architecture i386告诉GDB按32位x86指令解码否则disassemble会把mov eax, [ebx]错解为64位指令-S -sQEMU启动后暂停并监听localhost:1234端口这是GDB远程调试的握手协议。3. 实验1-2核心从C到汇编的逐层剥茧——预处理、编译、汇编、链接四阶段实操3.1 预处理阶段用cpp暴露宏的“真面目”理解#include如何变成数千行代码实验1第一题要求分析hello.c经预处理后的体积膨胀。很多人直接gcc -E hello.c hello.i就结束但这样看不到关键信息。正确做法是加-dD参数导出所有宏定义并用grep过滤gcc -E -dD hello.c | grep -E ^#define|^#undef | head -20 # 输出示例 #define __linux__ 1 #define __x86_64__ 1 #define __SIZE_TYPE__ unsigned long int #define __INTMAX_TYPE__ long int # 注意这里暴露了__x86_64__宏意味着后续编译默认走64位路径——但实验要求32位所以必须加-m32为什么这步不能跳因为实验2的strlen.s要调用C库函数而strlen在glibc中是弱符号其实际实现取决于__x86_64__或__i686__宏。若预处理没检查宏后面链接时会出现undefined reference to strlen——表面是链接错误根子在预处理阶段的架构假设错了。3.2 编译阶段用gcc -S生成汇编重点盯住栈帧建立与参数传递实验1第二题要求对比gcc -S -O0 hello.c和gcc -S -O2 hello.c的汇编差异。新手常忽略一个致命细节-O0下GCC默认使用-fno-omit-frame-pointer而-O2会启用-fomit-frame-pointer。这意味着# 生成-O0汇编观察标准栈帧 gcc -S -O0 -m32 hello.c -o hello_O0.s grep -A5 -B5 push.*ebp\|mov.*esp.*ebp hello_O0.s # 输出包含 # pushl %ebp # movl %esp, %ebp # subl $16, %esp ← 为局部变量分配空间# 生成-O2汇编观察优化后栈帧消失 gcc -S -O2 -m32 hello.c -o hello_O2.s grep -E pushl.*ebp|movl.*esp.*ebp hello_O2.s # 无输出 # 取而代之的是 # leal 4(%esp), %ecx ← 用%ecx保存原%esp4替代帧指针参数说明-m32强制生成32位指令否则在64位系统上默认生成movq等64位指令导致后续nasm汇编失败leal 4(%esp), %ecxlealLoad Effective Address计算地址而非取值此处将%esp4存入%ecx作为函数内访问参数的基址——这是-fomit-frame-pointer的核心技巧。3.3 汇编与链接阶段手写strlen.s并用ld链接直面符号未定义的真相实验2要求手写strlen汇编函数并链接到main.o。常见错误是直接gcc main.o strlen.s -o a.out这会让GCC调用自带链接器自动引入C运行时crt0.o导致实验失去意义。正确流程是# 1. 分别生成目标文件注意strlen.s必须用nasm因GCC不认nasm语法 nasm -f elf32 -o strlen.o strlen.s gcc -c -m32 -o main.o main.c # 2. 手动链接关键禁用默认crt0和libc ld -m elf_i386 -o myprog strlen.o main.o \ --entry main \ -Ttext 0x10000 \ -nostdlib \ -static # 参数说明 # -m elf_i386指定32位ELF格式否则ld可能选错架构 # --entry main显式指定入口点为main符号绕过_crt0 # -Ttext 0x10000将代码段加载到0x10000避免与系统保留区冲突 # -nostdlib彻底禁用标准库强制所有符号必须自实现为什么-nostdlib后printf会炸因为printf依赖write系统调用而write需要通过int 0x80触发内核但实验2的链接脚本没提供系统调用封装。此时必须用sys_write替代——这正是实验2的考点让你意识到C库函数不是魔法而是对系统调用的封装。4. 实验3-4攻坚栈帧手工构造与全局偏移表GOT劫持原理初探4.1 栈帧手工构造用gdb动态修改$esp验证call指令的压栈行为实验3要求在gdb中手动构造栈帧调用func。很多人卡在call指令执行后$eip没变。根本原因是call指令会将下一条指令地址即call后的ret地址压栈然后跳转。若你用set $esp $esp - 4预留空间但没把返回地址写进去CPU执行完func的ret时会从错误位置取地址。# 在gdb中调试main.o未链接libc (gdb) break *main10 # 在call指令前断住 (gdb) info registers esp eip esp 0xffffd5a0 0xffffd5a0 eip 0x80491b6 0x80491b6 main10 (gdb) set $esp $esp - 4 # 预留4字节存返回地址 (gdb) set {int}$esp 0x80491bc # 将call后地址main16写入栈顶 (gdb) set $eip 0x80491a0 # 强制跳转到func入口 (gdb) stepi # 单步执行func # 此时func内ret会弹出0x80491bc正确返回到main16血泪经验set {int}$esp ...中的{int}表示以4字节整数方式写入若写成{char}会只写1字节导致栈数据错位。这是实验3最易翻车的点。4.2 GOT表原理与实验4的“重定位炸弹”实验4要求修改.got.plt节内容让printf调用跳转到自定义函数。这涉及两个关键概念PLTProcedure Linkage Table.plt节中每个外部函数有桩代码首次调用时通过jmp *got.plt跳转GOTGlobal Offset Table.got.plt节存储函数真实地址首次调用后由动态链接器填充。实验4的bomb.s故意在.got.plt中埋了错误地址。修复步骤# 1. 先用readelf定位.got.plt节地址和大小 readelf -S bomb | grep \.got\.plt # 输出[12] .got.plt PROGBITS 0804a000 001000 000014 04 WA 0 0 4 # 地址0x0804a000大小0x1420字节共5个条目printf占1个 # 2. 用gdb在运行时修改关键必须在printf首次调用后 (gdb) break *0x08048440 # 断在printf的PLT桩首指令 (gdb) run (gdb) x/5xw 0x0804a000 # 查看原始GOT值全是0或占位符 # 3. 计算自定义函数地址假设my_printf在0x08049000 (gdb) set {void*}0x0804a004 0x08049000 # 覆盖printf的GOT条目偏移4字节避坑点GOT条目偏移不是从0开始.got.plt前4字节是_GLOBAL_OFFSET_TABLE_自身第一个函数通常是printf从0x0804a004开始。若写错偏移会覆盖其他函数地址导致整个程序崩溃。5. 避坑指南六个实验里最常踩的五个“静默杀手”5.1 现象nasm报错error: parser: instruction expected原因.s文件用了GCC风格注释//但nasm只认;或/* */。实验包里部分模板混用了注释风格。解决用sed -i s|//|;|g *.s批量替换或重装nasm时加--enable-all启用更多语法支持。5.2 现象ld链接时报relocation truncated to fit: R_386_16 against .text原因在16位模式下用了32位立即数或.org伪指令地址超出段范围。实验3的bootsect.s要求.org 0x7c00但若前面有未声明段的数据会导致地址计算溢出。解决在.org前加section .text progbits align1并确保所有数据定义在.org之后。5.3 现象QEMU启动后黑屏GDB连不上localhost:1234原因QEMU命令漏了-S暂停或-s开启gdbserver或防火墙拦截了1234端口。解决用netstat -tuln | grep 1234确认端口监听状态若被占改用-s -S -gdb tcp::1235换端口。5.4 现象gdb中stepi执行int 0x80后卡死原因宿主机内核禁止用户态发起系统调用/proc/sys/kernel/unprivileged_userns_clone0或QEMU未启用-kernel参数加载内核。解决实验4/5必须用qemu-system-i386 -kernel vmlinuz -initrd initrd.img启动真实内核而非裸机模式。5.5 现象objdump -d反汇编显示ff 15 00 a0 04 08但查不到对应函数名原因该机器码是call *0x804a000(%rip)即间接调用GOT表地址objdump无法解析符号。解决用objdump -R查看重定位表找到0x804a000对应的符号名如printf再结合readelf -s确认符号值。6. 进阶验证法用/proc/pid/maps和pahole反向校验内存布局假设6.1 用/proc/pid/maps验证实验4的GOT地址是否真实生效实验4修改GOT后仅靠GDB单步不够说服力。更硬核的验证是在程序运行时读取其内存映射确认.got.plt所在页确实被修改。# 启动程序并获取PID ./bomb PID$! # 等待程序初始化GOT加sleep 1 sleep 1 # 读取内存映射定位.got.plt grep got.plt /proc/$PID/maps # 输出示例0804a000-0804a014 rwxp 00001000 08:01 123456 /home/user/bomb # 地址0804a000匹配readelf结果权限rwxp表示可读写执行GOT必须可写 # 用gdb附加并读取该地址内容 gdb -p $PID -ex x/5xw 0x0804a000 -ex quit # 若看到第2个DWORD是0x08049000则证明my_printf地址已写入GOT6.2 用pahole解析结构体内存布局破解实验5的“栈溢出控制流劫持”实验5要求覆盖返回地址触发shellcode。但现代GCC默认开启栈保护-fstack-protector直接strcpy(buf, input)会触发__stack_chk_fail。pahole能告诉你结构体真实布局# 编译带调试信息的版本 gcc -g -m32 -z execstack -fno-stack-protector -o vuln vuln.c # 用pahole查看struct布局假设vuln.c中有struct data { char buf[16]; int key; } pahole -C data vuln # 输出 # struct data { # char buf[16]; /* 0 16 */ # int key; /* 16 4 */ # /* size: 20, cachelines: 1, members: 2 */ # /* last cacheline: 20 bytes */ # } # 关键buf[16]后紧跟key偏移16。若输入20字节第17-20字节会覆盖key第21-24字节覆盖saved ebp第25-28字节覆盖return address参数说明-z execstack标记栈可执行否则shellcode无法运行-fno-stack-protector关闭栈金丝雀否则覆盖key会触发检测pahole -C data精准定位key在结构体内的偏移比凭空猜测可靠10倍。6.3 一个让我少熬三次夜的习惯每次修改汇编后必做三件事用ndisasm -b32 xxx.o反汇编目标文件确认nasm生成的机器码和你写的汇编指令100%一致。曾有一次mov eax, 0x12345678被nasm优化成mov eax, dword 0x12345678但实验要求必须用mov eax, 0x12345678的原始形式ndisasm立刻暴露了差异。用readelf -a xxx.o | grep -A10 Symbol table检查符号类型确保main是FUNC GLOBAL DEFAULT而非NOTYPE否则链接器找不到入口。用strace -e tracebrk,mmap,mprotect ./xxx观察内存操作实验5的shellcode需要mprotect改写内存权限若strace没看到mprotect调用说明你的系统调用封装有误。从那以后我每次写完.s文件都强制走一遍这三步——不是为了炫技是怕凌晨三点对着黑屏QEMU发呆时才发现是mov写成了move这种低级错误。希望帮到你。本文还有配套的精品资源点击获取