为啥编译器在堆栈中分配的比需要的多?

Posted

技术标签:

【中文标题】为啥编译器在堆栈中分配的比需要的多?【英文标题】:Why does the compiler allocate more than needed in the stack?为什么编译器在堆栈中分配的比需要的多? 【发布时间】:2016-10-12 18:00:38 【问题描述】:

我有一个简单的 C 程序。比如说,我有一个长度为 20 的 int 和一个 char 数组。我总共需要 24 个字节。

int main()

   char buffer[20];
   int x = 0;
   buffer[0] = 'a';
   buffer[19] = 'a';

堆栈需要与 16 字节边界对齐,所以我假设编译器将保留 32 字节。但是当我用 gcc x86-64 编译这样的程序并读取输出程序集时,编译器会保留 64 个字节。

..\gcc -S -o main.s main.c

给我:

    .file   "main.c"
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    pushq   %rbp                        # RBP is pushed, so no need to reserve more for it
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe   %rbp, 0
    subq    $64, %rsp                   # Reserving the 64 bytes
    .seh_stackalloc 64
    .seh_endprologue
    call    __main
    movl    $0, -4(%rbp)                # Using the first 4 bytes to store the int
    movb    $97, -32(%rbp)              # Using from RBP-32 
    movb    $97, -13(%rbp)              # to RBP-13 to store the char array
    movl    $0, %eax
    addq    $64, %rsp                   # Restoring the stack with the last 32 bytes unused
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 5.2.0"

这是为什么呢?当我编写汇编程序时,我总是只保留我需要的最小内存,没有任何问题。这是编译器的限制,无法评估所需的内存还是有原因?

这里是 gcc -v

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=D:/Mingw64/bin/../libexec/gcc/x86_64-w64-mingw32/5.2.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../../../src/gcc-5.2.0/configure --host=x86_64-w64-mingw32 --build=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --prefix=/mingw64 --with-sysroot=/c/mingw520/x86_64-520-posix-seh-rt_v4-rev0/mingw64 --with-gxx-include-dir=/mingw64/x86_64-w64-mingw32/include/c++ --enable-shared --enable-static --disable-multilib --enable-languages=c,c++,fortran,objc,obj-c++,lto --enable-libstdcxx-time=yes --enable-threads=posix --enable-libgomp --enable-libatomic --enable-lto --enable-graphite --enable-checking=release --enable-fully-dynamic-string --enable-version-specific-runtime-libs --disable-isl-version-check --disable-libstdcxx-pch --disable-libstdcxx-debug --enable-bootstrap --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-gnu-as --with-gnu-ld --with-arch=nocona --with-tune=core2 --with-libiconv --with-system-zlib --with-gmp=/c/mingw520/prerequisites/x86_64-w64-mingw32-static --with-mpfr=/c/mingw520/prerequisites/x86_64-w64-mingw32-static --with-mpc=/c/mingw520/prerequisites/x86_64-w64-mingw32-static --with-isl=/c/mingw520/prerequisites/x86_64-w64-mingw32-static --with-pkgversion='x86_64-posix-seh-rev0, Built by MinGW-W64 project' --with-bugurl=http://sourceforge.net/projects/mingw-w64 CFLAGS='-O2 -pipe -I/c/mingw520/x86_64-520-posix-seh-rt_v4-rev0/mingw64/opt/include -I/c/mingw520/prerequisites/x86_64-zlib-static/include -I/c/mingw520/prerequisites/x86_64-w64-mingw32-static/include' CXXFLAGS='-O2 -pipe -I/c/mingw520/x86_64-520-posix-seh-rt_v4-rev0/mingw64/opt/include -I/c/mingw520/prerequisites/x86_64-zlib-static/include -I/c/mingw520/prerequisites/x86_64-w64-mingw32-static/include' CPPFLAGS= LDFLAGS='-pipe -L/c/mingw520/x86_64-520-posix-seh-rt_v4-rev0/mingw64/opt/lib -L/c/mingw520/prerequisites/x86_64-zlib-static/lib -L/c/mingw520/prerequisites/x86_64-w64-mingw32-static/lib '
Thread model: posix
gcc version 5.2.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 

【问题讨论】:

编译器确实使用了额外的内存,并且将生成的代码与纯编写的汇编代码进行比较总是存在开销。但是请相信编译器,它们比每个汇编程序员更好地管理内存,而且这些微小的开销不会计算在内并损害您的内存。 它可能需要空间来存放其他内容,例如寄存器溢出、来自复杂表达式的临时值、异常处理标记和缓冲区溢出缓解金丝雀。 @deepmax "确实编译器使用了额外的内存" 没有minimal reproducible example 是不可能的。这个简单的program 显示.comm arr,80,32 在我看来就像一个 32 字节的边界... @sleeptightpupper:我说一般来说,重点是,使用高级语言(至少比汇编级别更高),你会在代码中产生一些开销。当然,有很多例子表明 C 可以生成优化和高效的汇编代码。 @sleeptightpupper 分配的堆栈空间量与 ABI 和 v.v. 无关 【参考方案1】:

因为你还没有启用优化。

如果不进行优化,编译器不会尝试将生成代码中的任何内容所需的空间或时间减至最少——它只是以最直接的方式生成代码。

如果您希望编译器生成体面的代码,请添加-O2(或者甚至只是-O1)或-Os

【讨论】:

【参考方案2】:

我总共需要 24 个字节。

编译器需要空间来存放返回地址和基指针。因为您处于 64 位模式,所以又是 16 个字节。总共 40。将其四舍五入到 32 字节的边界,得到 64。

【讨论】:

基指针和返回地址分别在栈上分配(分别由pushq %rbpcall指令),所以不包含在subq $64, %rsp中。【参考方案3】:

编译器确实可以为自己保留额外的内存。

Gcc 有一个标志-mpreferred-stack-boundary,用于设置它将保持的对齐方式。根据the documentation,默认为4,这应该产生16字节对齐,这是SSE指令所需要的。

作为VermillionAzure noted in a comment,您应该提供您的gcc 版本和编译时选项(使用gcc -v 显示这些)。

【讨论】:

当我反汇编不同版本的编译后的 c 代码时,这对我真正有用。更改 -mpreferred-stack-boundary 是更改为局部变量分配的空间的选项。 请注意,将-mpreferred-stack-boundary 设置为小于 4 会破坏 ABI,例如 Linux 上的 i386 System V,或任何地方的 x86-64 System V,它们需要 16 字节堆栈对齐在call 之前,并将生成需要对齐的 SIMD 加载/存储到堆栈内存,如果违反此 ABI 保证,则会出错。例如glibc scanf Segmentation faults when called from a function that doesn't align RSP 此外,GCC 确实 有一个错过优化的错误,有时会导致分配超出对齐 + 本地所需的额外 16 个字节,即使在 -O3: @987654324 @ 演示它。

以上是关于为啥编译器在堆栈中分配的比需要的多?的主要内容,如果未能解决你的问题,请参考以下文章

为啥局部变量在 Java 中是线程安全的

为啥 C 中没有“memsize”,它返回使用 malloc 在堆中分配的内存块的大小?

为啥编译器分配的内存比需要的多? [复制]

如何防止声明为局部变量的对象在堆栈中分配?

std::string 在内存中分配的位置在哪里?

如何查找变量是在堆栈还是堆中分配?