编译器为我做了什么
- 1 视频也可以含恶意代码吗
- 2 Frida 入门
- 3 虚拟机保护
- 4 C#逆向入门
- 5 安卓刷机一点通
- 6 Z3-Solver教程
- 7 出题人的“恶意”
- 8 编译器为我做了什么 本文
- 9 CSAPP Lab2 Note
- 10 Hash系列算法
- 11 程序脱壳
- 12 安卓逆向介绍
- 13 IDA类型修复
- 14 IDA疑难杂症
- 15 花指令
- 16 反动态调试技术与应对手段
- 17 AES加密算法
- 18 如何解决魔改后的加密算法
- 19 常见加密算法识别与变种分析
- 20 TEA 加密算法
- 21 RC4 加密算法
- 22 Base64 编码
- 23 IDA Dynamic Debugging
- 24 IDA Usage With Reverse Basic
Backstory
考虑一下不同代码写法对性能的影响,例如:
int sum(const std::vector<int> &v){ int result = 0; for(size_t i = 0; i < v.size(); ++i) { result += v[i]; } return result;}int sum(const std::vector<int> &v){ int result = 0; for(int x : v) { result += x; } return result;}这两种写法在功能上等效。但如果想真正确认哪一种在性能上取胜,则需要使用基准测试 (benchmark)。在这里,写法 B 的性能更好。
与此同时,我们还想知道自己能否承担这种性能提升的代价,这时需要查看编译器输出的汇编结果了。
不要只看汇编结果。我们看不完那么多汇编指令,而且这还是经过编译器优化修改过的产物。如果需要比较,请一并做好基准测试。
x86_64 Assembly 101
寄存器
x86_64 架构下的寄存器有:
rax,rbx,rcx,rdx,rsp,rbp,rsi,rdi,r8-r15xmm0-xmm15rdi,rsi,rdx等用来存储变量rax用于存储返回值
eax?在 x86_64 架构中,rax 的长度是 64 位,eax 的长度是 32 位。当数据写入 eax 中时,实际上是将 rax 的高位的 32 个位清 0,仅在低位的 32 个位写入数据。这是一个寄存器相关的技巧。
指令格式
我们使用 Intel 语法格式:
opop destop dest, srcop dest, src1, src2这里的 op 指代任意汇编指令,比如 mov, call。dest 和 src 是寄存器或者内存指针,使用“基址+偏移”表示,即 base + [reg1] + [reg2 * (1, 2, 4 or 8)]
汇编指令与伪 C 代码的对照
mov eax, DWORD PTR [r14]add rax, rdiadd eax, DWORD PTR [r14+4]sub eax, DWORD PTR [r14+4*rbx]lea rax, [r14+4*rbx]xor edx, edxint eax = *r14 // int *r14;rax += rdi;eax += r14[1];eax -= r14[rbx];int *rax = &r14[rbx];edx = 0;有人会将 edx = 0 肉编译为 mov edx, 0,但这里却是用 xor edx, edx。有两个原因:1)mov 中使用的 0 必须以 4 字节存储,体积要大得多;2)指令集已经明白 xor 有清 0 的作用,因此进行对应优化——换句话说,xor 更偏向“重置”。
Compiler Explorer
最初的 Compiler Explorer 是一个本地 shell 脚本,主要功能部分长这样:
g++ /tmp/test.cc -O2 -c -S -o --masm=intel \ | c++filt \ | grep -vE '\s+\.'然后用 Unix 工具 watch 持续运行脚本,得到近实时编译结果:
sum(std::vector<int, std::allocator<int> > const&):.LFB786: mov rcx, QWORD PTR [rdi] mov rax, QWORD PTR 8[rdi] sub rax, rcx shr rax, 2 mov rsi, rax ; ...后面部署到线上,有了 GUI,可以在 Compiler Explorer 上探索。
通过示例,我们可以清晰地看到写法 A 和写法 B 的汇编区别:写法 B 的汇编明显更短。
本文的示例使用 x86-64 gcc 15.1 -O2 编译这些函数,而原作者使用 x86-64 gcc 8.1 编译,得到的结果会有差异。
通过引入 numeric 库,我们还有写法 C:
int sum(const std::vector<int> &v){ return accumulate(begin(v), end(v), 0);}写法 C 的汇编几乎和 B 相同,只有两个指令的差异——不过是位置的变化罢了。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!