浪费内存分配局部变量

时间:2016-02-07 04:37:46

标签: c assembly x86 gdb

这是我的计划:

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而不是0xbffff8ac中存储内容的变量0x00007a69?

缓冲区的问题。为什么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提供的书。

2 个答案:

答案 0 :(得分:6)

编译器试图在堆栈上保持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整除,因此保持对齐

上面添加的第二个和第三个数字是由编译器计算并在sub esp,0x28中使用的值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个字节。

然后我们将其用于更新buffer[0]字符&#39; A&#39;:

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

答案 1 :(得分:3)

除非你试图改进gcc的代码本身,否则理解为什么未经优化的代码与它一样糟糕,这主要是浪费时间。如果您想查看编译器对您的代码执行的操作,请查看-O3的输出;如果您希望查看源代码到asm的更直接的翻译,请查看-Og的输出。编写以args方式输入并以全局变量或返回值生成输出的函数,因此优化的asm不仅仅是ret

gcc -O0你不应该期待任何有效的方法。它是你脑源的最直接的文字翻译。

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

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

我首先想到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 允许被调用的函数在堆栈上破坏它的args,因此想要使用相同的args进行重复函数调用的调用者必须在调用之间继续存储它们。

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

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