1. Prerequisite knowledge
  2. Register
    1. Register access speed
    2. Register type
    3. Registers and C Types
      1. integer
      2. pointer
      3. Floating-point number
  3. Instruction
    1. Prerequisite knowledge
    2. Memory access
      1. ldr
      2. str
      3. ldp
      4. stp
      5. Three modes
      6. Pseudo-instruction
      7. literal pool
      8. Runtime address relocation
      9. Example
        1. Load (store) integers of different sizes
        2. Array indexing
        3. Faster memory copy
        4. Iterate over an array of structs
    3. Control flow
      1. cmp
      2. br
      3. ble
      4. bl
      5. cbz
      6. csel
    4. Shift operation
      1. lsl
      2. lsr
      3. asr
      4. ror
    5. Bit Manipulation
      1. mvn
      2. orr
      3. bfi
      4. ubfm
      5. ubfiz
    6. Others
      1. adr
      2. adrp
      3. smaddl
  4. Programming
    1. if statement
      1. if
        1. Rule of thumb
        2. temporary label
      2. if / else
        1. A complete example
    2. Loops
      1. while loop
      2. for loop
        1. continue
        2. break
    3. Structure
      1. alignment
        1. Example
      2. Defining a struct
        1. Using structs
        2. The this pointer in C++
    4. const
    5. switch and jump tables
      1. Implementing fall-through
      2. Handling gaps
      3. Other strategies for implementing switch
      4. Strategies for implementing if-else
    6. function
      1. Core Concept
        1. An example
      2. Inline functions
      3. Passing arguments to functions
        1. An example
      4. const
      5. Passing a pointer
      6. Passing by reference
      7. More than eight arguments
      8. Examples of calling some common C runtime functions
      9. System call
        1. What is a system call?
        2. The mechanism for making system calls
        3. Numbers associated with specific system calls
        4. getpid() example
    7. Floating-point number
      1. What are floating-point numbers?
      2. Register
      3. Truncation towards zero
        1. Example
      4. Truncation away from zero
      5. Rounding conversion
      6. Converting integers to floating-point values
      7. Floating-point literals
        1. Stuffing 32 bits into a 32-bit bag
      8. fmov
      9. half precision
      10. Bit Manipulation
      11. Byte Order
    8. Assembly macros
      1. General usage
        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. Load and Store
        1. GLD_PTR
        2. GLD_ADDR
        3. LLD_ADDR
        4. LLD_DBL
        5. LLD_FLT
    9. Performance
      1. Undoing stack pointer modifications
      2. Others
        1. Let the assembler calculate the length itself
    10. Atomic operations
      1. Load-linked, store-conditional
        1. Spinlock
  • Reference
  • Cover image for AArch64 ASM Book

    AArch64 ASM Book

    Words 14k
    Views
    Visitors

    Timeline

    Timeline

    2025-06-22

    init

    This article introduces the basics of AArch64 (ARMv8-A) assembly language. It first explains the byte sizes of bytes, bits, and common C types (char, short, int), and notes that all AArch64 instructions are 4 bytes wide and pointers are 8 bytes wide (though the actual address space is typically less than 64 bits). It then details the register system: x registers are used for 64-bit integers or pointers, w registers for 32-bit integers, d/s registers for double-precision and single-precision floating-point numbers respectively, and v registers for SIMD/Neon operations; it also explains that x29 is the stack frame pointer, x30 is the link register, and the mechanism for passing large return values through an indirect result location. The article also elaborates on the correspondence between registers and C types, and emphasizes that all pointers are stored in x registers. In the instructions section, it covers that each instruction is fixed at 4 bytes wide, most instructions have three operands, and syntax such as square brackets for dereferencing and exclamation marks for pre-decrement. Additionally, the article focuses on the usage of memory access instructions ldr/str/ldp/stp, including the three addressing modes: normal offset, post-indexed, and pre-indexed, and notes that unaligned access incurs a performance penalty.

    Prerequisite knowledge

    1 byte contains 8 bit

    • char occupies 1 bytes
    • short occupies 2 bytes
    • int occupies 4 bytes

    Each address represents a one-byte storage unit

    img
    img

    1234
    gcc -E hello.c -o hello.igcc -S hello.i -o hello.sgcc -c hello.s -o hello.ogcc hello.o -o hello
    • all The width of AARCH64 instructions is 4 bytes.

    • all The width of AARCH64 pointers is 8 bytes†.

      Strictly speaking, this is correct, but in Linux systems, usually only the lower 39, 42, or 48 bits of the address are used—that is, the virtual address space size of an ARM Linux process is less than 64 bits. When treating an address as an 8-byte value, the high bits are zeroed out.

    Register

    Register access speed

    Latency
    Latency

    The meaning of this diagram is: if accessing a register (which can be accessed at least once per CPU clock cycle) is compared to one second, then accessing RAM is like waiting 3.5 to 5.5 minutes.

    Register type

    • rn represents the nth register of “a certain type”.

    The type of register is specified by a letter, and which specific register of that type is specified by a number. There are also some exceptions. Below is an introductory overview:

    LetterType
    x64-bit integer or pointer
    w32-bit_or smaller_integer
    d64-bit floating-point number (double, double-precision)
    s32-bit floating-point number (float, single-precision)

    Some register types are omitted here.

    (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 stack frame pointer (FP)
    • x30 is the link register (LR, i.e., the return address)
    • When the return 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 “return value storage area” and passes the address of this area to the called function – the address of this memory is the “Indirect Result Location”. After the called function finishes execution, it writes the result to this address instead of returning it via registers.

    Registers used for floating-point types (and vector operations) are coincident:

    SIMD and floating-point register width correspondence
    SIMD and floating-point register width correspondence

    • qThe register width is 16 bytes—quad words. (alias of vn, mainly used forSIMD/Neon in instructions)
    • vregister width is also 16 bytes, isqa synonym for the register.
    • dregister is used fordouble(double precision), with a width of 8 bytes –double precisioneachvregister can hold 2.
    • sregister is used forfloat(single precision), with a width of 4 bytes. Eachvregister can hold 4.
    • hregister is used forhalf-precision floating-point numbers, with a width of 2 bytes. Eachvregister can hold 8.
    • bregister is used for byte operations. Eachvregister can hold 16.

    Registers and C Types

    integer

    This declares an integerActually this is an integer
    charwn
    shortwn
    intwn
    longxn

    pointer

    This declares a pointerActually this is a pointer
    type *xn

    All pointers are stored in x registers. x registers are 64-bit, but many operating systems do not support 64-bit address space, because managing such a large address space itself takes up a lot of space. Instead,operating systems typically use a 48 to 52-bit address space

    Floating-point number

    This declares a floating-point numberActually this is a floating-point number
    floatsn
    doubledn
    __fp16(half)hn

    Floating-point storage format in registers
    Floating-point storage format in registers

    Vector packing structure of floating-point registers
    Vector packing structure of floating-point registers

    vn is the true physical register name,recommended for use, supporting the most types of access (floating-point + SIMD)

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

    Instruction

    Prerequisite knowledge

    Each AARCH64 instructions are all 4 bytes wide. All the information the CPU needs to know about what this instruction is, what variant it might be, and what data it will use can be found within these 4 bytes.

    • Most (but not all) AARCH64 instructions have three_operands_. They are interpreted as follows:
    1
    op     ra, rb, rc

    means:

    1
    ra = rb op rc

    Example:

    12
    sub    x0, x0, x1 ; means x0 = x0 - x1mov    x0, x1     ; means x0 = x1
    • [ ]

    [and]functions the same as the asterisk in C/C++, meaning ‘dereference’. It meanstreating the contents of the brackets as an address to access memory

    When ! appears at [] at the end, for example:

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

    Finally, the exclamation mark indicates that the stack pointer should be modified (i.e., -16 is applied to it), which happens before the value of the stack pointer is used as a memory address (the address to which the register will be copied)_before_This is, again,predecrement

    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

    means:

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

    In ARM V8, the stack pointer can only be adjusted in multiples of 16.

    In ARM V8, the stack pointer can only be adjusted in multiples of 16.

    In ARM V8, the stack pointer can only be adjusted in multiples of 16.

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

    Memory access

    ldr

    load register

    1234
    ldr    x0, [sp]   // load 8 bytes from address specified by spldr    w0, [sp]   // load 4 bytes from address specified by spldrh   w0, [sp]   // load 2 bytes from address specified by spldrb   w0, [sp]   // load 1 byte  from address specified by sp

    When making unaligned accesses to RAM, the processor must slow down and access it byte by byte. This incurs a massive performance penalty. Properly aligned access is crucial for performance.

    str

    store register

    1234
    str    x0, [sp]   // store 8 bytes to address specified by spstr    w0, [sp]   // store 4 bytes to address specified by spstrh   w0, [sp]   // store 2 bytes to address specified by spstrb   w0, [sp]   // store 1 byte  to address specified by sp

    Type conversions between integer types are in some cases accomplished by255and65535(corresponding tocharandshort) performingand(AND) operations, or:

    Whenever a narrower part of a register is written, the rest of the register is zeroed. That is:ldrbwill overwritexthe least significant byte of the register and zero the upper 7 bytes.

    ldp

    load pair, same as ldr but loads a pair of values at once

    stp

    store pair, same as str but stores a pair of values at once

    offsets

    123
    1) LDR Xt, [Xn|SP{, #pimm}] ; 64-bit general registers2) LDR Xt, [Xn|SP], #simm ; 64-bit general registers, Post-index3) LDR Xt, [Xn|SP, #simm]! ; 64-bit general registers, Pre-index
    • simmrange is -256 to 255 (10-bit signed value).
    • pimmrange is 0 to 32760 and must be a multiple of 8.

    Three modes

    1. Offset addressing mode
    1
    LDR Xt, [Xn, #pimm]

    fromXn + pimmload data from the address intoXt; address registerXnremains unchanged;

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

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

    first useXn's original value as the address to load data intoXt, and then usesimmto updateXn; address registerXnchanges after reading memory;

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

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

    Pseudo-instruction

    1
    ldr     x1, =label
    • The assembler places the address of the label into a special area of memory called a “literal pool”. The key point isthis memory area is located right after (and thus close to) your code

    • Then, the assembler calculates the difference between the address of the current instruction (i.e.ldritself) and the address of the data generated by that label in the literal pool.

    • The assembler generates a differentldrinstruction that uses the offset of the data relative to the program counter (pc). Herepcis the address of the current instruction.

    • Since the literal pool for your code is located near the code, the offset from the current instruction to the data in the pool is a relativelysmallnumber. Small enough to fit into a 4-byteldrIn the instruction.

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

    One disadvantage of this method is that the literal pool from which addresses are loaded is located in RAM. This means that each suchldrPseudo-instructions always generate a memory access._

    literal pool

    Comparison

    12
    ldr x1, =qldr x1, q

    aarch64

    1234567891011121314151617181920212223242526
            .global     main       // expose main to linker        .text                  // begin to write code        .align      2          // the code should certainly begin on an even addressmain:   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        .dataq:      .quad       0x1122334455667788fmt:    .asciz      "address: %p value: %lx\n"        .end

    Disassemble this machine code:

    12345678910111213
    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

    as well as

    123
    000000000011010 <q>:   11010:   55667788   11014:   11223344
    • It shows000000000011010 <q>:This means that what follows is a label in the source code.qThe corresponding data. Note the relocatable address.11010We will explain ‘relocatable address’ below.

    • Now, looking at7b8The first line of disassembly code. It reads asldr x1, 11010So the disassembled executable file is saying “go to address 11010 and fetch its contents”, which is our1122334455667788

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

    Runtime address relocation

    All the addresses we have seen so far are not the final addresses that the program will actually use at runtime.All addresses will be relocated.

    One of the reasons is to guard against malware. One is called**Address Space Layout Randomization (ASLR)**The technique of preventing malware authors from knowing in advance where to modify your executable file, thereby making it impossible for them to achieve their ulterior motives.

    64-bit ARM Linux kernel allocates 39, 42, or 48 bits for the process’s virtual address spaceNote that 42- and 48-bit values require 6 bytes to accommodate. The virtual address space is the set of all addresses that a process can generate or use. Furthermore, all addresses used by a process are virtual addresses.

    This way, the use of literal pools can be avoided.

    12
    adrp    x0, sadd     x0, x0, :lo12:s

    Example

    Load (store) integers of different sizes

    InstructionMeaning
    ldr x0, [x1]fromx1Load a 64-bit value from the specified address intox0
    ldr w0, [x1]fromx1Load a 32-bit value from the specified address intow0
    ldrh w0, [x1]fromx1Load a 16-bit value from the specified address intow0
    ldrb w0, [x1]fromx1Load an 8-bit value from the specified address intox0
    • Pointers and longs usexregisters.
    • All other sizes of integers usewregisters, with the size specified by the instruction itself.

    Array indexing

    123456789
    long Sum(long * values, long length){    long sum = 0;    for (long i = 0; i < length; i++)    {        sum += values[i];    }    return sum;}

    Note that we use the subscript variableijust to iterate through the array. This (in this case) is extremely inefficient.

    12345678910
    long Sum(long * values, long length){    long sum = 0;    long * end = values + length;    while (values < end)    {        sum += *(values++);    }    return sum;}

    Note that we no longer use the subscript variable. Instead, we use the pointer itself to both dereference_and_determine when to end the loop.

    1234567891011121314151617181920212223
        .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 valueSum:    mov     x2, xzr              // x2 = 0    add     x1, x0, x1, lsl 3    //  x1 = x0+x1*8    b       2f1:  ldr     x3, [x0], 8    add     x2, x2, x32:  cmp     x0, x1    blt     1b    mov     x0, x2    ret    .end

    Faster memory copy

    Suppose you need tocopy 16 bytes of memoryfrom one place to another. You might do it like this:

    12345
    void SillyCopy16(uint8_t * dest, uint8_t * src){    for (int i = 0; i < 16; i++)        *(dest++) = *(src++);}

    This is especially stupid, because you could simply write it as follows, why loop 16 times:

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

    Implemented in aarch64:

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

    Use ldp:

    1234
    SillyCopy16:    ldp    x2, x3, [x0]    stp    x2, x3, [x1]    ret

    Use q registers:

    1234
    SillyCopy16:    ldr    q2, [x0]    str    q2, [x1]    ret

    Iterate over an array of structs

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
    #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  20int 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 there is one named elsewhereFindOldestPersonof the function. The function must have one specified with the same name.global, so that the linker can coordinate theFindOldestPersonreference.

    In-O2or-O3Optimize it,gccstoreOriginalFindOldestPerson()Compiled into 18 lines of assembly.

    123456789101112131415161718192021222324252627282930313233343536373839
            .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                                                                        // 12FindOldestPerson:                                                       // 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                                                                        // 211:      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               // 2610:     cmp     x3, x4              // has loop ptr reached end_ptr?    // 27        blt     1b                  // no, not yet                      // 28                                                                        // 2999:     ret                                                             // 30                                                                        // 31        .data                                                           // 32        .struct 0                                                       // 33p.fn:   .skip   8                                                       // 34p.ln:   .skip   8                                                       // 35p.age:  .skip   4                                                       // 36p.pad:  .skip   4                                                       // 37                                                                        // 38        .end                                                            // 39

    Control flow

    cmp

    compare

    Discards the result of the subtraction but records whether the result is less than, equal to, or greater than zero. It sets the condition flags.

    br

    Branch to Register (branch to register)

    1
    br <register>

    Unconditional jump, similar to

    1
    goto *(ptr)

    ble

    Branch less or equal (branch if less than 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 (Compare, branch if zero)

    1
    cbz <register>, <label>

    If<register>the value in is 0, jump to<label>

    Otherwise, continue executing the next instruction.

    csel

    Conditional Select (Conditional Selection)

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

    If is satisfied<condition>, then assign<src1>the value of to<dest>

    Otherwise, assign<src2>the value of to<dest>

    Example:

    12
    cmp w2, w5csel w2, w2, w5, gt    // If w2 > w5, then w2 remains unchanged; otherwise, update it to w5

    This isbranchless conditional assignment, which is moreif-elseefficient.

    Equivalent to

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

    Shift operation

    lsl

    Logical Shift Left (Logical Left Shift)

    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 two and preserves the sign bit.

    ror

    rotate right (circular right shift)

    The ROR instruction performs a bitwise rotation, wrapping the bits rotated out from the least significant bit (LSB) to the most significant bit (MSB).
    Namely:RORinstruction executionbitwise right rotationoperation:The bit rotated out from the least significant bit (LSB) is placed back into the most significant bit (MSB) position.

    Bit Manipulation

    mvn

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

    orr

    orr (bitwise inclusive OR) performs a bitwise OR operation on the two operands, and then writes the result to the destination register

    bfi

    bfi (Bit Field Insert) means ‘Bit Field Insert’.

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

    <Xd>: destination register (where the result is written)
    <Xn>: source register (where the low-order value is taken from)
    <lsb>: The starting bit in the target register to begin insertion (least significant bit start)
    <width>: Number of bits to insert (width)

    Assume:

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

    Execution:

    1
    bfi Xd, Xn, #1, #3

    Result:

    Insert the lower 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>: Target register

    <src>: Source register

    lsb: Starting bit (low bit index)

    msb: Ending bit (high bit index)

    This instruction extracts an unsigned bit field (i.e., a contiguous sequence of bits) from src, places it in the low bits of dst (starting from bit 0), and clears or ignores the other bits
    In other words:

    1. Starting from the lsb bit of src, take up to the msb bit
    2. Extract this bit field
    3. Right-align and place it in the low bits of dst (bit 0), clearing all other bits

    Example:

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

    ubfiz

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

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

    Instruction format:

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

    Simply put: ubfiz = insert the lower 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 least significant bit)

    All other bits in the destination register will be cleared.

    For example:

    1
    ubfiz   w1, w1, #3, #5

    It means:

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

    Others

    adr

    Address

    adrp

    Address of Page

    12345678
        .section .rodatafmt:    .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: loads the symbolfmt's located page address of the 4KB aligned pageload intox0.
    • adrp= Address of Page
    • It ignores the lower 12 bits of the symbol address, keeping only the upper bits.
    • For example: iffmtthe address of is0x400123, thenadrp x0, fmtwill round0x400000load intox0
    • adrp x0, fmtwill roundfmtthe address down to the nearest 4KB boundary(i.e., clearing the lower 12 bits)

    Why not directly useldr x0, =fmt

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

    smaddl

    Signed Multiply Add Long

    two 32-bit integer (signed) After multiplying, add a 64-bit integer, the result is stored in a 64-bit register.

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

    Perform the following operations:

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

    Programming

    if statement

    if

    1234
    if (a > b){    // CODE BLOCK}

    Implemented in aarch64:

    123456
        // Assume value of a is in x0    // Assume value of b is in x1    cmp     x0, x1    ble     1f    // CODE BLOCK1:
    • Ifa > b, thenx0 - x1Will_greater than zero_。
    • Ifa == b, thenx0 - x1Will_equal to zero_。
    • Ifa < b, thenx0 - x1Will_less than zero_。

    ble means: jump (or goto) if the result of the previous calculation indicates ‘less than or equal to’ zero

    Rule of thumb

    • In high-level languages, when the condition is true, you want to_Enter_the following code block.

    • In assembly language, when the condition is false, you want to_skip_the following code block.

    temporary label

    The target of a jump instruction is denoted as1f. This is atemporary labelexample.

    C and C++ use a lot of curly braces. Since labels often serve as the equivalent of{and}, assembly language also uses a lot of labels. But a label is just a position marker; it is not a scope.

    Temporary labels are labels represented only by numbers. Such labels can appear repeatedly (i.e., they can be reused). They become unique by their relative position before or after the point of use.

    • 1fLook forward (f) for the next label1
    • 1bLook backward (b) for the nearest label1

    if / else

    12345678
    if (a > b){    // CODE BLOCK IF TRUE}else{    // CODE BLOCK IF FALSE}

    There are two jumps built into this code!

    Implemented in aarch64:

    123456789
        // Assume value of a is in x0    // Assume value of b is in x1    cmp     x0, x1    ble     1f    // CODE BLOCK IF TRUE    b       2f1:    // CODE BLOCK IF FALSE2:

    A complete example

    123456789101112131415161718192021222324
        .global main    .textmain:    stp     x29, x30, [sp, -16]!    mov     x1, 10    mov     x0, 5    cmp     x0, x1    ble     1f    ldr     x0, =T                     //Pseudo Instruction    bl      puts    b       2f1:  ldr     x0, =F    bl      puts2:  ldp     x29, x30, [sp], 16    mov     x0, xzr    ret    .dataF:  .asciz  "FALSE"T:  .asciz  "TRUE"    .end

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

    Line 23andline 24of.ascizis a call to an_assembler pseudo-instruction_used to create a C string. Recall thatC strings are NULL-terminated. The NULL termination is represented by.ascizthe trailingz.

    There is also a similar pseudo-instruction.ascii, which_that does not_NULL-terminate the string.

    Loops

    while loop

    while loop
    while loop

    123
    while (a >= b) {    // CODE BLOCK}

    aarch64:

    123456789
        // Assume value of a is in x0    // Assume value of b is in x1 1: cmp     x0, x1    blt     2f    // CODE BLOCK    b       1b2:

    for loop

    1234
    for (set up; decision; post step){    // CODE BLOCK}

    for
    for

    1234
    for (long i = 0; i < 10; i++){    // CODE BLOCK}

    aarch64 (left flowchart)

    123456789101112
        // Assume i is implemented using x0    mov     x0, xzr1:  cmp     x0, 10    bge     2f    // CODE BLOCK    add     x0, x0, 1    b       1b2:

    aarch64 (right flowchart)

    123456789101112
        // Assume i is implemented using x0    mov     x0, xzr    b       2f1:    // CODE BLOCK    add     x0, x0, 12:  cmp     x0, 10    blt     1b

    continue

    123456
    for (long i = 0; i < 10; i++) {    // CODE BLOCK "A"    if (i == 5)        continue;    // CODE BLOCK "B"}

    Implemented in aarch64:

    123456789101112131415161718
        // Assume i is implemented using x0    mov x0, xzr1:  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   1b3:

    Another version:

    1234567891011121314151617181920
        // Assume i is implemented using x0    mov x0, xzr    b   3f1:    // CODE BLOCK "A"    // if (i == 5)    //      continue    cmp x0, 5    beq 2f    // CODE BLOCK "B"2:  add x0, x0, 13:  cmp x0, 10    blt 1b

    break

    breakThe implementation of is very similar tocontinue.

    123456
    for (long i = 0; i < 10; i++) {    // CODE BLOCK "A"    if (i == 5)        break;    // CODE BLOCK "B"}

    aarch64:

    12345678910111213141516171819202122
        // Assume i is implemented using x0    mov x0, xzr    b   3f1:    // CODE BLOCK "A"    // if (i == 5)    //      break;    cmp x0, 5    beq 4f    // CODE BLOCK "B"2:  add x0, x0, 13:  cmp x0, 10    blt 1b4:

    Structure

    alignment

    Data members follow natural alignment.

    Namely:

    • Alongwill appear at addresses that are multiples of 8.
    • Aintwill appear at addresses that are multiples of 4.
    • Ashortwill appear at even addresses.
    • Acharcan appear at any address.

    Example

    12345
    struct {    long a;    short b;    int c;};

    Layout:

    OffsetWidthMember
    08bytea
    82byteb
    102– gap –
    124bytec
    1234567
    struct Foo {    long a;    short b;    int c;};struct Foo Bar = { 0xaaaaaaaaaaaaaaaa, 0xbbbb, 0xcccccccc };

    The hex dump will show:

    1
    aaaa aaaa aaaa aaaa bbbb 0000 cccc cccc

    Note the gaps filled with zeros. Note that if this is a local variable, these zeros could be garbage values.

    Changing the order:

    1234567
    struct Foo {    short a;    char b;    int c;};struct Foo Bar = { 0xaaaa, 0xbb, 0xcccccccc };

    The hex dump will show:

    1
    aaaa 00bb cccc cccc

    Note that beforeint cthere is only a one-byte gap.

    Why is there a zero to the left of b?

    Because this ARM processor is running in_little endian_mode.

    Defining a struct

    1234567
    struct Foo {    short a;    char b;    int c;};struct Foo Bar = { 0xaaaa, 0xbb, 0xcccccccc };

    This is one way to define and access a struct:

    1. Hardcoded field offset
    1234567891011121314151617181920212223242526272829303132333435
        .section .rodatafmt:    .asciz "%p a: 0x%lx b: %x c: %x\n"    .databar:    .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 2main:    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, fmtwill roundfmtthe address down to the nearest 4KB boundary(i.e., clear the lower 12 bits), and then load this ‘page base address’ intox0

    For example:

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

    1. another way to define a structs is

    **Using.equPseudo-instruction to define symbolic constant

    1234567891011121314151617181920212223242526272829303132333435363738394041
        .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 4main:    stp     x29, x30, [sp, -16]!  // Save x29, x30 to the stack    mov     x29, sp               // Set up 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    .datafmt:	.asciz      "%p a: 0x%lx b: %x c: %x\n"   // printf format stringbar:    .short      0xaaaa                        // a    .byte       0xbb                          // b    .byte       0                               // padding    .word       0xcccccccc                    // c    .end
    1. the third way:(Linux only)

    **Using.structand field labels automatically derive offsets

    1234567891011121314151617181920212223242526272829303132333435363738394041424344
        .section .rodatafmt:    .asciz "%p a: 0x%lx b: %x c: %x\n"    // Use .struct to simulate field offsets of struct Foo    .set  Foo, 0    .struct 0Foo_a:  .struct Foo_a + 2      // short a: 2 bytesFoo_b:  .struct Foo_b + 1      // char b: 1 byte        .struct Foo_b + 1      // padding: 1 byteFoo_c:  .struct Foo_b + 2      // int c: starts at offset 4    // Now Foo_c is at offset 4    .databar:    .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 2main:    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, 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 the key points of usingstruct:

    • allstructall have a base address
    • the base address corresponds to the starting position of the first data member
    • all subsequent data members are offsets relative to the first member
    • To correctly usestruct, you must first calculate the offset of each data member
    • Sometimes there is padding between data members, because all data members need to be aligned to natural boundaries.

    The this pointer in C++

    • Every non-static method call uses a hidden first parameter. That’s it. That’s the trick. This hidden parameter is the this pointer.
    12
    TestClass tc;tc.SetString(test_string);

    It looks like we only passed one argument, test_string. But actually the compiler passed two arguments:

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

    2. The second is test_string, passed to register x1

    Seen in assembly:

    123
    adrp x1, _test_stringadrp x0, _tc         // Put the address of the tc object into x0 -- which is the this pointerbl __ZN9TestClass9SetStringEPc

    const

    constThe meaning and effect of this can only bePartreflected in assembly language.

    • constLocal variables andconstparameters are no different from other data to the assembly language.

    • constThe constancy of local variables and parameters is entirely enforced by the compiler.

    • constGlobal variables are guaranteed to be constant by hardware. Attempting to modify a variable protected in this way is like poking a hornet’s nest. Best to leave it alone.

    switch and jump tables

    When the C++ optimizer is enabled, it analyzes your various cases and chooses one of three structures to implement yourswitch

    Moreover, it can use any combination of the following! Compiler authors are smart!

    1. It might generate a long chain ofif / else
    2. It might use_binary search_to find the correctcase
    3. Finally, it might use ajump table

    Suppose our cases are roughly contiguous. Given that all jump instructions are the same byte length, we can do some arithmetic on the switch variable to somehow derive the address of the case we want.

    12345678910111213141516171819202122232425262728293031323334353637383940414243444546
    #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;}

    Note that in this examplecasethe values are contiguous.

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

    fmeans forward,bmeans backward

    At addressjtthere is a series of jump statements… that is, jumps. Since they are arranged contiguously, this is an example of a jump table. We will calculate the entry into this_instruction array_index, and then jump to it.

    1234
    lsl     x0, x0, 2ldr     x1, =jtadd     x1, x1, x0br      x1
    • Line 2 loads the base address starting from addressjtof the “instruction array”.

    Complete Example

    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
            .text        .align  4        .global mainmain:   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       7f0:      ldr     x0, =ZR        bl      puts        b       99f1:      ldr     x0, =ON        bl      puts        b       99f2:      ldr     x0, =TW        bl      puts        b       99f3:      ldr     x0, =TH        bl      puts        b       99f4:      ldr     x0, =FR        bl      puts        b       99f5:      ldr     x0, =FV        bl      puts        b       99f6:      ldr     x0, =SX        bl      puts        b       99f7:      ldr     x0, =SV        bl      puts        b       99f99:     mov     w0, wzr        ldr     x30, [sp], 16        ret        .data        .section    .rodataZR:     .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

    Implementing fall-through

    If the code following a case does not have a break, control flow will fall through directly to the next case.

    Here is a code snippet from the program linked above:

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

    Handling gaps

    The example above shows 8 consecutive cases. What happens if case 4 has no code? In other words, what happens if case 4 does not exist?

    The result is as follows:

    12345678910111213
    2:      ldr     x0, =TW        bl      puts        b       99f3:      ldr     x0, =TH        bl      puts        b       99f4:      b       99f5:      ldr     x0, =FV        bl      puts        b       99f

    Other strategies for implementing switch

    As mentioned above, the optimizer has at least three tools available to implement complexswitchstatements, and it can use a combination of these tools.

    1. For example, suppose your cases boil down to two roughly contiguous ranges of values. For instance, you have cases 0 to 9, and cases 50 to 59. You could implement this as two jump tables, and use aif / elseto select which one to use.

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

    1. Suppose you have acasestatement with manyswitchbranches, and thesecasevalues havevastly different numerical ranges, such as case 10, case 1000, case 50000…, then you canfirst use binary search to narrow down the search range, restrict the target value to asmaller range, and then use other techniques (such as jump tables, linear comparison, etc.) within this range to determine whichcasebranch it corresponds to.

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

    1. You might need to implementhierarchical jump tables, for example.

    ‘Hierarchical jump tables’ are an optimization structure suitable for the following situations:

    • casevalues are verysparsewith extremely wide ranges(for example,case 0, case 1000, case 2000...)
    • but they aredense in local ranges(such as1000~1009,2000~2009

    you can:

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

    Strategies for implementing if-else

    If you do need to implement a long chain ofif / elsestatements, consider how frequently a particular case is selected. Put the most common case at theif / elsebeginning of the sequence.

    This is what is known as “making the common case fast”.

    Making the common case fast is a great idea in computer science. Regardless of the language you use, it is an idea worth keeping in mind.

    function

    Core Concept

    blThe instruction stands for Branch with Link. The concept of Link allows a function (or method) to return to the instruction immediately following the call.

    The Branch with Link calculates the address of the instruction immediately following it.

    It places this address into a registerx30, then jumps to the provided label. It leaves a link in the “breadcrumb” trail used to return, and following itretallows the return.

    This is why if your function itself calls other functions, you absolutely must back up thex30

    An example

    123456789101112
            .text        .global main        .align  2main:   ldr     x0, =hw        bl      puts        ret        .datahw:     .asciz  "Hello World!"        .end

    The program will hang and must be killed with ^C.

    Someone calledmain()—it is a function, someone called it using theblinstruction. At the momentmain()is entered, the address it needs to return to is stored inx30.

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

    So, when line 7 executes, it puts the contents ofx30into the program counter and jumps there.

    Here is the fixed code:

    1234567891011121314
            .text        .global main        .align  2main:   str     x30, [sp, -16]!        ldr     x0, =hw        bl      puts        ldr     x30, [sp], 16        ret        .datahw:     .asciz  "Hello World!"        .end

    In the AARCH64 Linux-style calling convention, the return value is placed inx0, and sometimes it is returned in other temporary registers, though this is uncommon. (Note that if the function returns afloatordoublex0it could also bew0or the first floating-point register.)

    If your function calls_any_other functions,x30must be backed up to the stack and restored before returningx30

    C or C++ does not support functions with multiple return values, but they can be written in assembly language—the rules are yours to break.

    Inline functions

    declared as inline(inline) functions do not actually make a function call. Instead, the function’s code, after type checking, is inserted directly where the “call” occurs, after adjusting parameter names.

    Passing arguments to functions

    How arguments are passed to functions may vary by operating system. This chapter targets the standard implemented on Linux.

    For the purposes of the current discussion, we assume all arguments arelong int, and therefore are stored inxregisters.

    • Up to 8 arguments can be passed directly through temporary registers.(i.e.x0tox7) each argument can be up to the size of an address, long, or double (8 bytes).

      • Scratch means that the value of the register can be changed at will, without needing to back up or restore their values between function calls.

      • This means if your function calls any functions, you cannot expect the contents of scratch registers to remain unchanged.

    An example

    1234
    long func(long p1, long p2){    return p1 + p2;}

    is implemented as:

    12
    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 do whatever you want with the return values. Specifically, you can return multiple values.HoweverIf you do this, you give up the possibility of calling these functions from C or C++.

    const

    1234
    long func(const long p1, const long p2){    return p1 + p2;}

    How does assembly language change?

    Answer: Not at all!

    constis an instruction to the compiler, commanding it to prohibit changingp1andp2's value. We are smart humans, knowing that assembly language never intended to modifyp1andp2, so no changes are needed.

    Passing a pointer

    1234
    void func(long * p1, long * p2){    *p1 = *p1 + *p2;}
    12345
    func:   ldr x2, [x0]        ldr x3, [x1]        add x2, x2, x3        str x2, [x0]        ret

    Since this is avoidfunction, when returning,x0's value is usually undefined.

    Passing by reference

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

    Passing by reference is also an instruction to the compiler to treat the pointer slightly differently—this difference is not apparent here, so the only difference compared to the pointer-passing version is how we return the result.

    More than eight arguments

    1234567891011
    #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);}
    123456789101112131415161718192021222324252627282930313233343536373839404142434445
            .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.        retmain:        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        .datafmt:    .asciz    "This example hurts my brain: %ld %ld\n"        .end

    After executingline 24, the contents of the stack are:

    12
    sp + 0    former contents of frame pointersp + 8    return address for main

    After executingline 27, the contents of the stack are:

    1234
    sp + 0    9sp + 8    garbagesp + 16   former contents of frame pointersp + 24   return address for main

    After executingLine 14, the contents of the stack are:

    123456
    sp + 0    return address for SillyFunctionsp + 8    garbagesp + 16   9sp + 24   garbagesp + 32   former contents of frame pointersp + 40   return address for main

    This meansline 18takes from memoryp9and puts its value into x2 (where it becomesprintf()'s third argument).

    In AArch64, stack space is often aligned in 16-byte unitsallocated, but you might have only written a portion of the data, and the rest is not initialized, so we call it ‘garbage’ (undefined content)

    In ARM V8, the stack pointer can only be adjusted in multiples of 16.

    In ARM V8, the stack pointer can only be adjusted in multiples of 16.

    In ARM V8, the stack pointer can only be adjusted in multiples of 16.

    Examples of calling some common C runtime functions

    Incidentally, functions in the C runtime are broadly divided into two categories.

    • Some are primarily implemented by the C runtime itself.

    • Other functions in the C runtime act as wrappers for functions implemented by the operating system itself. These are called “system calls”.

    In terms of calling functions in the C runtime, there is no practical difference between these two types. But note that there is also a way to usesvcinstructions to directly call system calls.

    “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 usually called C runtime library, common implementations on different platforms include:

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

    What does the C runtime do?

    1. Program initialization
      • Inmain()Before 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. Resource management
      • such as lifecycle management of memory allocation, file handles, threads, etc.
    4. Provides system call wrappers
      • For example, when you callwrite()it actually calls a wrapper provided by the C runtimewhich ultimately accesses the kernel viasyscallorsvcinstructions.

    System call

    Many C runtime functions are just wrappers for system calls. For example, if you call open() from the C runtime, the function does some bookkeeping and then performs the actual system call.

    What is a system call?

    The short answer is: a system call is a function call serviced by the operating system itself, running in the operating system’s own private memory area and able to access its internal features and data structures.

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

    We can only manipulate kernel space through carefully controlled mechanisms—such as system calls. The technical name for where the kernel (or system) typically runs is EL1.

    There are two higher exception levels (EL2 and EL3), which are beyond the scope of this book.

    The mechanism for making system calls

    First, as with any function call, the arguments need to be set up. The first argument goes in the first register, and so on.

    Second, load the number corresponding to the specific system call we want to make into a specific register (w8).

    Finally, a special instruction, svc, triggers a trap, elevating us from userland to kernel space. In other words, svc causes a transition from EL0 to EL1. There, various checks are performed, 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 is a special instruction to elevate from EL0 to EL1, there is also a special instruction that does the opposite.

    Numbers associated with specific system calls

    Reference:

    getpid() example

    1234567
    #include <stdio.h>#include <unistd.h>int main() {    printf("Greetings from: %d\n", getpid());    return 0;}

    Written in assembly language using the C runtime:

    1234567891011121314151617
            .global main        .text        .align  2main:   stp     x29, x30, [sp, -16]!        bl      getpid        mov     w1, w0        ldr     x0, =fmt        bl      printf        ldp     x29, x30, [sp], 16        mov     w0, wzr        ret        .datafmt:    .asciz  "Greetings from: %d\n"        .end

    Finally: calling system calls directly

    123456789101112131415161718
            .global main        .text        .align  2main:   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        .datafmt:    .asciz  "Greetings from: %d\n"        .end

    We chose getpid() because it takes no arguments. When using the C runtime, we simply bl to it.Calling a system call directly is different; we must first load the number corresponding to getpid() under the AArch64 architecture into x8.

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
    /*  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    w27fd 		.req	w28main:   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       99f1:		// 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       50f20:     // 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       99f25:     // 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, wzr99:     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, 0600open_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       99f90:     // failure - ensure we return non-zero!        mov     x0, 199:     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        .dataprog:	.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) (excluding the trailing NULL)buffer: .word   0        .end

    Floating-point number

    What are floating-point numbers?

    Reference

    IEEE 754

    Register

    There are four top-level concepts regarding floating-point arithmetic on AARCH64.

    • Floating-point values have a completely separate set of registers.
    • There are instructions specifically for floating-point values.
    • There are special instructions (SIMD) that can operate on a set of floating-point values.
    • There are instructions to convert back and forth between integer and floating-point registers.

    regs
    regs

    The figure above shows the different views and access methods of the SIMD (Single Instruction, Multiple Data) register V0 in the ARM64 architectureincludingArrangement Specifiers of different bit widths and Lane indices

    Illustration

    This figure takes the V0 register as an exampleshowing how to access its contents using different arrangement specifiers

    LevelTypeDescription
    Lowest levelV0The entire 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)
    Further 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
    Highest levelB0,H0,S0,D0is an alias forV0alias, accessed by bit width (only accesses the lowest bit data)

    Truncation towards zero

    truncate

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

    12
    integer_variable = int(floating_variable);  // C++integer_variable = (int) floating_variable; // C

    This instruction is fcvtz—convert towards zero. Then, whether it produces a signed or unsigned result is determined by the last letter: u or s.

    MnemonicMeaning
    fcvtzuTruncation (always towards 0) produces an unsigned integer
    fcvtzsTruncation (always towards 0) produces a signed integer
    • fcvtzu: Float Convert to Unsigned integer, with truncation toward zero
    • fcvtzs: Float Convert to Signed integer, with truncation toward zero

    The ARM documentation says that this instruction, which completely discards the fractional part, does rounding rather than truncating.

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

    Source registerConvert a
    dXdoubleto integer
    sXfloatto integer
    Destination registerConvert a
    xX64-bit integer
    wX32-bit or smaller integer

    wheredisdoublefisfloatexample:

    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

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
        .section .text    .global main    .type main, @function // Indicates telling the assembler and linker: main is a function symbol    //.type <symbol>, @<type> is a pseudo-instruction of GAS (GNU Assembler), used to specify the type of a symbol.    // <symbol>: symbol name, such as main    // @<type>: symbol type, here @function, indicating this is a function, not a variable or labelmain:    stp     x29, x30, [sp, -16]!     // Save frame pointer and link register    mov     x29, sp    // Save floating-point register    stp     d20, d21, [sp, -16]!    stp     d22, d23, [sp, -16]!    // Load hint information    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 up (+∞)    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 (tie 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 towards 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 (tie 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 .rodatavless:    .double 5.49    .double 5.51    .double -5.49    .double -5.51fmt1:    .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"

    Note that all values are truncated to integers closer to zero.

    Truncation away from zero

    Truncation away from zero is not that simple. In fact, it cannot be done with a single instruction.

    In C (and C++):

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

    If fv is already equal to an integer, then the integer value is that integer. Otherwise, iv is the integer further from zero.

    In C++, a more complex version requires<cmath>, and might look like this:

    1234
    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 upward (towards more positive).

    12345678910
    RoundAwayFromZero:        fcmp    d0, 0        ble     1f        // Value is positive, truncate towards positive infinity (ceil)        frintp  d0, d0        b       2f1:      // Value is negative, truncate towards negative infinity (floor)        frintm  d0, d02:      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 that performs what we normally understand as ‘rounding’ is frinta. This is a ‘round to nearest, ties away from zero’ conversion. So, 5.5 becomes 6, as we expect from ‘rounding’.

    Converting integers to floating-point values

    In C / C++:

    12
    double_var = double(integer_var); // C++double_var = (double)integer_var; // C

    Handled by two instructions:

    • scvtf Converting signed integers to floating-point values
    • ucvtf Converting unsigned integers to floating-point values

    The name of the destination register controls which floating-point value is generated. For example, specifying dX generates a double, and so on.

    Floating-point literals

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

    Given that floating-point values themselves are at least 4 bytes long, using floating-point literals is severely restricted. For example:

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

    Line 1 passes, but line 2 will throw an error.

    To load a floating-point number, you can convert the value to binary and then do this:

    12345678910111213141516
            .text        .global main        .align    2main:   str        x30, [sp, -16]!        ldr        s0, =0x3fc00000        fcvt       d0, s0        ldr        x0, =fmt        bl         printf        ldr        x30, [sp], 16        mov        w0, wzr        ret        .datafmt:    .asciz    "%f\n"        .end

    printf() only knows how to print double-precision values. When you specify a float, it converts it to a double before outputting it.

    Manually converting floats and doubles to binary is not common for humans, although compilers are happy to do so.

    For us humans, it is more common to use the assembler directives .float and .double to specify float and double values and place them in RAM.
    An example:

    1234567891011121314151617181920212223242526272829303132333435363738
            .global main        .text        .align  2counter .req    x20dptr    .req    x21fptr    .req    x22        .equ    max, 4main:   stp     counter, x30, [sp, -16]!        stp     dptr, fptr, [sp, -16]!        ldr     dptr, =d        ldr     fptr, =f        mov     counter, xzr1:      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       1b2:      ldp     dptr, fptr, [sp], 16        ldp     counter, x30, [sp], 16        mov     w0, wzr        ret        .datafmt:    .asciz  "%d %f %f\n"d:      .double 1.111111, 2.222222, 3.333333, 4.444444f:      .float  1.111111, 2.222222, 3.333333, 4.444444        .end
    InstructionFull name / AbbreviationFunctionCommon usage example
    .reqregister require(Unofficial abbreviation)Assign toAlias a registerfoo .req x0Indicates writing laterfoois equivalent tox0
    .equequateDefine aConstant symbolBUF_SIZE .equ 64RepresentsBUF_SIZE = 64

    On Linux, just as w/x0 to w/x7 are temporary registers and used to pass parameters, so are s/d0 to s/d7, starting from register 0. That is:

    • Integer parameter passing: x0 ~ x7 (or 32-bit w0 ~ w7) are used to pass the first 8 integer 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.

    Stuffing 32 bits into a 32-bit bag

    1
    ldr s0, =0x3fc00000  // Pseudo-instruction! We thought it directly loaded 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 is:

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

    We expect line 6 to read:

    1
    ldr        s0, =0x3fc00000

    But actually it is:

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

    Scan downwards 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 address value
    .inst 0x3fc00000Manually insert a 32-bit data (not necessarily a valid instruction)Store constant (not execute)

    .instmeaning of:

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

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

    1
    ret

    Equivalent to:

    1
    .inst 0xd65f03c0

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

    Why not usemov reg, #imm

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

    fmov

    The fmov instruction is used to move floating-point values between floating-point registers, and to some extent, to move data between integer registers and floating-point registers.

    Load floating-point number as immediate value

    As we saw with integer registers, some values can be used as immediate values, while others cannot. This depends on how many bits are required to encode the value. Too many bits… and it won’t fit into a 4-byte instruction along with the opcode.

    For example, this allows:

    1
    mov    x0, 65535

    But this doesn’t work:

    1
    mov    x0, 65537

    The immediate constraints for fmov are much stricter because floating-point numbers are much 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 bitrepresents positive or negative
    exponent part3 bitscontrols magnitude (multiplies by powers of 2)
    mantissa part4 bitscan only be composed of combinations of 1/2, 1/4, 1/8, 1/16
    123456
    fmov d0, 1.0        // ✅ OK: integer 1 is 2⁰, exponent can be encodedfmov d0, 1.5        // ✅ OK: 1 + 0.5 = 2⁰ + 2⁻¹, both exponent and mantissa can be encodedfmov 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’. If you want to change numerical precision, you must use the fcvt series.

    half precision

    It does support half-precision (16-bit) floating-point values, but there is no fully unified convention on how different compilers support them. In fact, there is not just one but two competing half-precision formats: IEEE type and GOOGLE type. Additionally, many open-source developers have created their own implementations, whose naming conventions may conflict.

    123
    __fp16 Foo(__fp16 g, __fp16 f) {    return g + f;}

    Compiled as:

    12345
    fcvt    s1, h1fcvt    s0, h0fadd    s0, s0, s1fcvt    h0, s0ret

    Note that each half-precision value is converted to single-precision. Therefore, using half-precision values in C and C++ may be inefficient.

    On the other hand, if you are willing to use intrinsics and a certain SIMD instruction set provided by ARM, you can go all out. But be aware that doing so will tie your code to ARM processors in a way you might later regret.

    Bit Manipulation

    A bit field is a feature of the C and C++ languages that completely hides what is commonly known as ‘bit bashing’.

    The order of bits in a bit-field is not guaranteed to be the same across different platforms, or even across 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 structure, typically used in space-sensitive scenarios like hardware registers and protocol headers.
    Syntax format

    1234
    struct 结构体名 {    类型 成员名 : 位宽;    ...};

    Example:

    12345
    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, which is 1 byte

    1. Although each member has a bit width, the overall size is usually aligned to an integer type (here it is 1 byte, because 8 bits is exactly one byte).
    2. Different compilers may have slight differences in bit field alignment and padding details.
    3. When accessing, it can be used like a regular member:
    1234
    struct BF bf;bf.a = 1;bf.b = 3;bf.c = 31;

    The compiler will automatically perform masking and shifting on the bit fields.

    Consider a data structure that might have millions of instances in RAM, or billions of instances on disk. Suppose each instance requires 8 boolean members. The C++ standard does not define the size of bool, leaving it to the implementation. Some implementations treat bool as equivalent to int, with a length of 4 bytes. Some implement bool using char, with a length of 1 byte.

    We assume the minimum case, treating bool as equivalent to char. Our struct might have millions or billions of instances, requiring 8 bools, thus 8 bytes. Multiply that by millions or billions.

    Bit fields can help you here, using only one bit per boolean value. In the best case, 8 bytes are compressed into 1 byte. In the worse case, 8 × 4 = 32 bytes are compressed into 1 byte.

    Assuming the smallest unit is used, i.e., each bool is 1 byte:

    12345678910
    struct S {    bool b0;    bool b1;    bool b2;    bool b3;    bool b4;    bool b5;    bool b6;    bool b7;};

    The size of this struct is 8 bytes (1 byte × 8 bools). If there are a million instances, the memory occupied is 8MB; if there are a billion instances, it is 8GB. For a 4-byte bool implementation, the size directly becomes 32 bytes, and 100 million instances would be 3.2GB.

    Solution: Use bit fields to compress boolean values

    Using bit fields, define 8 boolean values with a size of 1 bit:

    12345678910
    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 combined exactly occupy 1 byte.

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

    In computer science, there is always an eternal trade-off between space and time. Below is a law:

    If you want to make something faster, it will consume more memory.

    If you want to save memory, it will take more time.

    This law manifests here… Remember the example where we wanted to save memory by packing 8 bools into 1 byte? To save that little memory, we slow down, because accessing the correct bit takes several instructions, while overwriting a bool implemented with an int takes only one instruction.

    As for the assembly language generated for bit fields, it depends on the optimization level. When unoptimized, the generated code will be longer and more cumbersome than ‘clever’ assembly language.

    Byte Order

    ARM supports both: little-endian and big-endian. However:

    Standard toolchains generate little-endian code. Installing a big-endian version of the toolchain is a major undertaking.

    Below 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.

    Common Intel processors are also little-endian.

    Assembly macros

    An early innovation of assemblers was the introduction of macro capabilities. Given that writing code in assembly is somewhat tedious, macros provide a simple form of metaprogramming, where a sequence of statements can be encapsulated within a macro. You can think of macros as an early form of C++ template functions (sort of, but not quite).

    Here is an example of an assembly language macro:

    1234
    .macro LLD_ADDR xreg, label        adrp    \xreg, \label@PAGE        add     \xreg, \xreg, \label@PAGEOFF.endm

    It will be expanded to:

    12
    adrp    x0, fmt@PAGEadd     x0, x0, fmt@PAGEOFF

    If the asm file ends with.s, gcc on Linux will not pass the assembly language file through the C preprocessor; but if the file ends with.S, it will.

    General usage

    AASCIZ

    AASCIZ label, string

    This macro calls .asciz with string as the string and label as the label. Additionally, this macro ensures that the string starts from a 4-byte aligned boundary.

    PUSH_P, PUSH_R, POP_P and POP_R

    These macros save some repetitive keystrokes. For example:

    1
    PUSH_P  x29, x30

    Expands to:

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

    START_PROC and END_PROC

    Place START_PROC after the label that introduces a function.

    Place END_PROC after the last ret of the function.

    They expand to: .cfi_startproc and .cfi_endproc。

    MIN and MAX

    Convenient macros for finding minimum and maximum values, which are more readable. Note that this macro executes a cmp, which subtracts src_a from src_b (discarding the result), with the purpose of setting the flags for the subsequent csel to interpret.

    Signature:

    1
    MIN     src_a, src_b, dest

    src_a and src_b, the smaller one is placed into dest.

    Signature:

    1
    MAX     src_a, src_b, dest

    src_a and src_The larger one in b is placed into dest.

    MOD

    The MOD macro used above is defined as:

    1234
    .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 so that it can be used externally.

    Signature:

    1
    GLABEL label

    will be prefixed with an underscore.

    CRT

    Call CRT (C runtime) function
    If you create your own function without an underscore, you can just call it normally.
    If you need to call functions such as those in the C runtime library, use this macro as follows:

    1
    CRT     strlen

    MAIN

    Declare main()
    Put MAIN on a separate line. Note that there is no colon.

    errno

    Externally defined errno is accessed through a CRT function, which is not visible when coding in C and C++. The name of this function differs on Mac and Linux. To get the address of errno, use:

    1
    ERRNO_ADDR

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

    Load and Store

    GLD_PTR

    Load the address of a label and then dereference it, where on Apple the label is in the global space, and on Linux it is a relatively near label.

    Signature:

    1
    GLD_PTR     xreg, label

    When this macro completes, the specified x register holds the 64-bit value at the specified label.

    GLD_ADDR

    Load the address of the label into the specified x register. No dereference is performed. 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, the double at the specified local label will be stored in the specified double register.

    LLD_FLT

    Signature:

    1
    LLD_FLT xreg, sreg, label

    When this macro completes, the float at the specified local label will be stored in the specified single-precision register.

    Performance

    Undoing stack pointer modifications

    A little trick about undoing stack pointer modifications. You might think that stack modifications made by pushing with str or stp and their kin must be undone with ldr or ldp and their kin.

    It depends.

    If you need to retrieve the original contents of the registers pushed onto the stack, then using ldr or ldp is appropriate. However, if you do not need to retrieve the original contents of the registers, then using addition to undo the stack modification is faster.

    Take the use of printf() as an example. On Apple Silicon systems, you must pass arguments to printf() by pushing them onto the stack. However, when printf() completes, you do not need the values you pushed. As shown above, simply add the correct value (a multiple of 16) to the stack pointer. This is faster because addition does not access RAM (or cache) like ldr does.

    Others

    Let the assembler calculate the length itself

    1234567891011121314151617181920212223242526272829
            .global        main        .align         2        .textmain:   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        .datas:      .asciz         "Hello, World!\n"ssize:  .word          ssize - s - 1        // accounts for null at endfmt:    .asciz         "str: %slen: %d\n"   // accounts for newline        .end

    Atomic operations

    Load-linked, store-conditional

    123456789101112131415
            .text        .p2align    2#if defined(__APPLE__)        .global     _LoadLinkedStoreConditional_LoadLinkedStoreConditional:#else        .global     LoadLinkedStoreConditionalLoadLinkedStoreConditional:#endif1:      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 “monitors” whether the address has been modified. You can modify this value (e.g., increment it by 1).

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

    llsc
    llsc

    In the second version of ARMv8 (called ARMv8.1), the implementation of operations on atomic variables was improved. Load-linked and store-conditional instructions are still available, but several new instructions have been added that can perform addition, subtraction, and various bitwise operations in a single atomic instruction.

    For example:

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

    Does the same work: atomically increments the value in memory pointed to by x0.

    Spinlock

    Below is the source code for an ARM V8 spinlock.

    Lock

    123456789
    Lock:        START_PROC        mov         w3, 1           // Value to store: 1 means "lock"1:      ldaxr       w1, [x0]        // Atomically 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 snatched the lock), then write the value of w3*x0, and put the result into w2 (0 means 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

    123456
    Unlock:        START_PROC        str         wzr, [x0]       // Writing 0 means 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):
    12345
    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):
    12
    *x0 = 0;       // unlockdmb(ISH);      // ensure all cores see the update

    Reference

    Loading comments…