资讯动态

天津大学计算机系统基础实验:从汇编到系统调用的实操穿透

发布时间:2026/10/20 2:29:54 来源:尧图企业网站定制
简介本资源是天津大学《计算机系统基础》课程配套的完整上机实验资料包面向计算机专业本科生及系统编程初学者聚焦底层原理实践与安全编程能力培养。内容覆盖位操作、整数与浮点数表示、缓冲区溢出分析拆弹专家、代码注入攻击防御、高速缓存优化五大核心实验模块直击计算机组成原理与系统安全的关键难点。压缩包共35个文件含14份PDF实验指导与报告、6个C语言源码含bomb、ctarget等可执行靶场程序、4份Word实验文档、2个汇编文件及PNG图示等类型丰富且高度对应实验环节整体大小为112.48MB结构清晰便于分模块学习与调试复现。已有921人下载学习提供从实验环境搭建、漏洞触发分析到缓存性能调优的全流程支撑材料特别适合夯实系统级编程基础、备战相关课程考核或CTF底层挑战。1. 天津大学计算机系统基础实验不是背概念的“汇编课”而是亲手拆解程序执行黑匣子的实操入口你是不是也经历过——学完《计算机系统基础》后能默写CSAPP里关于缓存行对齐、栈帧布局、ELF节区结构的定义但一打开gdb单步进main函数看到$rbp突然跳变、$rsp在call前被压入一个陌生地址、.plt节里跳转指令后面跟着一堆看不懂的符号瞬间怀疑自己学的是假书天津大学计算机系统基础实验就是专治这种“理论懂、动手懵”的硬核训练场。它不讲PPT里的抽象模型而是用一套完整闭环的上机任务链从手写x86-64汇编实现字符串比较到反汇编分析gcc -O2生成的优化代码从用objdump定位全局变量在.data节的偏移到用gdb watch点追踪malloc分配内存时brk值的变化再到最后用strace抓取open/read/write系统调用的参数与返回值。这不是验证课本结论的演示实验而是让你站在程序加载、链接、执行、终止的每一道关卡上亲手拧开螺丝、看清齿轮咬合。适合所有已修完C语言、了解基本Linux命令、但还没真正“看穿”一个可执行文件如何活起来的开发者——尤其适合那些准备啃操作系统内核、做性能调优或逆向分析的新手把第一块砖夯实在真实二进制世界里。2. 实验环境搭建与工具链验证用最小依赖集跑通第一个汇编Hello World天津大学这套实验对运行环境有明确约束必须基于x86-64 Linux推荐Ubuntu 20.04/22.04或CentOS 7禁用Wine、虚拟机嵌套、WSL1等非原生执行层。核心工具链不是“装了就行”而是要验证其行为是否符合实验设计预期——比如gcc默认启用栈保护而实验要求观察原始栈帧就必须关闭又如gdb默认不加载符号表而实验第3题要求用info registers比对寄存器状态就必须确保调试信息完整嵌入。下面这组命令不是安装清单而是有效性验证脚本每一步失败都意味着后续实验必然翻车。2.1 基础工具链安装与版本锚定# 严格按实验指导书要求安装禁止用conda/pip替代系统包管理器 sudo apt update sudo apt install -y build-essential gdb strace objdump binutils-dev # 验证关键工具版本实验文档隐含要求gcc 11.4因涉及__builtin_ia32_rdrand32_step内联汇编 gcc --version | head -n1 # 必须输出 gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 或更高 gdb --version | head -n1 # 必须输出 GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1 或更高提示若gcc --version显示低于11.4请勿用apt install gcc-12强行升级——实验中部分汇编模板如movq %rax, (%rdi)在gcc 12下会触发新警告导致make失败。正确做法是添加Ubuntu Toolchain PPA并指定安装sudo apt install gcc-11 g-11再用sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100切换。2.2 汇编Hello World验证NASM与LD工作流实验第1题要求用NASM语法编写hello.asm而非GCC内联汇编。这步失败率极高常见于NASM版本不兼容或链接脚本缺失。; hello.asm —— 天津大学实验标准模板注意必须用LF换行禁用CRLF section .data msg db Hello, Tianjin University!, 0 len equ $ - msg section .text global _start _start: ; sys_write(stdout, msg, len) mov rax, 1 ; sys_write syscall number mov rdi, 1 ; stdout file descriptor mov rsi, msg ; message address mov rdx, len ; message length syscall ; sys_exit(0) mov rax, 60 ; sys_exit syscall number mov rdi, 0 ; exit status syscall# 关键必须用-nostdlib且显式指定入口点否则ld会尝试链接C runtime nasm -f elf64 hello.asm -o hello.o ld -o hello hello.o -e _start -nostdlib # 验证无段错误、输出正确、且size可控实验要求可执行文件2KB ./hello # 应输出Hello, Tianjin University! ls -l hello # 必须显示 size 2048 bytes readelf -h hello | grep Class\|Data\|Machine # 必须为 ELF64, LSB, x86-64逻辑说明-e _start强制入口为汇编中定义的_start标签绕过C库的__libc_start_main-nostdlib禁用标准库链接避免引入无关符号readelf -h验证ELF头结构是后续所有实验如分析.rodata节位置、.bss节对齐的前提。若ls -l hello显示2KB大概率是误用了gcc hello.asm -o hello——这会自动链接libc并插入大量初始化代码直接导致实验第2题的“手动构造栈帧”无法观测。2.3 GDB调试环境预检确保能单步执行并查看寄存器变化实验第4题要求用stepi单步执行汇编指令并用info registers比对%rax、%rdx等寄存器值变化。若GDB无法停在第一条指令说明调试信息未嵌入或符号表损坏。# 重新编译带调试信息的版本实验允许用gcc生成但必须加-g gcc -g -no-pie -z norelro -o hello_gcc hello.c # hello.c内容同上但用C写 # 启动gdb并验证断点可达性 gdb ./hello_gcc (gdb) break _start (gdb) run # 此时必须停在_start第一行且(gdb) info registers中%rip应指向_start地址 (gdb) stepi # 执行一条指令 (gdb) info registers rax rdx # 查看%rax和%rdx是否按预期更新参数说明-no-pie禁用地址空间布局随机化ASLR确保每次加载地址固定方便实验记录-z norelro禁用RELRO保护避免.got.plt节只读导致gdb无法注入断点break _start而非break main因为实验要求从程序入口开始跟踪而非C库封装后的main。若run后提示Function _start not defined说明gcc未导出_start符号——此时需改用gcc -g -nostdlib -e _start hello.c -o hello_gcc重编译。3. 核心实验任务拆解从汇编实现到系统调用追踪的四层穿透天津大学计算机系统基础实验不是线性任务列表而是构建了一个“由外向内”的穿透式能力模型第1-2题聚焦指令级控制流你写的汇编如何变成CPU执行的脉冲第3-4题深入内存布局与数据组织变量在哪、怎么对齐、栈怎么长第5题跃迁至系统接口层程序如何向内核要资源第6题则直击链接与加载机制为什么printf能跨文件调用。下面按实验手册实际编号展开每项均给出可复现的代码、关键参数解释及验证方法。3.1 字符串比较汇编实现理解条件码与跳转指令的物理意义实验第1题要求用x86-64汇编实现strcmp功能返回值规则与C标准一致相等返回0s1s2返回正数s1s2返回负数。难点不在逻辑而在如何让CPU的ZF/SF/OF标志位真实反映字节比较结果。; strcmp.asm —— 天津大学实验标准实现关键用cmpb而非cmpq section .text global strcmp strcmp: push rbp mov rbp, rsp ; 参数rdis1, rsis2 mov rcx, 0 ; i 0 .loop: mov al, [rdi rcx] ; load byte from s1 mov bl, [rsi rcx] ; load byte from s2 cmp al, bl ; compare bytes jne .diff ; if not equal, jump to diff test al, al ; check if s1[i] \0 je .equal ; if yes, strings equal inc rcx jmp .loop .diff: sub al, bl ; return s1[i] - s2[i] mov rax, rax ; zero-extend al to rax pop rbp ret .equal: mov rax, 0 pop rbp ret验证方法nasm -f elf64 strcmp.asm -o strcmp.o gcc -c test_strcmp.c -o test_strcmp.o # test_strcmp.c含main调用strcmp gcc -o test_strcmp test_strcmp.o strcmp.o ./test_strcmp # 应输出0abcabc、1abdabc、-1ababc关键参数说明cmp al, bl必须用8位寄存器al/bl若误写为cmp rax, rbx则高位垃圾数据污染ZF标志test al, al用于检测空字符比cmp al, 0更高效不修改AF标志sub al, bl后mov rax, rax是玄学操作——实际是mov rax, raxx86-64 NOP但实验要求返回值为64位整数需将8位差值零扩展到rax此处用movzx rax, al更规范但实验模板接受此写法。若测试返回值异常用gdb ./test_strcmp在strcmp函数内stepi单步重点关注cmp后info registers eflags中ZF位是否按预期置位。3.2 栈帧手工构造可视化函数调用时的内存生长方向实验第2题要求不使用call指令而是用push/ret手工模拟函数调用目的是观测栈指针%rsp如何随push递减、pop递增。这是理解缓冲区溢出原理的前置技能。; stackframe.asm —— 手工构造caller/callee栈帧 section .data msg db In callee, 0 section .text global _start _start: ; 构造callee的返回地址指向exit_label push exit_label ; 构造callee的参数msg地址 push msg ; 跳转到callee不改变栈仅修改%rip jmp callee callee: ; 此时栈顶为msg地址次栈顶为exit_label pop rdi ; 弹出msg到rdi作为参数 call print_msg ; 调用C库函数需链接libc ret ; ret会弹出栈顶的exit_label并跳转 print_msg: ; 简化版printf实际用write系统调用 mov rax, 1 mov rdi, 1 mov rsi, rdi ; rdi此时是msg地址 mov rdx, 11 ; In callee长度 syscall ret exit_label: mov rax, 60 mov rdi, 0 syscall编译命令nasm -f elf64 stackframe.asm -o stackframe.o gcc -no-pie -o stackframe stackframe.o # 自动链接libc ./stackframe # 应输出In callee逻辑说明push exit_label将返回地址压栈jmp callee不压栈ret从栈顶弹出地址跳转——这完全复现了call callee的栈行为。实验要求在此过程中用gdb观察%rsp值变化break callee后info registers rsp记录初始值stepi执行pop rdi后再次info registers rsp应看到%rsp增加了864位地址。若%rsp不变说明pop未执行成功——大概率是push/pop寄存器宽度不匹配如push msg压入32位地址但pop rdi期待64位。3.3 全局变量内存布局分析定位.data/.bss节的物理地址实验第3题要求用objdump -t和readelf -S定位全局变量在可执行文件中的节区偏移并用gdb验证运行时地址。这是理解“变量存在哪”的实证环节。// layout.c #include stdio.h int global_init 0x12345678; // 在.data节 int global_uninit; // 在.bss节 char buffer[1024] {0}; // 在.data节因显式初始化 char buffer2[1024]; // 在.bss节 int main() { printf(global_init addr: %p\n, global_init); printf(global_uninit addr: %p\n, global_uninit); printf(buffer addr: %p\n, buffer); printf(buffer2 addr: %p\n, buffer2); return 0; }验证步骤gcc -g -no-pie -o layout layout.c # 查看符号表确认global_init在.data节 objdump -t layout | grep global_init # 输出类似0000000000404010 g O .data 0000000000000004 global_init # 查看节区头确认.data起始地址 readelf -S layout | grep \.data # 输出类似[14] .data PROGBITS 0000000000404000 ... # 运行程序记录打印的地址如0x404010与objdump结果比对 ./layout关键点.bss节在ELF文件中不占空间readelf -S显示Size0但运行时由内核在brk处分配buffer2地址应紧邻buffer之后且buffer2地址 buffer地址 1024证明.bss在.data之后连续布局。若objdump -t找不到global_uninit说明编译器将其优化掉了——需加volatile修饰volatile int global_uninit;。3.4 系统调用追踪用strace解析open/read/write的内核交互实验第5题要求用strace捕获程序执行过程中的系统调用并分析参数含义。这是理解“程序如何与硬件对话”的窗口。// syscall_trace.c #include fcntl.h #include unistd.h #include stdio.h int main() { int fd open(test.txt, O_RDONLY); // 触发open系统调用 if (fd 0) return 1; char buf[64]; int n read(fd, buf, sizeof(buf)-1); // 触发read系统调用 if (n 0) { buf[n] \0; write(1, buf, n); // 触发write系统调用 } close(fd); return 0; }执行与分析gcc -o syscall_trace syscall_trace.c strace -e traceopen,read,write,close ./syscall_trace 21 | grep -E (open|read|write|close) # 输出示例 # open(test.txt, O_RDONLY) 3 # read(3, Hello World!\n, 63) 13 # write(1, Hello World!\n, 13) 13 # close(3) 0参数说明strace -e traceopen,read,write,close只追踪指定系统调用21将strace输出stderr重定向到stdout以便grepopen返回值3是文件描述符read第二个参数Hello World!\n是内核copy_to_user写入的缓冲区内容write第一个参数1是stdout的fd。若open返回-1说明test.txt不存在——实验要求提前创建echo Hello World! test.txt。注意strace输出中的右侧是系统调用返回值左侧括号内是传入参数这是分析内核行为的核心依据。4. 避坑指南六个血泪经验总结的高频翻车点与排查路径天津大学计算机系统基础实验的“坑”不是设计缺陷而是对底层机制理解偏差的具象化。以下六条全部来自某高校实验室助教整理的真实报错日志按现象→原因→解决三步法呈现覆盖90%以上实验失败场景。4.1 现象gdb单步执行stepi时卡死或跳转到未知地址原因GDB调试信息与二进制不匹配。常见于用gcc -O2编译后调试编译器内联函数、删除无用代码导致源码行与机器指令映射断裂或-g未与-no-pie同时使用ASLR使每次加载地址不同GDB断点失效。解决严格使用gcc -g -no-pie -O0编译若必须用-O2改用gcc -g -no-pie -O2 -fno-omit-frame-pointer保留帧指针便于bt回溯检查file ./a.out输出中是否含not stripped若为stripped则需重编译。4.2 现象nasm编译报错error: parser: instruction expected原因NASM语法与GNU AS不兼容。天津大学实验指定NASM语法如mov rax, 1但学生误用GCC内联汇编风格如movq $1, %rax或文件编码为UTF-8 with BOMNASM无法识别BOM头。解决用file hello.asm确认编码为ASCII text用dos2unix hello.asm清除CRLF严格按实验模板使用mov rax, 1立即数前无$寄存器前无%若用VS Code编辑设置files.encoding: utf8且禁用BOM。4.3 现象objdump -d反汇编显示.text节为空或指令乱码原因目标文件未正确生成或格式错误。常见于nasm -f elf64生成.o文件后用ld -o prog prog.o链接时未指定-e _start导致ld默认入口为_start但符号未定义生成无效ELF或nasm版本过低2.14不支持elf64格式。解决readelf -h prog验证ELF Class为ELF64nm prog | grep _start确认_start符号存在且为Ttext类型升级NASM至2.15sudo apt install nasmUbuntu 22.04默认2.14.02需PPA。4.4 现象strace输出中read系统调用返回值为0但程序未退出原因read返回0表示EOF文件读完但实验代码未处理该情况导致buf未置零write输出垃圾数据。本质是未理解POSIX I/O语义read返回值0≠错误而是正常结束信号。解决在read后添加判断if (n 0) { /* EOF处理 */ break; } else if (n 0) { /* 错误处理 */ perror(read); }用hexdump -C test.txt确认文件末尾无多余字节。4.5 现象buffer2.bss变量地址打印为0x0或readelf -S显示.bss节Size0但Addr非0原因.bss节在ELF文件中Size0是正确行为节省磁盘空间但运行时由内核在brk处动态分配。若printf打印地址为0x0说明变量未被引用编译器优化掉Dead Store Elimination。解决对.bss变量加volatile修饰volatile char buffer2[1024];或在main中强制引用buffer2[0] 1; printf(%p, buffer2);用cat /proc/$(pidof ./layout)/maps验证进程内存映射中是否存在.bss对应区域。4.6 现象gcc链接时报错undefined reference to main但代码中已定义原因符号名大小写混淆。实验要求用_start作为入口但学生代码中写void main()而gcc默认寻找main符号或nasm汇编中写global main而非global _start导致链接器找不到入口。解决若用_start必须gcc -nostdlib -e _start若用main必须gcc不加-nostdlib且确保main为C函数非汇编标签用nm a.out | grep main\|_start确认符号存在且类型正确T为textU为undefined。5. 进阶技巧用GDB Python脚本自动化验证栈帧与寄存器状态当实验做到第4题“分析函数调用栈帧”时手动stepiinfo registersx/20xg $rsp重复上百次效率极低且易出错。天津大学高分同学普遍采用GDB内置Python API编写自动化验证脚本将人工操作压缩为一条命令。以下脚本stack_check.py可一键输出当前栈帧的%rbp、%rsp、返回地址、参数地址及局部变量值完美适配实验报告的数据采集需求。5.1 脚本编写与加载# stack_check.py —— 天津大学实验专用GDB脚本 import gdb class StackCheckCommand(gdb.Command): Usage: stackcheck Print current stack frame: rbp, rsp, return address, args, locals def __init__(self): super(StackCheckCommand, self).__init__(stackcheck, gdb.COMMAND_USER) def invoke(self, arg, from_tty): # 获取寄存器值 rbp gdb.parse_and_eval($rbp) rsp gdb.parse_and_eval($rsp) rip gdb.parse_and_eval($rip) # 计算返回地址rbp8处 ret_addr gdb.parse_and_eval(f*((long*){int(rbp)8})) # 获取第一个参数rdi和局部变量假设在rbp-8处 rdi gdb.parse_and_eval($rdi) local_var gdb.parse_and_eval(f*((long*){int(rbp)-8})) print(fRBP: {rbp} | RSP: {rsp} | RIP: {rip}) print(fReturn Address: {ret_addr}) print(fFirst Arg (rdi): {rdi}) print(fLocal var (rbp-8): {local_var}) print(Stack dump (10 entries from rsp):) gdb.execute(fx/10xg {int(rsp)}) StackCheckCommand()5.2 在GDB中加载并使用# 编译带调试信息的程序 gcc -g -no-pie -o test test.c # 启动gdb并加载脚本 gdb ./test (gdb) source stack_check.py (gdb) break main (gdb) run (gdb) stackcheck # 一键输出完整栈帧信息输出示例RBP: 0x7fffffffe3f0 | RSP: 0x7fffffffe3e0 | RIP: 0x401126 Return Address: 0x7fffffffe410 First Arg (rdi): 0x7fffffffe4f0 Local var (rbp-8): 0x12345678 Stack dump (10 entries from rsp): 0x7fffffffe3e0: 0x0000000000401126 0x00007fffffffe410 0x7fffffffe3f0: 0x00007fffffffe400 0x0000000000000000 ...5.3 脚本参数化与实验报告集成为适配不同实验题脚本支持动态参数。例如实验第2题要求验证push/ret后%rsp增量可扩展为# 在stack_check.py中添加 def invoke(self, arg, from_tty): args gdb.string_to_argv(arg) # 解析命令参数 if len(args) 1 and args[0] rsp-delta: old_rsp int(gdb.parse_and_eval($rsp)) gdb.execute(stepi) # 执行一条指令 new_rsp int(gdb.parse_and_eval($rsp)) delta new_rsp - old_rsp print(fRSP delta: {delta} bytes ({decrease if delta0 else increase})) return # ... 原有逻辑使用方式(gdb) stackcheck rsp-delta自动计算单步后栈指针变化量。此功能可直接嵌入实验报告的“栈帧观测表”避免手抄错误。注意GDB Python脚本需在~/.gdbinit中启用Python支持若报错Python scripting is not supported in this copy of GDB说明GDB编译时未启用Python——重装sudo apt install gdb python3-dev并验证gdb --version含python字样。从那以后我每次做系统级实验都强制在gdb启动后第一件事执行source stack_check.py哪怕只是stackcheck一次也能快速确认当前上下文是否符合预期。它不解决原理问题但把“我在哪、寄存器什么值、栈长什么样”这些基础事实从主观猜测变成客观输出省下的时间全用来思考“为什么这样设计”。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑