c - 禁用优化的 c alloca 函数的奇怪汇编代码 - gcc 使用 DIV 和 IMUL 为常数 16,并转换?
问题描述
我在c中有这个简单的代码
#include <stdio.h>
#include <alloca.h>
int main()
{
char* buffer = (char*)alloca(600);
snprintf(buffer, 600, "Hello %d %d %d\n", 1, 2, 3);
return 0;
}
我希望为 alloca 函数生成的汇编代码只会递减堆栈指针(一个子指令),并且可能会进行一些对齐(一个和指令),但是生成的汇编代码非常复杂,甚至比您预期的效率低。
这是objdump -d main.o
, 的输出gcc -c
(没有优化,所以默认-O0
)
0000000000400596 <main>:
400596: 55 push %rbp
400597: 48 89 e5 mov %rsp,%rbp
40059a: 48 83 ec 10 sub $0x10,%rsp
40059e: b8 10 00 00 00 mov $0x10,%eax
4005a3: 48 83 e8 01 sub $0x1,%rax
4005a7: 48 05 60 02 00 00 add $0x260,%rax
4005ad: b9 10 00 00 00 mov $0x10,%ecx
4005b2: ba 00 00 00 00 mov $0x0,%edx
4005b7: 48 f7 f1 div %rcx
4005ba: 48 6b c0 10 imul $0x10,%rax,%rax
4005be: 48 29 c4 sub %rax,%rsp
4005c1: 48 89 e0 mov %rsp,%rax
4005c4: 48 83 c0 0f add $0xf,%rax
4005c8: 48 c1 e8 04 shr $0x4,%rax
4005cc: 48 c1 e0 04 shl $0x4,%rax
4005d0: 48 89 45 f8 mov %rax,-0x8(%rbp)
4005d4: 48 8b 45 f8 mov -0x8(%rbp),%rax
4005d8: 41 b9 03 00 00 00 mov $0x3,%r9d
4005de: 41 b8 02 00 00 00 mov $0x2,%r8d
4005e4: b9 01 00 00 00 mov $0x1,%ecx
4005e9: ba a8 06 40 00 mov $0x4006a8,%edx
4005ee: be 58 02 00 00 mov $0x258,%esi
4005f3: 48 89 c7 mov %rax,%rdi
4005f6: b8 00 00 00 00 mov $0x0,%eax
4005fb: e8 a0 fe ff ff callq 4004a0 <snprintf@plt>
400600: b8 00 00 00 00 mov $0x0,%eax
400605: c9 leaveq
400606: c3 retq
400607: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
40060e: 00 00
知道这个生成的汇编代码的目的是什么吗?我正在使用 gcc 8.3.1。
解决方案
当然,通常的调试模式/反优化行为是将每个 C 语句编译到单独的块中,而非register
变量实际上在内存中。(为什么clang用-O0产生低效的asm(对于这个简单的浮点和)?)。
但是,是的,这超出了“未优化”的范围。没有理智的人会期望GCC 的固定指令序列(或 GIMPLE 或 RTL 逻辑,无论它扩展的任何阶段)的alloca
逻辑都涉及 2 的div
编译时常数幂,而不是移位或只是一个 AND。 x /= 16;
如果div
您自己用 C 源代码编写它,即使使用gcc -O0
.
通常,GCC 会尽可能多地对常量表达式进行编译时评估,就像x = 5 * 6
在运行时不使用 imul 一样。但是它扩展其alloca
逻辑的点必须在那点之后,可能很晚(在大多数其他通过之后)来解释所有那些错过的优化。因此,它不会从在 C 源逻辑上运行的相同传递中受益。
它在做两件事:
通过执行以下操作将分配大小向上舍入
600
(将其放入寄存器后的常量)为 16 的倍数:((16ULL - 1) + x) / 16 * 16
. 一个理智的编译器至少会使用右移/左移,如果不将其优化为(x+15) & -16
. 但不幸的是,GCC 使用16div
和imul
16,即使它是 2 的恒定幂。将分配空间的最终地址四舍五入为 16 的倍数(尽管它已经是因为 RSP 开始 16 字节对齐并且分配大小向上舍入。)它这样做
((p+15) >> 4) << 4
比 div/imul 更有效(尤其是对于 Ice Lake 之前的 Intel 上的 64 位操作数大小),但仍然比and $-16, %rax
. 当然,做已经毫无意义的工作也很愚蠢。
然后当然必须将指针存储到char* buffer
.
在下一条语句的 asm 块中,将其作为 arg for 重新加载sprintf
(效率低下到 RAX 中,而不是直接到 RDI 中,典型的 for gcc -O0
),同时设置寄存器 args。
alloca
所以这很糟糕,但是在大多数转换(“优化”)通道已经运行之后,对于 的固定逻辑的后期扩展很合理地解释了这一点。请注意,-O0
这并不意味着“没有优化”,它只是意味着“快速编译,并提供一致的调试”。
有关的:
gcc 如何选择从 -fverbose-asm 中对临时变量进行编号?- 另一个关于
-O0
alloca asm 的讨论,同样的猜测是在 GIMPLE 通道的后期,甚至在 RTL 中扩展它。还为 alloca / snprintf 优化了 asm,这要简单得多。事实上,这几乎是重复的;该问题也确实询问了alloca代码。做看似不需要的操作 (crackme) - 我非常轻松地评论了基本相同的 asm(对于 32 位模式),但主要是在讨论手动混淆的 asm。
GCC 如何实现变长数组?显示了这个糟糕代码的 32 位版本,但没有评论它有多糟糕。
推荐阅读
- java - 打开失败:android Q 中的 EACCES (Permission denied) 和 Targeting android 11
- php - json图像上传测试并从android恢复图像
- apache-flink - Apacheflink 的 DataStream API 如何支持事件的批处理
- javascript - 有没有办法避免这段代码中的嵌套循环?
- flutter - 如何在 SingleChildScrollView 内的列内居中小部件
- javascript - 如何实现考虑到用户拼写错误的过滤器?
- python - 是否可以从 .py 文件启动 Anaconda Prompt?
- websocket - 如何将 Web 应用程序连接到 gamefleet 实例?
- python - 如何在python随机模块中获取未选择的人口
- java - 将对象列表转换为 Map
> 使用流