AArch64/ARM64 Tutorial

Chapter 19: Assembler Directives

Before we can even begin writing out an Assembly Source File, you need to learn about Assembler Directives.

Directives are various instructions (these are **NOT** ARM instructions) that tell the Assembler about any symbols, macros, statements, etc that are present in the Source File and how to apply these to help the Assembling process.

When your Source File becomes quite large/complex, it will need Directives. You may have a chunk of code that is present multiple times in your source. Why write it out multiple times when you can just write it out once and have it applied to multiple locations? Not only that, are you running into situations of having to memorize a specific numerical value for something that is commonly known by name? Directives allow you to use Names to represent any numerical value.


Setting Symbols:

You can direct specific names to any numerical value you want. This is done via the SET directive. Anything that uses the SET directive is known as a Symbol.

Format~
.set name, expression

name = The name (label) you want to give the symbol
expression = Value/Number, but this can also be a name (more on names within names soon)

Let's say you want to set the name True for the numerical value of 1. You would write the following line anywhere in your source file~

.set True, 1

You can write it out in Hexadecimal as well

.set True, 0x1

With the name True set to the value of 1, you can now write True in your source wherever you need to value of 1 applied. Like this....

.set True, 1 //This line only needs to present once in your source and can be located anywhere
ldr x3, [x30]
cmp r3, True //Use 'True' in place of numerical value 1
beq end
add w4, w4, #4
stw w10, [x2, #8]
end:

Some more Symbol examples~

.set Offset, 0x1340
.set Memory_Address, 0x8045ECC4
.set Error, -1

---

Let's go over more about Symbols for Memory Addresses. Having Symbols set for commonly used Memory Addresses can come in handy. Like so...

.set Memory_Address, 0x8045ECC4

Therefore, if you need to set Memory_Address into a register (let's say x12), you can do so something like this...

.set Memory_Address, 0x8045ECC4

movz x12, :abs_g1:Memory_Address
movk x12, :abs_g0_nc:Memory_Address

This allows you to not have to include the lsl shifting mechanism that movz and movk typically require. The Assembler will read the special information in between the semi-colons and will apply the correct lsl's in the movz/movk for you.

Let's say our Memory Address was instead 64-bits in size, you would write it out via Symbols like this...

.set Memory_Address, 0x004000ABCCCC0008

movz x12, :abs_g3:Memory_Address
movk x12, :abs_g2_nc:Memory_Address
movk x12, :abs_g1_nc:Memory_Address
movk x12, :abs_g0_nc:Memory_Address


Using Math, Binary, and Logical Operations within Symbols:

Instead of writing just plane jane values for your Symbols. You can incorporate basic calculations. Like this..

.set add_values, 0x1000 + 0xB00
.set subtract_values, 100 - 50
.set divide_values, 33 / 11
.set multiply_values, 0xA0 * 0x14

You can beyond basic math and utilize basic binary logical operations

.set OR_values, 0x1008 | 0x400
.set AND_values, 0x800 & 0x000
.set XOR_values, 0xF8001CFD ^ 0x01000AAB

 You can bit shifting like this..
 
.set Shifted_left_value, 0x8 << 23
.set Shifted_right_value, 0xCCC >> 3


Names within Names:

You can use names within names for Symbols. Like this...

Example~
.set bowser, 0x1E00
.set auto_drift, 1
.set character_config, bowser | auto_drift

Another example~
.set table_address, 0x81500078
.set index_one, 4
.set index_two, index_one*2
.set index_three, index_two*2
.set table_special_data_address, table_address + index_three

As you can see, you can establish symbols that can be used directly from calculations of other symbols.


Psuedo-Ops:

There will be times when you will need to write out numerical values or text (string values) in a Source File without the use of ARM Instructions. These values will later be referenced by load instructions in your Source File. We call this "Pseudo-Ops".

Here's a list of all Pseudo-Ops~

Examples:
.long 0x80001500
.float 3.5
.asciz "/home/user/example"

There may be times where you will need your Psuedo-Op data to be aligned. All this means is that a certain amount of zero bytes are appended to the end so the block of data is divisible by a desired amount.

This is done via ".align X".

.align X Guide:

etc etc...


Macros:

While symbols can be handy, they are very limited. If you want your ARM64 instruction(s) to be tied to a particular Name, you will need to use what are called Macros.

Format of a Macro~

.macro Name Arg**
contents of Macro located HERE
.endm

**Arg is optional and will be discussed soon.

Let's say we have the following instructions...

movz x12, 0x0400, lsl #16
ldr x12, [x12, #20]
ldr x12, [x12]

...And these instructions occur multiple times in your source. Well instead of 'manually' writing these batch of instructions for every single occurrence, you can write out the Macro for it just once, with a name tied to it, and then literally just write out the name.

Like this..

.macro Load_Pointer //Write out the Macro
movz x12, 0x0400, lsl #16
ldr x12, [x12, #20]
ldr x12, [x12]
.endm

add w0, w4, w5
Load_Pointer //Insert batch of instructions (aka Macro) here
orr w3, w3, w0

IMPORTANT: You must have the macro established (via .macro) before (above) you use the macro itself in your Source. This does *not* apply to symbols unless the Macro contains a symbol(s). If so, then the establishment of that symbol *must* be above the establishment of the Macro.


Macros w/ Args:

Now we will discuss the optional Arg (Argument) in Macros. With the earlier macro example, you were 'forced' to use x12. If you wanted to use a different register and keep the original macro as well, you would have to write out a whole new 2nd macro. Well, with Arg options, you wouldn't need to resort to that.

A Macro can be given arg(s) with custom names.

Look at the following example..

.macro Load_Pointer xregister
movz \xregister, 0x0400, lsl #16
ldr \xregister, [\xregister, #20]
ldr \xregister, [\xregister]
.endm

As you can see, we have have the Macro equipped with 'xregister' as an Arg. The \xregister occurrences within the macro is how to use the arg.

When you use the Macro in your Source, you will need to supply the Arg. We named our Arg as xregister so we will know to supply an extended register.

add w0, w4, w5
Load_Pointer x12 // Use x12 as Arg for Macro
orr w3, w3, w0

If we wanted to use something other than x12, it's super simple to do...

add w0, w4, w5
Load_Pointer x27 // Use x27 as Arg for Macro
orr w3, w3, w0

Here's an example Macro using two args

.macro Multiply_Together_Store wreg, xreg // You must separate Macro Args with a comma
mul \wreg, \wreg, \wreg
str \wreg, [xreg]
.endm

add w0, w4, w5
Multiply_Together_Store w10, x11 // Use w10 for non-extended register usage in Macro, and x11 for extended register usage
orr w3, w3, w0

...and that's it for Assembler Directives. These can be super handy when writing gigantic Source Files.


Next Chapter

Tutorial Index