简介:本资源是天津大学计算机系统基础课程配套的完整上机实验资料包,面向计算机专业本科生及系统底层学习者,聚焦位运算、数值表示、安全攻防与缓存优化等核心能力训练。压缩包共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_main;printf输出乱码,查半天发现是栈帧被自己手写的汇编踩坏了;或者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 Mac,QEMU无法启用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-list=i386-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-list=i386-softmmu:强制编译出纯软件模拟的i386版QEMU,绕过KVM兼容性问题;set architecture i386:告诉GDB按32位x86指令解码,否则disassemble会把mov eax, [ebx]错解为64位指令;-S -s:QEMU启动后暂停并监听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保存原%esp+4,替代帧指针参数说明:-m32:强制生成32位指令,否则在64位系统上默认生成movq等64位指令,导致后续nasm汇编失败;leal 4(%esp), %ecx:leal(Load Effective Address)计算地址而非取值,此处将%esp+4存入%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 *main+10 # 在call指令前断住 (gdb) info registers esp eip esp 0xffffd5a0 0xffffd5a0 eip 0x80491b6 0x80491b6 <main+10> (gdb) set $esp = $esp - 4 # 预留4字节存返回地址 (gdb) set {int}$esp = 0x80491bc # 将call后地址(main+16)写入栈顶 (gdb) set $eip = 0x80491a0 # 强制跳转到func入口 (gdb) stepi # 单步执行func # 此时func内ret会弹出0x80491bc,正确返回到main+16血泪经验:set {int}$esp = ...中的{int}表示以4字节整数方式写入,若写成{char}会只写1字节,导致栈数据错位。这是实验3最易翻车的点。4.2 GOT表原理与实验4的“重定位炸弹”实验4要求修改.got.plt节内容,让printf调用跳转到自定义函数。这涉及两个关键概念:PLT(Procedure Linkage Table):.plt节中每个外部函数有桩代码,首次调用时通过jmp *got.plt跳转;GOT(Global 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,大小0x14=20字节,共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 align=1,并确保所有数据定义在.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_clone=0),或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 trace=brk,mmap,mprotect ./xxx观察内存操作:实验5的shellcode需要mprotect改写内存权限,若strace没看到mprotect调用,说明你的系统调用封装有误。从那以后我每次写完.s文件,都强制走一遍这三步——不是为了炫技,是怕凌晨三点对着黑屏QEMU发呆时,才发现是mov写成了move这种低级错误。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源是天津大学计算机系统基础课程配套的完整上机实验资料包,面向计算机专业本科生及系统底层学习者,聚焦位运算、数值表示、安全攻防与缓存优化等核心能力训练。压缩包共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_main;printf输出乱码,查半天发现是栈帧被自己手写的汇编踩坏了;或者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 Mac,QEMU无法启用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-list=i386-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-list=i386-softmmu:强制编译出纯软件模拟的i386版QEMU,绕过KVM兼容性问题;set architecture i386:告诉GDB按32位x86指令解码,否则disassemble会把mov eax, [ebx]错解为64位指令;-S -s:QEMU启动后暂停并监听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保存原%esp+4,替代帧指针参数说明:-m32:强制生成32位指令,否则在64位系统上默认生成movq等64位指令,导致后续nasm汇编失败;leal 4(%esp), %ecx:leal(Load Effective Address)计算地址而非取值,此处将%esp+4存入%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 *main+10 # 在call指令前断住 (gdb) info registers esp eip esp 0xffffd5a0 0xffffd5a0 eip 0x80491b6 0x80491b6 <main+10> (gdb) set $esp = $esp - 4 # 预留4字节存返回地址 (gdb) set {int}$esp = 0x80491bc # 将call后地址(main+16)写入栈顶 (gdb) set $eip = 0x80491a0 # 强制跳转到func入口 (gdb) stepi # 单步执行func # 此时func内ret会弹出0x80491bc,正确返回到main+16血泪经验:set {int}$esp = ...中的{int}表示以4字节整数方式写入,若写成{char}会只写1字节,导致栈数据错位。这是实验3最易翻车的点。4.2 GOT表原理与实验4的“重定位炸弹”实验4要求修改.got.plt节内容,让printf调用跳转到自定义函数。这涉及两个关键概念:PLT(Procedure Linkage Table):.plt节中每个外部函数有桩代码,首次调用时通过jmp *got.plt跳转;GOT(Global 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,大小0x14=20字节,共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 align=1,并确保所有数据定义在.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_clone=0),或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 trace=brk,mmap,mprotect ./xxx观察内存操作:实验5的shellcode需要mprotect改写内存权限,若strace没看到mprotect调用,说明你的系统调用封装有误。从那以后我每次写完.s文件,都强制走一遍这三步——不是为了炫技,是怕凌晨三点对着黑屏QEMU发呆时,才发现是mov写成了move这种低级错误。希望帮到你。本文还有配套的精品资源,点击获取