【问题标题】:Waste in memory allocation for local variables局部变量的内存分配浪费
【发布时间】:2016-05-16 22:38:47
【问题描述】:

这是我的程序:

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 是返回地址,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》一书,并使用了随书提供的 VM。

【问题讨论】:

  • 虽然您接受了答案,但我仍然对您使用的 GCC 很好奇。您能告诉我们您的操作系统(和操作系统版本)吗?您能否将此命令的输出添加到您的问题gcc -Q -v -g my_program.c 的底部,以便为我们提供可能有助于确切了解您从何处获得此特定汇编代码的信息编译器?
  • 感谢您提供更多信息。有机会我会做一些调查。
  • 乍一看,大于 4 字节的数组似乎与最近的 16 字节边界对齐。如果您使用-Os 编译,它似乎与我期望的边界对齐。 GCC 大于 4 的版本似乎没有这样做。旧的 GCC 编译器中可能存在对齐错误。

标签: c assembly x86 gdb disassembly


【解决方案1】:

除非您尝试改进 gcc 的代码本身,否则理解为什么未优化的代码会如此糟糕,主要是浪费时间。如果您想查看编译器对您的代码做了什么,请查看-O3 的输出,或者如果您想查看将源代码更直接地翻译成asm,请查看-Og 的输出。编写在 args 中接受输入并在全局变量或返回值中产生输出的函数,因此优化的 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 可变长度数组)。

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

一开始我以为test_function保留的额外空间是为了将它的args复制到它的堆栈帧中,但至少现代gcc不会这样做。 (64bit gcc does store its args into memory when they arrive in registers,但不一样。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,否则这是您将获得的最佳答案...


【讨论】:

    【解决方案2】:

    编译器试图在堆栈上保持 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 字节的返回地址压入堆栈。然后我们在调用后得到这些指令:

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

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

    0x08048347 <test_function+3>:   sub    esp,0x28
    
    • 由于返回地址(4 字节)和 EBP(4 字节),堆栈上已经有 0x08 个字节
    • 将堆栈对齐回 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
    

    那么奇怪的是为什么从[ebp+40](数组的开头)到[ebp+13](即28字节)会出现一个10字节的字符数组。我能做出的最好猜测是编译器认为它可以将 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';
    }
    

    在启用SSE2的情况下GodBolt for GCC 4.9.0生成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 会告诉您)。了解您的操作系统和操作系统版本也很有用。更好的是将此命令的输出添加到您的问题gcc -Q -v -g my_program.c

    【讨论】:

      猜你喜欢
      • 2012-08-12
      • 2011-08-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-09
      • 1970-01-01
      • 2012-05-26
      • 2023-03-10
      相关资源
      最近更新 更多