In C/C++, is the memory on the stack freed after finishing a scope block and can it be reused?
For example, let's say I have 100 free bytes on the stack after enetering the function.
void function(void)
{
{
uint8_t buffer1[80];
// Do something with the buffer
}
{
uint8_t buffer2[80];
// Do Something with the buffer
}
}
Can enough memory be allocated for buffer2 or is the memory used for buffer1 only freed at the end of the function?
What happens is compiler dependent ...
either_buffer[80];
)sp
) is decremented. The buffer is acted upon. The stack pointer is incremented.Of course, things placed in the function's stack frame are only "freed" when the function returns.
(1) Separate buffers in the function stack frame (pseudo code):
typedef unsigned char uint8_t;
void do_something(uint8_t *);
uint8_t *sp;
void
function(void)
{
uint8_t buffer1[80];
uint8_t buffer2[80];
{
// Do something with the buffer
do_something(buffer1);
}
{
// Do Something with the buffer
do_something(buffer2);
}
}
(2) A single area in the function stack frame (pseudo code):
typedef unsigned char uint8_t;
void do_something(uint8_t *);
uint8_t *sp;
void
function(void)
{
uint8_t either_buffer[80];
{
uint8_t *buffer1 = either_buffer;
// Do something with the buffer
do_something(buffer1);
}
{
uint8_t *buffer2 = either_buffer;
// Do Something with the buffer
do_something(buffer2);
}
}
(3) Stack pointer is incremented/decremented as needed (pseudo code):
typedef unsigned char uint8_t;
void do_something(uint8_t *);
uint8_t *sp;
void
function(void)
{
{
uint8_t *buffer1 = sp -= 80;
// Do something with the buffer
do_something(buffer1);
sp += 80;
}
{
uint8_t *buffer2 = sp -= 80;
// Do Something with the buffer
do_something(buffer2);
sp += 80;
}
}
(4) What actually happens (for x86, with gcc 8.3.1):
typedef unsigned char uint8_t;
void do_something(uint8_t *);
uint8_t *sp;
void
function(void)
{
{
uint8_t buffer1[80];
// Do something with the buffer
do_something(buffer1);
}
{
uint8_t buffer2[80];
// Do Something with the buffer
do_something(buffer2);
}
}
Here is the assembly code:
.file "orig.c"
.text
.comm sp,8,8
.globl function
.type function, @function
function:
.LFB0:
.cfi_startproc
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
subq $80, %rsp #,
# orig.c:14: do_something(buffer1);
leaq -80(%rbp), %rax #, tmp87
movq %rax, %rdi # tmp87,
call do_something #
# orig.c:20: do_something(buffer2);
leaq -80(%rbp), %rax #, tmp88
movq %rax, %rdi # tmp88,
call do_something #
# orig.c:22: }
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size function, .-function
.ident "GCC: (GNU) 8.3.1 20190223 (Red Hat 8.3.1-2)"
.section .note.GNU-stack,"",@progbits
This is the equivalent of the following pseudo code:
typedef unsigned char uint8_t;
void do_something(uint8_t *);
uint8_t *sp;
void
function(void)
{
{
uint8_t *buffer1 = sp - 80;
// Do something with the buffer
do_something(buffer1);
}
{
uint8_t *buffer2 = sp - 80;
// Do Something with the buffer
do_something(buffer2);
}
}
For clang
7.0.1, we get:
.text
.file "orig.c"
.globl function # -- Begin function function
.p2align 4, 0x90
.type function,@function
function: # @function
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $160, %rsp
leaq -80(%rbp), %rdi
callq do_something
leaq -160(%rbp), %rdi
callq do_something
addq $160, %rsp
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end0:
.size function, .Lfunc_end0-function
.cfi_endproc
# -- End function
.type sp,@object # @sp
.comm sp,8,8
.ident "clang version 7.0.1 (Fedora 7.0.1-6.fc29)"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym function
.addrsig_sym do_something
.addrsig_sym sp
For c++
8.3.1, it is similar to gcc
:
.file "orig2.cpp"
.text
.globl sp
.bss
.align 8
.type sp, @object
.size sp, 8
sp:
.zero 8
.text
.globl _Z8functionv
.type _Z8functionv, @function
_Z8functionv:
.LFB0:
.cfi_startproc
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
subq $80, %rsp #,
# orig2.cpp:14: do_something(buffer1);
leaq -80(%rbp), %rax #, tmp87
movq %rax, %rdi # tmp87,
call _Z12do_somethingPh #
# orig2.cpp:20: do_something(buffer2);
leaq -80(%rbp), %rax #, tmp88
movq %rax, %rdi # tmp88,
call _Z12do_somethingPh #
# orig2.cpp:22: }
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size _Z8functionv, .-_Z8functionv
.ident "GCC: (GNU) 8.3.1 20190223 (Red Hat 8.3.1-2)"
.section .note.GNU-stack,"",@progbits
For gcc
, with -m32
, we get:
.file "orig.c"
.text
.comm sp,4,4
.globl function
.type function, @function
function:
.LFB0:
.cfi_startproc
pushl %ebp #
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp #,
.cfi_def_cfa_register 5
subl $88, %esp #,
# orig.c:14: do_something(buffer1);
subl $12, %esp #,
leal -88(%ebp), %eax #, tmp87
pushl %eax # tmp87
call do_something #
addl $16, %esp #,
# orig.c:20: do_something(buffer2);
subl $12, %esp #,
leal -88(%ebp), %eax #, tmp88
pushl %eax # tmp88
call do_something #
addl $16, %esp #,
# orig.c:22: }
nop
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size function, .-function
.ident "GCC: (GNU) 8.3.1 20190223 (Red Hat 8.3.1-2)"
.section .note.GNU-stack,"",@progbits
For clang, with -m32
, we get:
.text
.file "orig.c"
.globl function # -- Begin function function
.p2align 4, 0x90
.type function,@function
function: # @function
# %bb.0:
pushl %ebp
movl %esp, %ebp
subl $168, %esp
leal -80(%ebp), %eax
movl %eax, (%esp)
calll do_something
leal -160(%ebp), %eax
movl %eax, (%esp)
calll do_something
addl $168, %esp
popl %ebp
retl
.Lfunc_end0:
.size function, .Lfunc_end0-function
# -- End function
.type sp,@object # @sp
.comm sp,4,4
.ident "clang version 7.0.1 (Fedora 7.0.1-6.fc29)"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym function
.addrsig_sym do_something
.addrsig_sym sp
The above [actual] asm is generated without optimization, so with (e.g. -O2
), the code could be slightly different [not shown].
As it is, the gcc
code seems to be slightly more optimal than the clang
code by default.