编译器是否允许通过重新排列局部变量来优化堆栈内存使用?

7

请考虑以下程序:

#include <stdio.h>

void some_func(char*, int*, char*);

void stack_alignment(void) {
    char a = '-';
    int i = 1337;
    char b = '+';
    some_func(&a, &i, &b); // to prevent the compiler from removing the local variables
    printf("%c|%i|%c", a, i, b);
}

它会生成以下汇编代码(我自己添加了注释,因为我是完全的汇编新手):
$ vim stack-alignment.c
$ gcc -c -S -O3 stack-alignment.c
$ cat stack-alignment.s
        .file   "stack-alignment.c"
        .section .rdata,"dr"
LC0:
        .ascii "%c|%i|%c\0"
        .text
        .p2align 2,,3
        .globl  _stack_alignment
        .def    _stack_alignment;       .scl    2;      .type   32;     .endef
_stack_alignment:
LFB7:
        .cfi_startproc
        subl    $44, %esp
        .cfi_def_cfa_offset 48
        movb    $45, 26(%esp)    // local variable 'a'
        movl    $1337, 28(%esp)  // local variable 'i'
        movb    $43, 27(%esp)    // local variable 'b'
        leal    27(%esp), %eax
        movl    %eax, 8(%esp)
        leal    28(%esp), %eax
        movl    %eax, 4(%esp)
        leal    26(%esp), %eax
        movl    %eax, (%esp)
        call    _some_func
        movsbl  27(%esp), %eax
        movl    %eax, 12(%esp)
        movl    28(%esp), %eax
        movl    %eax, 8(%esp)
        movsbl  26(%esp), %eax
        movl    %eax, 4(%esp)
        movl    $LC0, (%esp)
        call    _printf
        addl    $44, %esp
        .cfi_def_cfa_offset 4
        ret
        .cfi_endproc
LFE7:
        .def    _some_func;     .scl    2;      .type   32;     .endef
        .def    _printf;        .scl    2;      .type   32;     .endef

如您所见,有3个本地变量(aib),分别占用1字节、4字节和1字节的大小。包括填充,这将是12字节(假设编译器对齐为4字节)。

如果编译器将变量的顺序改为(abi),那么只需要8字节,这样不是更节省内存吗?

这里是一个“图形”表示:

    3 bytes unused                  3 bytes unused
     vvvvvvvvvvv                     vvvvvvvvvvv
+---+---+---+---+---+---+---+---+---+---+---+---+
| a |   |   |   | i             | b |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+

                |
                v

+---+---+---+---+---+---+---+---+
| a | b |   |   | i             |
+---+---+---+---+---+---+---+---+
         ^^^^^^^
      2 bytes unused

编译器是否被允许进行这种优化(按照C标准等)?

  • 如果不允许(根据我所看到的汇编输出),为什么?
  • 如果允许,为什么上面的情况没有发生?

假设符合标准等要求,那么是否这样做完全取决于各个编译器的实现。我想这应该由编译时的优化级别来控制。 - John3136
使用-Os有什么区别吗?使用O3可以告诉编译器使用优化,使代码更快,但可能会使代码变得更大。 - teppic
3
我认为编译器已经在优化内存空间。根据汇编代码,变量abi将分别存储在地址为26(%esp)27(%esp)28(%esp)的位置上。 - Bechir
没有具体的系统,任何关于优化的讨论都是毫无意义的。我试图读取OP的想法...嗯...这是一个32位Intel x86衍生的Linux或Windows系统。 - Lundin
@Lundin 这是运行在Intel Core i5处理器上的Windows 64位操作系统,使用MinGW gcc编译器。 - MarcDefiant
显示剩余2条评论
5个回答

7
编译器有权自由布局本地变量,甚至不必使用堆栈。
如果使用堆栈,它可以将局部变量存储在与声明顺序无关的顺序中。
“编译器是否允许进行此优化(按C标准等)?”
如果允许,为什么上面的情况没有发生?
这到底是一种优化吗?
这不太清楚。它可以节省几个字节,但很少有用。但在某些架构上,如果将char相邻存放,则读取一个char可能更快,特别是当char对齐时。因此,将char放在一起会强制其中至少一个char不对齐,使其读取速度变慢。

1
这并没有完全回答问题。在实现递归算法时,调用堆栈上的几个字节可能很重要。C是系统编程语言,因此尽可能节省内存确实是一个问题。问题是GCC是否错过了优化机会或选择了权衡的一方。几乎可以肯定的是,在某些体系结构上,从对齐位置读取char更快;但是汇编是为具体体系结构生成的。如果该体系结构不对未对齐读取施加惩罚,则会缺少优化。 - user4815162342

4

编译器是否允许进行此优化(根据C标准等)?

是的。

如果是,为什么上面没有发生这种情况?

它确实发生了。

仔细阅读汇编输出。

    movb    $45, 26(%esp)    // local variable 'a'
    movl    $1337, 28(%esp)  // local variable 'i'
    movb    $43, 27(%esp)    // local variable 'b'

变量a的偏移量为26。 变量b的偏移量为27。 变量i的偏移量为28。

使用您制作的图像,布局现在如下:

+---+---+---+---+---+---+---+---+
|   |   | a | b | i             |
+---+---+---+---+---+---+---+---+
 ^^^^^^^
 2 bytes unused

我真的没有注意到mov(l|b)命令的第二个参数中的数字 >.<,谢谢你指出来。嗯,还是很有趣为什么gcc会按照这个顺序排列这些命令,如果反过来可能更容易注意到。 - MarcDefiant
1
不知道为什么指令被排序成那样。如果我要猜测,可能是两个 movb 指令触及同一内存字,可能会停顿(虽然我们有存储缓冲区的原因,所以这不太可能),或者更有可能的是:这并不重要,所以它们按照编译器在内部语法树中拥有它们的顺序生成。 - Art

2
如果编译器改变变量的顺序,那么它会更加节省内存吗?
没有具体讨论特定的CPU、操作系统和编译器,我们无法得知答案。一般来说,编译器已经做到了最优化。要想有意义地优化代码,你需要深入了解特定系统的相关知识。
在你的情况下,编译器很可能是针对速度进行优化的。看起来编译器已经决定每个变量的对齐地址可以提供最有效的代码。在一些系统中,甚至必须分配偶数地址,因为某些CPU只能处理对齐访问。
编译器是否允许这种优化(符合C标准等)?
是的,C标准甚至不要求分配变量。编译器完全可以自由地处理这个问题,并且不需要记录如何或为什么。它可以在任何地方分配变量,可以将其完全优化掉,也可以将其分配到CPU寄存器、堆栈、或者你桌子下面的一个小木盒中。

0

通常在速度很重要的正常系统中,按单词阅读比按字符阅读更快。与速度增益相比的内存损失被忽略了。但是,在内存很重要的系统中,例如不同的交叉编译器为特定目标平台生成可执行文件(在非常通用的意义上),情况可能完全不同。编译器可以将它们打包在一起,甚至检查它们的生命周期和使用情况,根据这些减少位宽等等。因此,基本上它高度依赖于必要性。但是,通常每个编译器都会为您提供灵活性,如果您想要紧密“打包”它们,则可以查看手册。


0

具有堆栈缓冲区溢出保护的编译器(例如Microsoft的编译器中的/GS)可以重新排列变量作为安全功能。例如,如果您的本地变量是一些常量大小的char数组(缓冲区)和一个函数指针,那么可以溢出缓冲区的攻击者也可以覆盖函数指针。因此,本地变量被重新排序,以使缓冲区位于canary旁边。这样,攻击者就不能(直接)破坏函数指针,并且希望通过破坏的canary检测到缓冲区溢出。

警告:这种功能并不能防止被攻击者入侵,只是提高了攻击者的难度,但是熟练的攻击者通常会找到解决方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接