CS 327
Compiler Design
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.
/*
* 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 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.
*/
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
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 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
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.
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:
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.
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.
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.
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.
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. |
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
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.
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:
ldr reg =value which is automagically translated by the assembler into method 2.| Turnin Name | ftok |
| File Name | ftok.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:
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 |
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 |
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 |
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 |
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 |
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:
Write a program that reads two integers and prints the max and min. Label the output.
| Turnin Name | minmax |
| File Name | minmax.s |
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.
fadd, fsub, fmul, fdiv
fcmp
fmov
scvtf, ucvtf
fcvtzs, fcvtzu
/*
* 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.
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 |
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.
/*
* 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
/*
* 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.
/*
* 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
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
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 |
Redo Exercise 3 storing all values in local variables (on the stack).
| Turnin Name | countm |
| File Name | countm.s |
For call by reference, we pass the address of a variable instead of the value.
/*
* 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"
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 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.
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.
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 |
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.