Search code examples
cscopestack

Does the stack get freed after scope block?


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?


Solution

  • 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.