为啥 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 以这种方式对堆栈中的整数进行排序?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 GCC 会产生奇怪的方式来移动堆栈指针

编写程序以升序对堆栈进行排序

#19 基数排序(Radix Sort)

GCC/GProf - 以编程方式访问线程的当前函数/堆栈跟踪

以编程方式对 wpf 数据网格进行排序

为啥这个递归函数超过调用堆栈大小?