浪费内存分配局部变量

这是我的计划:

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); } 

我用debug选项编译这个程序:

 gcc -g my_program.c 

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

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

我拆卸了主要的:

 (gdb) disassemble main Dump of assembler code for function main: 0x08048357 : push ebp 0x08048358 : mov ebp,esp 0x0804835a : sub esp,0x18 0x0804835d : and esp,0xfffffff0 0x08048360 : mov eax,0x0 0x08048365 : sub esp,eax 0x08048367 : mov DWORD PTR [esp+12],0x4 0x0804836f : mov DWORD PTR [esp+8],0x3 0x08048377 : mov DWORD PTR [esp+4],0x2 0x0804837f : mov DWORD PTR [esp],0x1 0x08048386 : call 0x8048344  0x0804838b : leave 0x0804838c : 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 : 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 

注意:我读过“ 开发艺术 ”一书,我使用VM提供的书。

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

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

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

在上一条指令之后, ESP现在是16字节对齐的。 我们从ESP的堆栈顶部开始调用呼叫的参数。 这样做是为了:

 0x08048367 : mov DWORD PTR [esp+12],0x4 0x0804836f : mov DWORD PTR [esp+8],0x3 0x08048377 : mov DWORD PTR [esp+4],0x2 0x0804837f : mov DWORD PTR [esp],0x1 

然后, CALL在堆栈上推送一个4字节的返回地址。 然后,我们在通话后达到以下说明:

 0x08048344 : push ebp ; 4 bytes pushed on stack 0x08048345 : mov ebp,esp ; Setup stackframe 

这会在堆栈上推送另外4个字节。 使用返回地址中的4个字节,我们现在未对齐8个字节。 要再次达到16字节对齐,我们需要在堆栈上浪费额外的8个字节。 这就是为什么在这个声明中分配了额外的8个字节:

 0x08048347 : sub esp,0x28 
  • 由于返回地址(4字节)和EBP (4字节),堆栈上已经有0x08字节
  • 将堆栈对齐回16字节对齐需要0x08字节的填充
  • 局部变量分配所需的0x20字节= 32字节。 32/16可被16整除,因此保持对齐

上面加上的第二个和第三个数字是由编译器计算并在sub esp,0x28使用的值sub esp,0x28

 0x0804834a : 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 : mov BYTE PTR [ebp-40],0x41 

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

 #include  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将告诉您)之前,这可能无法肯定地回答。 了解您的操作系统和操作系统版本也很有用。 更好的方法是将此命令的输出添加到您的问题gcc -Q -v -g my_program.c

除非你试图改进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++ ,但你可以使用-xc将输入视为C,而不是将其编译为C ++。这有时会改变asm输出,即使你不使用C99 / C11但C ++有任何function没有。(例如C99可变长度数组)。

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

我一开始认为test_function保留的额外空间是将其args复制到其堆栈框架中,但至少现代gcc不会这样做。 ( 64位gcc在到达寄存器时 会将 其args存储到内存中 ,但这是不同的.32位gcc会在堆栈中增加一个arg而不复制它 。)

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

使用-O0 clang 3.7.1 -O0其args复制到本地 ,但仍然只保留32( 0x20 )个字节。

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