ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

简介:本资源是天津大学《计算机系统基础》课程配套的完整上机实验资料包,面向计算机专业本科生及系统编程初学者,聚焦底层原理实践与安全编程能力培养。内容覆盖位操作、整数与浮点数表示、缓冲区溢出分析(拆弹专家)、代码注入攻击防御、高速缓存优化五大核心实验模块,直击计算机组成原理与系统安全的关键难点。压缩包共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标准一致(相等返回0,s1>s2返回正数,s1<s2返回负数)。难点不在逻辑,而在如何让CPU的ZF/SF/OF标志位真实反映字节比较结果。

; strcmp.asm —— 天津大学实验标准实现(关键:用cmpb而非cmpq) section .text global strcmp strcmp: push rbp mov rbp, rsp ; 参数:rdi=s1, rsi=s2 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 # 应输出0("abc"=="abc")、1("abd">"abc")、-1("ab"<"abc")

关键参数说明: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, rax(x86-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增加了8(64位地址)。若%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显示Size=0),但运行时由内核在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 trace=open,read,write,close ./syscall_trace 2>&1 | 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 trace=open,read,write,close只追踪指定系统调用;2>&1将strace输出(stderr)重定向到stdout以便grep;open返回值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 BOM,NASM无法识别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为ELF64;nm prog | grep _start确认_start符号存在且为T(text)类型;升级NASM至2.15+:sudo apt install nasm(Ubuntu 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节Size=0但Addr非0

原因:.bss节在ELF文件中Size=0是正确行为(节省磁盘空间),但运行时由内核在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为text,U为undefined)。


5. 进阶技巧:用GDB Python脚本自动化验证栈帧与寄存器状态

当实验做到第4题“分析函数调用栈帧”时,手动stepi+info registers+x/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") # 计算返回地址(rbp+8处) 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(f"RBP: {rbp} | RSP: {rsp} | RIP: {rip}") print(f"Return Address: {ret_addr}") print(f"First Arg (rdi): {rdi}") print(f"Local var (rbp-8): {local_var}") print("Stack dump (10 entries from rsp):") gdb.execute(f"x/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(f"RSP delta: {delta} bytes ({'decrease' if delta<0 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一次,也能快速确认当前上下文是否符合预期。它不解决原理问题,但把“我在哪、寄存器什么值、栈长什么样”这些基础事实从主观猜测变成客观输出,省下的时间全用来思考“为什么这样设计”。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进