TOC
Open TOC
ICS Lab 攻略
Lab 1: 大整数运算
可移植性
PRIu64
CSAPP Page 47
main.c 中 test 函数所使用的宏 PRIu64:
64 位会被展开为 lu,使用 gcc -E main.c | indent - | vim -
观察:
32 位会被展开为 llu,使用 gcc -E -m32 main.c | indent - | vim -
观察:
总结来说:
ULL
常量后缀,代表 unsigned long long int,一定为 64-bit
参考实现与测试
使用 Python 进行,文件名为 cheat.py:
注意修改 cheat.py 文件的可执行性。
之后在 main 函数中添加 cheat 函数:
test 函数调用了 cheat 函数,通过字符串比较的方式判断正确性:
这里的 U64 定义如下:
最后在 main 函数中使用随机生成的 64 位无符号整数进行测试:
实际的实现
参考机组,即无符号整数的乘法和除法:
无符号整数的乘法注意在右移时需要考虑进位。
无符号整数的除法则是 128 位除法,最后取商的低 64 位作为结果。
submit
在完成所有工作后,在 ~/ics-workbench 进行 git add . 和 git commit。
然后修改 ~/ics-workbench/multimod 中的 Makefile 文件,添加学号和姓名:
最后在 ~/ics-workbench/multimod 中 make submit 即可。
卷积
TODO
一个部分正确的 Ο(1) 实现
ICS EX
Lab 2: x86-64 内联汇编
终于等到了……
Ref.
How to Use Inline Assembly Language in C Code
https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
GDB
工欲善其事,必先利其器
https://zhuanlan.zhihu.com/p/32843449
经过一番体验,最终选择了 gdbgui。
使用 pip 安装完成后,就像使用 gdb 一样键入:
便会弹出浏览器窗口,有时候需要刷新。
下面是一些快捷键:
- Run: r
- Continue: c
- Next: n or right arrow
- Step: s or down arrow
- Up: u or up arrow
- Next Instruction: m
- Step Instruction: ,
对寄存器的支持似乎不太行,可以使用 expressions 窗口观察
报错 OSError: [Errno 98] Address already in use
后的处理
实验要求
在本实验中,所有函数的所有部分都必须使用 inline assembly 实现。除定义临时变量 (可以赋常量初值) 和 return 返回临时变量/参数外,不得使用任何表达式/条件/循环等 C 语言成分。
以 64 位为例,编译优化级别应使得参数传递为 x86-64 风格(寄存器)
一些调试友好的选项:
asm_add
先简单熟悉一下。
asm_popcnt
其中寄存器 ecx 作为循环变量,寄存器 edx 保存中间结果。
由于跳转指令为相对跳转,故根据指令长度显式编码。
注意需要有显式的 return 语句,否则 OJ 会报错 control reaches end of non-void function
。
asm_memcpy
先看源代码反汇编的结果:
似乎直接调用 memcpy@plt
有亿点困难。
所以参考之前的实现:
给出对应的内联汇编:
一个小疑问,对参数的写为什么不需要在 output operands 中指出?
asm_setjmp / asm_longjmp
Ref.
Intro & i386: https://zhuanlan.zhihu.com/p/82492121
x86-64: https://blog.csdn.net/dog250/article/details/89742140
struct: https://blog.codingnow.com/2005/12/typedef_struct_array.html
实现及原理
先看函数签名:
这里的 asm_jmp_buf 是一个自定义数据结构:
有两处需要注意的地方:
STFM
可知保存寄存器 rsp 和 rip 就可以了,剩下的 6 个是被调用者保存寄存器(虽然讲不出什么道理)。
在使用函数时,类似这样:
参数是直接传入到函数中的,若将 context[1]
改为 context
,便成了值传递,不满足需求。使用 context[1]
,可以在函数调用时让寄存器 rdi 保存 context 数组的地址,这样就可以通过寄存器 rdi 来保存环境了。
下面看实现:
注意 asm_longjmp 中要特判 val 是否为 0:
还写了一个很有感觉的测试:
递归的十八层地狱……
通过 gdbgui 来分析一下执行过程:
- 从 main 进入 asm_setjmp 后,返回地址为 0x555555555248,即 (%rsp),此时 rsp 的值为 0x7fffffffddc8。我们保存寄存器 rsp 到环境,并将解引用的值存入环境中的 rip。返回值为 0。
- 从 main 进入 asm_longjmp 后,rsp 的值为 0x7fffffffddc8(与环境中的恰好相同,由于是同层),我们将环境中 rip 的值写 (%rsp),从而改变了 main 为 asm_longjmp 准备的返回地址,于是 asm_longjmp 便返回到了 asm_setjmp 返回的地方。由于寄存器 rax 的值也被改写了,所以此时 r 的值为 123。
- recursion 进行递归调用下的测试,环境中 rsp 为 0x7fffffffddb8,rip 为 0x5555555551fb。多次递归后,实际 rsp 的值在减少。最终到达 asm_longjmp 后,将 rsp 又改为 0x7fffffffddb8,相当于将低地址的栈全部 unwind 了,并改写返回地址,于是便成功进行了 nonlocal goto。
协程
使用 setjmp / longjmp
可以实现异常处理外,还可以实现协程:
https://www.cnblogs.com/sewain/p/14360853.html
编译选项如下:
对于其中的 -D_FORTIFY_SOURCE
,参阅 https://stackoverflow.com/questions/13517526/difference-between-gcc-d-fortify-source-1-and-d-fortify-source-2
部分执行结果如下:
其原理并不难看懂。
Duff’s Device
起源于循环展开,却可以来实现协程……
https://zhuanlan.zhihu.com/p/284223705
https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
https://mthli.xyz/coroutines-in-c/
Lab 3: 性能调优
Premature optimization is the root of all evil.
Inner Timer
在 main.c
中:
以微秒为单位。
Outer Timer
基本的计时功能:
Trace
系统调用计时和库函数调用记录:
Profiler
每隔一段时间对程序的执行采样。
例如如果我们希望了解哪个函数使用了最多的时间,可以想到如下的实现:
- 借助系统的机制,每秒给进程发送若干 (例如 100) 次“中断”,中断到来后,会跳转到一个“中断”处理程序。
- 在“中断”处理程序中,我们遍历函数的调用栈,获得当前的调用链并记录。
- 程序执行完毕后,根据采样的得到的信息,就能推断出哪些函数 (近似) 消耗了多少时间。
Linux 为我们提供了 perf tools 系列的性能诊断工具,安装如下:
然后以管理员权限运行:
此时在当前目录下会生成 perf.data
文件,我们键入:
就可以进行一番探索了……
此外,可以用 perf stat
来查看一次运行的统计信息……
For more:
Makefile
具体的优化
Set N = 9000000
原始
perf stat
查看运行时间:
筛至平方根
小优化:
欧拉筛
运行时间似乎并不乐观:
只筛奇数
还是差一点:
放弃了……