Does the stack get freed after scope block?

74 Views Asked by At

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?

1

There are 1 best solutions below

4
Craig Estey On BEST ANSWER

What happens is compiler dependent ...

  1. Both buffers could be put in the function stack frame (separately)
  2. An area could be put in the function stack frame that is (re)used by both blocks (e.g. either_buffer[80];)
  3. At the start of each block, the stack pointer (e.g. sp) is decremented. The buffer is acted upon. The stack pointer is incremented.
  4. At the start of each block the buffer is assigned a negative offset from the stack pointer (the stack pointer is unchanged).

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.