Assembly Language Tutorial

CS 327
Compiler Design

Fred Sullivan
Wilkes University

Introduction

In assembly language programming, each statement of a program corresponds to the execution of a single machine instruction. This means that assembly language programs are architecture dependent. For example, the assembly language for an Intel Xeon X5550 processor is quite different from the assembly language for an ARM Cortex A-53 processor. Also, because each assembly statement only executes one machine instruction, assembly language programs are much longer than programs written in a higher level language.

Although the details vary from one architecture to another, a CPU comes equipped with a set of registers, which are special high-speed memory locations, and random access memory. Registers have names (or numbers), while individual memory locations in RAM are referenced by their addresses. You can think of RAM as a large array, and an address as an index into the array. Data can be moved from memory to registers and vice versa (via move instructions). All arithmetic operations involve registers. On some processors, arithmetic operations only involve registers, while on the Intel family, they can involve two registers or a register and a memory location. Most architectures have floating point registers which are specialized for floating point operations, and general purpose registers that are used for integer computations and everything else.

Memory is divided into words. The smallest addressible unit of memory is the byte. Word sizes vary from one architecture to another. The word size for the Intel family is 16 bits, or 2 bytes. An int variable in C occupies 32 bits, and is referred to as a double word. A long int is 64 bits, which is a quadword. Floats are double words, while doubles are quadwords.

In this course we will use the 64 bit version of the ARM family architecture running on a Raspberry Pi 5 which has an ARM Cortex A-76 CPU. The instruction set is referred to as the Armv8-A architecture. There are 31 64 bit general purpose regis+ters and 32 128-bit floating point registers. The 64 bit architecture is called AArch64. Addresses (pointers) are 64 bits (quadwords) and occupy 16 bytes.

Although it is possible to do direct system calls for input and output from assembly language, it is much more convenient to use an established framework. We will use the C library for input, output, and getting a program started. This means that a program will have to be linked with the C library. The assembler (as, for GNU assembler) will produce an object file that contains machine instructions. An object file cannot be directly executed. The loader (ld) links the file with any external functions that are needed and puts the file into a format that can executed.

When a program in a higher level language (like C) is compiled with cc, there are actually several (invisible) steps that take place. For example, if we compile the program foo.c using the command

  cc -o foo foo.c

the C compiler compiles the source file, producing an assembly language source. The assembler reads the assembly language program, producing an object file. Finally, ld produces an executable file called foo. The process is usually invisible because the intermediate files are automagically deleted. To see this in action, try compiling a file with the flag. The compiler will print all of the commands that it is executing. In fact, cc is not the C compiler. It’s just a driver program that looks at file names and decides what needs to be done. For example, the command

  cc -o foo foo.c plugh.s xyzzy.o

will assemble plugh.s into an object file, compile foo.c into assembly code and then assemble it into an object file, and finally ld will link the files together with the C library to produce an executablecc on foo.s, because that’s the easiest way to get a program linked with the C library. Since we are using the C library, we will have to follow certain conventions about how functions are called. For a program that is in a single file, the make command makes it very easy to assemble and link the program.

Example: Hello, world (hello.s)

/* 
 * File:        hello.s
 * Description: Prints a warm, friendly greeting.
 */

        .arch   armv8-a                 // The CPU type
        .global main                    // Make main available to the linker

        .text                           // Start of the text (code) segment
        .align  2                       // Align code on an address divisible by 4

main:                                   // The main function
        stp     fp, lr, [sp,-16]!       // Push the frame pointer and link register
        mov     fp, sp                  // Copy the stack pointer to the frame pointer
        
        adr     x0, fmt                 // Pass the format string in x0
        bl      printf                  // Call printf

        mov     x0, #0                  // Put the return value for main into x0
        ldp     fp, lr, [sp], 16        // Pop the frame pointer and link register
        ret                             // Return from main
        
        .section .rodata                // Start of the readonly data section

fmt:    .string "Hello, world!\n"       // The format string for printf

Now we’ll dissect the program in detail. Assembly programs consist of instructions and directives. An instruction is directly translated into a machine language instruction. A directive tells the assembler to do something, but is not translated.

Comments

Comments vary from one version of gas to another and generally follow the traditional comment convention for a particular architecture. This version of gas accepts C style comments.

Multiline comments start with /*, end with */, and cannot be nested.

Single line comments start with // end extend to the end of the line.

/* 
 * File:        hello.s
 * Description: Prints a warm, friendly greeting.
 */

Architecture type

The .arch directive specifies the CPU type. This is important because the available instruction set varies from one version to another.

        .arch   armv8-a                 // The CPU type

Global Symbols

In order for the linker to connect symbols from one source file to another, they must be declared as global. The main function must be global since it is called from the C library startup code.

        .global main                    // Make main available to the linker

The Text Segment and Alignment

The memory available to a program is divided into segments. The text segment contains the executable code. It is by default marked as no writable to prevent attacks that rewrite code in memory.

On ARM64, Instructions must be aligned on 4 byte word boundaries (they are all exactly 4 bytes long). In other words, their addresses must be divisible by 4. The .align n directory specifies alignment on an address that is divisible by 2n, .align 2 specifies the proper alignment.

        .text                           // Start of the text (code) segment
        .align  2                       // Align code on an address divisible by 4

The main Function

Since we are using the C library, execution begins with a call to the main function.

Functions in assembly are simply addresses (given by labels). When we enter a function, we have to set up the stack frame. This requires storing information that is use to return from the function.

The frame pointer contains the address of the beginning of the current stack frame. The link register contains the return address (the address that the function will return to when it finishes. We have to push that information onto the runtime stack so it can be restored (by popping the stack) when the function returns.

The exact way the stack frame is set up is determined by the procedure call standard for a particular architecture. It must be followed if the program is going to call any functions from external libraries (that use the standard). Since we are using the C library (in particular, the library startup code and printf) we must follow it.

The stack pointer (the sp register) contains a pointer to the top of the stack. The stack actually grows downward, so pushes must decrement the stack pointer. The (64 bit) architecture requires that the stack pointer is always on a 16 byte boundary.

The stp instruction in the addressing mode shown here stores two registers at the specifed address and decrements the register afterwords (like the postfix decrement operator in C), effectively pushing the frame pointer and link register onto the stack.

main:                                   // The main function
        stp     fp, lr, [sp,-16]!       // Push the frame pointer and link register
        mov     fp, sp                  // Copy the stack pointer to the frame pointer

Next we’ll call the printf function. By convention, the first 8 arguments are passed in the registers x0 - x7. The adr instruction stores an address (expressed as a label) into a register.

Note that in most instructions, the direction of data movement is right to left (like an assigment statement).

The bl instruction (branch and link) calls a function by transferring control to the specified address. It also stores the return address (the address following the bl instruction) in the link register.

        adr     x0, fmt                 // Pass the format string in x0
        bl      printf                  // Call printf

Next we return from the main function. The return value is placed into register x0. The mov instruction stores a value into a register. The # is used to designate an immediate operand (a constant). Immediate values are actually stored internally in an instruction.

The ldp instruction is the opposite of stp. It moves data from the given address into a pair of registers. In this form, it decrements the address (after moving the data), effectively popping the frame pointer and link register from the stack into their registers.

The ret instruction returns by copying the address stored into the link register into the program counter, so we jump to the return address.

        mov     x0, #0                  // Put the return value for main into x0
        ldp     fp, lr, [sp], 16        // Pop the frame pointer and link register
        ret                             // Return from main

The .rodata section is marked as readonly memory. It is used to store constants, in particular, string literals.

        .section .rodata                // Start of the readonly data section

The output string is stored as data when the program is loaded. It’s address is marked with the label fmt. The .string directive allocates storage and stores a string literal, including a null terminator.

fmt:    .string "Hello, world!\n"       // The format string for printf

Everything in this program except the call to printf and the rodata section is essentially boilerplate that will appear in every assembly program that we write.

To compile the program, we can just use make. It will automagically link the program with the C library, making printf available.

pi > make hello
pi > ./hello
Hello, world!
pi >

When a function is called, (some of) its arguments are passed in registers. There are conventions about exactly which registers are used for certain purposes. In addition, a function will typically change the values in some of the registers. The arm64 function calling conventions require that functions not alter the values of some of the registers (or if they do, they must restore the registers to their original values before returning). This means that when we call functions, we must be aware of which registers might change during function calls, and write our code accordingly.

Registers

A register is a small, extremely fast storage location located directly within the CPU. Unlike system memory RAM, which is located outside the processor, registers provide the immediate data access required for the CPU to execute instructions.

The primary purpose of registers is to hold the data that the CPU is currently processing. Their functions can be categorized into three main areas:

  1. Data Manipulation and Storage

Before any arithmetic or logical operation (like addition or bitwise comparison) can occur, the operands must be moved from RAM into registers. Registers store the temporary results of calculations before the final value is written back to memory. Registers operate at the same speed as the processor itself, typically orders of magnitude faster than the fastest RAM.

  1. Execution Control and Instruction Tracking

Special-purpose registers manage the flow of the program. They ensure the CPU knows what to do next and keep track of the current state of execution.

The program counter holds the memory address of the next instruction to be executed. The link register stores the return address when a function is called.

  1. State and Context Management

Status registers hold bits that represent the outcome of the most recent operation; for example, whether the result was zero, negative, or caused an overflow.

The stack pointer points to the top of the stack, while the frame pointer points to the bottom of the current stack frame.

AArch64 Register Conventions

The AArch64 Procedure Call Standard (PCS) defines how the 31 general-purpose registers (x0–x30) are utilized.

Nonvolatile registers are registers whose values must not be changed by a procedure call. If a procedure changes the value in a nonvolatile register, it has to store the original value (usually by pushing it onto the runtime stack) and then restore the original value before returning.

When a procedure is called, there is no guarantee that a volatile register retains its value after the procedure returns. So if the value is needed following a procedure call, it is the responsibility of the caller to save it and restore it.

General-Purpose Registers

x0–x7 Argument/Result. Volatile
Used to pass the first eight integer or pointer arguments. x0 holds the return value.
x8 Indirect Result. Volatile.
Used to pass the address for returning large structures.
x9–x15 Volatile.
Temporary registers for local calculations.
x16–x17 IP0/IP1 Volatile
Intra-procedure-call “scratch registers”.
x18 (Platform Register)
Reserved for platform-specific uses.
x19–x28 Nonvolatile
Registers that must be preserved across function calls. If a function uses these, it must restore the original values before returning.

Special Purpose Registers

The following registers have dedicated roles within the architecture:

Register Name Description
x29 fp Frame Pointer: Points to the current stack frame record.
x30 lr Link Register: Stores the return address for ret.
sp sp Stack Pointer: Must be 16-byte aligned at all times.
xzr xzr Zero Register: Always reads as zero; writes are ignored.

Example: Add Two Integers add.s

This program prints a prompt, reads two integers, and prints their sum.

/*
 * File:        hello.s
 * Description: Adds two (long) integers and prints the result.
 */
        .arch   armv8-a

        .global main

        .text
        .align  2
main:
        stp     fp, lr, [sp,-16]!
        mov     fp, sp
        
        adr     x0, prompt              // Pass the prompt string
        bl      printf                  // Call printf

        adr     x0, infmt               // Pass the input format in x0
        adr     x1, x                   // Pass the address of x in x1
        adr     x2, y                   // Pass the address of y in x2
        bl      scanf                   // Call scanf

        adr     x0, outfmt              // Pass the output format in x0
        adr     x1, x                   // Pass the contents of x in x1
        ldr     x1, [x1]
        adr     x2, y                   // Pass the contents of y in x2
        ldr     x2, [x2]
        add     x3, x1, x2              // Add, putting the result in x3
        
        bl      printf                  // Call printf
        
        mov     x0, #0
        ldp     fp, lr, [sp], 16
        ret
        
        .data                           // Start of data segment
        .align  3                       // Align on an address divisible by 8

x:      .quad   0                       // The first integer
y:      .quad   0                       // The second integer

        .section .rodata

prompt:   .string "Enter two integers: "
infmt:    .string "%ld %ld"
outfmt:   .string "%ld + %ld = %ld\n"

To call scanf, we pass as arguments the addresses of the format string and the variables where we want to store the numbers that are read.

        adr     x0, infmt               // Pass the input format in x0
        adr     x1, x                   // Pass the address of x in x1
        adr     x2, y                   // Pass the address of y in x2
        bl      scanf                   // Call scanf

Next we’ll set up to print the sum. We put the output format in x0 (first argument). Since we’ll print x, y, and the sum, we need x in x1, y in x2, and the sum in x3.

It appears that the address of x is already in x1. However, it is (very) likely that the call to scanf has overwritten that register. Only the called saved registers are guaranteed to maintain their values after a function call.

The ldr instruction can copy data from a memory location to a register. We use adr to put the address of x into x1. We won’t need it any more, so we can use ldr to copy the value stored in x into x1 (this overwrites the address). Similarly, we copy the value of y into x2. Finally, the add instruction add the contents of x1 and x2, putting the result into x3.

        adr     x0, outfmt              // Pass the output format in x0
        adr     x1, x                   // Pass the contents of x in x1
        ldr     x1, [x1]
        adr     x2, y                   // Pass the contents of y in x2
        ldr     x2, [x2]
        add     x3, x1, x2              // Add, putting the result in x3
        
        bl      printf                  // Call printf

The data section stores static data. Numberic data should aligned, which means that it is stored at addresses that are multiples of their data size.

The .quad directive allocates 8 bytes of storage, and initializes it to the given value. We want them aligned on 8 byte boundaries.

        .data                           // Start of data segment
        .align  3                       // Align on an address divisible by 8

x:      .quad    0                      // The first integer
y:      .quad    0                      // The second integer

Integer Operations

Immediate operands are of the form #nnn. The number is restricted to an 8-bit integer. However, the assembler can handle any number that can be obtained from an 8-bit integer by a shift, rotate, and/or a complement. For example, #1024 is allowable because is equal to 128 left-shifted by 4.

Many instructions have the format “instruction destination, source”, where the source is not changed but the destination is. Thus mov x0, x1 moves a quadword from x1 to x0, mov x0, #5, moves the immediate value 5 to x0, and add x1, 22 adds the contents of x2 to x1.

The following table gives allowed combinations of source and destination for the basic arithmetic instructions. I means immediate (a literal), and R means a register. For example, with add and sub the source operand can be a register or immediate but with mul and idiv it can only be a register.

Instruction Effect Dst Src1 Src2
add dst, src1, src2 dst = src1 + src2 R R IR
sub dst, src1, src2 dst = src1 - src2 R R IR
mul dst, src1, src2 dst = src1 * src2 R R R
sdiv dst, src1, src2 dst = src1 / src2 (signed) R R R
udiv dst, src1, src2 dst = src1 / src2 (unsigned) R R R

The destination register can be omitted, in which case it is the same as the first source register.

Exercise 1

Write a program that reads a Fahrenheit temperature and converts it to Kelvin. Since we don’t know about floating point numbers yet, use integers. Give your answer rounded to the nearest integer. Hints:

  1. Multiply by 100, add 50 to the answer for rounding, and divide by 100.
  2. You’ll need to load 27315 into a register. It won’t fit in an 8-bit integer. You can do this in 3 ways:
  1. Construct it in pieces, use shift operations to move the pieces to the left and add them.
  2. Store it as a constant in memory and load it into a register.
  3. (The preferred way) The assembler supports a pseudo instruction ldr reg =value which is automagically translated by the assembler into method 2.
Turnin Name ftok
File Name ftok.s

Example: A Loop loop.s

The next program does a simple loop. It reads a number n and prints the numbers from 0 to n-1.

/* 
 * File:        loop.s
 * Description: Prints the numbers from 1 to n.
 */


        
        .arch   armv8-a

        .global main

        .text
        .align  2
main:
        stp     fp, lr, [sp,-16]!
        mov     fp, sp
        
        adr     x0, prompt         // Pass the prompt in x0
        bl      printf             // Call printf
        
        adr     x0, infmt          // Pass the input format in x0
        adr     x1, n              // Pass the address of n in x1
        bl      scanf              // Call scanf
        
        adr     x0, i              // Put the address of i in x0
        str     xzr, [x0]          // Initialize i to 0
test:                              // Top of loop
        adr     x1, i              // Copy i to x1
        ldr     x1, [x1]
        adr     x2, n              // Copy n to x2
        ldr     x2, [x2]
        cmp     x1, x2             // Compare i and n
        beq     done               // Jump out of loop if equal
        
        adr     x0, outfmt         // Pass the output format in x0
        bl      printf             // Call printf
        
        adr     x2, i              // Copy i to x1
        ldr     x1, [x2]

        add     x1, x1, #1         // Increment
        str     x1, [x2]
        b       test               // Jump to the top of the loop
done:   
        mov     x0, #0
        ldp     fp, lr, [sp], 16
        ret
        
        .data                      // Start of data segment
        .align  3                  // Align on an address divisible by 8

i:      .quad   0                 // Loop control
n:      .quad   0                 // Upper bound
        
prompt: .string "n = "             // Prompt string
infmt:  .string "%ld"              // Input format
outfmt: .string "%ld\n"            // Output format

We’ll examine the program.

First we initialize i to 0. The xzr register always has the value 0. This saves putting 0 into some other register and then storing it into i.

        adr     x0, i              // Put the address of i in x0
        str     xzr, [x0]          // Initialize i to 0

Next we’ll start the loop. It just begins with a label.

test:                              // Top of loop

Next we compare i to n. The cmp instruction compares two registers and sets bits in the status register depending on the outcome. The beq (branch equal) instruction branches when the registers have the same value.

        adr     x1, i              // Copy i to x1
        ldr     x1, [x1]
        adr     x2, n              // Copy n to x2
        ldr     x2, [x2]
        cmp     x1, x2             // Compare i and n
        beq     done               // Jump out of loop if equal

After printing i, we copy i back to x1. This is necessary because x1 probably got clobbered by the call to printf.

        adr     x2, i              // Copy i to x1
        ldr     x1, [x2]

Now we increment i and jump back to the top of the loop. The b instruction is an unconditional jump.

        add     x1, x1, #1         // Increment
        str     x1, [x2]
        b       test               // Jump to the top of the loop

We jump to the done label then the loop is finished.

done:   

Here is pseudocode for doing a loop:

  start:
    do test
    if test fails, jump to end
    body of loop
    jump to start
  end:

Exercise 2

Modify loop.s so that it stores both the upper bound and the loop control in registers. You’ll have to read the upper bound into memory and then move it to a register. Use registers that are saved by the callee do that they aren’t clobbered by printf.

Turnin Name loop
File Name loop.s

Exercise 3

Write a program that reads an integer n and prints two columns. The first will contain the integers from 0 to n-1. The second column will contain the same numbers, but in reverse order. Make the columns line up nicely.

Like this:

 0  3
 1  2
 2  1
 3  0
Turnin Name count
File Name count.s

Exercise 4

Write a program that reads an integer n and prints a square of numbers from 0 to n-1. If the input is 4, the output will be

 0  1  2  3
 0  1  2  3
 0  1  2  3
 0  1  2  3
Turnin Name square
File Name square.s

Exercise 5

Write a program that reads an integer n and prints a triangle of numbers from 0 to n-1. If the input is 4, the output will be

 0  1  2  3
 0  1  2
 0  1
 0
Turnin Name triangle
File Name triangle.s

Comparisons

When comparing two integers, we use the cmp instruction, followed by a branch (jump). The cmp instruction sets condition codes in a special register and the branch instructions consult the condition codes to decide what to do.

  cmp %x1, %x2
  blt foo

jumps to the label foo if x1 contains a smaller integer than x2. The following table gives the branch instructions for various conditions.

Instruction Meaning
b branch always
beq branch if equal
blt branch if less than
ble branch if less than or equal
bgt branch if greater than
bge branch if greater than or equal

Conditionals

To do the equivalent of the following C if-else statement:

  if (x == 0) {
    y = 1;
    z = 2;
  }
  else {
    z = 2;
    y = 1;
  }

use the following assembly code:

  movq x, %x13        # test
  test %x13, %x13
  jnz else            # test failed, so jump to else
  mov $1, y           # if part
  mov $2, z
  jmp done            # jump over the else part
else:                 
  movq $2, z          # else part
  movq $1, y
done:                 # end

We do the test and jump to the else part if it fails. If it succeeds, we do the if part and then jump over the else part.

A template for conditionals:

  do test
  if test fails, jump to else
  body of if
  jump to end
else:
  body of else
end:

Exercise 6

Write a program that reads two integers and prints the max and min. Label the output.

Turnin Name minmax
File Name minmax.s

Floating Point Arithmetic

In addition to the integer registers, AArch64 has 32 floating point registers. They are actually 128 bit registers, accessed as v0-v31. For our purposes, we will access than as double precision (64 bit) d0-d31. They also support 16 or 8 bit computations.

d0-d7 Volatile
d8-d15 Nonvolatile
d16-d31 Volatile

When a procedure is called, the first 8 floating point arguments are passed in d0–d7. Remaining arguments are pushed onto the stack. The return value is placed in d0.

Floating Point Instructions

Arithmetic
fadd, fsub, fmul, fdiv
Comparison
fcmp
Moving data
fmov
Converting integers to floating point
scvtf, ucvtf
Converting floating point to integers
fcvtzs, fcvtzu

Example: Adding Floating Point Numbers (addreal.s)

/* 
 * File:        addreal.s
 * Description: Adds two floating pointer numbers.
 */

        .arch   armv8-a

        .global main

        .text
        .align  2
main:
        stp     fp, lr, [sp,-16]!
        mov     fp, sp

        adr     x0, prompt
        bl      printf

        adr     x0, infmt               // Pass the input format in x0
        adr     x1, x                   // Pass the address of x in x1
        adr     x2, y                   // Pass the address of y in r2
        bl      scanf                   // Call scanf

        adr     x0, outfmt              // Pass the output format in x0
        adr     x1, x                   // Pass the contents of x in x1
        ldr     d0, [x1]
        adr     x2, y                   // Pass the contents of y in x2
        ldr     d1, [x2]
        fadd    d2, d0, d1              // Add x and y

        bl      printf
        
        mov     x0, #0
        ldp     fp, lr, [sp], 16
        ret
        
        .data                           // Start of data segment
        .align  3                       // Align on an address divisible by 8

x:      .quad   0                       // The first double
y:      .quad   0                       // The second double

        .section .rodata
        
prompt:   .string "Enter two reals: "
infmt:    .string "%lf %lf"
outfmt:   .string "%f + %f = %f\n"

The only thing in this program that’s really new is the use of ldr to load floating point registers and fadd to add the numbers.

Exercise 7

Redo Exercise 1 to use floating point numbers rather than integers. Print your answer with four places after the decimal.

Turnin Name ftokf
File Name ftokf.s

Functions

A function is simply a label (with the function name) followed by code. However, we must set up a stack frame for the function call. When a function is called, we have to push the frame pointer, link register, and arguments if there are too many to be passed in registers. To allocate space for local variables, we enlarge the stack frame by decrementing the stack pointer. When the function, we must deallocate the extra space in the stack frame by incrementing the stack pointer. We pop the link register and frame pointer back into their registers and then execute a ret instruction. Integer return values are put in the x0 register and floating point return values are put in the d0 register.

There is also an alignment requirement for the stack pointer. It must always be a multiple of 16. Therefore, when we allocate space for local variables, we must allocate extra space, if necessary, to make sure that the first argument to enter is a multiple of 16.

Example: An Absolute Value Function (abs.s)

/* 
 * File:        abs.s
 * Description: Computes absolute value.
 */

        .arch armv8-a

        .global main 

        .text
        .align  2
main:
        stp     fp, lr, [sp,-16]!       // Set up the stack frame
        mov     fp, sp
        
        adr     x0, prompt              // Pass the prompt string
        bl      printf                  // Call printf

        adr     x0, infmt               // Pass the input format in x0
        adr     x1, x                   // Pass the address of x in x1
        bl      scanf                   // Call scanf

        adr     x0, x                   // Move x into x0
        ldr     x0, [x0]                
        mov     x1, x0                  // Put a copy of x into x1
        bl      abs                     // Call abs

        mov     x2, x0                  // Put the return value from abs into x2
        
        adr     x0, outfmt              // Pass the output format in x0
        bl      printf                  // Call printf
        
        mov     x0, #0                  // Put the return value for main into x0
        ldp     fp, lr, [sp], 16        // Pop the frame pointer and link register
        ret                             // Return from main
        
        
abs:                                    // The abs function
        stp     fp, lr, [sp,-16]!       // Set up the stack frame
        mov     fp, sp

        cmp     x0, #0                  // Check if x0 is positive
        bge     done                    
        neg     x0, x0                  // Negate x0

done:
        ldp     fp, lr, [sp], 16        // Return from abs
        ret                             
        
        .data                           // Start of data segment
        .align  3                       // Align on an address divisible by 8

x:      .quad  0                        // The first integer

        .section .rodata
        
prompt:   .string "Enter an integer: "  // Prompt string
infmt:    .string "%ld"                 // Scanf format
outfmt:   .string "|%ld| = %ld\n"       // Printf format

The main function is straightforward. We’ll look at the abs function.

The setup is the same as for main (and any other function). We start with a label and set up the stack frame. After doing the computation (in this case, just a comparison) we make sure that the function value is in the x0 register. Then we pop the link register and frame pointer and return.

abs:                                    // The abs function
        stp     fp, lr, [sp,-16]!       // Set up the stack frame
        mov     fp, sp

        cmp     x0, #0                  // Check if x0 is positive
        bge     done                    
        neg     x0, x0                  // Negate x0

done:
        ldp     fp, lr, [sp], 16        // Return from abs
        ret                             

Example: Factorial (fact.s)

/* 
 * File:        fact.s
 * Description: An iterative factorial function.
 */
        .arch   armv8-a

        .global factorial

        .text
        .align  2

factorial:                              // The factorial function
        stp     fp, lr, [sp,-16]!       
        mov     fp, sp
        sub     sp, sp, #32             // Allocate stack space for
                                        // three local variables

        // Initialize variables
        // n is the argument, stored at [fp, -8]
        // f accumulates the product, stored at [fp, -16]
        // i is the loop control, stored at [fp, -24]
        
        str     x0, [fp, -8]            // Store n
        mov     x0, #1                  // Store f
        str     x0, [fp, -16]
        mov     x0, #2                  // Store i
        str     x0, [fp, -24]

loop:   
        ldr     x0, [fp, -24]           // Compare i to n
        ldr     x1, [fp, -8]
        cmp     x0, x1
        bgt     done

        ldr     x0, [fp, -24]           // Multiply f by i
        ldr     x1, [fp, -16]
        mul     x1, x1, x0
        str     x1, [fp, -16]

        ldr     x0, [sp, 16]            // Increment i
        mov     x1, #1                  
        add     x0, x0, #1              
        str     x0, [fp, -24]
        b       loop                    // Jump to the top of the loop

done:
        
        ldr     x0, [fp, -16]           // Return from fact
        add     sp, sp, #32
        ldp     fp, lr, [sp], 16
        ret

This example stores its variables as local variables in the stack frame.

To allocate storage on the stack, we decrement the stack pointer. We need space for three variables, the argument, the loop control, and the accumulator (call them n, i, and f). This requires 24 bytes, but since the stack pointer has to be a multiple of 16, we decrement by 32.

        sub     sp, sp, #32             // Allocate stack space for
                                        // three local variables

Now we decide (arbitrarily) which stack position stores which variable. We choose [fp, -8] for n, [fp, -16] for f, and [ftp, -24] for i. Of course, they could just be stored in registers, but the point of this example is to show how local variables are stored on the stack.

        str     x0, [fp, -8]            // Store n
        mov     x0, #1                  // Store f
        str     x0, [fp, -16]
        mov     x0, #2                  // Store i
        str     x0, [fp, -24]

When the function returns, we must copy the result (f) to x0 and deallocate the extra storage in the stack frame.

        ldr     x0, [fp, -16]           // Return from fact
        add     sp, sp, #32
        ldp     fp, lr, [sp], 16
        ret

Here is a C program that tests calling the factorial function.

(testfact.c)

/*
 * File:        testfact.c
 * Description: This function tests the factorial function.
 */

#include <stdio.h>

extern long factorial(long n);

int main() {
  for (long i = 0; i <= 15; i++) {
    printf("%ld! = %ld\n", i, factorial(i));
  }
}

We can compile the C program together with factorial function like this:

cc -o testfact testfact.c fact.s

Example: Recursive Factorial (factr.s)

There’s nothing special about a recursive function – the entry and return sequences are the same as for other functions.

/* 
 * File:        factr.s
 * Description: A recursive factorial function.
 */
        .arch   armv8-a

        .global factorial

        .text
        .align  2

factorial:                              // The main function
        stp     fp, lr, [sp,-16]!
        mov     fp, sp
        sub     sp, sp, #16             // Allocate space for a local variable

        cmp     x0, #0                  // Test for 0
        beq     zero
        
        str     x0, [fp, -8]            // Store n on the stack
        sub     x0, x0, 1               // Decrement n
        bl      factorial               // Do a recursive call

        ldr     x1, [fp, -8]            // Multiply the return value by n
        mul     x0, x0, x1
        b       done
zero:
        mov     x0, 1
done:
        add     sp, sp, #16             // Return from fact
        ldp     fp, lr, [sp], 16
        ret

Exercise 8

Redo Exercise 7 with a function that does the conversion. The function should take the fahrenheit temperature as a single double argument and return the kelvin temperature as a double. Input and output should be done in the main program.

Turnin Name ftokfunc
File Name ftokfunc.s

Exercise 9

Redo Exercise 3 storing all values in local variables (on the stack).

Turnin Name countm
File Name countm.s

Call by Reference

For call by reference, we pass the address of a variable instead of the value.

Example: (sumprod.s)

/* 
 * File:        sumprod.s
 * Description: Defines a function that computes the sum
 *              and product of two numbers, passing arguments
 *              by reference.       
 */
        .arch   armv8-a

        .global main

        .text
        .align  2
main:                                   // The main function
        stp     fp, lr, [sp,-16]!
        mov     fp, sp
        
        adr     x0, prompt              // Print the prompt
        bl      printf

        adr     x0, infmt               // Read x and y
        adr     x1, x
        adr     x2, y
        bl      scanf

        adr     x0, x                   // Call sumprod
        adr     x1, y
        bl      sumprod

        adr     x0, outfmt              // Print the results
        adr     x1, x
        ldr     x1, [x1]
        adr     x2, y
        ldr     x2, [x2]
        bl      printf
        
        mov     x0, #0                  // Retrurn from main
        ldp     fp, lr, [sp], 16
        ret

        // sumprod - Two integer arguments are passed by reference. Their
        //           sum and  product is computed. The sum is passed back
        //           in the first argument and the product in the second.

sumprod:
        stp     fp, lr, [sp, -16]!
        mov     fp, sp

        ldr     x2, [x0]                // Get the value of x
        ldr     x3, [x1]                // Get the value of y
        add     x4, x2, x3              // Add x and y
        mul     x5, x2, x3              // Multiply x and y
        str     x4, [x0]                // Store x
        str     x5, [x1]                // Store y

        ldp     fp, lr, [sp], 16        // Return from sumprod
        ret
        
        .data
        .align  3                       // Align on an address divisible by 8
        
x:      .quad  0
y:      .quad  0

        .section .rodata

prompt: .string "Enter two integers: "
infmt:  .string "%ld %ld"
outfmt: .string "sum = %ld product = %ld\n"

Saving Registers

If a function needs to use nonvolative registers, it is obligated to save them on entry to the function and restore them before it returns. This is usually done by pushing them onto the stack on entry and restoring their values on exit.

Note that we must preserve the requirement that the stack pointer is a multiple of 16. If there is an odd number of registers involved, we can always allocate one extra slot on the stack and just not use it.

Arrays

Arrays are referenced by the start address, as in C. They can be statically allocated in the data section or dynamically allocated on the stack (simply by decrementing the stack pointer by an appropriate amount. The next example shows how to access array items using indexed addressing.

Example: Arrays (array.s)

This example creates an array, reads integers, stores them in the array, and prints them back out in reverse order.

/* 
 * File:        array.s
 * Description: This program illustrates array access.
 *              It reads the number of entries (up to 100).
 *              Then it prints the numbers in reverse.
 */
        .arch   armv8-a

        .global main

        .text
        .align  2
main:                              // The main function
        stp     fp, lr, [sp,-16]!  
        mov fp, sp
        
        // Read the number of numbers
        adr     x0, prompt1        // Pass the prompt in x0
        bl      printf             // Call printf
        
        adr     x0, infmt          // Pass the input format in x0
        adr     x1, n              // Pass the address of n in x1
        bl      scanf              // Call scanf

        // The loop control is stored in x19
        // n is stored in x20
        // Because we are calling printf and scanf, we need registers
        // that are preserved by the function calls.
        mov     x19, #0            // Intialize the loop control
        adr     x20, n             // Store n
        ldr     x20, [x20]

        // Read the numbers 

        adr     x0, prompt2        // Pass the prompt in x0
        bl      printf             // Call printf
        
test1:                             // Top of loop

        cmp     x19, x20           // Compare x19 and x0
        beq     done1              // Jump out of loop if equal
        
        adr     x0, infmt          // Pass the format in x0
        adr     x1, a              // Load the address of a into x1
        mov     x2, x19            // Get the index
        lsl     x2, x2, #3         // Multiply by 8
        add     x1, x1, x2         // Add the offset
        bl      scanf              // Call scanf
        
        add     x19, x19, #1       // Increment
        
        b       test1              // Jump to the top of the loop

done1:   

        // Print the numbers
        
        mov     x19, x20           // Initialize loop control
        sub     x19, x19, #1    

test2:                             // Top of loop

        cmp     x19, #0            // Compare x5 and 0
        blt     done2              // Jump out of loop if equal
        
        adr     x0, outfmt         // Pass the output format in x0
        adr     x1, a              // Load the address of a into x1
        ldr     x1, [x1,x19,lsl #3]   // Load the array element into x1
        bl      printf             // Call printf
        
        sub     x19, x19, #1       // Decrement
        b       test2              // Jump to the top of the loop

done2:   
        
        mov     x0, #0             // Return 0 in x0
        ldp     fp, lr, [sp], 16   // Pop the frame pointer and link register
        ret                        // Return from main

        .data                      // Start of data segment
        .align  3                  // Align on an even address

n:      .quad   0                  // Upper bound
a:      .fill   100, 8             // The array
        
prompt1: .string "how many? "              // Prompt string
prompt2: .string "enter the numbers: "     // Prompt string
infmt:   .string "%ld"                     // Input format
outfmt:  .string "%ld\n"                   // Output format

We’ll allocate static storage for up to 100 integers.

        .data                      // Start of data segment
        .align  3                  // Align on an even address

n:      .quad   0                  // Upper bound
a:      .fill   100, 8             // The array

This .fill directive allocates an array of size 100 of 8-byte chunks.

To read a number, as usual we have to pass its address to scanf. However, now we have to compute the address. We take tha base address, multiply the loop index by 8 and add it to the base address.

        adr     x0, infmt          // Pass the format in x0
        adr     x1, a              // Load the address of a into x1
        mov     x2, x19            // Get the index
        lsl     x2, x2, #3         // Multiply by 8
        add     x1, x1, x2         // Add the offset
        bl      scanf              // Call scanf

We we print the numbers, we can use indexed addressing. This form of the ldr instruction takes a base address (in x1), an index (in x19) and a does a logical left shift on the index by 3 bits to multiply the index by 3.

        adr     x0, outfmt         // Pass the output format in x0
        adr     x1, a              // Load the address of a into x1
        ldr     x1, [x1,x19,lsl #3]   // Load the array element into x1
        bl      printf             // Call printf

We could first read a value into another variable and then use indirect addressing to store it into the array, but that would require an extra memory reference.

Exercise 10

Rewrite the array example so that all variables (including the array) are stored as local variables on the stack.

Turnin Name array
File Name array.s

Example: Passing Arguments on the Stack (stack.s)

The first 8 integer or floating point arguments are passed in registers, but any additional ones are passed by pushing them onto the stack.

/*
 * File: stack.s
 * Description: This program illustrates passing arguments in registers
 * and on the stack.
 */
        .arch   armv8-a

        .global main
        .text
        .align 2
main:                                   // The main function
        stp     fp, lr, [sp,-16]!
        mov     fp, sp
        sub     sp, sp, #32

        adr     x0, fmt                 // Arg 0 (Integer 1)
        mov     x1, #1                  // Arg 1 (Integer 2)
        mov     x2, #2                  // Arg 2 (Integer 3)
        mov     x3, #3                  // Arg 3 (Integer 4)
        mov     x4, #4                  // Arg 4 (Integer 5)
        fmov    d0, #-5                 // Arg 5 (Double  1)
        mov     x5, #6                  // Arg 6 (Integer 6)
        mov     x6, #7                  // Arg 7 (Integer 7)
        fmov    d1, #8                  // Arg 8 (Double 2)
        mov     x7, #9                  // Arg 9 (Integer 8)
        fmov    d2, #10                 // Arg 10 (Double 3)
        mov     x10, #11                // Arg 11 (Integer 9)
        str     x10, [sp]
        fmov    d3, #12                 // Arg 12 (Double 4)
        fmov    d4, #13                 // Arg 13 (Double 5)
        fmov    d5, #14                 // Arg 14 (Double 6)
        fmov    d6, #15                 // Arg 15 (Double 7)
        fmov    d7, #16                 // Arg 16 (Double 8)
        fmov    d10, #17                // Arg 17 (Double 9)
        str     d10, [sp, 8]
        mov     x10, #8                 // Arg 18 (Double 10)
        str     x10, [sp, 16]
        bl      printf
        add     sp, sp, #32
        
        mov     x0, #0                  // Return from main
        ldp     fp, lr, [sp], 16
        ret

        .section .rodata
        
fmt:    .ascii  "a1: %ld\n"
        .ascii  "a2: %ld\n"
        .ascii  "a3: %ld\n"
        .ascii  "a4: %ld\n"
        .ascii  "a5: %2.1f\n"
        .ascii  "a6: %ld\n"
        .ascii  "a7: %ld\n"
        .ascii  "a8: %2.1f\n"
        .ascii  "a9: %ld\n"
        .ascii  "a10: %2.1f\n"
        .ascii  "a11: %ld\n"
        .ascii  "a12 %2.1f\n"
        .ascii  "a13: %2.1f\n"
        .ascii  "a14: %2.1f\n"
        .ascii  "a15: %2.1f\n"
        .ascii  "a16: %2.1f\n"
        .ascii  "a17: %2.1f\n"
        .string  "a18: %ld\n"

This example does not illustrate retrieving stack arguments, but they are located at [fp, 16], [fp, 24], etc.