Technically, this code goes far beyond what the C standard defines, so it can do something. He makes a huge number of assumptions that he has no right, and these assumptions, of course, are not universal. However, I can put forward a very probable explanation of why you see the conclusion you are making:
You are right until the moment when you copied the code of the function g() to the memory occupied by the local variable of the array a .
To understand the next line, you need to know a little about how functions usually invoke common stack-based architectures. When a function is called, parameters are pushed onto the stack, then the return address is pushed onto the stack, and execution moves to the starting point of the function. Inside the function, the previous frame pointer is pushed onto the stack, then a place is created for local variables. Stacks tend to grow down in memory (from high addresses to low addresses), although this does not apply to all common architectures.
So, when the main calls to the f() function, the stack first looks like this (the frame pointer and the stack pointer are two CPU registers containing the addresses of the locations on the stack):
| ... | (higher addresses) | char **argv (parameter) | |-------------------------| | int argc (parameter) | |-------------------------| FRAME POINTER -> | saved frame pointer | |-------------------------| | int a | |-------------------------| | int x (parameter) | &x |-------------------------| STACK POINTER -> | return address | &x - 1 |-------------------------| | ... | (lower addresses)
The function prolog then saves the frame pointer of the calling function and moves the stack pointer to create space for local variables in f() . Therefore, when the C code in f() starts execution, the stack now looks something like this:
| ... | (higher addresses) | char **argv (parameter) | |-------------------------| | int argc (parameter) | |-------------------------| | saved frame pointer | |-------------------------| | int a | |-------------------------| | int x (parameter) | &x |-------------------------| | return address | &x - 1 |-------------------------| FRAME POINTER -> | saved frame pointer | |-------------------------| | a[99] | &a[99] | a[98] | &a[98] | ... | ... STACK POINTER -> | a[0] | &a[0] | ... | (lower addresses)
What is a frame pointer? It is used to refer to local variables and parameters inside a function. The compiler knows that when f() executed, the address of the local variable a always FRAME_POINTER - 100 * sizeof(int) , and the address of the x parameter is FRAME_POINTER + sizeof(FRAME_POINTER) + sizeof(RETURN_ADDRESS) . All local variables and parameters can be accessed as a fixed offset from the frame pointer, regardless of how the stack pointer moves when the stack space is allocated and freed.
Anyway, back to the code. When this line is executed:
x = *(&x-1) ;
It copies the value, which is stored 1 integer size lower in memory than x , to x . If you look at my ASCII art, you will see that it is a return address. So actually doing this:
x = RETURN_ADDRESS;
Next line:
*(&x-1) = (int)(&a) ;
Then it sets the return address to the address of array a . It really says:
RETURN_ADDRESS = &a;
The cast is required because you are treating the return address as an int and not a pointer (so this code will only work on architectures where the int is the same size as the pointer - this will NOT work on 64-bit POSIX systems, for example! )
Now the C code in the f() function is executed, and the epilogue function does not select local variables (by moving the stack pointer back) and restores the frame pointer of the caller. At the moment, the stack is as follows:
| ... | (higher addresses) | char **argv (parameter) | |-------------------------| | int argc (parameter) | |-------------------------| FRAME POINTER -> | saved frame pointer | |-------------------------| | int a | |-------------------------| | int x (parameter) | &x |-------------------------| STACK POINTER -> | return address | &x - 1 |-------------------------| | saved frame pointer | |-------------------------| | a[99] | &a[99] | a[98] | &a[98] | ... | ... | a[0] | &a[0] | ... | (lower addresses)
Now the function returns, going to the value RETURN_ADDRESS, but we set it to &a , so instead of returning to where it was called, it goes to the value of the beginning of array a - now it executes the code from the stack. Here you copied the code from the g() function, so that the code (apparently) would happily execute. Please note that since the stack pointer was moved above the array here, any asynchronous code that runs on the same stack (for example, a UNIX signal that arrives at the wrong time) will overwrite the code!
So, here is what the stack now looks like at the start of g() , before the prologue function:
| ... | (higher addresses) | char **argv (parameter) | |-------------------------| | int argc (parameter) | |-------------------------| FRAME POINTER -> | saved frame pointer | |-------------------------| | int a | |-------------------------| STACK POINTER -> | int x (parameter) | |-------------------------| | return address | |-------------------------| | saved frame pointer | |-------------------------| | a[99] | | a[98] | | ... | | a[0] | | ... | (lower addresses)
The prolog for g() then sets the stack frame as usual, executes it, and unwinds it, leaving a pointer to the frame and stack pointer, as in the previous diagram above.
Now g() returns, so it looks for the return value at the top of the stack, but the top of the stack (where the stack pointer indicates) is actually the place where the parameter x performs the function f() live - and that's where we hid the original return value earlier, so it goes back to where f() is called from.
As a side note, the stack is now desynchronized in main() since it expected the stack pointer to be where it was when it called f() (which points to where x was stored), but now it actually points to a local variable a . This will cause some strange effects - if you call another function from main this time, the contents of a will be changed!
I hope you (and others) have learned something valuable from this discussion, but it’s important to remember that this is similar to the “Five Pointed Palm Heart”. Programming Technique - NEVER uses it in a real system. A new gift architecture, compiler, or even just different compiler flags can and will change the runtime environment enough to make such code too smart on the floor, completely compromised by all kinds of exciting and fun ways.