Yes...
Suppose you have a language, such as C, that allows recursion. To do this, each instance of the function must be autonomous from other instances of this function. The stack is an ideal place, because the code can "allocate" and refer to elements in the distribution without knowing the physical address; everyone accesses it by reference. All you care about is keeping track of this link in the context of the function and restoring the stack pointer to where it was when you introduced this function.
You should now have a calling convention that is suitable for recursion, etc. Two popular options (using a simplified model) are register transfer and stack transfer. In fact, you can have and actually have hybrids (based on the registers, you will end the registers and should go back to the stack for the rest of the parameters).
Suppose for a moment that the fictional hardware that I'm talking about magically processes the return address without messing around with registers or the stack.
passing registration. Define a specific set of hardware / processor registers that will contain parameters, say, r0 is always the first parameter, r1 is the second, r2 is the third. and lets say that the return value is r0 (this is simplified).
stack. allows you to determine the first thing you click on the stack is the last parameter, and then next to the last to the first parameter. When you return, let's say that the return value is the first thing on the stack.
Why announce a calling agreement? Thus, both the caller and the callee know exactly what the rules are and where to find the parameters. Registering a passage looks great on the surface, but when you run out of registers, you need to keep things on the stack. If you want to move from the caller to another function, you may need to store items in the calling registers so that you do not lose these values. And you are on the stack.
int myfun ( int a, int b, int c) { a = a + b; b+=more_fun(a,c) return(a+b+c); }
a, b and c are used after calling more_fun, more_fun with the minimum requirements r0 and r1 pass the parameters a and c, so you need to save r0 and r1 somewhere so that you can: 1) use them to call more_fun () and 2) so that you do not lose the values ββof a and b that you will need after returning from more_fun (). you can save them in other registers, but how to protect these registers from modifying the called functions. Ultimately, the material is stored on the stack, which is dynamic and accessible by reference instead of physical addresses. So
Someone wants to call myfun, and we use registration.
r0 = a r1 = b r2 = c call myfun ;return value in r0 myfun: r0 = r0 + r1 (a = a + b) ;save a and b so we dont lose them push r0 (a) push r1 (b) r0 = r0 (a) (dead code, can be optimized out) r1 = r2 (c) call more_fun ;morefun returns something in r0 pop r1 (recover b) r1 = r1 + r0 (b = b+return value) pop r0 (recover a) ;r0 is used for returning a value from a function r0 = r0 + r1 (= a+b) r0 = r0 + r2 (=(a+b)+c) return
The calling function (the calling party) knows to prepare the three parameters in r0, r1, r2 and take the return value in r0. The caller knows what to take r0, r1, r2 as input parameters and return to r0 And he knows that he needs to save some things when he becomes the caller for some other function.
And if we use the stack to pass parameters using our calling convention
int myfun ( int a, int b, int c) { a = a + b; b+=more_fun(a,c) return(a+b+c); }
Now we have to create some registration rules, we define call rules to say that 1) you can destroy any register (but sp and pc and psr), 2) that you must save each register so that when you return the calling function, never seeing that its registers are changed or you determine 3) that some registers are scratches and can be changed at will, and some should be saved if they are used. I'm going to say that you can destroy registers besides sp, pc and spr for simplicity.
We have one more problem. Who clears the stack? When I call more, I have two elements on the stack, and only a return value on the output that clears the stack. Two choices, caller cleaning, cleaning, I go with a cleaner. This means that the caller must return from the function with the stack as it was found, it leaves something on the stack, and it does not take up too many things from the stack.
subscriber:
push c push b push a call myfun pop result pop and discard pop and discard
Assume with this equipment, the stack pointer sp points to the current item in the stack
myfun: ;sp points at a load r0,[sp+0] (get a) load r1,[sp+1] (get b) add r0,r1 (a = a+b) store [sp+0],r0 (the new a is saved) ;prepare call to more_fun load r0,[sp+2] (get c) load r1,[sp+0] (get a) push r0 (c) push r1 (a) call more_fun ;two items on stack have to be cleaned, top is return value pop r0 (return value) pop r1 (discarded) ;we have cleaned the stack after calling more_fun, our offsets are as ;they were when we were called load r1,[sp+1] (get b) add r1,r0 (b = b + return value) store [sp+1],r1 load r0,[sp+0] (get a) load r1,[sp+1] (get b) load r2,[sp+2] (get c) add r0,r1 (=a+b) add r0,r2 (=(a+b)+c) store [sp+0],r0 (return value) return
So, I wrote all this on the fly, there may be a mistake. The key to all this is to define a calling convention, and if everyone (the caller and the callee) follows the calling convention, it makes compilation easier. The trick is to create a working call agreement, as you can see above, we needed to change the agreement and add rules to make it work even for such a simple program.
How about a stack frame?
int myfun ( int a, int b) { int c; c = a + b; c+=more_fun(a,b) return(c); }
using stack
subscriber
push b push a call myfun pop result pop and discard
called
;at this point sp+0 = a, sp+1 = b, but we need room for c, so sp=sp-1 (provide space on stack for local variable c) ;sp+0 = c ;sp+1 = a ;sp+2 = b load r0,[sp+1] (get a) load r1,[sp+2] (get b) add r0,r1 store [sp+0],r0 (store c) load r0,[sp+1] (get a) ;r1 already has b in it push r1 (b) push r0 (a) call more_fun pop r0 (return value) pop r1 (discarded to clean up stack) ;stack pointer has been cleaned, as was before the call load r1,[sp+0] (get c) add r1,r0 (c = c+return value) store [sp+0],r1 (store c)(dead code) sp = sp + 1 (we have to put the stack pointer back to where ;it was when we were called ;r1 still holds c, the return value store [sp+0],r1 (place the return value in proper place ;relative to callers stack) return
A call, if it uses the stack and moves the stack pointer, it must return it to where it was when it was called. You create a stack frame by adding the right amount of stuff to the local storage stack. You may have local variables, and during the compilation process you may know in advance that you also need to save a certain number of registers. The easiest way is to simply add all this and move the stack pointer once for the whole function and return it one time before returning. You can become smarter and keep moving the stack pointer around adjusting offsets along the way, making it much harder to code and error-prone. Compilers like gcc tend to move the stack pointer only to a function and return it before leaving.
Some set commands add things to the stack when called and delete them when they return, and you need to adjust the offsets accordingly. Similarly, your creation and cleanup around calling another function may require processing involving the use of stack hardware, if any.
Suppose that hardware, when you make a call, pops the return value at the top of the stack.
int onefun ( int a, int b ) { return(a+b) } onefun: ;because of the hardware ;sp+0 return address ;sp+1 a ;sp+2 b load r0,[sp+1] (get a) load r1,[sp+2] (get b) add r1,r2 ;skipping over the hardware use of the stack we return on what will be the ;top of stack after the hardware pops the return address store [sp+1],r1 (store a+b as return value) return (pops return address off of stack, calling function pops the other two ;to clean up)
Some processors use a register to store the return value when the function is called, sometimes the hardware dictates which register, sometimes the compiler chooses one and uses it as a convention. If your function does not call any other function, you can either not use the return address register, but use it to return, or you can push it on the stack at some point, and then use it to return before returning it If your function calls another function, you must save this return address so that the call to the next function does not destroy it, and you cannot find your way home. so you either save it in a different register if you can or push it on the stack
Using the aforementioned register calling convention that we have defined, plus we have a register named rx, which when you call the hardware puts the return address for you in rx.
int myfun ( int a, int b) { return(some_fun(a+b)); } myfun: ;rx = return address ;r0 = a, first parameter ;r1 = b, second parameter push rx ; we are going to make another call we have to save the return ; from myfun ;since we dont need a or b after the call to some_fun we can destroy them. add r0,r1 (r0 = a+b) ;we are all ready to call some_fun first parameter is set, rx is saved ;so the call can destroy it call some_fun ;r0 is the return from some_fun and is going to be the return from myfun, ;so we dont have to do anything it is ready pop rx ; get our return address back, stack is now where we found it ; one push, one pop mov pc,rx ; return