10.2
热烈的启程
这篇日记写于启程前往杭州的高铁上。
先下二城
Architecture Lab
昨日几乎完成了 arch lab,由于暂时没有检索到正解,所以 part C 只停留在了 52/60,可以看出 part C 实际是很有难度的(毕竟 std 最好也只有 7.48 CPE,满分设到 7.50 还是太有挑战性了),以下是参考过的解答:
这是前三个 lab 中体量最大的一个 lab,出题人直接写了一整套完整的 YAS Assembler,ISA Simulator,SEQ Simulator,PIPE Simulator(甚至还有支持 step over 的 GUI Debug 版本)以及完整的 Benchmark Test 和 Regression Test,使用了各种奇妙的古董工具包括 Perl 脚本、TCL/TK 图形库、FLEX 语法检测,可以看出倾注了很多心血。
本来想读一下各种工具的源码的,不过在 Make 构建中发现了很多因为版本过于老旧的不兼容问题,包括但不限于
- gcc 默认 -fcommon 更新为了 -fno-common,所以书中的那一套强弱引用的 Symbol Resolution Rule 也过时了,现在应该都是 ODR
- TCL 库的 Interp Result 的存取通过直接指针访问被遗弃,需要通过包装函数
- TM 库的符号莫名其妙未定义 matherr
所以还是放弃了
Part A 是用 y86-64 ISA 完成三个函数:sum 用循环实现链表键值求和,rsum 用递归实现链表键值求和,copy_block 复制 len 长的 long 数组 src 到 dest。体验了与 Attack Lab 不同的 y86-64 指令集汇编代码编写。由于此时书中进度还没有到 Linking 和 Virtual Memory,所以采用了最简单的从 PC = 0x00 开始运行,还需要手动开栈。可以提升对程序 private 内存架构的理解。
Part B 是在 SEQ 的 hcl 添加 iaddq immed, %reg
的实现,是对非流水线的分段的处理器组合逻辑很好的复习
Part C 是在 PIPE 下优化 ncopy 函数(复制 src 数组到 dst,并统计正数个数)以达到最小化 CPE 的目的,可以修改 ncopy.ys 和 pipe-full.hcl。
- 首先在 PIPE 中实现 iaddq 的必要性不是很大,因为实际用下来空闲寄存器很多,将立即数在循环外提前存在空闲寄存器就好了,不过实现 iaddq 可以大大提升可读性简洁性(机器码总长不能超过 1k bytes)。
- ./psim -g 可以用 GUI Debugger step over 来看那些地方出现了 bubble 导致流水线空转,主要位置在 jle 判断正负 miss 和 mrmovq 紧接使用的 Load/Use Hazard 处,后者通过排列指令很好处理,前者本想通过 Conditional Transfer 处理,不过效果并不好(稳定 4 cycles 对比不稳定 2 / 5 cycles,假定数据均匀分布前者会慢 0.5 cycles)
- 最后就是 unrolling,可以优化掉循环中稳定的循环变量迭代的 cycle。由于不是 Superscalar 和 Out-of-Order Execution,所以 unrolling 的第二维意义不大,8x1 就行了。不过仍然可以通过交替使用寄存器并合理安排指令顺序来减少数据依赖。自己上手写一遍 unrolling 确实很有感觉
- 最后观察到 len 较小时,CPE 极大,所以 1-8 可以舍弃循环进行二分 + 枚举的特殊处理
- 还有什么优化空间?
由于现代处理器的 Out-of-Order Execution 和 Superscalar 机制还是与 PIPE 的 Scalar pipeline 有一定差距,Part C 中的部分优化技术的理解似乎并不能直接迁移到现代处理器上。
Attack Lab
参考过的解答
本实验聚焦于两种利用 Buffer Overflow 的攻击技术 Code Injection 和 Return-Oriented Programming。可以强化一些机制理解 private virtual memory layout(stack),control transfer(rip, rsp, return address),x86-64 instruction encoding。
CI 包括 level 1, 2, 3
- Level 1 的目标是调用
touch1()
,直接将 return address 改为 touch1 的地址即可 - Level 2 的目标是调用
touch2(cookie)
,由于 cookie 是存放在 .data 的已知数据,所以需要设置 rdi,所以返回地址需要执行注入的代码部分,注入代码部分依次包括设置 rdi、jmp touch2 - Level 3 的目标是调用
touch3(cookie_str)
,由于 cookie 对应的字符串表示并没有直接储存,需要注入数据(注入 char * 的二进制表示),整体过程同 Level 2,将 rdi 设置为注入的数据地址即可。
Return-Oriented Programming 包括 Level 4, 5,由于现代操作系统、编译器的 Random Stack Address 机制、栈数据不可执行的权限机制(如果只有第一点可以通过 pad nop 一定程度上突破。另外,实际上有了 Canary 之后直接 ban 掉了修改返回地址,Buffer Overflow Attack 又没落一分),无法知道自己注入的代码的准确位置,也无法执行栈上注入的代码,所以催生了 ROP 这种利用已有程序的片段(gadget,如 op + op + ... + op + ret)来进行组合,通过在 RA 之上排列一连串精心选择的 gadget 以达到自己的目的。
- Level 4 的目标是调用
touch2(cookie)
,不看 writeup advice 几乎无法想到 popq 可以不使用数据的地址而将数据取到寄存器中,绝妙! - Level 5 是调用
touch3(cookie_str)
,难点在于清楚自己可以用的指令到底有什么(每一个 gadget farm 里的函数都有多种拆分方式)以及在繁多的选择中找到一条路径达到 %rdi = cookie_str_address,鉴定为大型枚举题,做不出来一点。(不过或许可以锻炼从繁多的细节中找到目标的敏锐力)
留白
由于旅行期间完全没动过笔记本,所以实际上这是一篇并没有写完的日记。