首页 > 解决方案 > 为什么 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 处的不同序列。再一次,据我所知,这与重新编译和代码更改是一致的,除非我引用代码中的其他地址。

如果我不知道更好,看起来整数变量是按声明顺序放置的,除非代码对寻址进行任何操作,此时它开始以某种确定性的方式对它们进行加扰。

一些不满足这个问题的回答如下:

为什么 GCC 编译器以这种方式布局整数变量?

什么解释了这里看到的一致的重新排序?

编辑:我想仔细检查一下,我触摸的变量总是放在 中[rbp-0x10],然后其他变量放在声明顺序之后。为什么会有好处?请注意,据我所知,打印任何这些变量的值似乎都不会触发相同的重新排序。

标签: c++gccmemorystack

解决方案


您应该编译您的daniel.ccC++ 代码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 的书


推荐阅读