"CSAPP大作业"是哈尔滨工业大学计算机系统课程(Computer Systems: A Programmer's Perspective)的经典实践项目。作为计算机专业学生接触系统级编程的第一道门槛,这个系列实验贯穿了从底层硬件到上层应用的完整知识链条。
我在大三完成这套实验时,曾连续三周泡在实验室调试代码。最深刻的体会是:纸上谈兵永远无法真正理解计算机系统。只有当你在GDB中单步跟踪汇编指令、看着缓存命中率随着矩阵分块大小变化、亲手实现一个简易Shell时,那些课本上的概念才会突然变得鲜活。
这个实验要求仅用极简运算符(如~ & ^ | + <<)实现特定功能。比如isLessOrEqual(x,y)函数,在不使用if-else和比较运算符的情况下判断x≤y。核心训练点包括:
我在调试中发现一个典型陷阱:直接使用x-y判断大小会导致INT_MIN - 1的溢出错误。正确解法应该是:
c复制int isLessOrEqual(int x, int y) {
int sign_diff = ((y + (~x + 1)) >> 31) & 1; // y-x的符号位
int sign_x = (x >> 31) & 1;
int sign_y = (y >> 31) & 1;
return (sign_x & !sign_y) | (!(sign_x ^ sign_y) & !sign_diff);
}
通过逆向工程拆除"炸弹"的程序设计。每个阶段需要分析汇编代码,找出特定输入字符串。这个实验教会我:
有个阶段需要识别出密码是"Public speaking is very easy."的MD5哈希前6位。我通过观察字符串常量的内存地址,配合反汇编结果定位到strcmp调用点,最终用x/s命令直接打印出预期字符串。
利用代码注入和ROP技术攻击存在漏洞的程序。最烧脑的是构造精确的栈帧布局:
在Phase5中,需要通过ROP链调用touch3函数。我使用objdump找到gadget地址,构造出这样的攻击字符串:
code复制00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
... [填充至40字节] ...
ab 19 40 00 00 00 00 00 /* pop %rdi */
fa 97 b9 59 00 00 00 00 /* cookie值 */
a2 19 40 00 00 00 00 00 /* touch3地址 */
需要编写一个缓存模拟器,能解析valgrind内存访问日志并统计命中率。关键数据结构设计:
c复制typedef struct {
int valid_bit;
unsigned long tag;
int lru_counter;
} CacheLine;
typedef struct {
CacheLine *lines;
} CacheSet;
typedef struct {
CacheSet *sets;
int S; // 组数
int E; // 相联度
int b; // 块偏移位数
} Cache;
处理每条内存访问记录时:
在B=32的测试用例中,基础实现会出现大量冲突不命中。通过分块技术可显著提升性能:
c复制void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
int blk_size = 8;
for (int i = 0; i < N; i += blk_size) {
for (int j = 0; j < M; j += blk_size) {
for (int k = i; k < i + blk_size && k < N; k++) {
for (int l = j; l < j + blk_size && l < M; l++) {
B[l][k] = A[k][l];
}
}
}
}
}
实测显示8×8分块可使miss数从1183降至343。更精细的优化还包括:
核心是正确处理SIGCHLD信号:
c复制void sigchld_handler(int sig) {
int old_errno = errno;
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG|WUNTRACED)) > 0) {
if (WIFEXITED(status)) {
deletejob(jobs, pid);
} else if (WIFSIGNALED(status)) {
printf("Job [%d] terminated by signal %d\n", pid, WTERMSIG(status));
deletejob(jobs, pid);
}
}
errno = old_errno;
}
前台/后台作业管理需要维护一个作业列表:
c复制struct job_t {
pid_t pid;
int jid;
int state;
char cmdline[MAXLINE];
};
关键操作包括:
在seq-full.hcl基础上实现iaddq指令:
code复制# iaddq V, rB
wordsig V 'V'
icode:ifun <- M1[PC+1]
rB <- M1[PC+2]
valP <- PC + 3
valA <- R[rB]
valE <- valA + V
R[rB] <- valE
PC <- valP
通过修改pipe-full.hcl实现:
p $raxdisassemblex/20xb 0x123456break *0x400500 if $rdi==5bt查看调用栈./test-trans -M 32 -N 32最深刻的教训来自Shell Lab:最初没有正确处理SIGCHLD的竞态条件,导致僵尸进程累积。后来通过三种方式彻底解决: