局部变量的内存分配浪费

Waste in memory allocation for local variables

这是我的程序:

void test_function(int a, int b, int c, int d){
    int flag;
    char buffer[10];

   flag = 31337;
   buffer[0] = 'A';
}

int main() {
    test_function(1, 2, 3, 4);
}

我用调试选项编译这个程序:

gcc -g my_program.c

我使用 gdb 并使用 intel 语法反汇编 test_function:

(gdb) disassemble test_function
Dump of assembler code for function test_function:
0x08048344 <test_function+0>:   push   ebp
0x08048345 <test_function+1>:   mov    ebp,esp
0x08048347 <test_function+3>:   sub    esp,0x28
0x0804834a <test_function+6>:   mov    DWORD PTR [ebp-12],0x7a69
0x08048351 <test_function+13>:  mov    BYTE PTR [ebp-40],0x41
0x08048355 <test_function+17>:  leave  
0x08048356 <test_function+18>:  ret    
End of assembler dump.

还有我主要拆解的:

(gdb) disassemble main
Dump of assembler code for function main:
0x08048357 <main+0>:    push   ebp
0x08048358 <main+1>:    mov    ebp,esp
0x0804835a <main+3>:    sub    esp,0x18
0x0804835d <main+6>:    and    esp,0xfffffff0
0x08048360 <main+9>:    mov    eax,0x0
0x08048365 <main+14>:   sub    esp,eax
0x08048367 <main+16>:   mov    DWORD PTR [esp+12],0x4
0x0804836f <main+24>:   mov    DWORD PTR [esp+8],0x3
0x08048377 <main+32>:   mov    DWORD PTR [esp+4],0x2
0x0804837f <main+40>:   mov    DWORD PTR [esp],0x1
0x08048386 <main+47>:   call   0x8048344 <test_function>
0x0804838b <main+52>:   leave  
0x0804838c <main+53>:   ret    
End of assembler dump.

我在这个地址下了一个断点:0x08048355(为 test_function 保留指令)并且我 运行 程序。

我是这样看栈的:

(gdb) x/16w $esp
0xbffff7d0:     0x00000041      0x08049548      0xbffff7e8      0x08048249
0xbffff7e0:     0xb7f9f729      0xb7fd6ff4      0xbffff818      0x00007a69
0xbffff7f0:     0xb7fd6ff4      0xbffff8ac      0xbffff818      0x0804838b
0xbffff800:     0x00000001      0x00000002      0x00000003      0x00000004

0x0804838b 是 return 地址,0xbffff818 是保存的帧指针(主 ebp),标志变量进一步存放 12 个字节。为什么是 12?

我不明白这条指令:

0x0804834a <test_function+6>:   mov    DWORD PTR [ebp-12],0x7a69

为什么我们不在 ebp-4 中存储内容变量 0x00007a69 而不是 0xbffff8ac?

缓冲区的相同问题。为什么是 40?

我们不浪费内存? 0xb7fd6ff4 0xbffff8ac 和 0xb7f9f729 0xb7fd6ff4 0xbffff818 0x08049548 0xbffff7e8 0x08048249 没有使用?

这是命令 gcc -Q -v -g my_program.c:

的输出
Reading specs from /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/specs
Configured with: ../src/configure -v --enable-languages=c,c++ --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-gxx-include-dir=/usr/include/c++/3.3 --enable-shared --enable-__cxa_atexit --with-system-zlib --enable-nls --without-included-gettext --enable-clocale=gnu --enable-debug i486-linux-gnu
Thread model: posix
gcc version 3.3.6 (Ubuntu 1:3.3.6-15ubuntu1)
 /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/cc1 -v -D__GNUC__=3 -D__GNUC_MINOR__=3 -D__GNUC_PATCHLEVEL__=6 notesearch.c -dumpbase notesearch.c -auxbase notesearch -g -version -o /tmp/ccGT0kTf.s
GNU C version 3.3.6 (Ubuntu 1:3.3.6-15ubuntu1) (i486-linux-gnu)
        compiled by GNU C version 3.3.6 (Ubuntu 1:3.3.6-15ubuntu1).
GGC heuristics: --param ggc-min-expand=99 --param ggc-min-heapsize=129473
options passed:  -v -D__GNUC__=3 -D__GNUC_MINOR__=3 -D__GNUC_PATCHLEVEL__=6
 -auxbase -g
options enabled:  -fpeephole -ffunction-cse -fkeep-static-consts
 -fpcc-struct-return -fgcse-lm -fgcse-sm -fsched-interblock -fsched-spec
 -fbranch-count-reg -fcommon -fgnu-linker -fargument-alias
 -fzero-initialized-in-bss -fident -fmath-errno -ftrapping-math -m80387
 -mhard-float -mno-soft-float -mieee-fp -mfp-ret-in-387
 -maccumulate-outgoing-args -mcpu=pentiumpro -march=i486
ignoring nonexistent directory "/usr/local/include/i486-linux-gnu"
ignoring nonexistent directory "/usr/i486-linux-gnu/include"
ignoring nonexistent directory "/usr/include/i486-linux-gnu"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/include
 /usr/include
End of search list.
 gnu_dev_major gnu_dev_minor gnu_dev_makedev stat lstat fstat mknod fatal ec_malloc dump main print_notes find_user_note search_note
Execution times (seconds)
 preprocessing         :   0.00 ( 0%) usr   0.01 (25%) sys   0.00 ( 0%) wall
 lexical analysis      :   0.00 ( 0%) usr   0.01 (25%) sys   0.00 ( 0%) wall
 parser                :   0.02 (100%) usr   0.01 (25%) sys   0.00 ( 0%) wall
 TOTAL                 :   0.02             0.04             0.00
 as -V -Qy -o /tmp/ccugTYeu.o /tmp/ccGT0kTf.s
GNU assembler version 2.17.50 (i486-linux-gnu) using BFD version 2.17.50 20070103 Ubuntu
 /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../../crt1.o /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../../crti.o /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/crtbegin.o -L/usr/lib/gcc-lib/i486-linux-gnu/3.3.6 -L/usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../.. /tmp/ccugTYeu.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/crtend.o /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../../crtn.o

注意:我阅读了《The art of exploitation》一书,并且使用了本书提供的虚拟机。

除非您试图改进 gcc 的代码本身,否则了解为什么未优化的代码如此糟糕将主要是浪费时间。如果您想查看编译器对您的代码做了什么,请查看 -O3 的输出,或者如果您想查看源代码到 asm 的更直接的翻译,请查看 -Og 的输出。编写在 args 中接受输入并在全局变量或 return 值中产生输出的函数,因此优化的 asm 不仅仅是 ret.


您不应该期望 gcc -O0 有任何高效。它对您的来源进行了最脑残的直译。

我无法在 http://gcc.godbolt.org/ 上使用任何 gcc 或 clang 版本重现该 asm 输出。 (gcc 4.4.7 到 gcc 5.3.0,clang 3.0 到 clang 3.7.1)。 (请注意,godbolt 使用 g++,但您可以使用 -x c 将输入视为 C,而不是将其编译为 C++。这有时会更改 asm 输出,即使您不使用任何C99 / C11 具有但 C++ 没有的功能。(例如 C99 可变长度数组)。

某些版本的 gcc 默认会发出额外的代码,除非我使用 -fno-stack-protector

起初我以为 test_function 保留的额外 space 是将其 args 向下复制到其堆栈框架中,但至少现代 gcc 不会这样做。 (64bit gcc does store its args into memory when they arrive in registers, but that's different. 32bit gcc will increment an arg in place on the stack, without copying it.)

ABI 确实 允许被调用函数在堆栈上破坏其参数,因此想要使用相同参数进行重复函数调用的调用者必须继续存储它们在通话之间。

clang 3.7.1 with -O0 does copy its args down into locals,但仍然只保留 32 (0x20) 个字节。

除非您告诉我们您使用的是哪个版本的 gcc,否则这是您将获得的最佳答案...


编译器试图在堆栈上保持 16 字节对齐。这也适用于如今的 32 位代码(不仅仅是 64 位)。这个想法是,在执行 CALL 指令之前,堆栈必须与 16 字节边界对齐。

因为你编译时没有优化,所以有一些无关的指令。

0x0804835a <main+3>:    sub    esp,0x18        ; Allocate local stack space
0x0804835d <main+6>:    and    esp,0xfffffff0  ; Ensure `main` has a 16 byte aligned stack
0x08048360 <main+9>:    mov    eax,0x0         ; Extraneous, not needed
0x08048365 <main+14>:   sub    esp,eax         ; Extraneous, not needed

ESP 现在在上面最后一条指令之后是 16 字节对齐的。我们将调用的参数从堆栈顶部 ESP 开始。这是通过以下方式完成的:

0x08048367 <main+16>:   mov    DWORD PTR [esp+12],0x4
0x0804836f <main+24>:   mov    DWORD PTR [esp+8],0x3
0x08048377 <main+32>:   mov    DWORD PTR [esp+4],0x2
0x0804837f <main+40>:   mov    DWORD PTR [esp],0x1

然后 CALL 将一个 4 字节的 return 地址压入堆栈。然后我们在通话后到达这些说明:

0x08048344 <test_function+0>:   push   ebp     ; 4 bytes pushed on stack
0x08048345 <test_function+1>:   mov    ebp,esp ; Setup stackframe

这会将另外 4 个字节压入堆栈。使用来自 return 地址的 4 个字节,我们现在错位了 8 个字节。要再次达到 16 字节对齐,我们需要在堆栈上浪费额外的 8 个字节。这就是为什么在此语句中分配了额外的 8 个字节:

0x08048347 <test_function+3>:   sub    esp,0x28
  • 0x08 字节已经在堆栈上,因为 return 地址(4 字节)和 EBP(4 字节)
  • 将堆栈对齐回 16 字节对齐需要 0x08 字节的填充
  • 局部变量分配需要 0x20 个字节 = 32 个字节。 32/16 可以被 16 整除所以保持对齐

上面的第二个和第三个数字相加就是编译器计算出的值0x28,用在sub esp,0x28.

0x0804834a <test_function+6>:   mov    DWORD PTR [ebp-12],0x7a69

那么为什么要在这条指令中使用[ebp-12]呢?前 8 个字节 [ebp-8][ebp-1] 是用于使堆栈 16 字节对齐的对齐字节。之后本地数据将出现在堆栈中。在这种情况下,[ebp-12][ebp-9] 是 32 位整数 flag 的 4 个字节。

然后我们用字符 'A':

更新 buffer[0]
0x08048351 <test_function+13>:  mov    BYTE PTR [ebp-40],0x41

奇怪的是为什么一个 10 字节的字符数组会出现从 [ebp+40](数组的开头)到 [ebp+13],即 28 字节。我能做出的最佳猜测是编译器认为它可以将 10 字节字符数组视为 128 位(16 字节)向量。这将强制编译器将缓冲区对齐到 16 字节边界,并将数组填充到 16 字节(128 位)。从编译器的角度来看,您的代码似乎很像定义为:

#include <xmmintrin.h>
void test_function(int a, int b, int c, int d){
    int flag;
    union {
        char buffer[10];
        __m128 m128buffer;      ; 16-byte variable that needs to be 16-bytes aligned
    } bufu;

   flag = 31337;
   bufu.buffer[0] = 'A';
}

GodBolt for GCC 4.9.0 在启用 SSE2 的情况下生成 32 位代码的输出如下所示:

test_function:
        push    ebp     #
        mov     ebp, esp  #, 
        sub     esp, 40   #,same as: sub esp,0x28
        mov     DWORD PTR [ebp-12], 31337 # flag,
        mov     BYTE PTR [ebp-40], 65     # bufu.buffer,
        leave
        ret

这看起来与您在 GDB 中的反汇编非常相似。

如果您使用优化进行编译(例如 -O1-O2-O3),优化器可能会简化 test_function,因为它是您的叶函数例子。叶函数是不调用另一个函数的函数。编译器可能应用了某些快捷方式。

至于为什么字符数组好像是对齐到16字节的边界,然后填充成16字节?在我们知道您使用的 GCC 编译器(gcc --version 会告诉您)之前,可能无法确定地回答这个问题。了解您的 OS 和 OS 版本也很有用。更好的方法是将此命令的输出添加到您的问题 gcc -Q -v -g my_program.c