1. preknowledge
  2. register
    1. register access speed
    2. register type
    3. register and C type
      1. Integers
      2. Pointers
      3. Floating Point
  3. instructions
    1. preknowledge
    2. memory access
      1. ldr
      2. str
      3. ldp
      4. stp
      5. three patterns
      6. pseudo instruction
      7. literal pool
      8. relocation of address when executing
      9. examples
        1. loading (storing) various sizes of integers
        2. array indexing
        3. faster memory copy
        4. indexing through an array of struct
    3. control flow
      1. cmp
      2. br
      3. ble
      4. bl
      5. cbz
      6. csel
    4. shift Operations
      1. lsl
      2. lsr
      3. asr
      4. ror
    5. bit manipulation
      1. mvn
      2. orr
      3. bfi
      4. ubfm
      5. ubfiz
    6. other
      1. adr
      2. adrp
      3. smaddl
  4. programming
    1. if statement
      1. if
        1. a rule of thumb
        2. temporary label
      2. if / else
        1. a complete example
    2. loop
      1. while loop
      2. for loop
        1. continue
        2. break
    3. structs
      1. alignment
        1. example
      2. defining structs
        1. using structs
        2. this pointer in c++
    4. const
    5. switch and jump table
      1. implement falling through
      2. implementing gaps
      3. other strategies for implementing switch
      4. strategies for implementing if-else
    6. functions
      1. bottom line concept
        1. a example
      2. inline functions
      3. passing parameters to functions
        1. a example
      4. const
      5. passing pointers
      6. passing reference
      7. more than eight parameters
      8. examples of calling some common C runtime functions
      9. system calls
        1. What IS a system call?
        2. Mechanism of making a system call
        3. the number associated with a particular system call
        4. example getpid()
    7. floating point
      1. what are floating point numbers?
      2. register
      3. truncation towards zero
        1. example
      4. Truncation Away From Zero
      5. rounding conversion
      6. converting an integer to a float point value
      7. floating point literals
        1. Fitting 32 bits into a 32 bit bag
      8. fmov
      9. half precision
      10. bit manipulation
      11. endian
    8. assembly macros
      1. General Use
        1. AASCIZ
        2. PUSH_P, PUSH_R, POP_P and POP_R
        3. START_PROC and END_PROC
        4. MIN and MAX
        5. MOD
        6. GLABEL
        7. CRT
        8. MAIN
        9. errno
      2. Loads and Stores
        1. GLD_PTR
        2. GLD_ADDR
        3. LLD_ADDR
        4. LLD_DBL
        5. LLD_FLT
    9. performance
      1. Undoing Stack Pointer Changes
      2. other stuff
        1. let the assembler itself calculate the length for you
    10. atomic operations
      1. Load Linked, Store Condition
        1. spin-lock
  • Reference
  • Cover image for AArch64 ASM Book

    AArch64 ASM Book


    Timeline

    Timeline

    2025-06-22

    init

    This article introduces the basics of AArch64 assembly language, covering fundamental data types, instruction and pointer widths, differences in register access speed, and the mapping relationships between various registers (such as integer, pointer, floating-point, and vector registers) and C language data types. It also summarizes the functions of special registers like the stack frame pointer and link register, as well as the mechanism for indirect return results.

    preknowledge

    1 byte has 8 bits

    • char has 1 byte
    • short has 2 bytes
    • int has 4 bytes

    Each address represents a storage unit of one byte

    img
    img

    1
    2
    3
    4
    gcc -E hello.c -o hello.i
    gcc -S hello.i -o hello.s
    gcc -c hello.s -o hello.o
    gcc hello.o -o hello
    • All AARCH64 instructions are 4 bytes in width.

    • All AARCH64 pointers are 8 bytes in width†.

      While this is technically true, typically only the lower 39, 42 or 48 bits of addresses in Linux systems are used - i.e. the virtual address space of an ARM Linux process is smaller than 64 bits. The upper bits are set to zero when considering the address as an 8-byte value.

    register

    register access speed

    Latency
    Latency

    This says that if we liken accessing a register (which can be done at least once per CPU Clock Cycle) to one second, accessing RAM would be like a 3.5 to 5.5 minute wait.

    register type

    • rn means register “of some type” number n.

    The kind of register is specified by a letter. Which register within a given type is specified by a number. There are some exceptions to this. Here is an introductory summary:

    LetterType
    x64 bit integer or pointer
    w32 bit or smaller integer
    d64 bit floats (doubles)
    s32 bit floats

    Some register types have been left out.

    (Chapter 9.1)(Cortex-A Series Programmer’s Guide for ARMv8-A)

    Cortex-A Series Programmer's Guide for ARMv8-A Chapter 9.1
    Cortex-A Series Programmer's Guide for ARMv8-A Chapter 9.1

    • x29 is the frame pointer (FP)
    • x30 is the link register (LR, i.e., the return address)
    • When the return value type of a function is too large (e.g., exceeding 64 bits, or being a large structure or union) to be passed directly through general-purpose registers (X0~X7), the caller pre-allocates a block of memory as a “storage area for the return result” and passes the address of this area to the called function — the address of this memory block is the “Indirect Result Location.” After the called function finishes execution, it writes the result to this address rather than returning it via registers.

    The registers used for floating point types (and vector operations) are coincident:

    image-20250511210301586
    image-20250511210301586

    • qregisters are a massive 16 bytes wide - quad words. (alias for vn, mainly used inSIMD/Neoninstructions)
    • vregisters are also 16 bytes wide and are synonyms for theqregisters.
    • dregisters fordoubleswhich are 8 bytes wide - double precision. 2 perv.
    • sregisters forfloatswhich are 4 bytes wide - single precision. 4 perv.
    • hregisters forhalf precision floatswhich are 2 bytes wide. 8 perv.
    • bregisters for byte operations. 16 perv.

    register and C type

    Integers

    This declares an integerThis IS an integer
    charwn
    shortwn
    intwn
    longxn

    Pointers

    This declares a pointerThis IS a pointer
    type *xn

    All pointers are stored in x registers. X registers are 64 bits long but many operating systems do not support 64 bit address spaces because keeping track of that big of an address space itself would use a lot of space. Instead OS’s typically have 48 to 52 bit address spaces.

    Floating Point

    This declares a floatThis IS a float
    floatsn
    doubledn
    __fp16(half)hn

    image-20250511210512600
    image-20250511210512600

    image-20250511210539205
    image-20250511210539205

    vn is the actual physical register name,recommended to use, supports the most types of access (floating-point + SIMD)

    qn is an alias for vn, mainly used inSIMD/Neoninstructions (Single Instruction - Multiple Data)

    instructions

    preknowledge

    EVERY AARCH64 instruction is 4 bytes wide. Everything the CPU needs to know about what the instruction is and what variation it might be plus what data it will use will be found in those 4 bytes.

    • Most (but not all) AARCH64 instructions have three operands. These are read in the following way:
    1
    op     ra, rb, rc

    means:

    1
    ra = rb op rc

    examples:

    1
    2
    sub    x0, x0, x1 ; means x0 = x0 - x1
    mov x0, x1 ; means x0 = x1
    • [ ]

    the[and]serve the same purpose as the asterisk in C and C++ indicating “dereference.” It means use what’s inside the brackets as an address for going out to memory.

    when a ! is at the end of [] , for example:

    1
    2
    3
    stp     x21, x30, [sp, -16]!

    stp x29, x30, [sp, -16]!

    Lastly, the exclamation point means that the stack pointer should be changed (i.e. the -16 applied to it) before the value of the stack pointer is used as the address in memory to which the registers will be copied. Again, this is a predecrement.

    it means:

    1. sp = sp - 16(stack pointer moves down by 16 bytes)
    2. Storex29into[sp], storex30into[sp + 8]

    Corresponds to:

    1
    ldp     x29, x30, [sp], 16

    it means:

    1. From[sp]read 8 bytes intox29, from[sp + 8]read 8 bytes intox30
    2. sp = sp + 16(release stack frame space)

    The stack pointer in ARM V8 can only be manipulated in multiples of 16.

    The stack pointer in ARM V8 can only be manipulated in multiples of 16.

    The stack pointer in ARM V8 can only be manipulated in multiples of 16.

    x29 is the stack frame register, but it is not mandatory to save

    memory access

    ldr

    load register

    1
    2
    3
    4
    ldr    x0, [sp]   // load 8 bytes from address specified by sp
    ldr w0, [sp] // load 4 bytes from address specified by sp
    ldrh w0, [sp] // load 2 bytes from address specified by sp
    ldrb w0, [sp] // load 1 byte from address specified by sp

    When misaligned accesses to RAM are made, the processor must slow down and access each byte individually. This is a big performance hit. Properly aligned access is critical to performance.

    str

    store register

    1
    2
    3
    4
    str    x0, [sp]   // store 8 bytes to address specified by sp
    str w0, [sp] // store 4 bytes to address specified by sp
    strh w0, [sp] // store 2 bytes to address specified by sp
    strb w0, [sp] // store 1 byte to address specified by sp

    Casting between integer types is in some cases accomplished byandingwith255and65535(forcharandshort) or :

    Whenever a narrower portion of a register is written to, the remainder of the register is zero’d out. That is:ldrboverwrites the least significant byte of anxregister and zeros out the upper 7 bytes.

    ldp

    load pair, same as ldr but load a pair of value

    stp

    store pair, same as str but load a pair of value

    offsets

    1
    2
    3
    1) LDR Xt, [Xn|SP{, #pimm}] ; 64-bit general registers
    2) LDR Xt, [Xn|SP], #simm ; 64-bit general registers, Post-index
    3) LDR Xt, [Xn|SP, #simm]! ; 64-bit general registers, Pre-index
    • simmcan be in the range of -256 to 255 (10 byte signed value).
    • pimmcan be in the range of 0 to 32760 in multiples of 8.

    three patterns

    1. Normal offset mode
    1
    LDR Xt, [Xn, #pimm]

    FromXn + pimm’s address load data intoXt; address registerXnunchanged;

    pimmis apositive immediate, must be a multiple of 8, with a maximum of 32760.

    1. Post-indexed addressing mode
    1
    LDR Xt, [Xn], #simm

    First useXnoriginal value as the address to load data intoXt, then usesimmto updateXn;** address registerXnchanges after reading memory**;

    1. Pre-indexed addressing mode
    1
    LDR Xt, [Xn, #simm]!

    FirstXn = Xn + simm, then useXnas the address to load data intoXt, address registerXnchanges before reading memory;

    pseudo instruction

    1
    ldr     x1, =label
    • the assembler puts the address of the label into a special region of memory called a “literal pool.” What matters is this region of memory is placed immediately after (therefore nearby) your code.

    • Then, the assembler computes the difference between the address of the current instruction (theldritself) and the address of the data in the literal pool made from the labeled data.

    • The assembler generates a differentldrinstruction which uses the difference (or offset) of the data relative to the program counter (pc). Thepcis non-other the address of the current instruction.

    • Because the literal pool for your code is located nearby your code, the offset from the current instruction to the data in the pool is a relatively small number. Small enough, to fit inside a four byteldrinstruction.

    1
    ldr    x1, [pc, offset to data in literal pool]

    A downside of this approach is that the literal pool, from which the address is loaded, resides in RAM. This means each of theseldrpseudo instructions incurs a memory reference.

    literal pool

    compare

    1
    2
    ldr x1, =q
    ldr x1, q

    aarch64

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
            .global     main       // expose main to linker
    .text // begin to write code
    .align 2 // the code should certainly begin on an even address

    main: str x30, [sp, -16]!

    ldr x0, =fmt
    ldr x1, =q
    ldr x2, [x1]
    bl printf

    ldr x0, =fmt
    ldr x1, q
    ldr x2, [x1]
    bl printf

    ldr x30, [sp], 16
    mov w0, wzr
    ret

    .data
    q: .quad 0x1122334455667788
    fmt: .asciz "address: %p value: %lx\n"

    .end

    disasembling the binary machine code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    0000000000007a0 <main>:
    7a0: f81f0ffe str x30, [sp, #-16]!
    7a4: 58000160 ldr x0, 7d0 <main+0x30>
    7a8: 58000181 ldr x1, 7d8 <main+0x38>
    7ac: f9400022 ldr x2, [x1]
    7b0: 97ffffb4 bl 680 <printf@plt>
    7b4: 580000e0 ldr x0, 7d0 <main+0x30>
    7b8: 580842c1 ldr x1, 11010 <q>
    7bc: f9400022 ldr x2, [x1]
    7c0: 97ffffb0 bl 680 <printf@plt>
    7c4: f84107fe ldr x30, [sp], #16
    7c8: 2a1f03e0 mov w0, wzr
    7cc: d65f03c0 ret

    and

    1
    2
    3
    000000000011010 <q>:
    11010: 55667788
    11014: 11223344
    • It says000000000011010 <q>:. This means that what comes next is the data corresponding to what is labeledqin our source code. Notice the relocatable address of11010. We will explain “relocatable address” below.

    • Now, look at the disassembled code on the line beginning with7b8. It readsldr x1, 11010. So the disassembled executable is saying “go to address 11010 and fetch its contents” which are our1122334455667788.

    InstructionMeaning
    ldr r, =labelLoad the address of the label into r
    ldr r, labelLoad the value found at the label into r

    relocation of address when executing

    None of the addresses we have seen so far are the final addresses that will be used once the program is actually running. All addresses will be relocated.

    One reason for this is a guard against malware. A technique called Address Space Layout Randomization (ASLR) prevents malware writers from being able to know ahead where to modify your executable in order to accomplish their nefarious purposes.

    64 bit ARM Linux kernels allocate 39, 42 or 48 bits for the size of a process’s virtual address space. Notice 42 and 48 bit values require 6 bytes to hold them. A virtual address space is all of the addresses a process can generate / use. Further, all addresses used by processes are virtual addresses.

    using this can avoid literal pool

    1
    2
    adrp    x0, s
    add x0, x0, :lo12:s

    examples

    loading (storing) various sizes of integers

    InstructionMeaning
    ldr x0, [x1]Fetches a 64 bit value from the address specified byx1and places it inx0
    ldr w0, [x1]Fetches a 32 bit value from the address specified byx1and places it inw0
    ldrh w0, [x1]Fetches a 16 bit value from the address specified byx1and places it inx0
    ldrb w0, [x1]Fetches an 8 bit value from the address specified byx1and places it inx0
    • Pointers and longs usexregisters.
    • All other integer sizes usewregisters where the instruction itself specifies the size.

    array indexing

    1
    2
    3
    4
    5
    6
    7
    8
    9
    long Sum(long * values, long length)
    {
    long sum = 0;
    for (long i = 0; i < length; i++)
    {
    sum += values[i];
    }
    return sum;
    }

    Notice we’re using the index variableifor nothing more than traipsing through the array. This is fantastically inefficient (in this case).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    long Sum(long * values, long length)
    {
    long sum = 0;
    long * end = values + length;
    while (values < end)
    {
    sum += *(values++);
    }
    return sum;
    }

    Notice we don’t use an index variable any longer. Instead, we use the pointer itself for both the dereferencing and to tell us when to stop the loop.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
        .global Sum
    .text
    .align 4

    // x0 is the pointer to data
    // x1 is the length and is reused as `end`
    // x2 is the sum
    // x3 is the current dereferenced value

    Sum:
    mov x2, xzr // x2 = 0
    add x1, x0, x1, lsl 3 // x1 = x0+x1*8
    b 2f

    1: ldr x3, [x0], 8
    add x2, x2, x3
    2: cmp x0, x1
    blt 1b

    mov x0, x2
    ret

    .end

    faster memory copy

    Suppose you needed to copy 16 bytes of memory from one place to another. You might do it like this:

    1
    2
    3
    4
    5
    void SillyCopy16(uint8_t * dest, uint8_t * src)
    {
    for (int i = 0; i < 16; i++)
    *(dest++) = *(src++);
    }

    This is especially silly as why would you go through 16 loops when you could have simply:

    1
    2
    3
    4
    5
    void SillyCopy16(uint64_t * dest, uint64_t * src)
    {
    *(dest++) = *(src++); // 3
    *dest = *src; // 4
    }

    in aarch64

    1
    2
    3
    4
    5
    6
    SillyCopy16:              // 1
    ldr x2, [x0], 8 // 2
    str x2, [x1], 8 // 3
    ldr x2, [x0] // 4
    str x2, [x1] // 5
    ret

    using ldp

    1
    2
    3
    4
    SillyCopy16:
    ldp x2, x3, [x0]
    stp x2, x3, [x1]
    ret

    using q register

    1
    2
    3
    4
    SillyCopy16:
    ldr q2, [x0]
    str q2, [x1]
    ret

    indexing through an array of struct

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    #include <stdio.h>

    struct Person
    {
    char * fname;
    char * lname;
    int age;
    };

    extern int rand();
    extern struct Person * FindOldestPerson(struct Person *, int);

    struct Person * OriginalFindOldestPerson(struct Person * people, int length)
    {
    int oldest_age = 0;
    struct Person * oldest_ptr = NULL;

    if (people)
    {
    struct Person * end_ptr = people + length;
    while (people < end_ptr)
    {
    if (people->age > oldest_age)
    {
    oldest_age = people->age;
    oldest_ptr = people;
    }
    people++;
    }
    }
    return oldest_ptr;
    }

    #define LENGTH 20

    int main()
    {
    struct Person array[LENGTH];
    for (int i = 0; i < LENGTH; i++)
    {
    array[i].age = rand() % 5000;
    }
    struct Person * oldest = FindOldestPerson(array, LENGTH);
    for (int i = 0; i < LENGTH; i++)
    {
    printf("%d", array[i].age);
    if (oldest == &array[i])
    printf("*");
    printf("\n");
    }
    }

    Line 11tells us that somewhere else, there is a function calledFindOldestPerson. That function must have a.globalspecifying the same name so that the linker can reconcile the reference toFindOldestPerson.

    gccwith-O2or-O3optimization renderedOriginalFindOldestPerson()into 18 lines of assembly language.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
            .global FindOldestPerson                                        // 1
    .text // 2
    .align 2 // 3
    // 4
    // x0 has struct Person * people // 5
    // will be used for oldest_ptr as this is the return value // 6
    // w1 has int length // 7
    // w2 used for oldest_age // 8
    // x3 used for Person * // 9
    // x4 used for end_ptr // 10
    // w5 used for scratch // 11
    // 12
    FindOldestPerson: // 13
    cbz x0, 99f // short circuit // 14
    mov w2, wzr // initial oldest age is 0 // 15
    mov x3, x0 // initialize loop pointer // 16
    mov x0, xzr // initialize return value // 17
    mov w5, 24 // struct is 24 bytes wide // 18
    smaddl x4, w1, w5, x3 // initialize end_ptr // 19
    b 10f // enter loop // 20
    // 21
    1: ldr w5, [x3, p.age] // fetch loop ptr -> age // 22
    cmp w2, w5 // compare to oldest_age // 23
    csel w2, w2, w5, gt // update based on cmp // 24
    csel x0, x0, x3, gt // update based on cmp // 25
    add x3, x3, 24 // increment loop ptr // 26
    10: cmp x3, x4 // has loop ptr reached end_ptr? // 27
    blt 1b // no, not yet // 28
    // 29
    99: ret // 30
    // 31
    .data // 32
    .struct 0 // 33
    p.fn: .skip 8 // 34
    p.ln: .skip 8 // 35
    p.age: .skip 4 // 36
    p.pad: .skip 4 // 37
    // 38
    .end // 39

    control flow

    cmp

    compare

    discards the result of the subtraction but keeps a record of whether or not the result was less than, equal to or greater than zero. It sets the condition bits

    br

    Branch to Register

    1
    br <register>

    unconditional jump, similar to

    1
    goto *(ptr)

    ble

    Branch less or equal

    bl

    Branch with Link

    jump to a function (subroutine) address and save the return address tox30register (also calledlr,Link Register)

    cbz

    Compare and Branch if Zero

    1
    cbz <register>, <label>

    if<register>value is 0, jump to<label>

    otherwise continue to the next instruction.

    csel

    Conditional Select

    1
    csel <dest>, <src1>, <src2>, <condition>

    if condition<condition>is met, then assign<src1>value to<dest>

    otherwise assign<src2>value to<dest>

    examples:

    1
    2
    cmp w2, w5
    csel w2, w2, w5, gt // if w2 > w5, w2 remains unchanged; otherwise update to w5

    this isBranchless conditional assignment, which is more efficient thanif-else.

    this is equal to

    1
    w2 = (w2 > w5) ? w2 : w5;

    shift Operations

    lsl

    Logical Shift Left

    The LSL instruction performs multiplication by a power of 2.

    lsr

    Logical Shift Right

    The LSR instruction performs division by a power of 2.

    asr

    Arithmetic Shift Right

    The ASR instruction performs division by a power of 2, preserving the sign bit.

    ror

    rotate right

    The ROR instruction performs a bitwise rotation, wrapping the bits rotated from the LSB into the MSB.
    That is:RORinstruction executionBitwise right rotateoperation:Bits rotated out from the least significant bit (LSB) are reinserted into the most significant bit (MSB) position.

    bit manipulation

    mvn

    The mvn (Move Not) instruction performs a bitwise NOT on the operand and places the result into the destination register.

    orr

    The orr (bitwise inclusive OR) instruction performs abitwise ORoperation on two operands and writes the result to the destination register

    bfi

    bfi (Bit Field Insert) is a bit field insertion operation.

    1
    bfi <Xd>, <Xn>, #<lsb>, #<width>

    <Xd>: destination register (result written here)
    <Xn>: source register (low-order bits taken from here)
    <lsb>: Starting bit position in the destination register for insertion (least significant bit as start)
    <width>: Number of bits to insert (width)

    Assumption:

    Xd = 0b1111 0000, Xn = 0b1011 (only low 4 bits used), lsb=1, width=3

    Execution:

    1
    bfi Xd, Xn, #1, #3

    Result:
    Insert the low 3 bits 011 of Xn into bits 1~3 of Xd, replacing the original value
    The result is Xd = 1111 0110

    ubfm

    ubfm = Unsigned BitField Move

    Basic format:

    1
    ubfm <dst>, <src>, #lsb, #msb

    <dst>: Destination register

    <src>: Source register

    lsb: Starting bit (low bit index)

    msb: Ending bit (high bit index)

    This instruction extracts an unsigned bit field (a contiguous sequence of bits) from src, places it into the low bits of dst (starting from bit 0), and clears or ignores the other bits
    That is:

    1. From the lsb bit of src, take up to the msb bit
    2. Extract this bit field
    3. Right-align it into the low bits of dst (bit 0), and clear all other bits

    Example:

    1
    ubfm    w1, w2, #8, #15
    1. Extract bits 8 to 15 (8 bits total) from w2
    2. Place it into bits 0~7 of w1

    ubfiz

    ubfiz (Unsigned Bit Field Insert Zeroed) inserts a low-order bit field of an unsigned number into a specified position of another register, but the destination register is cleared before insertion.

    It is actually a specialized form of ubfm (Unsigned Bit Field Move) and has similar semantics to UBFM.

    Instruction format:

    1
    ubfiz  <dst>, <src>, #lsb, #width

    Simply put: ubfiz = insert the low width bits of src into dst starting at bit lsb, and clear all other positions.

    Where:

    <src>: source register (e.g., w1)

    <dst>: destination register (e.g., w2), where the final result is placed

    lsb: starting bit position for insertion in the destination (starting from 0)

    width: number of bits to insert (from<src>counting from the lowest bit)

    All other bits of the destination register will be cleared.

    For example:

    1
    ubfiz   w1, w1, #3, #5

    The meaning is as follows:

    1. Extract from the lowest 5 bits (bit 0 to bit 4) of w1
    2. Insert into bits 3 to 7 of the destination (w1) register
    3. All other bits of w1 (bits 0-2 and 8-31) are cleared

    other

    adr

    Address

    adrp

    Address of page

    1
    2
    3
    4
    5
    6
    7
    8
        .section .rodata
    fmt:
    .asciz "%p a: 0x%lx b: %x c: %x\n"

    .text

    adrp x0, fmt
    add x0, x0, :lo12:fmt // The assembler automatically extracts the lower 12 bits of fmt as an immediate value to calculate the page offset
    • Function: Load the symbolfmtlocated in thepage address of the 4KB aligned pageintox0.
    • adrp= Address of Page
    • It ignores the lower 12 bits of the symbol address, retaining only the higher bits.
    • For example, iffmtthe address is0x400123, thenadrp x0, fmtwill0x400000load intox0
    • adrp x0, fmtwillfmtround the address down to the nearest4KB boundary(i.e., clear the lower 12 bits)

    Why not directly useldr x0, =fmt

    • Under ARM64, usingldr x0, =fmtmay implicitly introducea literal pool, which is detrimental to relocatable code, especially in dynamic linking or PIE (Position Independent Executable) environments.
    • adrp+addisRecommended relocatable code writing (relocatable and PIC-compliant)
    • The dynamic linker (ld.so) under Linux supports this mode better.
    InstructionMeaningSupported offset rangeCommonly used for
    adrGetnear the current instructionaddress±1MBLocal jumps, temporary variables, etc.
    adrpGetthe high address part aligned to 4KB page±4GB (page-aligned offset)Get global variable addresses, strings, constant table addresses, etc.

    smaddl

    Signed Multiply Add Long

    Two32-bit signed integerAfter multiplication, add a64-bit integer, the result is stored in a64-bit register.

    1
    smaddl <Xd>, <Wn>, <Wm>, <Xa>

    Perform the following operation:

    1
    Xd = (int64_t)(int32_t)Wn * (int64_t)(int32_t)Wm + Xa;

    programming

    if statement

    if

    1
    2
    3
    4
    if (a > b)
    {
    // CODE BLOCK
    }

    in aarch64

    1
    2
    3
    4
    5
    6
        // Assume value of a is in x0
    // Assume value of b is in x1
    cmp x0, x1
    ble 1f
    // CODE BLOCK
    1:
    • Ifa > bthenx0 - x1will be greater than zero.
    • Ifa == bthenx0 - x1will be equal to zero.
    • Ifa < bthenx0 - x1will be less than zero.

    ble means branch (a jump or goto) if the previous computation showsless than or equal tozero

    a rule of thumb

    • In the higher level language, you want to enter the following code block if the condition is true.

    • In assembly language, you want to avoid the following code block if the condition is false.

    temporary label

    The target of the branch instruction is given as1f. This is an example of a temporary label.

    There are a lot of braces used in C and C++. Since labels frequently function as equivalents to{and}, there can be a lot of labels used in assembly language. But label is only a position label, it is not a scope

    A temporary label is a label made using just a number. Such labels can appear over and over again (i.e. they can be reused). They are made unique by virtue of their placement relative to where they are being used.

    • 1flooksforward in the code for the next label1.
    • 1blooks in thebackward direction for the most recent label1.

    if / else

    1
    2
    3
    4
    5
    6
    7
    8
    if (a > b)
    {
    // CODE BLOCK IF TRUE
    }
    else
    {
    // CODE BLOCK IF FALSE
    }

    There are two branches built into this code!

    in aarch64:

    1
    2
    3
    4
    5
    6
    7
    8
    9
        // Assume value of a is in x0
    // Assume value of b is in x1
    cmp x0, x1
    ble 1f
    // CODE BLOCK IF TRUE
    b 2f
    1:
    // CODE BLOCK IF FALSE
    2:

    a complete example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
        .global main
    .text

    main:
    stp x29, x30, [sp, -16]!
    mov x1, 10
    mov x0, 5
    cmp x0, x1
    ble 1f
    ldr x0, =T //Pseudo Instruction
    bl puts
    b 2f

    1: ldr x0, =F
    bl puts

    2: ldp x29, x30, [sp], 16
    mov x0, xzr
    ret

    .data
    F: .asciz "FALSE"
    T: .asciz "TRUE"
    .end

    Line 11is one way of loading the address represented by a label. In this case, the labelTcorresponds to the address to the first letter of the C string “TRUE”.Line 15loads the address of the C string containing “FALSE”.

    The occurrences of.ascizonline 23andline 24are invocations of an assembler directive the creates a C string. Recall that C strings are NULL terminated. The NULL termination is indicated by thezwhich ends.asciz.

    There is a similar directive.asciithat does not NULL terminate the string.

    loop

    while loop

    while loop
    while loop

    1
    2
    3
    while (a >= b) {
    // CODE BLOCK
    }

    aarch64:

    1
    2
    3
    4
    5
    6
    7
    8
    9
        // Assume value of a is in x0
    // Assume value of b is in x1

    1: cmp x0, x1
    blt 2f
    // CODE BLOCK
    b 1b

    2:

    for loop

    1
    2
    3
    4
    for (set up; decision; post step)
    {
    // CODE BLOCK
    }

    for
    for

    1
    2
    3
    4
    for (long i = 0; i < 10; i++)
    {
    // CODE BLOCK
    }

    aarch64 (the flow chart on the left)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        // Assume i is implemented using x0
    mov x0, xzr

    1: cmp x0, 10
    bge 2f

    // CODE BLOCK

    add x0, x0, 1
    b 1b

    2:

    aarch64 (the flow chart on the right)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        // Assume i is implemented using x0

    mov x0, xzr
    b 2f

    1:

    // CODE BLOCK

    add x0, x0, 1
    2: cmp x0, 10
    blt 1b

    continue

    1
    2
    3
    4
    5
    6
    for (long i = 0; i < 10; i++) {
    // CODE BLOCK "A"
    if (i == 5)
    continue;
    // CODE BLOCK "B"
    }

    in aarch64

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        // Assume i is implemented using x0

    mov x0, xzr

    1: cmp x0, 10
    bge 3f
    // CODE BLOCK "A".
    // if (i == 5)
    // continue

    cmp x0, 5
    beq 2f
    // CODE BLOCK "B"

    2: add x0, x0, 1
    b 1b

    3:

    another one

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
        // Assume i is implemented using x0

    mov x0, xzr
    b 3f

    1:

    // CODE BLOCK "A"

    // if (i == 5)
    // continue

    cmp x0, 5
    beq 2f

    // CODE BLOCK "B"

    2: add x0, x0, 1
    3: cmp x0, 10
    blt 1b

    break

    The implementation ofbreakis very similar to that ofcontinue.

    1
    2
    3
    4
    5
    6
    for (long i = 0; i < 10; i++) {
    // CODE BLOCK "A"
    if (i == 5)
    break;
    // CODE BLOCK "B"
    }

    aarch64:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
        // Assume i is implemented using x0

    mov x0, xzr
    b 3f

    1:

    // CODE BLOCK "A"

    // if (i == 5)
    // break;

    cmp x0, 5
    beq 4f

    // CODE BLOCK "B"

    2: add x0, x0, 1
    3: cmp x0, 10
    blt 1b

    4:

    structs

    alignment

    Data members exhibit natural alignment.

    That is:

    • alongwill be found at addresses which are a multiple of 8.
    • anintwill be found at addresses which are a multiple of 4.
    • ashortwill be found at addresses which are even.
    • acharcan be found anywhere.

    example

    1
    2
    3
    4
    5
    struct {
    long a;
    short b;
    int c;
    };

    Layout:

    OffsetWidthMember
    08bytea
    82byteb
    102– gap –
    124bytec
    1
    2
    3
    4
    5
    6
    7
    struct Foo {
    long a;
    short b;
    int c;
    };

    struct Foo Bar = { 0xaaaaaaaaaaaaaaaa, 0xbbbb, 0xcccccccc };

    A hex dump will show:

    1
    aaaa aaaa aaaa aaaa bbbb 0000 cccc cccc

    Notice the gap filled in which zeros. Note, if this were a local variable, the zeros might be garbage.

    change the order:

    1
    2
    3
    4
    5
    6
    7
    struct Foo {
    short a;
    char b;
    int c;
    };

    struct Foo Bar = { 0xaaaa, 0xbb, 0xcccccccc };

    A hex dump will show:

    1
    aaaa 00bb cccc cccc

    Notice there is only one byte of gap before theint cstarts.

    why are the zeros to the left of the b’s?

    This ARM processor is running as a little endian machine.

    defining structs

    1
    2
    3
    4
    5
    6
    7
    struct Foo {
    short a;
    char b;
    int c;
    };

    struct Foo Bar = { 0xaaaa, 0xbb, 0xcccccccc };

    Here is one way of defining and accessing the struct:

    1. Hardcoded field offset
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
        .section .rodata
    fmt:
    .asciz "%p a: 0x%lx b: %x c: %x\n"

    .data
    bar:
    .short 0xaaaa // a: short 2 byte
    .byte 0xbb // b: char 1 byte
    .byte 0x00 // padding
    .word 0xcccccccc // c: int 4 byte

    .text
    .global main
    .align 2
    main:
    stp x29, x30, [sp, -16]! // Save stack frame
    mov x29, sp

    adrp x0, fmt
    add x0, x0, :lo12:fmt // printf format string address

    adrp x1, bar
    add x1, x1, :lo12:bar // Address of bar

    ldrh w2, [x1, 0] // short a
    ldrb w3, [x1, 2] // char b
    ldr w4, [x1, 4] // int c

    bl printf // Call printf(&bar, a, b, c)

    // Explicit exit system call
    mov x8, #93 // syscall number for exit
    mov x0, xzr // exit code 0
    svc 0 // make syscall

    :lo12:fmtwill be replaced by the assembler withfmtthe lower 12 bits of the address.

    adrp x0, fmtwillfmtround the address down to the nearest4KB boundary(i.e., clear the lower 12 bits), then load this “page base address” intox0

    For example:

    Iffmt = 0x12345678, then:adrp x0, fmtwill yield0x12345000(lower 12 bits cleared)

    1. another way to define a structs is

    Using.equpseudo-instruction to define symbolic constants

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
        .global main                // main function declaration
    .text
    .p2align 2

    .equ foo_a, 0 // like #define foo_a 0
    .equ foo_b, 2 // like #define foo_b 2
    .equ foo_c, 4 // like #define foo_c 4

    main:
    stp x29, x30, [sp, -16]! // save x29, x30 onto the stack
    mov x29, sp // Set new frame pointer

    // Load addresses of fmt and bar
    ldr x0, =fmt // Address of fmt string
    ldr x1, =bar // Address of bar
    ldrh w2, [x1, foo_a] // Load bar.a into w2
    ldrb w3, [x1, foo_b] // Load bar.b into w3
    ldr w4, [x1, foo_c] // Load bar.c into w4

    // Call printf, passing arguments
    mov x0, x0 // First argument: address of fmt
    mov x1, w2 // Second argument: value of a
    mov x2, w3 // Third argument: value of b
    mov x3, w4 // Fourth argument: value of c
    bl printf // Call printf

    // Restore stack and registers
    ldp x29, x30, [sp], #16 // Restore x29 and x30
    ret // Return

    .data
    fmt:
    .asciz "%p a: 0x%lx b: %x c: %x\n" // printf format string
    bar:
    .short 0xaaaa // a
    .byte 0xbb // b
    .byte 0 // padding
    .word 0xcccccccc // c

    .end

    1. the third way:(Linux only)

    Using.structand field labels to automatically derive offsets

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
        .section .rodata
    fmt:
    .asciz "%p a: 0x%lx b: %x c: %x\n"

    // Simulate field offsets of struct Foo with .struct
    .set Foo, 0
    .struct 0
    Foo_a: .struct Foo_a + 2 // short a: 2 bytes
    Foo_b: .struct Foo_b + 1 // char b: 1 byte
    .struct Foo_b + 1 // padding: 1 byte
    Foo_c: .struct Foo_b + 2 // int c: starts at offset 4
    // Now Foo_c is offset 4

    .data
    bar:
    .short 0xaaaa // a: short 2 byte
    .byte 0xbb // b: char 1 byte
    .byte 0x00 // padding
    .word 0xcccccccc // c: int 4 byte

    .text
    .global main
    .align 2
    main:
    stp x29, x30, [sp, -16]! // Save stack frame
    mov x29, sp

    adrp x0, fmt
    add x0, x0, :lo12:fmt // Address of printf format string

    adrp x1, bar
    add x1, x1, :lo12:bar // Address of bar

    ldrh w2, [x1, Foo_a] // Load bar.a (short)
    ldrb w3, [x1, Foo_b] // Load bar.b (char)
    ldr w4, [x1, Foo_c] // Load bar.c (int)

    bl printf // printf(bar, a, b, c)

    // Explicit exit
    mov x8, #93 // syscall number for exit
    mov x0, xzr // exit code 0
    svc 0 // syscall

    using structs

    To summarize usingstructs:

    • Allstructshave a base address
    • The base address corresponds to the beginning of the first data member
    • All subsequent data members are offsets relative to the first
    • In order to use astructcorrectly, you must have first calculated the offsets of each data member
    • Sometimes there will be padding between data members due to the need to align all data members on natural boundaries.

    this pointer in c++

    • Every non-static method call employs a hidden first parameter. That’s it. That’s the slight of hand. The hidden argument is the this pointer.
    1
    2
    TestClass tc;
    tc.SetString(test_string);

    It seems we only passed one parameter test_string. But in fact, the compiler passed two parameters:

    1. The first is the this pointer: that is, the address of tc, passed to register x0

    2. The second is test_string, passed to register x1

    In the assembly, we see:

    1
    2
    3
    adrp x1, _test_string
    adrp x0, _tc // Put the address of the tc object into x0 — that is, the this pointer
    bl __ZN9TestClass9SetStringEPc

    const

    The meaning and function ofconstonlypartiallytranslates to assembly language.

    • constlocal variables andconstparameters are just like any other data to assembly language.

    • The constant nature ofconstlocal variables and parameters is implemented solely in the compiler.

    • constglobals are made constant by the hardware. Attempting to modify a variable protected in this manner will be like poking a dragon. Best not to poke dragons.

    switch and jump table

    When the C++ optimizer is enabled, it will look at your cases and choose between three different constructs for implementing yourswitch.

    And, it can use any combination of the following! Compiler writers are smart!

    1. It may emit a long string ofif / elseconstructs.
    2. It may find the rightcaseusing a binary search.
    3. Finally, it might use a jump table.

    Suppose our cases are largely consecutive. Given that all branch instructions are the same length in bytes, we can do math on the switch variable to somehow derive the address of the case we want.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    #include <stdlib.h>
    #include <stdio.h>
    #include <time.h>

    int main()
    {
    int r;

    srand(time(0));
    r = rand() & 7;
    switch (r)
    {
    case 0:
    puts("0 returned");
    break;

    case 1:
    puts("1 returned");
    break;

    case 2:
    puts("2 returned");
    break;

    case 3:
    puts("3 returned");
    break;

    case 4:
    puts("4 returned");
    break;

    case 5:
    puts("5 returned");
    break;

    case 6:
    puts("6 returned");
    break;

    case 7:
    puts("7 returned");
    break;
    }
    return 0;
    }

    Notice that thecasevalues are all, in this case, consecutive.

    1
    2
    3
    4
    5
    6
    7
    8
    jt:     b       0f
    b 1f
    b 2f
    b 3f
    b 4f
    b 5f
    b 6f
    b 7f

    fmeans forward,bmeans backward

    At addressjtthere are a sequence of branch statements… jumps if you will. Being in a sequence, this is an example of a jump table. We’ll compute the index into this array of instructions and then branch to it.

    1
    2
    3
    4
    lsl     x0, x0, 2
    ldr x1, =jt
    add x1, x1, x0
    br x1
    • Line 2 loads the base address of the “instruction array” starting at addressjt.

    complete example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
            .text
    .align 4
    .global main

    main: str x30, [sp, -16]!
    mov x0, xzr // set up call to time(nullptr)
    bl time // call time setting up srand
    bl srand // call srand setting up rand
    bl rand // get a random number
    and x0, x0, 7 // ensure its range is 0 to 7
    // note use of x register is on purpose
    lsl x0, x0, 2 // multiply by 4
    ldr x1, =jt // load base address of jump table
    add x1, x1, x0 // add offset to base address
    br x1

    // If, as in this case, all the "cases" have the same number of
    // instructions then this intermediate jump table can be omitted saving
    // some space and a tiny amount of time. To omit the intermediate jump
    // table, you'd multiply by 12 above and not 4. Twelve because each
    // "case" has 3 instructions (3 x 4 == 12).

    // Question for you: If you did omit the jump table, relative to what
    // would you jump (since "jt" would be gone).

    jt: b 0f
    b 1f
    b 2f
    b 3f
    b 4f
    b 5f
    b 6f
    b 7f

    0: ldr x0, =ZR
    bl puts
    b 99f

    1: ldr x0, =ON
    bl puts
    b 99f

    2: ldr x0, =TW
    bl puts
    b 99f

    3: ldr x0, =TH
    bl puts
    b 99f

    4: ldr x0, =FR
    bl puts
    b 99f

    5: ldr x0, =FV
    bl puts
    b 99f

    6: ldr x0, =SX
    bl puts
    b 99f

    7: ldr x0, =SV
    bl puts
    b 99f

    99: mov w0, wzr
    ldr x30, [sp], 16
    ret

    .data
    .section .rodata

    ZR: .asciz "0 returned"
    ON: .asciz "1 returned"
    TW: .asciz "2 returned"
    TH: .asciz "3 returned"
    FR: .asciz "4 returned"
    FV: .asciz "5 returned"
    SX: .asciz "6 returned"
    SV: .asciz "7 returned"

    .end

    implement falling through

    If there is no break falling the code for a case, control will simply fall through to the next case

    Here is a snippet from the program linked just above

    1
    2
    3
    4
    5
    6
    7
    0:      ldr     x0, =ZR
    bl puts
    b 99f

    1: ldr x0, =ON
    bl puts
    b 99f

    implementing gaps

    The example above present shows 8 consecutive cases. What if there was no code for case 4? In other words, what if case 4 didn’t exit?

    Here is the result:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    2:      ldr     x0, =TW
    bl puts
    b 99f

    3: ldr x0, =TH
    bl puts
    b 99f

    4: b 99f

    5: ldr x0, =FV
    bl puts
    b 99f

    other strategies for implementing switch

    As indicated above, an optimizer has at least three tools available to it to implement complexswitchstatements. And, it can combine these tools.

    1. For example, suppose your cases boil down to two ranges of fairly consecutive values. For example, you have cases 0 to 9 and also cases 50 to 59. You can implement this as two jump tables with anif / elseto select which one you use.

    Suppose yourswitchstatement,casevalues are mainly concentrated intwo small contiguous ranges, for example: one set iscase 0tocase 9, another set iscase 50tocase 59, then you can usetwo jump tablesto handle these two ranges, and then use anotherif / elseto decide which jump table to use.

    1. Suppose you have a largeswitchstatement with widely rangingcasevalues. In this case, you can implement a binary search to narrow down to a small range in which another technique becomes viable to narrow down to a singlecase.

    Suppose you have acasestatement with manyswitchbranches, and thesecasevalues havea large difference in numerical range, such as case 10, case 1000, case 50000…, then you canfirst use binary search to narrow down the search range, limiting the target value to asmaller range, and then within this range use other techniques (such as jump tables, linear comparisons, etc.) to determine whichcasebranch it corresponds to.

    1. You might have need to implement hierarchical jump tables, for example.

    “Hierarchical jump tables” are an optimization structure suitable for the following scenarios:

    • casevalues are verysparsewith an extremely wide range(e.g.,case 0, case 1000, case 2000...)
    • but they aredense within local ranges(e.g.,1000~1009,2000~2009

    You can:

    1. First, use a “first-level jump table” to jump based on high bits or rangesto a sub-jump table (sub-range).
    2. Then perform specific jumps within the sub-jump table
      This forms a “hierarchical structure” — a tree-like jumping process.

    strategies for implementing if-else

    If you do choose to implement a long chain ofif / elsestatements, consider how frequently a given case might be chosen. Put the most common cases at the top of theif / elsesequence.

    This is known as making the common case fast.

    Making the common case fast is one of the Great Ideas in Computer Science. One, you would do well to remember no matter what language you’re working with.

    functions

    bottom line concept

    Theblinstruction stands for Branch with Link. The Link concept is what enables a function (or method) to return to the instruction after the call.

    Branch-with-link computes the address of the instruction following it.

    It places this address into registerx30and then branches to the label provided. It makes one link of a trail of breadcrumbs to follow to get back following aret.

    This is why it is absolutely essential to backupx30inside your functions if they call other functions themselves.

    a example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
            .text
    .global main
    .align 2

    main: ldr x0, =hw
    bl puts
    ret

    .data
    hw: .asciz "Hello World!"

    .end

    The program hung and had to be killed with ^C.

    Somebody calledmain()- it’s a function and someone called it with ablinstruction. At the momentmain()entered, the address to which it needed to return was sitting inx30.

    Then,main()called a function - in this caseputs()but which function is called doesn’t matter - it called a function. In doing so, it overwrote the address to whichmain()needed to return with the address of line 7 in the code. That is whereputs()needs to return.

    So, when line 7 executes it puts the contents ofx30into the program counter and branches to it.

    Here is a fixed version of the code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
            .text
    .global main
    .align 2

    main: str x30, [sp, -16]!
    ldr x0, =hw
    bl puts
    ldr x30, [sp], 16
    ret

    .data
    hw: .asciz "Hello World!"

    .end

    In the AARCH64 Linux style calling convention, values are returned inx0and sometimes also returned in other scratch registers though this is uncommon.(Note thatx0could also bew0or the first floating point register if the function is returning afloatordouble.)

    If your functions call any other functions,x30must be backed up on the stack and then restored intox30before returning.

    A function with more than one return value is not supported by C or C++ but they can be written in assembly language where the rules are yours to break.

    inline functions

    Functions that are declared as inline don’t actually make function calls. Instead, the code from the function is type checked and inserted directly where the “call” is made after adjusting for parameter names.

    passing parameters to functions

    How parameters are passed to functions can be different from OS to OS. This chapter is written to the standard implemented for Linux.

    For the purposes of the present discussion, we assume all parameters arelong intand are therefore stored inxregisters.

    • Up to 8 parameters can be passed directly via scratch registers.(These arex0throughx7) Each parameter can be up to the size of an address, long or double (8 bytes).

      • Scratch means the value of the register can be changed at will without any need to backup or restore their values across function calls.

      • This means that you cannot count on the contents of the scratch registers maintaining their value if your function makes any function calls.

    a example

    1
    2
    3
    4
    long func(long p1, long p2)
    {
    return p1 + p2;
    }

    is implemented as:

    1
    2
    func:   add x0, x0, x1
    ret

    If you are the author of both the caller and the callee and both are in assembly language, you can play loosey goosey with how you return values. Specifically, you can return more than one value. But if you do so, you give up the possibility of calling these functions from C or C++.

    const

    1
    2
    3
    4
    long func(const long p1, const long p2)
    {
    return p1 + p2;
    }

    how would the assembly language change?

    Answer: no change at all!

    constis an instruction to the compiler ordering it to prohibit changing the values ofp1andp2. We’re smart humans and realize that our assembly language makes no attempt to changep1andp2so no changes are warranted.

    passing pointers

    1
    2
    3
    4
    void func(long * p1, long * p2)
    {
    *p1 = *p1 + *p2;
    }
    1
    2
    3
    4
    5
    func:   ldr x2, [x0]
    ldr x3, [x1]
    add x2, x2, x3
    str x2, [x0]
    ret

    The value ofx0on return is, in the general sense, undefined because this is avoidfunction.

    passing reference

    1
    2
    3
    4
    long func(long & p1, long & p2)
    {
    return p1 + p2;
    }
    1
    2
    3
    4
    func:   ldr x0, [x0]
    ldr x1, [x1]
    add x0, x0, x1
    ret

    Passing by reference is also an instruction to the compiler to treat pointers a little differently - the differences don’t show up here so there the only change to our pointer passing version is how we return the answer.

    more than eight parameters

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>

    void SillyFunction(long p1, long p2, long p3, long p4,
    long p5, long p6, long p7, long p8,
    long p9) {
    printf("This example hurts: %ld %ld\n", p8, p9);
    }

    int main() {
    SillyFunction(1, 2, 3, 4, 5, 6, 7, 8, 9);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
            .text
    .global main

    /* Demonstration of using more than 8 arguments to a function. This
    demo is LINUX only as APPLE will put all arguments beyond the first
    one on the stack anyway.

    On LINUX, all parameters to a function beyond the eight go on the
    stack. The first 8 go in registers x0 through x7 as normal (for
    LINUX).
    */

    SillyFunction:
    stp x29, x30, [sp, -16]! // Changes sp.
    mov x29, sp // set new sp
    ldr x0, =fmt
    mov x1, x7 // the eighth parameter
    ldr x2, [sp, 16] // This does not alter the sp, the ninth parameter
    bl printf
    ldp x29, x30, [sp], 16 // Undoes change to sp.
    ret

    main:
    stp x29, x30, [sp, -16]! // sp down total of 16.
    mov x29, sp
    mov x0, 9
    str x0, [sp, -16]! // sp down total of 32.
    mov x0, 1
    mov x1, 2
    mov x2, 3
    mov x3, 4
    mov x4, 5
    mov x5, 6
    mov x6, 7
    mov x7, 8
    bl SillyFunction
    add sp, sp, 16 // undoes change of sp by 16 due
    // to function call.
    ldp x29, x30, [sp], 16 // undoes change to sp of 16.
    ret

    .data
    fmt: .asciz "This example hurts my brain: %ld %ld\n"

    .end

    After executingLine 24, the stack will have:

    1
    2
    sp + 0    former contents of frame pointer
    sp + 8 return address for main

    After executingLine 27, the stack will have:

    1
    2
    3
    4
    sp + 0    9
    sp + 8 garbage
    sp + 16 former contents of frame pointer
    sp + 24 return address for main

    After executingLine 14, the stack will have:

    1
    2
    3
    4
    5
    6
    sp + 0    return address for SillyFunction
    sp + 8 garbage
    sp + 16 9
    sp + 24 garbage
    sp + 32 former contents of frame pointer
    sp + 40 return address for main

    This means thatLine 18fetchesp9from memory and puts its value into x2 (where it becomes the third argument toprintf()).

    In AArch64, the stack space is oftenaligned to 16-byte boundariesallocated, but you mayonly write a portion of the data, and the rest remains uninitialized, so we call it**“garbage” (undefined content)**。

    The stack pointer in ARM V8 can only be manipulated in multiples of 16.

    The stack pointer in ARM V8 can only be manipulated in multiples of 16.

    The stack pointer in ARM V8 can only be manipulated in multiples of 16.

    examples of calling some common C runtime functions

    There are, by the way, two broad types of functions within the C runtime.

    • Some are implemented largely in the C runtime itself.

    • Others that exist in the C runtime act as wrappers for functions implemented within the OS itself. These are called “system calls”.

    For the purposes of calling functions in the C runtime, there is no practical difference between these two types. Note however, there are ways of calling system calls directly using thesvcinstruction.

    “C runtime”(C runtime) refers to a set of functions, variables, and basic mechanisms that provide support during program execution,mainly used to support the C standard library and program initialization/termination. This system is commonly called theC runtime library, and common implementations on different platforms include:

    • Under GNU/Linux,glibc
    • Under Windows,MSVCRT
    • Under macOS,libSystem.dylib (which includes libc)

    What does the C runtime do?

    1. Program initialization
      • Beforemain()execution, the C runtime sets up the stack, initializes global variables, calls constructors, etc.
      • The typical entry point is_start__libc_start_main()main()
    2. Provides standard library functions
      • Such asprintf(),malloc(),exit(),fopen(), etc. These functions are implemented or wrapped by the C runtime.
    3. Manages resources
      • For example, lifecycle management of memory allocation, file handles, threads, etc.
    4. Provides system call wrappers
      • For example, when you callwrite(), it actually calls awrapper provided by C runtime, ultimately throughsyscallorsvcinstruction to access the kernel.

    system calls

    Many C runtime functions are just wrappers for system calls. For example if you call open() from the C runtime, the function will perform a few bookkeeping operations and then make the actual system call.

    What IS a system call?

    The short answer is a system call is a sort-of function call that is serviced by the operating system itself, within its own private region of memory and with access to internal features and data structures.

    Our programs run in “userland”. The technical name for userland on the ARM64 processor is EL0 (Exception Level 0).

    We can operate within the kernel’s space only through carefully controlled mechanisms - such as system calls. The technical name for where the kernel (or system) generally operates is called EL1.

    There are two higher Exception Levels (EL2 and EL3) which are beyond the scope of this book.

    Mechanism of making a system call

    First, like any function call, parameters need to be set up. The first parameter goes in the first register, etc.

    Second, a number associated with the specific system call we wish to make is loaded in a specific register (w8).

    Finally, a special instruction svc causes a trap which elevates us out of userland into kernel space. Said differently, svc causes a transition from EL0 to EL1. There, various checks are done and the actual code for the system call is run.

    A description of returning from a system call is beyond the scope of this book. Hint: just as there’s a special instruction that escalates from EL0 to EL1, there is a special instruction that does the reverse.

    the number associated with a particular system call

    reference:

    example getpid()

    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>
    #include <unistd.h>

    int main() {
    printf("Greetings from: %d\n", getpid());
    return 0;
    }

    Written in assembly language using C runtime

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
            .global main
    .text
    .align 2

    main: stp x29, x30, [sp, -16]!
    bl getpid
    mov w1, w0
    ldr x0, =fmt
    bl printf
    ldp x29, x30, [sp], 16
    mov w0, wzr
    ret

    .data
    fmt: .asciz "Greetings from: %d\n"

    .end

    And finally: calling the system call directly

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
            .global main
    .text
    .align 2

    main: stp x29, x30, [sp, -16]!
    mov x8, 172 // getpid on ARM64
    svc 0 // trap to EL1
    mov w1, w0
    ldr x0, =fmt
    bl printf
    ldp x29, x30, [sp], 16
    mov w0, wzr
    ret

    .data
    fmt: .asciz "Greetings from: %d\n"

    .end

    We chose getpid() because it doesn’t require any parameters. Using the C runtime, we simply bl to it. Calling the system call directly is different in that we must first load x8 with the number that corresponds to getpid() for the AARCH64 architecture.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    /*  Perry Kivolowitz
    Example of file operations.
    */
    .text
    .global main
    .align 2

    /* This program will
    * open() a file in the current directory,
    * write() some text to it,
    * seek back to the beginning of the file,
    * read() each line, printing it
    * close() the file
    */
    // Use .req to alias registers for readability. For example, fd is actually w28, representing a file descriptor.
    retval .req w27
    fd .req w28

    main: stp x29, x30, [sp, -16]!
    stp x27, x28, [sp, -16]!
    bl open_file

    // w0 will contain either the file descriptor of the new
    // file or -1 for a failure. Note that the value in w0
    // has also been copied to "fd" - a register alias.
    cmp w0, wzr
    bge 1f

    // If we get here, the open has failed. Use perror() to
    // print a meaningful error and branch to exit. The return
    // code of the program will be set to non-zero inside fail.
    ldr x0, =fname
    bl fail
    b 99f

    1: // When we get here, the file is open. Write some data to it.
    // If write_file returns non-zero, it signifies an error. If
    // so, branch to the file closing code since the file is open
    // after printing an error message.
    bl write_data
    cbz w0, 10f

    // If we get here, there was an error in write_data. Print
    // a reasonable error message then branch to the clean usleep
    // code.
    ldr x0, =wf // load legend
    bl fail // print error
    b 50f // branch to clean up.

    // Seek back to position zero preparing to read the file back.
    // The return value in x0 (off_t) is the return value of
    // lseek().
    10: bl seek_zero
    cbz x0, 20f

    // If we get here, the seek failed. Cause a reasonable
    // message to be printed then branch to the clean up code.
    ldr x0, =sf
    bl fail
    b 50f

    20: // When we get here, we have to read from the file and print
    // the results. To ignore the complexity of memory allocation
    // and buffer overrun potential, we'll read one character at a
    // time looking the end-of-file.

    // ssize_t read(int fildes, void *buf, size_t nbyte);
    mov w0, fd
    ldr x1, =buffer
    mov x2, 1
    bl read
    // Check the return value - should be 1.
    cbz x0,50f // zero means EOF - that's OK.
    // If x0 is negative, that IS a problem.
    cmp x0, xzr
    bge 25f
    // The return value is negative - this is an error.
    ldr x0, =rf
    bl fail
    b 99f

    25: // Write the character sitting in buffer to the console.
    mov w0, 1
    ldr x1, =buffer
    mov x2, 1
    bl write
    // We will ignore the return value for the sake of brevity.
    // There are plenty of examples of handling a potential error
    // elsewhere in this code.
    // --
    b 20b

    // When we get here, we are done. Close the file.
    50: mov w0, fd
    bl close
    mov retval, wzr

    99: ldp x27, x28, [sp], 16
    ldp x29, x30, [sp], 16
    mov w0, retval
    ret

    /* open_file()
    This function attempts to open a file for both reading and
    writing. Return values will be checked to ensure the file is
    opened. If successful, the fd is returned (and is squirreled
    away in register "fd"). If unsuccessful, the -1 returned by
    open() is passed back to the caller.

    Explanation of the magic numbers:

    int open(const char *pathname, int flags, mode_t mode);

    octal 102 for flags is O_RDWR | O_CREAT
    octal 600 for mode is rw------- i.e. read and write for
    the owner but no permissions for anyone else.

    There is a version of open() that takes two parameters. However,
    if O_CREAT is specified, the three parameter version is required.
    */

    .equ O_FLAGS, 0102
    .equ O_MODE, 0600

    open_file:
    stp x29, x30, [sp, -16]!
    ldr x0, =fname
    mov w1, O_FLAGS
    mov w2, O_MODE
    bl open
    mov fd, w0
    ldp x29, x30, [sp], 16
    ret


    /* This function uses perror() to print a meaningful error
    message in the event of a failure. The string value
    passed to perror() arrives to us as a pointer in x0.
    */

    fail:
    stp x29, x30, [sp, -16]!
    bl perror
    mov retval, 1
    ldp x29, x30, [sp], 16
    ret

    /* ssize_t write(int fd, const void *buf, size_t count);

    This function will write a string to the file descriptor contained
    in "fd" (a register alias).
    */

    write_data:
    stp x29, x30, [sp, -16]!
    str x20, [sp, -16]!
    mov w0, fd // file descriptor
    ldr x1, =txt // address to print from
    ldr x2, =txt_s // load pointer to size
    ldr x2, [x2] // dereference the pointer
    mov w20, w2 // need this value for error check.
    bl write
    cmp x0, x20 // Did we write the expected amount?
    bne 90f
    // successful write - return 0
    mov x0, xzr
    b 99f
    90: // failure - ensure we return non-zero!
    mov x0, 1
    99: ldr x20, [sp], 16
    ldp x29, x30, [sp], 16
    ret

    /* off_t lseek(int fd, off_t offset, int whence);
    */
    seek_zero:
    stp x29, x30, [sp, -16]!
    mov w0, fd // file descriptor
    mov x1, xzr // beginning of file
    mov w2, wzr // SEEK_SET - absolute offset
    bl lseek
    ldp x29, x30, [sp], 16
    ret

    .data
    prog: .asciz "file_ops"
    wf: .asciz "write failed"
    rf: .asciz "read failed"
    sf: .asciz "lseek failed"
    fname: .asciz "test.txt"
    txt: .asciz "some data\n"
    txt_s: .word txt_s - txt - 1 // strlen(txt), the total length of txt: "some data"
    buffer: .word 0
    .end

    floating point

    what are floating point numbers?

    reference

    IEEE 754

    register

    There are four highest level ideas relating to floating point operations on AARCH64.

    • There is another complete register set for floating point values.
    • There are alternative instructions just for floating point values.
    • There are exotic instructions that operate on sets of floating point values (SIMD).
    • There are instructions to go back and forth to and from the integer registers.

    regs
    regs

    The above figure showsdifferent views and access methods of the SIMD (Single Instruction, Multiple Data) register V0 in the ARM64 architecture, includingarrangement specifiers of different bit widths and lane indices

    illustration

    This figure usesthe V0 register as an example, showinghow to access its content using different arrangement specifiers

    LevelTypeDescription
    Bottom levelV0Entire 128-bit V0 register
    UpwardV0.2D,V0.4S,V0.8H,V0.16BAccess V0 with data views of different sizes:
    - D = 64-bit(2 × 64bit)
    - S = 32-bit(4 × 32bit)
    - H = 16-bit(8 × 16bit)
    - B = 8-bit(16 × 8bit)
    Next level upV0.2D[0],V0.4S[0]etc.Index of each lane, for example:
    -V0.4S[2]Represents the 3rd 32-bit unit
    -V0.16B[15]Represents the 16th 8-bit byte
    Top levelB0,H0,S0,D0Is an alias ofV0Accessed by bit width (only accesses the lowest bit data)

    truncation towards zero

    truncate

    In C and C++, truncation is what we get from:

    1
    2
    integer_variable = int(floating_variable);  // C++
    integer_variable = (int) floating_variable; // C

    The instruction is fcvtz - convert towards zero. Then, the choice as to whether to produce a signed or unsigned result is defined by the final letterL u or s.

    MnemonicMeaning
    fcvtzuTruncate (always towards 0) producing an unsigned int
    fcvtzsTruncate (always towards 0) producing a signed int
    • fcvtzu: Float Convert to Unsigned integer, with truncation toward zero
    • fcvtzs: Float Convert to Signed integer, with truncation toward zero

    this instruction which completely discards the fractional value is said by the ARM documentation as doing rounding not truncating.

    The the choice of source register defined whether you are converting a double or single precision floating point value.

    Source RegisterConverts a
    dXdoubleto an integer
    sXfloatto an integer
    Destination RegisterConverts a
    xX64 bit integer
    wX32 bit or less integer

    Examples wheredis adoubleandfis afloat:

    C++Instruction
    int32_t(d)fcvtzs w0, d0
    uint32_t(d)fcvtzu w0, d0
    int64_t(d)fcvtzs x0, d0
    uint64_t(d)fcvtzu x0, d0

    example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
        .section .text
    .global main
    .type main, @function // Indicates to the assembler and linker that main is a function symbol
    //.type <symbol>, @<type>is a pseudo-instruction in GAS (GNU Assembler) used to specify the type of a symbol.
    // <symbol>: symbol name, e.g., main
    // @<type>: symbol type, here @function, indicating it is a function, not a variable or label


    main:
    stp x29, x30, [sp, -16]! // Save frame pointer and link register
    mov x29, sp

    // Save floating-point registers
    stp d20, d21, [sp, -16]!
    stp d22, d23, [sp, -16]!

    // Load prompt message
    ldr x0, =leg
    bl printf

    // Load vless data into d20-d23
    ldr x0, =vless
    ldr d20, [x0] // dless = 5.49
    ldr d21, [x0, #8] // dmore = 5.51
    ldr d22, [x0, #16] // ndless = -5.49
    ldr d23, [x0, #24] // ndmore = -5.51

    // fcvtps: Round toward plus infinity
    fcvtps x1, d20
    fcvtps x2, d21
    ldr x0, =fmt1
    bl printf

    fcvtps x1, d22
    fcvtps x2, d23
    ldr x0, =fmt1
    bl printf

    // fcvtns: Round to nearest, ties to even
    fcvtns x1, d20
    fcvtns x2, d21
    ldr x0, =fmt2
    bl printf

    fcvtns x1, d22
    fcvtns x2, d23
    ldr x0, =fmt2
    bl printf

    // fcvtzs: Round toward zero
    fcvtzs x1, d20
    fcvtzs x2, d21
    ldr x0, =fmt4
    bl printf

    fcvtzs x1, d22
    fcvtzs x2, d23
    ldr x0, =fmt4
    bl printf

    // fcvtas: Round to nearest, ties away from zero
    fcvtas x1, d20
    fcvtas x2, d21
    ldr x0, =fmt3
    bl printf

    fcvtas x1, d22
    fcvtas x2, d23
    ldr x0, =fmt3
    bl printf

    // Restore floating-point registers and return address
    ldp d22, d23, [sp], #16
    ldp d20, d21, [sp], #16
    ldp x29, x30, [sp], #16
    mov w0, wzr
    ret

    .section .rodata
    vless:
    .double 5.49
    .double 5.51
    .double -5.49
    .double -5.51

    fmt1:
    .asciz "fcvtps less: %ld more: %ld\n"
    fmt2:
    .asciz "fcvtns less: %ld more: %ld\n"
    fmt3:
    .asciz "fcvtas less: %ld more: %ld\n"
    fmt4:
    .asciz "fcvtzs less: %ld more: %ld\n"
    leg:
    .asciz "less values are +/- 5.49. more values are +/- 5.51.\n"

    Notice all the values were truncated to the whole number that is closer to zero.

    Truncation Away From Zero

    Truncation away from zero is not as easy. In fact, it cannot be performed with a single instruction.

    In C (and C++):

    1
    iv = (int(fv) == fv) ? int(fv) : int(fv) + ((fv < 0) ? -1 : 1);

    If the fv is already equal to a whole number, the integer value will be that whole number. Other wise the iv is the whole number further away from zero.

    In C++, a more sophisticated version would require and could look like:

    1
    2
    3
    4
    template <typename T>
    int MyTruncate(T x) {
    return int((x < 0) ? floor(x) : ceil(x));
    }

    floor() always truncates downward (towards more negative).
    ceil() always truncates upwards (towards more positive).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    RoundAwayFromZero:
    fcmp d0, 0
    ble 1f
    // Value is positive, truncate towards positive infinity (ceil)
    frintp d0, d0
    b 2f
    1: // Value is negative, truncate towards negative infinity (floor)
    frintm d0, d0
    2: fcvtzs x0, d0
    ret
    • frintpRound toward +∞)

    • frintmRound toward -∞)

    • frintzRound toward 0)

    • frintaRound to nearest, tie away from 0)

    • frintnRound to nearest, tie to even)

    rounding conversion

    rounding

    An instruction which does what we normally think of as rounding is frinta. This is the conversion “to nearest with ties going away.” So, 5.5 goes to 6 as one would expect from “rounding.”

    converting an integer to a float point value

    In C / C++:

    1
    2
    double_var = double(integer_var); // C++
    double_var = (double)integer_var; // C

    Is handled by two instructions:

    • scvtf converts a signed integer to a floating point value
    • ucvtf converts an unsigned integer to a floating point value

    The name of the destination register controls which kind of floating point value is made. For example, specifying dX makes a double etc.

    floating point literals

    Recall that all AARCH64 instructions are 4 bytes long. Recall also that this means that there are constraints on what can be specified as a literal since the literal must be encoded into the 4 byte instruction. If the literal is too large, an assembler error will result.

    Given that floating point values are always at least 4 bytes long themselves, using floating point literals is extremely constrained. For example:

    1
    2
    fmov    d0, 1     // 1
    fmov d0, 1.1 // 2

    Line 1 will pass muster but Line 2 will cause an error.

    To load a float, you could translate the value to binary and do as the following:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
            .text
    .global main
    .align 2

    main: str x30, [sp, -16]!
    ldr s0, =0x3fc00000
    fcvt d0, s0
    ldr x0, =fmt
    bl printf
    ldr x30, [sp], 16
    mov w0, wzr
    ret

    .data
    fmt: .asciz "%f\n"
    .end

    printf() only knows how to print double precision values. When you specify a float, it will convert it to a double before emitting it.

    Translating floats and doubles by hand isn’t a common practice for humans, though compilers are happy to do so.

    Instead for us humans, the assembler directives .float and .double are used more frequently to specify float and double values putting them into RAM.
    a example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
            .global main
    .text
    .align 2

    counter .req x20
    dptr .req x21
    fptr .req x22
    .equ max, 4

    main: stp counter, x30, [sp, -16]!
    stp dptr, fptr, [sp, -16]!
    ldr dptr, =d
    ldr fptr, =f
    mov counter, xzr

    1: cmp counter, max
    beq 2f

    ldr d0, [dptr, counter, lsl 3]
    ldr s1, [fptr, counter, lsl 2]
    fcvt d1, s1
    ldr x0, =fmt
    add counter, counter, 1
    mov x1, counter
    bl printf
    b 1b

    2: ldp dptr, fptr, [sp], 16
    ldp counter, x30, [sp], 16
    mov w0, wzr
    ret

    .data
    fmt: .asciz "%d %f %f\n"
    d: .double 1.111111, 2.222222, 3.333333, 4.444444
    f: .float 1.111111, 2.222222, 3.333333, 4.444444

    .end
    InstructionFull Name / AbbreviationFunctionCommon Usage Example
    .reqregister require(Unofficial Abbreviation)Givean alias to a registerfoo .req x0Indicates that from now on, writingfoois equivalent tox0
    .equequateDefine aconstant symbolBUF_SIZE .equ 64meansBUF_SIZE = 64

    On Linux, just as w/x0 through w/x7 are scratch registers and used to pass parameters, s/d0 and s/d7 are as well beginning with the 0 register. That is:

    • Integer parameter passing: x0 ~ x7 (or 32-bit w0 ~ w7) are used to pass the first 8 integer-type parameters (int, pointer, long, etc.). More than 8 are passed via the stack.
    • Floating-point parameter passing: d0 ~ d7 (64-bit double type) or s0 ~ s7 (32-bit float type) are used to pass the first 8 floating-point parameters. More than 8 floating-point parameters are also passed via the stack.

    Fitting 32 bits into a 32 bit bag

    1
    ldr s0, =0x3fc00000  // Pseudo-instruction! We thought it directly loads 0x3fc00000 into s0

    The compiler cannot directly hardcode an arbitrary 32-bit value into an instruction (because an ARM instruction itself is only 32 bits).

    So it actually does:

    1. Write the literal value 0x3fc00000 to some location in memory (usually near the bottom of the current function).
    2. Generate an ldr instruction to load this value from that address using PC-relative load. This area is called a literal pool, which is a collection of constants.

    We expected line 6 to read:

    1
    ldr        s0, =0x3fc00000

    Instead we find:

    1
    b+ 0x784 <main+4>          ldr     s0, 0x7a0 <main+32>

    Scan downward to find 0x7a0:

    1
    0x7a0 <main+32>         .inst   0x3fc00000 ; undefined
    Pseudo-instructionActual effectActual assembly seen in GDB
    ldr s0, =0x3fc00000Load constant intos0registerldr s0, #literal_addr
    literal_addr: .inst 0x3fc00000
    ldr x0, =fmtLoad string pointer addressldr x0, #literal_addr
    literal_addr: .inst 地址值
    .inst 0x3fc00000Manually insert a 32-bit data (not necessarily a valid instruction)Store constants (not executed)

    .instMeaning:

    • Full name:.inst= insert instruction
    • Purpose: Directly insert the machine code of an ARM instruction (usually a 32-bit hexadecimal value)
    1
    .inst 0xd65f03c0   // It is actually the ret instruction

    In this example, the machine code 0xd65f03c0 after .inst is the 32-bit encoding of the ret instruction. That is:

    1
    ret

    Equivalent to:

    1
    .inst 0xd65f03c0

    In the above example, you can use.instDefine an address and load from that address

    Why not usemov reg, #imm

    • mov has immediate value encoding limitations and cannot load arbitrary 32-bit values.
    • When out of range, you must use ldr to load from memory.

    fmov

    The fmov instruction is used to move floating point values in and out of floating point registers and to some degree, moving data between integer and floating point registers.

    loading floating point numbers as immediate values

    Just as we saw with integer registers, some values can be used as immediate values and some cannot. It comes down to how many bits are necessary to encode the value. Too many bits… not enough room to fit in a 4 byte instruction plus the opcode.

    For example, this works:

    1
    mov    x0, 65535

    but this does not:

    1
    mov    x0, 65537

    The constraints placed on immediate values for fmov are much tighter because floating point numbers are far more complex than integers.

    fmov d0, #immWhether it works depends on whether the floating-point number can be precisely represented within the 8-bit encoding space:

    StructureBitsDescription
    Sign bit1 bitIndicates positive or negative
    Exponent part3 bitsControls magnitude (multiply by power of 2)
    Mantissa part4 bitsCan only be composed of combinations of 1/2, 1/4, 1/8, 1/16
    1
    2
    3
    4
    5
    6
    fmov d0, 1.0        // ✅ OK: Integer 1 is 2⁰, exponent can be encoded
    fmov d0, 1.5 // ✅ OK: 1 + 0.5 = 2⁰ + 2⁻¹, both exponent and mantissa can be encoded
    fmov d0, 1.75 // ✅ OK:1 + 0.5 + 0.25 = 2⁰ + 2⁻¹ + 2⁻²
    fmov d0, 1.875 // ✅ OK:+ 2⁻³
    fmov d0, 1.9375 // ✅ OK:+ 2⁻⁴
    fmov d0, 1.96875 // ❌ Not possible: requires 2⁻⁵, mantissa exceeds 4 bits

    Large floats cannot use fmov; use ldr instead.

    fmov is a ‘bit copier’, not a ‘precision converter’. To change numerical precision, you must use the fcvt family.

    half precision

    Support for half precision (16 bit) floating point values does exist but there is no complete agreement on how different compilers support them. Indeed, there are not one but two competing half precision formats out there. These are the IEEE and GOOGLE types. Further still, many open source developers have created their own implementations with potentially clashing naming conventions.

    1
    2
    3
    __fp16 Foo(__fp16 g, __fp16 f) {
    return g + f;
    }

    compiles to:

    1
    2
    3
    4
    5
    fcvt    s1, h1
    fcvt s0, h0
    fadd s0, s0, s1
    fcvt h0, s0
    ret

    Notice each half precision value is converted to single precision. So, from C and C++ working with half precision values can be inefficient.

    On the other hand, if you are willing to use intrinsics and one of the SIMD instruction sets offered by ARM, then knock yourself out. Be aware that doing so ties your code to the ARM processor in ways which you might regret later.

    bit manipulation

    Bit fields are a feature of the C and C++ language which completely hide what is often called “bit bashing”.

    the ordering of bits in a bit field is not guaranteed to be the same on different platforms and even between different compilers on the same platform.

    Bit fields are a syntax used to precisely control the number of binary bits occupied by members within a struct, commonly used in space-sensitive scenarios such as hardware registers and protocol headers.
    Syntax format

    1
    2
    3
    4
    struct 结构体名 {
    类型 成员名 : 位宽;
    ...
    };

    example:

    1
    2
    3
    4
    5
    struct BF {
    unsigned char a : 1;
    unsigned char b : 2;
    unsigned char c : 5;
    };
    • a uses 1 bit, can represent 0 or 1
    • b uses 2 bits, can represent 0 ~ 3
    • c uses 5 bits, can represent 0 ~ 31

    The three members occupy a total of 1 + 2 + 5 = 8 bits, i.e., 1 byte

    1. Although each member is one bit wide, the overall size is usually aligned to an integer (here it is 1 byte, because 8 bits make exactly one byte).
    2. Different compilers may have slight differences in bit-field alignment and padding details.
    3. They can be accessed like ordinary members:
    1
    2
    3
    4
    struct BF bf;
    bf.a = 1;
    bf.b = 3;
    bf.c = 31;

    The compiler automatically handles masking and shifting for bit-fields.

    Consider a data structure for which there will be potentially millions of instances in RAM. Or, perhaps billions of instances on disc. Suppose you need 8 boolean members in every instance. The C++ standard does not define the size of a bool instead leaving it to be implementation dependent. Some implementations equate bool to int, four bytes in length. Some implement bool with a char, or 1 byte in length.

    Let’s assume the smallest case and equate a bool with char. Our struct, for which there may be millions or billions of instances requires 8 bool so therefore 8 bytes. Times millions or billions.

    Bit fields can come to your aid here by using a single bit per boolean value. In the best case, 8 bytes collapse to 1 byte. In a worse case, 8 x 4 = 32 bytes collapsed into 1.

    Assuming the smallest unit, where each bool is 1 byte:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct S {
    bool b0;
    bool b1;
    bool b2;
    bool b3;
    bool b4;
    bool b5;
    bool b6;
    bool b7;
    };

    This struct is 8 bytes in size (1 byte × 8 bools). With a million instances, it occupies 8MB of memory; with a billion instances, it’s 8GB. For a 4-byte bool implementation, the size becomes 32 bytes, and per 100 million instances, it’s 3.2GB.

    Solution: Use bit-fields to compress boolean values

    Use bit-fields to define 8 boolean values as 1-bit each:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct S {
    unsigned char b0 : 1;
    unsigned char b1 : 1;
    unsigned char b2 : 1;
    unsigned char b3 : 1;
    unsigned char b4 : 1;
    unsigned char b5 : 1;
    unsigned char b6 : 1;
    unsigned char b7 : 1;
    };

    8 1-bit members together occupy exactly 1 byte.

    This compresses 8 bytes into 1 byte, saving a lot of space.

    In Computer Science there is an eternal tension between space and time. The following is a law:

    If you want something to go faster, it will cost more memory.

    If you want to save memory, what you’re doing will take more time.

    This law shows up here… recall the example of where we wanted to save memory by collapsing 8 bool into 1 byte? To save that memory we will slow down because accessing the right bits takes a couple of instructions where overwriting a bool implemented as an int takes just one instruction.

    As for the assembly language that bit field will produce, it depends upon optimization level. Unoptimized, the code produced will be much longer and cumbersome than the “sophisticated” assembly language.

    endian

    the ARM swing both ways: the litte-endian and the big-endian. But:

    The standard toolchain emits little endian code. It is a big task to install the big-endian version of the toolchain.

    Here is a quote from Wikipedia:

    1
    ARM, C-Sky, and RISC-V have no relevant big-endian deployments, and can be considered little-endian in practice.

    The common Intel processors are also little-endian.

    assembly macros

    An early innovation in assemblers was the introduction of a macro capability. Given what could be considered a certain amount of tedium in coding in asm, macros provide a simple form of meta programming where a series of statements can be encapsulated by a single macro. Think of a macro as an early form of C++ templated function (kinda but not really).

    Here’s an example of an assembly language macro:

    1
    2
    3
    4
    .macro LLD_ADDR xreg, label
    adrp \xreg, \label@PAGE
    add \xreg, \xreg, \label@PAGEOFF
    .endm

    This gets expanded to:

    1
    2
    adrp    x0, fmt@PAGE
    add x0, x0, fmt@PAGEOFF

    gcc on Linux does not run assembly language files through the C pre-processor if the asm file ends in .s but WILL if the file ends in .S

    General Use

    AASCIZ

    AASCIZ label, string

    This macro invokes .asciz with the string set to string and the label set to label. In addition, this macro ensures that the string begins on a 4-byte-aligned boundary.

    PUSH_P, PUSH_R, POP_P and POP_R

    These macros save some repetitive typing. For example:

    1
    PUSH_P  x29, x30

    resolves to:

    1
    stp     x29, x30, [sp, -16]!

    START_PROC and END_PROC

    Place START_PROC after the label introducing a function.

    Place END_PROC after the last ret of the function.

    These resolve to: .cfi_startproc and .cfi_endproc respectively.

    MIN and MAX

    Handy more readable macros for determining minima and maxima. Note that the macro performs a cmp which subtracts src_b from src_a (discarding the results) in order to set the flags to be interpreted by the following csel.

    Signature:

    1
    MIN     src_a, src_b, dest

    The smaller of src_a and src_b is put into dest.

    Signature:

    1
    MAX     src_a, src_b, dest

    The larger of src_a and src_b is put into dest.

    MOD

    MOD macro used above is defined as:

    1
    2
    3
    4
    .macro  MOD         src_a, src_b, dest, scratch
    sdiv \scratch, \src_a, \src_b
    msub \dest, \scratch, \src_b, \src_a
    .endm

    GLABEL

    Mark a label as global, Makes a label available externally.

    Signature:

    1
    GLABEL label

    An underscore is prepended.

    CRT

    Calling CRT(C runtime) functions
    If you create your own function without an underscore, just call it as usual.
    If you need to call a function such as those found in the C runtime library, use this macro in this way:

    1
    CRT     strlen

    MAIN

    Declaring main()
    Put MAIN on a line by itself. Notice there is no colon.

    errno

    The externally defined errno is accessed via a CRT function which isn’t seen when coding in C and C++. The function is named differently on Mac versus Linux. To get the address of errno use:

    1
    ERRNO_ADDR

    This macro makes the correct CRT call and leaves the address of errno in x0.

    Loads and Stores

    GLD_PTR

    Loads the address of a label and then dereferences it where, on Apple the label is in the global space and on Linux is a relatively close label.

    Signature:

    1
    GLD_PTR     xreg, label

    When this macro finishes, the specified x register contains what 64 bit value lives at the specified label.

    GLD_ADDR

    Loads the address of the label into the specified x register. No dereferencing takes place. On Apple machines, the label will be found in the global space.

    Signature:

    1
    GLD_ADDR    xreg, label

    When this macro completes, the address of the label is in the x register.

    LLD_ADDR

    Similar to GLD_ADDR this macro loads the address of a “local” label.

    Signature:

    1
    LLD_ADDR xreg, label

    When this macro completes, the address of the label is in the x register.

    LLD_DBL

    Signature:

    1
    LLD_DBL xreg, dreg, label

    When this macro completes, a double that lives at the specified local label will sit in the specified double register.

    LLD_FLT

    Signature:

    1
    LLD_FLT xreg, sreg, label

    When this macro completes, a float that lives at the specified local label will sit in the specified single precision register.

    performance

    Undoing Stack Pointer Changes

    A small tip concerning undoing changes to the stack pointer. You might think that changes to the stack made by str or stp and their cousins must be undone with ldr or ldp and their cousins.

    This depends.

    If you need to get back the original contents of a register pushed onto the stack, then an ldr or ldp is appropriate. However, if you don’t need to get the original contents of a register back, then it is faster to undo a change to the stack using addition.

    Take for example the use of printf(). On Apple Silicon systems, you must send arguments to printf() by pushing them onto the stack. However, when printf() completes, you have no need for the values that you pushed. As shown above, simply add the right (multiple of 16) to the stack pointer. This is faster as the addition makes no reference to RAM (or caches) as the ldr would.

    other stuff

    let the assembler itself calculate the length for you

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
            .global        main
    .align 2
    .text

    main: str x30, [sp, -16]!
    mov w0, 1 // stdout
    ldr x1, =s // pointer to string
    ldr x2, =ssize // pointer to computed length
    ldr w2, [x2] // actual length of string
    bl write

    ldr x0, =fmt
    ldr x1, =s
    ldr x2, =ssize
    ldr w2, [x2]
    bl printf

    ldr x30, [sp], 16
    mov w0, wzr
    ret

    .data

    s: .asciz "Hello, World!\n"
    ssize: .word ssize - s - 1 // accounts for null at end
    fmt: .asciz "str: %slen: %d\n" // accounts for newline

    .end

    atomic operations

    Load Linked, Store Condition

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
            .text
    .p2align 2

    #if defined(__APPLE__)
    .global _LoadLinkedStoreConditional
    _LoadLinkedStoreConditional:
    #else
    .global LoadLinkedStoreConditional
    LoadLinkedStoreConditional:
    #endif
    1: ldaxr w1, [x0]
    add w1, w1, 1
    stlxr w2, w1, [x0]
    cbnz w2, 1b
    ret

    LL/SC is an optimistic concurrency control mechanism. Its general logic is:

    • Load-Linked (LDAXR): Loads the value at an address and ‘watches’ whether that address is modified. You can modify this value (e.g., add 1).

    • Store-Conditional (STLXR): Attempts to write this new value; if the address content has not been modified by others in the meantime, the write succeeds; otherwise, it fails. Success or failure is indicated by the return value of STLXR (0 means success, non-zero means failure).

    llsc
    llsc

    Implementations of operations on atomic variables were improved in the second version of ARMv8, called ARMv8.1. The load linked and store conditional instructions are still available but several new instructions were added which perform certain operations such as addition, subtraction and various bitwise operations in a single atomic instruction.

    For example:

    1
    2
    mov       w1, 1
    ldaddal w1, w0, [x0]

    does the same work of atomically adding one to the value in memory pointed to by x0.

    spin-lock

    Here is the source code to the spin-lock for ARM V8.

    Lock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Lock:
    START_PROC
    mov w3, 1 // Value to be stored: 1 means 'locked'
    1: ldaxr w1, [x0] // Atomic load and mark exclusive access
    cbnz w1, 1b // If the lock is not 0 (held by someone else), continue spinning
    stlxr w2, w3, [x0] // Attempt atomic write; if successful, w2=0
    cbnz w2, 1b // If it fails (contention), continue spinning
    ret
    END_PROC

    stlxr: If the exclusive tag is still valid (no one has taken the lock), then write the value of w3*x0, and put the result into w2 (0 indicates success)

    1. ldaxr dereferencing the lock itself (once again an int32_t) and marks the location of the lock as being hopefully, exclusive.
    2. Having gotten the value of the lock, its value is inspected and if found to be non-zero, we branch back to attempting to get it again - this is the spin.
    3. If the contents of the lock is 0, its value in w1 is changed to non-zero. Note, this could be made a bit better if a value of 1 was stored in another w register and simply used directly on line 10.
    4. stlxr w2, w3, [x0] conditionally stores the changed value back to the location of the lock. If the stlxr returns 0, we got the lock. If not, we start over - somebody else got in there ahead of us. Perhaps this happened because we were descheduled. Perhaps we lost the lock to another thread running on a different core.

    unlock

    1
    2
    3
    4
    5
    6
    Unlock:
    START_PROC
    str wzr, [x0] // Writing 0 indicates releasing the lock
    dmb ish // Memory barrier, cross-core synchronization
    ret
    END_PROC
    1. All it does is set to value of the lock to zero. The correct operation of the lock requires that no bad actor simply stomps on the lock by calling Unlock without first owning the lock. Just say no to lock stompers.

    2. dmb ishsets up a data memory barrier across each processor - it makes sure threads running on different cores see the update correctly. This code seemed to work without this line but intuition suggests it could be important. In Lock() the stlxr instruction has an implied data memory barrier.

    Summary (from a pseudocode perspective)

    • Lock(x0):
    1
    2
    3
    4
    5
    do {
    w1 = *x0; // atomic exclusive load
    if (w1 != 0) continue;
    result = atomic_store_exclusive(x0, 1); // try to set lock
    } while (result != 0); // someone else beat us
    • Unlock(x0):
    1
    2
    *x0 = 0;       // unlock
    dmb(ISH); // ensure all cores see the update

    Reference