Skip to content

Latest commit

 

History

History
252 lines (192 loc) · 7.69 KB

File metadata and controls

252 lines (192 loc) · 7.69 KB

Languages: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية

← DynCode Compiler

ARM64 (AArch64) Assembly Tutorial — DynCode Perspective

For readers unfamiliar with ARM64, focusing on instructions generated by the dyncode compiler. Each instruction includes annotations and before/after comparisons.

1. Register Overview

General-purpose registers:
  x0 ~ x30    64-bit general-purpose registers
  w0 ~ w30    corresponding low 32-bit aliases
  x29 (fp)    frame pointer
  x30 (lr)    link register (holds return address)
  sp          stack pointer (not x31!)
  xzr / wzr   zero register (reads always return 0, writes are discarded)

Special registers:
  pc          program counter (cannot be written directly)

Apple ABI reserved:
  x16, x17    platform reserved (intra-procedure-call scratch)
  x18         platform reserved (TLS base, Apple exclusive)

Calling convention (AAPCS64):
  Arguments:    x0~x7 (integer), d0~d7 (floating-point)
  Return value: x0 (integer), d0 (floating-point)
  Callee-saved: x19~x28, x29(fp), x30(lr), sp
  Caller-saved: x0~x18, d0~d31
  Red zone:     128 bytes below sp (leaf functions can use directly)

2. Branches and Calls

b label — unconditional branch

b .Lloop    ; branch to .Lloop (PC-relative, ±128MB range)

bl label — branch with link (function call)

bl _fib     ; branch to _fib, saving return address in lr (x30)
            ; encoded as imm26 offset → produces ARM64_RELOC_BRANCH26!

Why dyncode must avoid bl: bl uses a 26-bit immediate offset that the linker fills in. For external symbols, this produces an ARM64_RELOC_BRANCH26 relocation. DynCode has no linker, so this must be replaced with blr.

br xN / blr xN — register indirect branch/call

blr x8      ; branch to address in x8, save return address in lr
br  x8      ; branch to address in x8, no link save (tail call)

ret — function return

ret         ; equivalent to br lr, jumps to address saved in x30

3. PC-Relative Addressing

adr xN, label — PC-relative load (±1MB)

adr x0, .Ldata   ; x0 = PC + offset_to_.Ldata

adrp xN, label@PAGE + add xN, xN, label@PAGEOFF

adrp x0, _sym@PAGE        ; x0 = (PC & ~0xFFF) + page_offset
add  x0, x0, _sym@PAGEOFF ; x0 += within-page offset
; produces ARM64_RELOC_PAGE21 + ARM64_RELOC_PAGEOFF12 relocations

Equivalent to x86_64's lea rax, [rip + _sym], but split into two instructions.

4. Immediate Loading

mov xN, #imm16 — load 16-bit immediate

mov x0, #0x4142    ; x0 = 0x4142

movk xN, #imm16, lsl #shift — keep other bits, write 16-bit fragment

; Construct 64-bit value 0x0001_0002_0003_0004:
mov  x0, #0x0004           ; x0 = 0x0000_0000_0000_0004
movk x0, #0x0003, lsl #16  ; x0 = 0x0000_0000_0003_0004
movk x0, #0x0002, lsl #32  ; x0 = 0x0000_0002_0003_0004
movk x0, #0x0001, lsl #48  ; x0 = 0x0001_0002_0003_0004

Why this is the core of Data2TextPass: constant data (like string "hello\n") cannot go in .data (would produce relocations), so Data2TextPass converts them into mov/movk sequences stored on the stack.

5. Memory Access

ldr / str — load/store

ldr x0, [sp, #16]    ; x0 = *(sp + 16), 64-bit load
str x0, [sp, #16]    ; *(sp + 16) = x0, 64-bit store
ldrb w0, [x1]        ; w0 = *(uint8_t*)x1, zero-extended to 32-bit
strb w0, [x1, #3]    ; *(uint8_t*)(x1 + 3) = low 8 bits of w0

ldp / stp — load/store register pair

stp x29, x30, [sp, #-16]!  ; sp -= 16 first, then store x29 and x30
                            ; pre-index notation ("!" means writeback)
ldp x29, x30, [sp], #16    ; load x29 and x30, then sp += 16
                            ; post-index notation

Stack frame example

; Prologue
stp x29, x30, [sp, #-16]!    ; save frame pointer and return address
mov x29, sp                   ; establish frame pointer

; ... function body ...

; Epilogue
ldp x29, x30, [sp], #16      ; restore frame pointer and return address
ret                           ; return

6. Arithmetic and Logic

add  x0, x1, x2        ; x0 = x1 + x2
sub  x0, x1, #42       ; x0 = x1 - 42
and  x0, x1, #0xFF     ; x0 = x1 & 0xFF
orr  x0, x1, x2        ; x0 = x1 | x2
eor  x0, x1, x2        ; x0 = x1 ^ x2
lsr  x0, x1, #1        ; x0 = x1 >> 1 (logical right shift)
lsl  x0, x1, #3        ; x0 = x1 << 3

7. Compare and Conditional Branch

cmp x0, #0              ; compare x0 with 0 (set condition codes)
b.eq .Lzero             ; branch if equal
b.ne .Lnonzero          ; branch if not equal
b.lt .Lnegative         ; branch if less than (signed)
b.gt .Lpositive         ; branch if greater than (signed)

; Conditional select
csel x0, x1, x2, eq    ; x0 = (eq) ? x1 : x2

8. Typical Instruction Sequences Generated by This Project

Pure computation add(3, 4) → 7

_main:
    add  w0, w0, w1     ; w0 = 3 + 4 = 7
    ret                 ; return 7

Recursive Fibonacci

; ZeroRelocPass forces always_inline → compiler unrolls recursion into loop
_main:
    mov  w8, #0         ; fib[0] = 0
    mov  w9, #1         ; fib[1] = 1
.Lloop:
    cmp  w0, #1
    b.le .Ldone
    add  w10, w8, w9    ; fib[i] = fib[i-1] + fib[i-2]
    mov  w8, w9
    mov  w9, w10
    sub  w0, w0, #1
    b    .Lloop
.Ldone:
    mov  w0, w9
    ret

String inlining to stack (Data2TextPass)

; After Data2TextPass + backend:
_main:
    mov  w8, #0x4241    ; w8 = 'A' | ('B' << 8) = 0x4241 (little-endian)
    strh w8, [sp, #-4]! ; store to stack (2 bytes + padding)
    mov  w9, #0         ; store '\0'
    strb w9, [sp, #2]
    ldrb w0, [sp]       ; load 'A' = 65
    ldrb w1, [sp, #1]   ; load 'B' = 66
    add  w0, w0, w1     ; 65 + 66 = 131
    add  sp, sp, #4
    ret

Syscall (SyscallStubPass — direct svc)

Darwin arm64 BSD syscall ABI:

  • x16 = syscall number
  • x0..x7 = arguments
  • svc #0x80 triggers trap
  • Return value in x0, error sets Carry flag
_main:
    sub  sp, sp, #16
    ; Construct "hi\n" on stack (Data2TextPass style)
    mov  w8, #0x6968         ; 'hi' little-endian
    strh w8, [sp, #12]
    mov  w9, #0x0a           ; '\n'
    strb w9, [sp, #14]

    ; write(1, &msg, 3) — direct svc #0x80
    mov  x16, #4             ; SYS_write
    mov  x0,  #1             ; fd   = 1
    add  x1,  sp, #12        ; buf  = &msg
    mov  x2,  #3             ; n    = 3
    svc  #0x80               ; ← no bl/blr, no import, no relocation

    ; exit(0)
    mov  x16, #1             ; SYS_exit
    mov  x0,  #0             ; status
    svc  #0x80

    add  sp, sp, #16
    ret

The entire instruction stream is 100% within __TEXT,__text. The .o relocation table is empty except for intra-section branches — this is "true dyncode".

9. Key Summary

Concept x86_64 equivalent ARM64 DynCode notes
Function call call rel32 bl imm26 Intra-section: extractor patches BRANCH26; cross-section: use blr xN
Address load lea rax, [rip+sym] adrp+add Intra-section: extractor patches PAGE21/PAGEOFF12; cross-section: rejected
64-bit immediate mov rax, imm64 mov+movk ×4 No relocation, core of Data2TextPass
Prologue push rbp; mov rbp,rsp stp x29,x30,[sp,#-16]! Single instruction saves register pair
Return ret ret Equivalent to br lr
Syscall syscall svc #0x80 Darwin BSD: x16 = nr, x0~x7 = args