abdo Salm
abdo Salm

Reputation: 1841

C local variables doesn't get destructed after the end of their scope

in order to understand what happens I used a sample code which is :

#include <stdio.h>
int main()
{
  for(int i=0;i<5;i++)
  {
      int a=i;
      printf("a=%x\n",&a);
  }

    return 0;
}

and I generated assembly file produced by gcc using this command line gcc -fverbose-asm main1.c -S -o main1.s. and here is the file output :

        .file   "main1.c"
 # GNU C17 (MinGW.org GCC Build-2) version 9.2.0 (mingw32)
 #  compiled by GNU C version 9.2.0, GMP version 6.1.2, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.21-GMP

 # GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
 # options passed:  -iprefix c:\mingw\bin\../lib/gcc/mingw32/9.2.0/ main1.c
 # -mtune=generic -march=i586 -auxbase-strip main1.s -fverbose-asm
 # options enabled:  -faggressive-loop-optimizations -fassume-phsa
 # -fasynchronous-unwind-tables -fauto-inc-dec -fcommon
 # -fdelete-null-pointer-checks -fdwarf2-cfi-asm -fearly-inlining
 # -feliminate-unused-debug-types -ffp-int-builtin-inexact -ffunction-cse
 # -fgcse-lm -fgnu-runtime -fgnu-unique -fident -finline-atomics
 # -fipa-stack-alignment -fira-hoist-pressure -fira-share-save-slots
 # -fira-share-spill-slots -fivopts -fkeep-inline-dllexport
 # -fkeep-static-consts -fleading-underscore -flifetime-dse
 # -flto-odr-type-merging -fmath-errno -fmerge-debug-strings -fpeephole
 # -fplt -fprefetch-loop-arrays -freg-struct-return
 # -fsched-critical-path-heuristic -fsched-dep-count-heuristic
 # -fsched-group-heuristic -fsched-interblock -fsched-last-insn-heuristic
 # -fsched-rank-heuristic -fsched-spec -fsched-spec-insn-heuristic
 # -fsched-stalled-insns-dep -fschedule-fusion -fsemantic-interposition
 # -fset-stack-executable -fshow-column -fshrink-wrap-separate
 # -fsigned-zeros -fsplit-ivs-in-unroller -fssa-backprop -fstdarg-opt
 # -fstrict-volatile-bitfields -fsync-libcalls -ftrapping-math
 # -ftree-cselim -ftree-forwprop -ftree-loop-if-convert -ftree-loop-im
 # -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
 # -ftree-phiprop -ftree-reassoc -ftree-scev-cprop -funit-at-a-time
 # -funwind-tables -fverbose-asm -fzero-initialized-in-bss -m32 -m80387
 # -m96bit-long-double -maccumulate-outgoing-args -malign-double
 # -malign-stringops -mavx256-split-unaligned-load
 # -mavx256-split-unaligned-store -mfancy-math-387 -mfp-ret-in-387
 # -mieee-fp -mlong-double-80 -mms-bitfields -mno-red-zone -mno-sse4
 # -mpush-args -msahf -mstack-arg-probe -mstv -mvzeroupper

    .text
    .def    ___main;    .scl    2;  .type   32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "a=%x\12\0"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB13:
    .cfi_startproc
    pushl   %ebp     #
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp   #,
    .cfi_def_cfa_register 5
    andl    $-16, %esp   #,
    subl    $32, %esp    #,
 # main1.c:3: {
    call    ___main  #
 # main1.c:4:   for(int i=0;i<5;i++)
    movl    $0, 28(%esp)     #, i
 # main1.c:4:   for(int i=0;i<5;i++)
    jmp L2   #
L3:
 # main1.c:6:       int a=i;
    movl    28(%esp), %eax   # i, tmp84
    movl    %eax, 24(%esp)   # tmp84, a
 # main1.c:7:       printf("a=%x\n",&a);
    leal    24(%esp), %eax   #, tmp85
    movl    %eax, 4(%esp)    # tmp85,
    movl    $LC0, (%esp)     #,
    call    _printf  #
 # main1.c:4:   for(int i=0;i<5;i++)
    addl    $1, 28(%esp)     #, i
L2:
 # main1.c:4:   for(int i=0;i<5;i++)
    cmpl    $4, 28(%esp)     #, i
    jle L3   #,
 # main1.c:10:  return 0;
    movl    $0, %eax     #, _5
 # main1.c:11: }
    leave   
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret 
    .cfi_endproc
LFE13:
    .ident  "GCC: (MinGW.org GCC Build-2) 9.2.0"
    .def    _printf;    .scl    2;  .type   32; .endef

notice that from following lines of code in the assembly file :

 # main1.c:6:       int a=i;
movl    28(%esp), %eax   # i, tmp84
movl    %eax, 24(%esp)   # tmp84, a

which means that local variable named a is stored in stack at byte number 24 from stack pointer and the local variable named i is stored in stack at byte position number 28 .

so let's make other version of the code where the new code is :

    #include <stdio.h>
int main()
{
    for(int i=0;i<5;i++)
    {
        int a=i;
        printf("a=%x\n",&a);
    }
    int y = 10;
    int a = 5;

    return 0;
}

and the new generated assembly file is :

        .file   "main1.c"
 # GNU C17 (MinGW.org GCC Build-2) version 9.2.0 (mingw32)
 #  compiled by GNU C version 9.2.0, GMP version 6.1.2, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.21-GMP

 # GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
 # options passed:  -iprefix c:\mingw\bin\../lib/gcc/mingw32/9.2.0/ main1.c
 # -mtune=generic -march=i586 -auxbase-strip main2.s -fverbose-asm
 # options enabled:  -faggressive-loop-optimizations -fassume-phsa
 # -fasynchronous-unwind-tables -fauto-inc-dec -fcommon
 # -fdelete-null-pointer-checks -fdwarf2-cfi-asm -fearly-inlining
 # -feliminate-unused-debug-types -ffp-int-builtin-inexact -ffunction-cse
 # -fgcse-lm -fgnu-runtime -fgnu-unique -fident -finline-atomics
 # -fipa-stack-alignment -fira-hoist-pressure -fira-share-save-slots
 # -fira-share-spill-slots -fivopts -fkeep-inline-dllexport
 # -fkeep-static-consts -fleading-underscore -flifetime-dse
 # -flto-odr-type-merging -fmath-errno -fmerge-debug-strings -fpeephole
 # -fplt -fprefetch-loop-arrays -freg-struct-return
 # -fsched-critical-path-heuristic -fsched-dep-count-heuristic
 # -fsched-group-heuristic -fsched-interblock -fsched-last-insn-heuristic
 # -fsched-rank-heuristic -fsched-spec -fsched-spec-insn-heuristic
 # -fsched-stalled-insns-dep -fschedule-fusion -fsemantic-interposition
 # -fset-stack-executable -fshow-column -fshrink-wrap-separate
 # -fsigned-zeros -fsplit-ivs-in-unroller -fssa-backprop -fstdarg-opt
 # -fstrict-volatile-bitfields -fsync-libcalls -ftrapping-math
 # -ftree-cselim -ftree-forwprop -ftree-loop-if-convert -ftree-loop-im
 # -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
 # -ftree-phiprop -ftree-reassoc -ftree-scev-cprop -funit-at-a-time
 # -funwind-tables -fverbose-asm -fzero-initialized-in-bss -m32 -m80387
 # -m96bit-long-double -maccumulate-outgoing-args -malign-double
 # -malign-stringops -mavx256-split-unaligned-load
 # -mavx256-split-unaligned-store -mfancy-math-387 -mfp-ret-in-387
 # -mieee-fp -mlong-double-80 -mms-bitfields -mno-red-zone -mno-sse4
 # -mpush-args -msahf -mstack-arg-probe -mstv -mvzeroupper

    .text
    .def    ___main;    .scl    2;  .type   32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "a=%x\12\0"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB13:
    .cfi_startproc
    pushl   %ebp     #
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp   #,
    .cfi_def_cfa_register 5
    andl    $-16, %esp   #,
    subl    $32, %esp    #,
 # main1.c:3: {
    call    ___main  #
 # main1.c:4:     for(int i=0;i<5;i++)
    movl    $0, 28(%esp)     #, i
 # main1.c:4:     for(int i=0;i<5;i++)
    jmp L2   #
L3:
 # main1.c:6:         int a=i;
    movl    28(%esp), %eax   # i, tmp84
    movl    %eax, 16(%esp)   # tmp84, a
 # main1.c:7:         printf("a=%x\n",&a);
    leal    16(%esp), %eax   #, tmp85
    movl    %eax, 4(%esp)    # tmp85,
    movl    $LC0, (%esp)     #,
    call    _printf  #
 # main1.c:4:     for(int i=0;i<5;i++)
    addl    $1, 28(%esp)     #, i
L2:
 # main1.c:4:     for(int i=0;i<5;i++)
    cmpl    $4, 28(%esp)     #, i
    jle L3   #,
 # main1.c:9:     int y = 10;
    movl    $10, 24(%esp)    #, y
 # main1.c:10:  int a = 5;
    movl    $5, 20(%esp)     #, a
 # main1.c:12:     return 0;
    movl    $0, %eax     #, _7
 # main1.c:13: }
    leave   
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret 
    .cfi_endproc
LFE13:
    .ident  "GCC: (MinGW.org GCC Build-2) 9.2.0"
    .def    _printf;    .scl    2;  .type   32; .endef

now notice the following lines :

 # main1.c:6:         int a=i;
movl    28(%esp), %eax   # i, tmp84
movl    %eax, 16(%esp)   # tmp84, a

which means that local variable named a inside loop is stored in stack at byte number 16 from stack pointer base and the local variable named i is stored in stack at byte position number 28 offset from base esp register.

after the loop ends there are 2 other local variables created which are a and y from the following lines of assembly code :

# main1.c:9:     int y = 10;
movl    $10, 24(%esp)    #, y
# main1.c:10:  int a = 5;
movl    $5, 20(%esp)     #, a

this means that variable a and y using addresses 20 and 24 offset from stack pointer and not reusing the destroyed places of previous local variables named a and i , so why is that ?

let's take a look to another code example :

    #include <stdio.h>
int main()
{
    int *ptr;
    for(int i=0;i<5;i++)
    {
        int a=10;
        ptr = &a;
        int x;
    }
    int y = 10;

    printf("a = %d\n",*ptr); // how come a = 10?
    return 0;
}

in this code , I made a dangling pointer and notice the output :

enter image description here

so why gcc doesn't destroy variables whose scope ended and save some memory ?

Upvotes: 0

Views: 234

Answers (3)

sudo pkill depression
sudo pkill depression

Reputation: 181

I can speak to some of your questions. I think you're expecting behavior that is not required by the standard.

In your second example:

int *p;
{
  int a = 10;
  p = &a;
}
printf("%d", *p);

There is nothing illegal happening. But this is definitely a common bug. What youre doing is:

  1. creating a pointer called p that points to an int
  2. stuff
  3. printing the value (interpreted as an int) found at the address p points to

C is pretty raw, you asked it to print the value at that address, it did. It just happened to be the value that you assigned it. There is no guarantees that it will be this value. There is no guarantees that you own the memory p points to. There are no guarantees on what value will be there. Seems like your compiler didnt free up a's memory, so you got lucky and didnt get a segfault.

This is all dandy..but it seems like you understand the behavior and are asking why. In other words, "why did gcc let me do this?". The answer is simple but unsatisfying. gcc did what it is required to do, no more. It would actually be inefficient for gcc to immediately put the memory back. (Note I say inefficient for simplicity. How gcc determines stuff is very complex).
Imagine code like below:

int main()
{
  { int a; printf("%d", a); }
  { int a; printf("%d", a); }
  { int a; printf("%d", a); }
  // 100 more of these lines
  return 0;
}

It would be legal for gcc to keep memory after every block. It would also be legal for gcc to cleanup memory after every block. It is much more efficient if gcc repurposed the code as below (written in c for easy comparison with the last code block):

int main()
{
  int a;
  { printf("%d", a); }
  { printf("%d", a); }
  { printf("%d", a); }
  // 100 more of these lines
  return 0;
}

As to your question regarding Why does gcc put items in the stack not immediately near the top, I am not sure. I am not familiar with the algorithm for adding to the stack. I will leave it to someone more knowledgeable than me for that.
legal disclaimer: The stack behavior is the work of gcc and is not the opinion of c. The c standards don't dictate how to modify the stack, as long as the effects are correct.

Edit: I'm dumb and apparently answered a question you never asked? I'm tired :(

Upvotes: 0

chrslg
chrslg

Reputation: 13346

Note that if you do the same experiment, but with arrays (so stored also in the stacks, but much bigger), you'll that the memory is recycled.

int testfunc(){
   for(int i=0; i<10; i++){
      int arr[1000];
      arr[100]=23; // I use specific numbers to find it faster in generated code
   }
   int arr2[1000];
   arr[100]=43;
}

Generates

    (...)
    movl    $23, -3616(%rbp)
    (...)
    movl    $43, -3616(%rbp)

So, no push nor pop, nor any changes on rbp and rsp. It is not like a new local scope is started and then poped back. But memory of arr is used again for arr2.

So, apparently, when it is important, inaccessible memory stays not "allocated" in the stack.

Upvotes: 2

Stephen Newell
Stephen Newell

Reputation: 7838

To "destruct" variables in C, it would require adjusting the stack pointer after leaving scope. Instead, gcc can just move the stack pointer once, when the function terminates, and you get better runtime. Memory usage increases as you've noticed, but this is usually an okay tradeoff.

Upvotes: 0

Related Questions