I am trying to write assembly code for the following function:
#include <iostream>
void f(int x) {
if (x > 0) {
std::cout << x << std::endl;
f(x-1);
std::cout << x << std::endl;
}
}
int main() {
f(1);
}
The output of this function script is 1 1. I try to write the assembly code for the so-called "low-cost computer" assembler, a computer invented by Anthony Dos Reis for his book "C and C++ under the hood". The assembly code I wrote is:
startup jsr main
halt ; back to operating system
;==============================================================
; #include <stdio.h>
greater dout
nl
sub r1, r0, 1
push lr
push fp
mov fp, sp
push r1
jsr f
add sp, sp, 1
dout
nl
mov sp, fp
pop fp
pop lr
ret
;==============================================================
f push lr ; int f()
push fp ; {
mov fp, sp
ldr r0, fp, 2
cmp r0, 0
brgt greater
mov sp, fp
pop fp
pop lr
ret
;==============================================================
main push lr
push fp
mov fp, sp
mov r0, 1
push r0
jsr f
add sp, sp, 1
mov sp, fp
pop fp
pop lr
ret
The code prints 1 0 to stdout, and is obviously false. The reason for the false output lies in the fact that the register r0 contains 1 before it jumps to the function f during evaluation of the branch greater, and then the function f modifies the register and sets r0 to 0 when doing the comparison cmp. This let me wonder how I the assembler can keep the registers invariant during function calls. I thought of the following:
I thought the solution 1 is very defensive and likely costly, whilst solution 2 is probably complicated and assumes a lot of knowledge. Most likely, I simply made a mistake in writing the assembly, but I still don't understand how the assembler can keep its registers invariant when it needs to in general cases, or how one can address such a problem as outlined above. Does somebody know what to do in this case? I am grateful for any help or suggestions!
As the others are saying, usually each register is assigned a usage model by an agreement called the calling convention. There are several usage models:
Call clobbered — sometimes also called "scratch", these registers are understood to be clobbered by a function call, and as such, they can be used in between calls, and are free to be used by any function.
Sometimes these are called "caller saves" because the caller is responsible for preserving any of their values if they are needed after the call; also known with the term "volatile". In practice, however, once moved to memory, they don't need to be restored until their values are actually needed; those values don't need to be restored to the same register they were in when stored to memory, and, on some architectures, the values can be used directly from memory as well.
Call preserved — these registers are understood to be preserved by function calling, which means that in order to use one of these the original contents of the register must be preserved (typically on function entry) and restored later (typically at function exit). Sometimes also called "callee saves" because as the caller can rely their values being preserved, a callee must save & restore them if used; also known with the term non-volatile.
Others — on some processors certain registers are dedicated to parameter passing and return values; because of these uses, they don't necessarily behave strictly as call clobbered or call preserved — i.e. it is not necessarily the called function that clobbers them but the caller may be required to clobber them in merely making the call, i.e. in parameter passing before the call. A function can have formal parameter values in these registers that are needed after a call, and yet need to place actual arguments into them in the instruction sequence of calling another function. When this occurs, the parameter values needed after a call are typically relocated (to memory or to call preserved registers).
From somehow gleaning what the function call thus and then "protecting" the registers if it needs to.
This can work. The calling convention is a general purpose agreement that is particularly useful when caller or callee does not know the implementation details of the other, or when an indirect function call (call by pointer) could call one of several different actual functions. However, when both a callee and caller are known in implementation, then as an optimization we can deviate from the standard calling convention. For example, we can use a scratch register to hold a value live across a particular call if we know the called function does not modify that scratch register.
Commonly, a computing platform includes an Application Binary Interface (ABI) that specifies, among other things, protocols for function calls. The ABI specifies that certain processor registers are used for passing arguments or return results, certain registers may be freely used by the called routine (scratch or volatile registers), certain registers may be used by the called routine but must be restored to their original values (preserved or non-volatile registers), and/or certain registers must not be altered by the called routine. Rules about calling routines may also be called a “calling convention.”
If your routine uses one of the registers that called functions are free to use, and it wants the value of that register to be retained across a function call, it must save it before the function call and restore it afterward.
Generally, the ABI and functions seek a balance in which registers they use. If the ABI said all registers had to be saved and restored, then a called function would have to save and restore each register it used. Conversely, if the ABI said no registers have to be saved and restored, then a calling function would have to save and restore each register it needed. With a mix, a calling routine may often have some of its data in registers that are preserved (so it does not have to save them before the call and restore them afterward), and a called routine will not use all of the registers it must preserve (so it does not have to save and restore them; it uses the scratch registers instead), so the overall number of register saves and restores that are performed is reduced.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With