ArmAssemblyInt:RTS:A

Copyright 2018, Clinton A Staley

Concepts

  1. Compiler generated assembly
  2. Function calls
  3. Runtime stack
  4. Stack frames

Overview

This is the first lecture in a module covering assembly-level function calls and the data structures and assembly instructions that support them. It's also the first lecture in which we'll look at assembly written by a compiler, not directly by a human.

Compiler Generated Assembly

The assembly examples used thus far were written by hand, and in practice some assembly code is hand-written. But it's also common to use and modify assembly code created by a compiler, such as gcc, the Unix C compiler.

A compiler already translates high-level code, e.g. C, to machine language, so it's not hard for it to generate an assembly language translation instead. Indeed, some compilers generate only an assembly language translation, relying on an assembler, which the compiler automatically runs as a subprocess, to do the assembly to machine language translation.

Why generate assembly with a compiler?

There are several advantages to using assembly code generated by a compiler:

Using gcc to generate assembly

The gcc compiler will generate (nonoptimized for now) assembly code if given the -S commandline argument. This command translates Prg4.c to Prg4FromC.s:

gcc -S Prg4.c -o Prg4FromC.s

A quick look at Prg4.c shows that it has a function rangeSum to compute the sum of values from lo to hi. The main function calls rangeSum to get a sum, and calls printf to print the sum. Thus it has two function calls. We'll examine gccs assembly translation of this to see how function calls are done in assembly, and especially how the parameters and local data for a function call are stored in memory.

And, note, you might get somewhat different assembly output from gcc than what we show. This is due to changing versions of the compiler, and also because the lecture version has a couple small tweaks to make this first example easier to understand.

Simplifying the compiler's translation

A quick look at the generated Prg4FromC.s file shows that it's a lot more complex than earlier examples, and in particular has a lot of assembler directives. Most of these can be removed without harm, and we'll use the redacted copy in Prg4.s for the rest of the lecture. But first let's get a sense of what the directives are about. Here's a brief summary of a few of them:

.arch and .eabi_attribute

All ARM processors share the same machine language, but they vary in subtle details of architecture that can affect speed of execution. And the ARM ISA has versions, with a few new machine language instructions added in later versions. The .arch directive tells the assembler what ARM version we're using, version 6 in our case. And each .eabi_attribute directive describes, via its numerical arguments, an architectural detail of this particular processor.

.fpu

We'll talk about the floating point unit or (FPU) when we deal with floating point operations. This is a separate area of the CPU devoted to such operations, and it comes in various versions. The .fpu directive describes the FPU version.

.file

Some directives amount to formal documentation. For instance, the .file directive simply indicates the original source file from which the assembly is derived.

Function calling and RTS

Before we examine Prg4.s, let's talk a bit about function calls, and in particular, how we store a function's local variables and parameters during a call. From here on we'll use the term "local state" for the locals and parameters, because it's shorter, and because local state will include more than just parameters and local variables as we explore further.

The problem

A simple answer to how to store local state might be to reserve labelled space for the local state in the data segment, like we did for valA in Prg3.s. But there are two problems with this. First, the local state is only needed during the function call, and needn't exist before or after the call. Saving permanent space for it is thus wasteful. And second, functions can call themselves, either directly via recursion, or indirectly by calling another function that eventually re-calls them. Thus multiple instances of the same function may be running at the same time and they each need independent copies of the local state. If we have two instances of rangeSum running at the same time, one having called the other, they both need independent copies of lo, hi, and sum.

The solution: a runtime stack (RTS)

Every programming language designed in the last half century solves this problem by storing local state in an area of memory called a runtime stack or RTS.

Here's a diagram of an RTS. The RTS is divided into areas called stack frames, or in an academic context, activation records. (Professional software developers always call them "stack frames".) Each stack frame stores local state for one running function.

The RTS starts with just one stack frame holding local state for main. This stack frame is the top of the stack or TOS. In Prg4.s, that frame contains local variable total. When main calls a function, e.g. rangeSum, the program adds a new stack frame holding rangeSums local state: lo hi and sum. This new stack frame is TOS, and the main stack frame waits below it, until rangeSum returns. During the call of rangeSum, the code for rangeSum makes use of rangeSum's stack frame. When the program returns from rangeSum it removes rangeSum's stack frame, returning the RTS to the state before the call of rangeSum, with main's stack frame as TOS.

Question 1 If rangeSum called yet another function during its execution, say f2, where do you think f2's stack frame would go, and what stack frame would be beneath it on the RTS?

Answer 1 It would go on top of the frame for rangeSum, and the RTS would be three frames high, with f2s frame being the TOS during the call of f2. If f2 has a local variable like f, that goes in f2's stack frame. Once we return from f2, we'd go back to the state where rangeSum's frame was at TOS.

A stack is a natural arrangement for storing local state, given the order of function calls, in which one function calls another, which calls another, etc. and in which each function waits till the ones it has called return.

So, in summary:

  1. During the run of a program, the RTS holds stack frames for all running function calls, possibly dozens of calls deep if the program has deep sequences of function calls.
  2. The RTS increases by a stack frame as each new call starts.
  3. The RTS reduces by a stack frame as each call returns.
  4. The currently running function's stack frame is at TOS. The other functions' stack frames are below it, waiting to resume work once they are returned to.

It's a useful exercise to think about whether local variables retain their values between function calls, given how the RTS works.

Question 2 Say we have a function f2 with a local variable f. Could f be at the same location in memory each time f2 is called? Is this guaranteed? Why or why not?

Answer 2 It might be at the same location each time, if the same sequence of calls leads up to f each time, so that f's stack frame is in the same spot. But this is not guaranteed. If a different sequence of function calls leads up to f2 each time, then f2s stack frame, and the variable f inside it, will fall at a different memory location each time.

Question 3 From what you have learned in prior programming classes, does a local variable retain its value between one call and the next? If a local variable x has the value 42 after one call of f:

int f() {
   int x;

   x = 42;
}

will x still contain 42 the next time f is called? Again, why or why not?

Answer 3 This is really the same answer as the prior question. If x happens to fall at the same location of memory on a second call, then the 42 might still be there. But if other function calls had used that area of memory in the interim, then the 42 would be long gone. And if x is not at the same location in successive calls of f, then of course it'll have a different initial value each time.

In the next segment we'll look more closely at how TOS is tracked in an assembly program, and we'll show a lot more detail on the contents of stack frames.