Overview

An Instruction Set Architecture (ISA) is part of the abstract model of a computer. It defines how software controls the processor.

The Arm ISA allows you to write software and firmware that conforms to the Arm specifications. This mean that, if your software or firmware conforms to the specifications, any Arm-based processor will execute it in the same way.

This guide introduces the A64 instruction set, used in the 64-bit Armv8-A architecture, also known as AArch64.

We will not cover every single instruction in this guide. All instructions are detailed in the Arm Architecture Reference Manual (Arm ARM). Instead, we will introduce the format of the instructions, the different types of instruction, and how code written in assembler can interact with compiler-generated code.

At the end of this guide, you can check your knowledge. You will have learned about the main classes of instructions, the syntax of data-processing instructions, and how the use of W and X registers affects instructions. The key outcome that we hope you will learn from this guide is to be able to explain how generated assembler code maps to C statements, when given a C program and the compiler output for it. Finally, this guide will show you how to write a function in assembler that can be called from C.

Why you should care about the ISA

As developers, you may not need to write directly in assembler in our day-to-day role. However, assembler is still important in some areas, such as the first stage boot software or some low-level kernel activities.

Even if you are not writing assembly code directly, understanding what the instruction set can do, and how the compiler makes use of those instructions, can help you to write more efficient code. It can also help you to understand the output of the compiler. This can be useful when debugging.

Instruction sets in the Armv8-A

Armv8-A supports three instruction sets: A32, T32 and A64.

The A64 instruction set is used when executing in the AArch64 Execution state. It is a fixed-length 32-bit instruction set. The ‘64’ in the name refers to the use of this instruction by the AArch64 Execution state. It does not refer to the size of the instructions in memory.

The A32 and T32 instruction sets are also referred to as ‘ARM’ and ‘Thumb', respectively. These instruction sets are used when executing in the AArch32 Execution state. In this guide, we do not cover the A32 and T32 instruction sets. To find out more about these instruction sets, see the Related Information section of this guide.

Instruction set resources

Each version of the Arm architecture has its own Arm Architecture Reference Manual (Arm ARM), which can be found on the Arm Developer website. Every Arm ARM provides a detailed description of each instruction, including:

  • Encoding - the representation of the instruction in memory.
  • Arguments - inputs to the instruction.
  • Pseudocode - what the instruction does, as expressed in Arm pseudocode language.
  • Restrictions - when the instruction cannot be used, or the exceptions it can trigger.

The instruction descriptions for A64 are also available in XML and HTML. The XML and HTML formats are useful if you need to refer to the instructions often. The XML and HTML formats can be found on the Arm Developer website. You can find a link in the Related information section of this guide. The XML can be downloaded as a compressed archive and the HTML can be viewed and searched using a web browser.

Note: The information in the XML/HTML and the Arm ARM are taken from the same source but may be formatted slightly differently.

Simple sequential execution

The Arm architecture describes instructions following a Simple Sequential Execution (SSE) model. This means that the processor behaves as if the processor fetched, decoded and executed one instruction at a time, and  in the order in which the instructions appeared in memory.

In practice, modern processors have pipelines that can execute multiple instructions at once, and may do so out of order. This diagram shows an example pipeline for an Arm Cortex processor:

Arm Cortex Pipeline 

You will remember that the architecture is a functional description. This means that it does not specify how an individual processor works. Each processor must behave consistently with the simple sequential execution model, even if it is re-ordering instructions internally.

General-purpose registers

Most A64 instructions operate on registers. The architecture provides 31 general purpose registers. Each register can be used as a 64-bit X register (X0..X30), or as a 32-bit W register (W0..W30). These are two separate ways of looking at the same register. For example, this register diagram shows that W0 is the bottom 32 bits of X0, and W1 is the bottom 32 bits of X1:

 

For data processing instructions, the choice of X or W determines the size of the operation. Using X registers will result in 64-bit calculations, and using W registers will result in 32-bit calculations. This example performs a 32-bit integer addition:

ADD W0, W1, W2

This example performs a 64-bit integer addition:

ADD X0, X1, X2

When a W register is written, as seen in the example above, the top 32 bits of the 64-bit register are zeroed.

There is a separate set of 32 registers used for floating point and vector operations. These registers are 128-bit, but like the general-purpose registers, can be accessed in several ways. Bx is 8 bits, Hx is 16 bits and so on to Qx which is 128 bits.

 

The name you use for the register determines the size of the calculation. This example performs a 32-bit floating point addition:

FADD S0, S1, S2

This example performs a 64-bit floating point addition:

FADD D0, D1, D2

These registers can also be referred to as V registers. When the V form is used, the register is treated as being a vector. This means that it is treated as though it contains multiple independent values, instead of a single value. This example performs vector floating point addition:

FADD V0.2D, V1.2D, V2.2D

This example performs vector integer addition:

ADD  V0.2D, V1.2D, V2.2D

We will look at vector instructions in more detail later in this guide.

Other registers

Here are some other registers in the A64 that you should know about:

The zero registers, ZXR and WZR, always read as 0 and ignore writes.

You can use the stack pointer (SP) as the base address for loads and stores. You can also use the stack pointer with a limited set of data-processing instructions, but it is not a regular general purpose register.

Armv8-A has multiple stack pointers, and each one is associated with a specific Exception level. When SP is used in an instruction, it means the current stack pointer. The guide to the exception model explains how the stack pointer is selected.

X30 is used as the Link Register and can be referred to as LR.

Note: Separate registers, ELR_ELx, are used for returning from exceptions. This is discussed in more detail in the guide to the exception model.

The Program Counter (PC) is not a general-purpose register in A64, and it cannot be used with data processing instructions. The PC can be read using:

ADR Xd, .

The ADR instruction returns the address of a label, calculated based on the current location. Dot (‘.’) means 'here', so the shown instruction is returning the address of itself. This is equivalent to reading the PC. Some branch instructions, and some load/store operations, implicitly use the value of the PC.

Note: In the A32 and T32 instruction sets, the PC and SP are general purpose registers. This is not the case in A64 instruction set.

System registers

As well as general purpose registers, the architecture defines system registers. System registers are used to configure the processor and to control systems such as the MMU and exception handling.

System registers cannot be used directly by data processing or load/store instructions. Instead, the contents of a system register need to be read into an X register, operated on, and then written back to the system register. There are two specialist instructions for accessing system registers:

MRS	Xd, <system register>

reads the system register into Xd.

MSR	<system register>, Xn

write Xn to the system register.

System registers are specified by name, for example SCTLR_EL1:

MRS	X0, SCTLR_EL1

reads SCTLR_EL1 into X0.

System register names end with _ELx. The _ELx specifies the minimum privilege necessary to access the register. For example:

SCTLR_EL1

requires EL1 or higher privilege.

SCTLR_EL2

requires EL2 or higher privilege.

SCTLR_EL3

requires EL3 privilege

Attempting to access the register with insufficient privilege results in an exception.

Note: Sometimes you will see _EL12 or _EL01. These are used as part of virtualization. Refer to the guide on virtualization for more information.

Arithmetic and logic operations

The basic format of logical and integer arithmetic instructions is:

The parts of the instruction are as follows:

  • Operation. This defines what the instruction does. For example, ADD does addition and AND performs a logical AND.
    An S can be added to the operation to set flags. For example, ADD becomes ADDS. Thistells the processor to update the ALU flags based on the result of instruction. We discuss ALU flags in the section on generating condition code.
  • Destination: The destination of the instruction is always a register, and specifies where the result of the operation is placed. Most instructions have a single destination register. A few instructions have two destination registers. When the destination is a W register, the upper 32 bits of the corresponding X register are set to 0.
  • Operand 1: This will always be a register. This is the first input to the instruction.
  • Operand 2: This will be a register or a constant, and is the second input to the instruction.
    When operand 2 is a register, it may include an optional shift.
    When operand 2 is a constant, it is encoded within the instruction itself. This means that the range of constants available is limited.

You should be aware of a couple of special cases, such as the MOV and MVN instructions. MOV moves a constant, or the contents of another register, into the register specified as the destination. MOV and MVN only require a single input operand, which can be either a register or a constant, as shown here:

 MOV	X0, #1

sets: X0 = 1

 MVN	W0, W1

sets: W0 = ~W1

Floating point

Floating-point operations follow the same format as integer data-processing instructions and use floating-point registers. Like with the integer data-processing instructions, the size of the operation determines the size of the register that is used.  The operation part of a floating-point instruction always starts with an F. For example, this instruction sets H0 = H1 / H2 with half precision:

FDIV	H0, H1, H2

This instruction sets S0 = S1 + S2 with single precision:

FADD	S0, S1, S2	

This instruction sets D0 = D1 - D2 with double precision:

FSUB	D0, D1, D2

Support for half precision (16 bit) was added in Armv8.2-A and is optional. Support for half-precision is reported by ID_AA64PFR0_EL1.

Access to floating point registers can be trapped. This means that any attempt to use floating point registers will generate an exception. Trapping is discussed in more detail the exception model guide.

Is floating point support optional?

No. Support for floating point is mandatory in Armv8-A. The architecture specifies that it is required whenever a rich operating system, such as Linux, is used.

You are technically permitted to omit floating point support, if you are running an entirely proprietary software stack. Most toolchains, including GCC and Arm Compiler 6, will assume floating point support.

Bit manipulation

There are a set of instructions for manipulating bits within a register. This figure shows some examples:

The BFI instruction inserts a bit field into a register. In the preceding figure, BFI is taking a 6-bit field from the source register (W0) and inserting it at bit position 9 in the destination register.

UBFX extracts a bit field. In the preceding figure, UBFX is taking a 7-bit field from bit position 18 in the source register, and placing it in the destination register.

Other instructions can reverse byte or bit order, as you can see in this figure:

 

REV16 and RBIT are particularly useful when you are handling data that is in a different endianness.

Extension and saturation

Sometimes it is necessary to convert data from one size to another. The SXTx (sign extend) and UXTx (unsign extend) instructions are available for this conversion. In this conversion, the x determines the size of the data being extended, as shown in this figure:

 

In the first instruction, SXTB, the B means byte. It takes the bottom byte of W0 and sign extends it to 32 bits.

UXTH is an unsigned extension of a halfword (H). It takes the bottom 16 bits of W1 and zero extends it to 32 bits.

The first two examples have W registers as a destination, meaning the extension is to 32 bits. The third example has an X register, meaning the sign extension is to 64 bits.

Sub-register-sized integer data processing

Some instructions perform saturating arithmetic. This means that if the result is larger or smaller than the destination can hold, then the result is set to the largest or smallest value of the destination's integer range.

The data-processing instructions can efficiently handle 32-bit data and 64-bit data. In practice, you often see saturation instructions when handling sub-register calculations. Sub-register calculations are calculations of 16 bits or 8 bits. This table shows some examples of sub-register calculations in C and the generated assembler code:

C Generated assembler
uint32_t add32(uint32_t a, uint32_t b)
{
return a+b;
}
add32:
 ADD W0,W1,W0
 RET
uint16_t uadd16(uint16_t a, uint16_t b)
{
return a+b;
}
uadd16:
AND W8,W1,#0xffff
ADD W0,W8,W0,UXTH
RET
int16_t sadd16(int16_t a, int16_t b)
{
return a+b;
}
sadd16:
SXTH W8,W1
ADD W0,W8,W0,SXTH
RET

In the first example in the table, the 32-bit addition maps onto W registers and therefore can be handled easily.

For the 16-bit examples in the table, an extra instruction is necessary. The third example in the table takes the 16-bit inputs, extends them to 32 bits, and then performs the addition. The sequence converts the 16-bit input to 32 bits, using:

SXTH  W8,W1

Then, this instruction performs the addition and saturates the result to signed 16 bits:

ADD   W0,W8,W0,SXTH

Adding , SXTH to the end of the operand list of the ADD operation causes the result to use saturating arithmetic. Because the destination is a W register, the ADD will saturate to a 16-bit integer range.

Format conversion

We have seen that the MOV and MVN instructions copy the value from one register to another. Similarly, FMOV can be used to copy between floating-point and general purpose registers.

However, using FMOV copies the literal bit pattern between the registers. There are also instructions that can convert to the closest representation, as this figure shows:

 

In this example, imagine that X0 contains the value 2 (positive integer 2):

X0 = 0x0000_0000_0000_0002

Then, the following sequence is executed:

FMOV D0, X0

SCVTF D1, X0

Both instructions “copy” X0 into a D register. However, the results are quite different:

D0 = 0x0000_0000_0000_0002 = 9.88131e-324

D1 = 0x4000_0000_0000_0002 = 2.0

The FMOV copied the literal bit pattern, which is a very different value when interpreted as a floating-point value. The SCVTF converted the value in X0 to the closest equivalent in floating-point.

Similarly, FCTxx can be used to convert a floating-point value to its closest integer representation. In this instance, different values of 'xx' control the rounding mode used.

Vector data

The A64 architecture also provides support for vector data processing. The two types of vector processing available are:

  • Advanced SIMD, which is also known as NEON.
  • Scalable Vector Extension, which is abbreviated to SVE.
  • We will cover both types of vector processing in a later guide on vector programming.

    Note: The name Advanced SIMD derives from the existence of SIMD instructions that operated on regular 32-bit general-purpose registers in Armv6. In Armv7, the term Advanced SIMD was used for  instructions that could operate on 128-bit vectors. The Armv6 style instructions do not exist in A64, but the naming convention remains.

Loads and stores

The basic load and store operations are: LDR (load) and STR (store). These operations transfer a single value between memory and the general-purpose registers. The syntax for these instructions is:

LDR<Sign><Size>	<Destination>, [<address>]

STR<Size> 	<Destination>, [<address>]

Size

The size of the load or store is determined by the register type X or W and the <Size> field. X is used for 32 bits and W is used for 64 bits. For example, this instruction loads 32 bits from <address> into  W0:

LDR	W0, [<address>]

This instruction loads 64 bits from <address> into X0:

LDR	X0, [<address>]

The <Size> field allows you to load a sub-register sized quantity of data. For example, this instruction stores the bottom byte (B) of W0 to <address>:

STRB	W0, [<address>]

This instruction stores the bottom halfword (H) of W0 to <address>:

STRH	W0, [<address>]

Finally, this instruction stores the bottom word (W) of X0 to <address>:

STRW	X0, [<address>]

 

Zero and sign extension

By default, when a sub-register-sized quantity of data is loaded, the rest of the register is zeroed, as shown in this figure:

 

Note: Remember that whenever a W register is written, the top half of the X register is zeroed.

Adding an S to the operation causes the value to be sign extended instead. How far the size extension goes depends on whether the target is a W or X register, as shown in this figure:

Addressing

The addresses for load and store instructions appear within the square brackets, as shown in this example:

LDR  W0,  [X1]

There are several addressing modes that define how the address is formed.

  • Base register - The simplest form of addressing is a single register. Base register is an X register that contains the full, or absolute, virtual address of the data being accessed, as you can see in this figure:
 
  •  Offset addressing modes - An offset can be applied optionally to the base address, as you can see in this figure:
 

In the preceding figure, X1 contains the base address and #12 is a byte offset from that address. This means that the accessed address is X1+12. The offset can be either a constant or another register. This type of addressing might be used for structs, for example. The compiler maintains a pointer to the base of struct using the offset to select different members.

  • Pre-index addressing modes - In the instruction syntax, pre-indexing is shown by adding an exclamation mark ! after the square brackets,as this figure shows:
 

Pre-indexed addressing is like offset addressing, except that the base pointer is updated as a result of the instruction. In the preceding figure, X1 would have the value X1+12 after the instruction has completed.

  • Post-index addressing modes - With post-index addressing, the value is loaded from the address in the base pointer, and then the pointer is updated, as this figure shows:

Post-index addressing is useful for popping off the stack. The instruction loads the value from the location pointed at by the stack pointer, and then moves the stack pointer on to the next full location in the stack.

 

Load pair and store pair

So far, we have discussed the load and store of a single register. A64 also has load (LDP) and store pair (STP) instructions.

These pair instructions transfer two registers to and from memory. The first instruction loads [X0] into W3, and loads [X0 + 4] into W7:

LDP	W3, W7, [X0]

This second instruction stores D0 into [X4] and stores D1 to [X4 + 8]:

STP	D0, D1, [X4]		

Load and store pair instructions are often used for pushing, and popping off the stack. This first instruction pushes X0 and X1 onto the stack:

STP	X0, X1, [SP, #-16]!

This second instruction pops X0 and X1 from the stack:

LDP	X0, X1, [SP], #16

Remember that in AArch64 the stack-pointer must be 128-bit aligned.

Using floating point registers

Loads and stores can also be carried out using the floating-point registers, as we will see here. The first instruction loads 64-bits from [X0] into D1:

LDR	D1, [X0]

This second instruction stores 128-bits from Q0 to [X0 + X1]:

STR	Q0, [X0, X1]

Finally, this instruction loads a pair of 128-bit values from X5, then increments X5 by 256:

LDP	Q1, Q3, [X5], #256

There are some restrictions:

  • The size is specified by the register type only.
  • There is no option to sign extend loads.
  • The address must still be an X register.

Load and stores using floating-point registers can be found in unexpected cases. It is common for memcpy() type routines to use them. This is because the wider register means that fewer iterations are needed. Just because your code does not use floating-point values, don't assume that you won't need to use the floating-point registers.

Program flow

Ordinarily, a processor executes instructions in program order. This means that a processor executes instructions in the same order that they are set in memory. One way to change this order is to use branch instructions. Branch instructions change the program flow and are used for loops, decisions and function calls.

The A64 instruction set also has some conditional branch instructions. These are instructions that change the way they execute, based on the results of previous instructions.

Note: Armv8.3-A and Armv8.5-A introduced instructions to protect against return-oriented programming and jump-oriented programming.

 

Loops and decisions

In this section, we will examine how loops and decisions let you change the flow of your program code using branch instructions. There are two types of branch instructions: unconditional and conditional.

Unconditional branch instructions 

The unconditional branch instruction B <label> performs a direct, PC-relative, branch to <label>. The offset from the current PC to the destination is encoded within the instruction. The range is limited by the space available within the instruction to record the offset and is +/-128MB.

When you use BR <Xn>, BR performs an indirect, or absolute, branch to the address specified in Xn.

Conditional branch instructions 

The conditional branch instruction B.<cond> <label> is the conditional version of the B instruction. The branch is only taken if the condition <cond> is true. The range is limited to +/-1MB.

The condition is tested against the ALU flags stored in PSTATE and needs to be generated by a previous instruction such as a compare (CMP).

CBZ <Xn> <label> and CBNZ <Xn> <label>

This instruction branches to <label> if Xn contains 0 (CBZ), and branches to label if Xn does not contain 0 (CBNZ).

TBZ <Xn>, #<imm>, <label> and TBNZ <Xn>, #<imm>, <label>

TBX works in a similar way to CBZ/CBNZ, but tests the bit specified by <imm>.

Note: The direct, or PC-relative, branches store the offset to the destination within the instruction. The conditional branches have a smaller range. This is because some bits are needed to store the condition itself, which leaves fewer bits for the offset.

Mapping these on to what you might write in C, this table shows examples of using branches for loops and decisions:

C Typical output from a compiler

if (a == 5)
  b = 5;

  CMP W0, #5
  B.NE skip
  MOV W8, #5
 skip:
 …

 while (a != 0)
 {
  b = b + c;
  a = a - 1;
 }

loop:
 CBZ W8, skip
 ADD W9, W9, W10
 SUB W8, W8, #1
 B loop
skip:
 …

Note: The labels shown in the output would not be created by a compiler. They are included here to aid readability.

Generating condition code

In Program flow - loops and decisions, we learned that the  <cond> is tested against the ALU flags stored in PSTATE.

The ALU flags are set as a side effect of data-processing instructions. To recap, an S at the end operation causes the ALU flags to be updated. This is an example of an instruction in which the ALU flags are not updated:

ADD	X0, X1, X2

This is an example of an instruction in which the ALU flags are updated with the use of S:

ADDS	X0, X1, X2

This method allows software to control when the flags are updated or not updated. The flags can be used by subsequent condition instructions. Let's take the following code as an example:

SUBS	X0, X5, #1 
AND	X1, X7, X9 
B.EQ	label

The SUBS instruction performs a subtract and updates the ALU flags. Then the AND instruction performs an and operation, and does not update the ALU flags. Finally, the B.EQ instruction performs a conditional branch, using flags set as result of the subtract.

The flags are:

  • N - Negative
  • C - Carry
  • V - Overflow
  • Z - Zero

Let’s take the Z flag as an example. If the result of the operation was zero, then the Z flag is set to 1. For example, here the Z flag will be set if X5 is 1, otherwise it will be cleared:

SUBS	X0, X5, #1 

The condition codes map on to these flags and come in pairs. Let's take EQ (equal) and NE (not equal) as an example, and see how they map to the Z flag:

The EQ code checks for Z==1. The NE code checks for Z==0.

Taking the following code as an example:

SUBS	W0, W7, W9

W0 = W7 - W9, Z 

B.EQ	label

In the first line, we have a subtract operation. In the second line, the flag is set if the result is zero. In the final line, there is a branch to label if Z==1.

If w7==w9, the result of the subtraction will be zero, and the Z flag would have been set. Therefore, the branch to label will be taken if w7 and w9 are equal.

In addition to the regular data-processing instructions, other instructions are available that only update the ALU flags:

CMP - Compare

TST - Test

These instructions are aliases of other instructions. For example:

CMP X0, X7     //an alias of SUBS XZR, X0, X7
TST W5, #1 //an alias of ANDS WZR, W5, #1

By using the Zero register as a destination, we are discarding the result of the operation and only updating the ALU flags.

Conditional select instructions

So far, we have seen examples that use branches to handle decisions. The A64 instruction set also provides conditional select instructions. In many cases, these instructions can be used as an alternative to branches.

There are many variants, but the basic form is:

CSEL	Xd, Xn, Xm, cond

This means that:

if cond then
  Xd = Xn
else
  Xd = Xm

You can see an example in this code:

CMP	W1, #0
CSEL W5, W6, W7, EQ

Which gives the same result as:

if (W1==0) then
  W5 = W6
 else
  W5 = W7

There are variants that combine another operation with the conditional select. For example, CSINC performs a select and addition:

CSINC	Xd, Xn, Xm, cond

This means that:

 if cond then
  Xd = Xn
 else
  Xd = Xm + 1

To just conditionally increment, you could write:

 CSINC	X0, X0, X0, cond

Which equates to:

if cond then
  X0 = X0
 else
  X0 = X0 + 1

However, the architecture provides an alias, CINC, which is the equivalent to this command.

Compilers choose the most efficient method to implement the functionality in your program. Compilers will often use a conditional select for small if ... else statements performing simple operations, because conditional selects can be more efficient than branches.

Here are some simple if ... else examples that compare implementations using branches to equivalent implementations using conditional select instructions:

C Branching Conditional select
if (a != 0)
  b = b + 1;
 CMP W0, #0
 B.EQ else
 ADD W1, W1, #1
else:
 …
CMP W0, #0
CINC W0, W1, NE
if (a == 0)
  y = y + 1;
else
  y = y - 1;
 CMP W0, #0
 B.NE else
 ADD W1, W1, #1
 B end
else:
 SUB W1, W1, #1
end:
 …
 CMP W0, #0
 SUB W2, W1, #1
 CSINC W1, W2, W1, NE

In these types of examples, conditional selects have some advantages. The sequences are shorter and take the same number of instructions, regardless of the outcome.

Importantly, conditional selects also remove the need to branch. In modern processors, this kind of branch can be difficult for the branch prediction logic to predict correctly. A mispredicted branch can have a negative effect on performance, it is better that you remove branches where possible.

Function calls

When calling a function or sub-routine, we need a way to get back to the caller when finished. Adding an L to the B or BR instructions turns them into a branch with link. This means that a return address is written into LR (X30) as part of the branch.

Note: The names LR and X30 are interchangeable. An assembler, such as GNU GAS or armclang, will accept both.

There is a specialist function return instruction, RET. This performs an indirect branch to the address in the link register. Together, this means that we get:

 

Note: The figure shows the function foo() written in GAS syntax assembler. The keyword .global exports the symbol and .type indicates that the exported symbol is a function.

Why do we need a special function return instruction? Functionally, BR LR would do the same job as RET. Using RET tells the processor that this is a function return. Most modern processors, and all Cortex-A processors, support branch prediction. Knowing
that this is a function return allows processors to more accurately predict the branch.

Branch predictors guess the direction the program flow will take across branches. The guess is used to decide what to load into a pipeline with instructions waiting to be processed. If the branch predictor guesses correctly, the pipeline has the correct instructions and the processor does not have to wait for instructions to be loaded from memory.

Procedure Call Standard

The Arm architecture places few restrictions on how general purpose registers are used. To recap, integer registers and floating-point registers are general purpose registers.However, if you want your code to interact with code that is written by someone else, or with code that is produced by a compiler, then you need to agree rules for register usage. For the Arm architecture, these rules are called the Procedure Call Standard, or PCS.

The PCS specifies:

  • Which registers are used to pass arguments into the function.
  • Which registers are used to return a value to the function doing the calling, known as the caller.
  • Which registers the function being called, which is known as the callee, can corrupt.
  • Which registers the callee cannot corrupt.

Consider a function foo(), being called from main():

The PCS says that the first argument is passed in X0, the second argument in X1, and so on up to X7. Any further arguments are passed on the stack. Our function, foo(), takes two arguments: a and b. Therefore, a will be in W0 and b will be in W1.

Why W and not X? Because the arguments are a 32-bit type, and therefore we only need a W register.

Note: In C++, X0 is used to pass the implicit this pointer that points to the called function.

Next, the PCS defines which registers can be corrupted, and which registers cannot be corrupted. If a register can be corrupted, then the called function can overwrite without needing to restore, as this table of PCS register rules shows:

X0-X7 X8-X15 X16-X23 X24-X30
Parameter and Result Registers
(X0-X7)
XR (X8) IP0 (X16) Callee-saved Registers
(X24-X28)
Corruptible Registers
(X9-X15)
IP1 (X17)
PR (X18)
Callee-saved Registers
(X19-X23)
FP (X29)
LR (X30)

For example, the function foo() can use registers X0 to X15 without needing to preserve their values. However, if  foo() wants to use X19 to X28 it must save them to stack first, and then restore from the stack before returning.

Some registers have special significance in the PCS:

  • XR - This is an indirect result register. If foo() returned a struct, then the memory for struct would be allocated by the caller, main() in the earlier example. XR is a pointer to the memory allocated by the caller for returning the struct.
  • IP0 and IP1 - These registers are intra-procedure-call corruptible registers. These registers can be corrupted between the time that the function is called and the time that it arrives at the first instruction in the function. These registers are used by linkers to insert veneers between the caller and callee. Veneers are small pieces of code. The most common example is for branch range extension. The branch instruction in A64 has a limited range. If the target is beyond that range, then the linker needs to generate a veneer to extend the range of the branch.
  • FP - Frame pointer.
  • LR - X30 is the link register (LR) for function calls.

Note: We previously introduced the ALU flags, which are used for conditional branches and conditional selects. The PCS says that the ALU flags do not need to be preserved across a function call.

There is a similar set of rules for the floating-point registers:

D0-D7 D8-D15 D16-D23 D24-D31
Parameter and Result Registers
(D0-D7)
Callee-saved Registers
(D8-D15)
Callee-saved Registers
(D16-D31)

System calls

Sometimes it is necessary for software to request a function from a more privileged entity. This might happen when, for example, an application requests that the OS opens a file.

In A64, there are special instructions for making such system calls. These instructions cause an exception, which allows controlled entry into a more privileged Exception level.

  • SVC - Supervisor call
    Causes an exception targeting EL1.
    Used by an application to call the OS.
  • HVC - Hypervisor call
    Causes an exception targeting EL2.
    Used by an OS to call the hypervisor, not available at EL0.
  • SMC - Secure monitor call
    Causes an exception targeting EL3.
    Used by an OS or hypervisor to call the EL3 firmware, not available at EL0.

If an exception is executed from an Exception level higher than the target exception level, then the exception is taken to the current Exception level. This means that an SVC at EL2 would cause exception entry to EL2. Similarly, an HVC at EL3 causes exception entry to EL3. This is consistent with the rule that an exception can never cause the processor to lose privilege.

Check your knowledge

Related information

Here are some resources related to material in this guide: 

Here are some resources related to topics in this guide:

Instruction set resources

Procedure Call Standard

Useful links to training:

Next steps

Using the Arm Instruction Set Architecture (ISA), you can write software or firmware that any Arm-based processor will execute in the same way, if that software or firmware conforms to the Arm specifications. In this guide, we introduced the A64 instruction set, which is used in Armv8-A AArch64. We introduced the format of the instructions, the different types of instruction, and how code written in assembler can interact with compiler-generated code. We explained the main classes of instructions, the syntax of data-processing instructions, and how the use of W and X registers affects instructions.

Based on the material learned in this guide, you can explain how generated assembler code maps to C statements when given a C program and compiler output, and how to write a function in assembler that can be called from C.  You will also understand how to find detailed descriptions for each instruction on the Arm Developer website, and concepts such as registers, data processing, program flow, and loads and stores.

To keep learning about the Armv8-A architecture, see more in our series of guides.

To check your knowledge of A64 assembler, try the ISA lab exercises (coming soon). The lab exercises require the Arm DS-5, Ultimate Edition. A 30-day evaluation version is available and can be used to complete the exercises.