3

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:

  1. By simply pushing the entire register to the stack and loading it again afterwards
  2. From somehow gleaning what the function call thus and then "protecting" the registers if it needs to.

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!

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
fabian
  • 1,413
  • 1
  • 13
  • 24
  • 3
    You don't have to push all registers, just the ones that need protection. – Barmar Jan 08 '21 at 19:17
  • 2
    The term you are looking for is 'calling convention'. The compiler will push certain registers, and TRUST that the asm code won't change others.. If that trust is violated by certain assembly routines (or mismatched calling conventions), unpredictable behavior results. – Halt State Jan 08 '21 at 19:45

3 Answers3

6

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.

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53
  • Thank you very much for the detailed reply, Eric! It is really a pleasure to read about the different usage models described. – fabian Jan 09 '21 at 07:49
  • Good explanation of call clobbered vs. preserved, but arg-passing registers are normally just plain call-clobbered. The question is whether the caller can assume the value is unchanged after the `call` instruction, e.g. to make another `call` with the same args or other purpose. Not whether the caller had to put specific things in them. Every register-arg function-calling convention I've ever seen (except the toy Irvine32 convention on x86) has call-clobbered the arg regs, but *system*-calling conventions normally preserve all the regs. (so it's more like having to use CL for x86 shifts.) – Peter Cordes Jan 09 '21 at 08:26
  • When you talk about an ISA having "dedicated" arg-passing registers, what that really means is that the register-naming convention reflects the (vendor-recommended) ABI. We call these the "ABI names" for registers, e.g. `$a0-$a3` on MIPS as opposed to `$4-$7`. They're not actually special in the ISA proper. (e.g. [ABI Register Names for RISC-V Calling Convention](https://stackoverflow.com/q/30636566) is about the register-number for `sp` changing in a revision of the ABI. Which is documented in the ISA manuals, despite it being irrelevant to the machine code, only asm text.) – Peter Cordes Jan 09 '21 at 08:32
  • Everything you say in "other" follows from the fact that a reg is used for arg-passing as well as being "call clobbered". call-clobbered + used for arg passing is not an *alternative* to call-clobbered, so it doesn't really belong as a 3rd point in that list. Being used for arg-passing is orthogonal to call-clobbered or not (in theory, and in practice if you consider system calls and obscure stuff; functions normally own even their stack args). The stuff you have to say about it is relevant, but IMO doesn't belong as part of a 3-element bullet list, and shouldn't be considered its own thing. – Peter Cordes Jan 09 '21 at 17:21
3

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.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • 1
    @fabian: See for example [What registers are preserved through a linux x86-64 function call](https://stackoverflow.com/q/18024672). And in general [What are callee and caller saved registers?](https://stackoverflow.com/a/56178078) for the concept of call-preserved vs. call-clobbered and what that actually means for callers and callees. (Re: designing a *good* calling convention, see [args in XMM regs?](//stackoverflow.com/q/33707228) and [Windows x64 different convention from all OSes?](//stackoverflow.com/a/35619528/224132) for some discussion of what makes a calling convention efficient.) – Peter Cordes Jan 09 '21 at 02:47
  • This collection is fantastic, @PeterCordes - thank you. I was lacking the understanding of the material to search appropriately for related questions and am glad to see these related questions. – fabian Jan 09 '21 at 07:53
2

Architecture/Platform combinations such as Windows-on-x64, or Linux-on-ARM32, have what's called ABIs, or Application Binary Interfaces.

The ABI specifies precisely how registers are used, how functions are called, how exceptions work, and how function arguments are passed. An important aspect of this is, during a function call, what registers must be saved, and who saves them, and what registers may be destroyed.

Registers that can be destroyed during a function call are called volatile registers. Registers that must be preserved by a function call are called non-volatile registers. Typically, if a caller wants to preserve a volatile register, it pushes the value onto the stack, calls the function, and then pops it off when done. If a called function (callee) wants to use a non-volatile register, it must save it similarly.

Read the Windows x64 calling convention here.

Tumbleweed53
  • 1,491
  • 7
  • 13
  • Thank you very much for your answer and the the very interesting link - both were very informative for me ! – fabian Jan 08 '21 at 20:36
  • 1
    @fabian: Minor note about this answer: normal compilers *never* push/pop around a single call (even when that might actually be an optimization: [Why do compilers insist on using a callee-saved register here?](//stackoverflow.com/q/61375336), which is only possible in a function that makes exactly one function call and it's not in a loop). They'll typically save a call-preserved (non-volatile) register at the start of the whole function, and use it to keep values across calls. – Peter Cordes Jan 09 '21 at 08:12
  • 1
    Or if the var can't (conveniently / at all) live in a register across the call (e.g. no more non-volatile registers, or you're passing a pointer to the var), they'll spill (store) the variable to its address in the stack frame with a normal store instruction like x86 `mov`, and reload it when they next need it. The misleading "caller-saved" term for volatile / [call-clobbered registers](//stackoverflow.com/a/56178078) is named for a really braindead style of code-gen that's sub-optimal e.g. for a call in a loop, or a function that makes 2 or more calls. (@fabian) – Peter Cordes Jan 09 '21 at 08:18