My short explanation: every time you call a function, its vars get allocated on the stack. When you return from the function, you "pop" the stack, but all that means is that the stack pointer is now pointing to main. There is no reason to actually clear the stack memory as that would be a waste of CPU cycles. Therefore, if main() calls foo(), then foo() returns, the contents of the variables of foo() is still one frame higher on the frame stack and given the right memory address you can still access it.
As others point out, this is not something you should rely on. On the other hand if you are trying to overflow the stack or somehow break the program, this is definitely something to try.
The converse of this is that local variables of main() can be made accessibly to foo():
void foo(int *x) {
printf("a = %d\n", *x);
}
void main() {
int a = 12;
foo(&a);
}
This of course makes sense: when you are in the middle of foo(), main()'s variables have to be stored somewhere and are accessible.
Edit: here's another fun way to get a from foo():
void foo() {
int *x;
x = (int *) (&x + sizeof(int));
printf("a = %d\n", *x);
}
int main() {
int a = 12;
foo();
return 0;
}
Therefore, if main() calls foo(), then foo() returns, the contents of the variables of foo() is still one frame higher on the frame stack and given the right memory address you can still access it.
Unless an interrupt occurs between returning from foo() and reading those leftover stack variables. foo()'s abandoned stack space gets smashed by at least the return address from the interrupt handler, plus anything the handler itself pushes.
This falls into the class of use-after-free bugs. Like most instances of such, the technique works until something makes it not do so.
Great point! Yes, that's exactly what this is: use-after-free. That's why I am saying that this is useful when you are trying to somehow break the program, but not useful when you are doing constructive things. Presumably, when you are trying to break the program, you can run it multiple times, and chances are that at some point the interrupt will not happen.
"every time you call a function, its vars get allocated on the stack."
That's 'may get allocated'.
Also, even if you compile with -OnegativeInfinity, your 'another fun way' won't do what you think it does on architectures that store return addresses and locals on the same stack (= most CPUs that mortals will program) or that grow the stack 'up' (rare)
That -OnegativeInfinity is a pun. With many compiler chains, -O0 (hyphen-oh-zero) does 'no' optimization, -O1 does a bit more, -O2 some more, -O3 even more. That 'no' is on quotes because there is no such thing as 'no optimization'. For example, given enough locals, a compiler must do register allocation.
The hypothetical -OnegativeInfinity would truly do no optimization.
As others point out, this is not something you should rely on. On the other hand if you are trying to overflow the stack or somehow break the program, this is definitely something to try.
The converse of this is that local variables of main() can be made accessibly to foo():
This of course makes sense: when you are in the middle of foo(), main()'s variables have to be stored somewhere and are accessible.Edit: here's another fun way to get a from foo():