c++ - 为什么 GCC 以这种方式在堆栈上排序整数?
问题描述
关于堆栈上变量的 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]
,然后其他变量放在声明顺序之后。为什么会有好处?请注意,据我所知,打印任何这些变量的值似乎都不会触发相同的重新排序。
解决方案
您应该编译您的daniel.cc
C++ 代码g++ -O -fverbose-asm -daniel.cc -S -o daniel.s
并查看生成的汇编代码daniel.s
对于您的第一个示例,调用框架中的许多常量和插槽已经消失,因为优化:
.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
如果出于某种原因您确实需要调用帧以已知顺序包含槽,则需要使用 astruct
作为自动变量(并且该方法可移植到其他 C++ 编译器)。
如果您需要了解 GCC 为何以这种方式编译您的代码,请下载 GCC 的源代码,阅读GCC internals 的文档,研究它(它是免费软件)。
你应该对GCC 开发者选项感兴趣,他们转储了很多关于编译器内部状态的东西。
一旦您对 GCC 的实际作用有所了解,请订阅一些 GCC 邮件列表(例如gcc@gcc.gnu.org
)并在那里提问。或者,对您的GCC 插件进行编码以改进其行为、更改调用框架的组织、添加转储例程。
如果您需要了解或改进 GCC,请预算几个月的全职工作,并在之前阅读Dragon 的书。
推荐阅读
- c++ - 迭代在 cicle C++ 期间改变大小的无序集
- javascript - 在 add_action WooCommerce 中运行 JS 函数
- validation - Kotlin 数据类和 Bean 验证:Double 字段上的 @NotNull 不起作用
- python - 将列表设置为字典值会导致 TypeError: unhashable type: 'list'
- django - 如何建立一个 django 模型来存储 twitter 对话?
- django - Python MongoDB ReferenceError:弱引用对象不再存在
- javascript - 为什么在函数中重新声明标识符会屏蔽同名参数?
- javascript - 如何修改嵌套对象的属性?
- terraform - 运行 terraform 输出时无法获取输出
- java - cloudrun 因 find_vma 失败而失败