为啥 GCC 以这种方式对堆栈中的整数进行排序?
Posted
技术标签:
【中文标题】为啥 GCC 以这种方式对堆栈中的整数进行排序?【英文标题】:Why does GCC order integers on the stack in this way?为什么 GCC 以这种方式对堆栈中的整数进行排序? 【发布时间】:2021-06-21 21:07:42 【问题描述】:存在一些关于堆栈上变量的 GCC 排序的问题。但是,这些通常涉及混合变量和数组,而事实并非如此。我正在使用 GCC 9.2.0 64 位版本,没有特殊标志。如果我这样做:
#include <iostream>
int main()
int a = 15, b = 30, c = 45, d = 60;
// std::cout << &a << std::endl;
return 0;
那么内存布局看这里的反汇编:
0x000000000040156d <+13>: mov DWORD PTR [rbp-0x4],0xf
0x0000000000401574 <+20>: mov DWORD PTR [rbp-0x8],0x1e
0x000000000040157b <+27>: mov DWORD PTR [rbp-0xc],0x2d
0x0000000000401582 <+34>: mov DWORD PTR [rbp-0x10],0x3c
所以:四个变量在 RBP 的偏移量 0x04、0x08、0x0C、0x10 处按顺序排列;也就是说,按照它们被声明的相同顺序进行排序。这是一致的和确定的;我可以重新编译,添加其他代码行(随机打印语句,其他后期变量等)并且布局保持不变。
但是,只要我添加了一条触及地址或指针的行,布局就会发生变化。例如,这个:
#include <iostream>
int main()
int a = 15, b = 30, c = 45, d = 60;
std::cout << &a << std::endl;
return 0;
产生这个:
0x000000000040156d <+13>: mov DWORD PTR [rbp-0x10],0xf
0x0000000000401574 <+20>: mov DWORD PTR [rbp-0x4],0x1e
0x000000000040157b <+27>: mov DWORD PTR [rbp-0x8],0x2d
0x0000000000401582 <+34>: mov DWORD PTR [rbp-0xc],0x3c
所以:一个加扰的布局,其中变量的偏移量现在分别位于 0x10、0x04、0x08、0x0C。同样,这与任何重新编译、我认为要添加的大多数随机代码等都是一致的。
但是,如果我只是像这样触摸不同的地址:
#include <iostream>
int main()
int a = 15, b = 30, c = 45, d = 60;
std::cout << &b << std::endl;
return 0;
然后变量按如下顺序排列:
0x000000000040156d <+13>: mov DWORD PTR [rbp-0x4],0xf
0x0000000000401574 <+20>: mov DWORD PTR [rbp-0x10],0x1e
0x000000000040157b <+27>: mov DWORD PTR [rbp-0x8],0x2d
0x0000000000401582 <+34>: mov DWORD PTR [rbp-0xc],0x3c
也就是说,偏移量 0x04、0x10、0x08、0x0C 处的不同序列。再一次,据我所知,这与重新编译和代码更改是一致的,除非我在代码中引用了其他地址。
如果我不知道更好,看起来整数变量是按声明顺序放置的,除非代码对寻址进行任何操作,此时它开始以某种确定性的方式对它们进行加扰。
将不满足这个问题的一些回答如下:
“行为在 C++ 标准中未定义”——我不是在询问 C++ 标准,而是专门询问这个 GCC 编译器如何决定布局。 “编译器可以为所欲为”——没有回答编译器在这种特定的、一致的情况下如何决定它“想要”什么。为什么 GCC 编译器会这样布局整数变量?
什么解释了这里看到的一致的重新排序?
编辑:我想仔细观察,我触摸到的变量总是放在[rbp-0x10]
,然后其他的按声明顺序排列。为什么会有好处?请注意,据我所知,打印任何这些变量的 值 似乎不会触发相同的重新排序。
【问题讨论】:
用-O0
编译怎么样?这似乎是合理的,这只是一些优化的一部分,在这种特定情况下实际上没有任何区别。
@super 通过优化,编译器通常不会分配未使用的变量:godbolt.org/z/dPq5Ks5Wd.
我假设 gcc 将使用的变量放在最对齐的地址上,这可以提高访问速度、缓存使用或类似的东西。
为什么调用框架中局部变量的顺序对您很重要? C 标准n3337 没有提到它们,你也不应该在意!任何编译器都可以将 CPU 寄存器用于变量!你的问题是XY problem,你应该用书面英语解释你为什么关心变量顺序和布局
这并不能解释“为什么局部变量的顺序对你很重要”。例如,您的代码可能由带有插件的 GCC 编译,或者由另一个版本的 GCC 或 Clang 编译,它们以不同的方式对局部变量进行排序。你可能有理由问你的问题,值得解释一下这个原因
【参考方案1】:
您应该使用g++ -O -fverbose-asm -daniel.cc -S -o daniel.s
编译您的daniel.cc
C++ 代码并查看生成的汇编代码daniel.s
对于您的第一个示例,您的 call frame 中的许多常量和插槽已经消失,因为优化:
.text
.globl main
.type main, @function
main:
.LFB1644:
.cfi_startproc
endbr64
subq $24, %rsp #,
.cfi_def_cfa_offset 32
# daniel.cc:2: int main()
movq %fs:40, %rax # MEM[(<address-space-1> long unsigned int *)40B], tmp89
movq %rax, 8(%rsp) # tmp89, D.41631
xorl %eax, %eax # tmp89
# daniel.cc:3: int a = 15, b = 30, c = 45, d = 60;
movl $15, 4(%rsp) #, a
# /usr/include/c++/10/ostream:246: return _M_insert(__p);
leaq 4(%rsp), %rsi #, tmp85
leaq _ZSt4cout(%rip), %rdi #,
call _ZNSo9_M_insertIPKvEERSoT_@PLT #
movq %rax, %rdi # tmp88, _4
# /usr/include/c++/10/ostream:113: return __pf(*this);
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@PLT #
# daniel.cc:6:
movq 8(%rsp), %rax # D.41631, tmp90
subq %fs:40, %rax # MEM[(<address-space-1> long unsigned int *)40B], tmp90
jne .L4 #,
movl $0, %eax #,
addq $24, %rsp #,
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L4:
.cfi_restore_state
call __stack_chk_fail@PLT #
.cfi_endproc
.LFE1644:
.size main, .-main
.type _GLOBAL__sub_I_main, @function
如果出于某种原因您确实需要调用帧以已知顺序包含槽,则需要使用 struct
作为自动变量(并且该方法可移植到其他 C++ 编译器)。
如果您需要了解 GCC 为何以这种方式编译您的代码,请下载 GCC 的源代码,阅读documentation of GCC internals,研究它(它是免费软件)。 p>
您应该对GCC developer options 感兴趣,他们转储了很多关于编译器内部状态的信息。
一旦您对 GCC 的实际作用有所了解,请订阅一些 GCC 邮件列表(例如gcc@gcc.gnu.org
)并在那里提问。或者,对您的 GCC plugin 进行编码以改善其行为、更改调用框架的组织、添加转储例程。
如果您需要了解或改进 GCC,请预算几个月的全职工作,并在之前阅读Dragon book。
【讨论】:
我想我看不出采用我未优化的程序集的建议的意义,它显示了我正在调查的变量,并将它们优化出来。以上是关于为啥 GCC 以这种方式对堆栈中的整数进行排序?的主要内容,如果未能解决你的问题,请参考以下文章