The assembly language has nothing to do with the recursion, that just happens to work due to the C language and the calling conventions and implementation. Just implement the C in assembler and dont care about recursion. I think I touched on this on a pet project http://github.com/dwelch67/lsasim unless I changed it out the last lesson is manually converting recursion in C to assembler. Its not mips so no worries about this being a homework problem.
Anyway the key to starting is to simply implement the C in assembly.
For example you have a function with input parameters.
int fix(int i, int x)
You will need to declare yourself a calling convention or use an existing one, to implement C this means you need some place for the input parameters, either push them on the stack or bring them in in registers. Assuming no optimization, you preserve these variables throughout the function and clean up at the end. So if you need to call ANY function to anywhere in the code (recursion, calling the same function, is a very small subset of ANY but falls into that category and IS NOT SPECIAL) you need to preserve these variables. IF the calling convention brings these in on the stack then you are already done, if the calling convention brings these in in registers then you need to preserve them before the call and restore after
push i
push x
implement call to function
pop x
pop i
and continue implementing the function.
that is it, the rest will take care of itself.
If you happen to notice that the function you created as an example, does not have a path where the input variables need to be preserved after the call to a function within this function. And the input variables are modified and used as inputs to the next call. so an optimization to your implementation of the C code would be to not worry about preserving those variables. simply modify them and pass them on. Using registers in the calling convention would be the simplest way to do this for this specific function. This is what a compiler would do anyway when optimizing (not preserve if using register based calling convention).
You could also do a tail optimization if that is what it is called. Normally when calling a function you use whatever the instruction normally does to perform a "call" which is different from a simple jump or branch because there is a return value kept somewhere. And there is some sort of return function that undoes this and returns back to the instruction after the call. nested calls mean nesting the return values, keeping track of all of them. IN this case though and other cases where the last thing you do the execution path of a function is call another function, you can instead (depends on the instruction set) branch to the function and not have to nest another set of return values. Looking at the arm instruction set for example:
Some code calls the first function:
bl firstfun:
In arm bl means branch link. register 14 will be filled in with the return value and the program counter will be filled in with the address to the function, firstfun.
typically if you need to call a function from a function you need to save r14 so you can return from that function, without this tail optimization:
firstfun:
...
push {r14}
bl secondfun
pop {r14}
bx r14
...
secondfun:
bx r14
bx lr just means branch to the contents in r14 which in this case is the return. the optimization looks like this, it is important to note that in the first function the call to the second function is the last thing you do before returning from the first function. that is the key to this optimization.
firstfun:
...
b secondfun
...
secondfun:
bx r14
b just means branch, not a branch link simply modifies the pc and does not modify r14 or any other register or stack. The execution of the two implementations is the same functionally the outer function makes a call to firstfun and there is a return (bx r14) in the proper execution path.
Other folks have pointed out that this code may completely optimize itself into nothing since you return zero the original caller.
fix:
return 0