← alle beiträge

Einstieg in Assembly

/ Aug. 2023, assembly, low-level

Vor einer Weile wurde ich neugierig. So richtig. Wie funktionieren Computer eigentlich, ganz unten? Die Antwort hieß Assembly. Statt mir ein 500-Seiten-Handbuch reinzuziehen, habe ich eine kleine Sammlung simpler Programme gebaut, von denen mir jedes ein anderes Konzept beigebracht hat. Daraus wurde BergerAPI/asm, und dieser Beitrag stellt dir Assembly vor. Keine Angst, wir fangen sachte an.

Voraussetzungen

Bevor du Assembly schreibst, brauchst du (einen Teil von) diesem Werkzeug. Hier geht’s um NASM (Netwide Assembler) auf x86-64-Linux.

  1. NASM (Netwide Assembler)

    sudo apt-get install nasm
    
  2. GNU Binutils (bringt den Linker ld und objdump mit)

    sudo apt-get install binutils
    
  3. GNU Make (auf den meisten Linuxen eh schon da)

    sudo apt-get install build-essential
    
  4. GDB (optional, aber wärmstens empfohlen)

    sudo apt-get install gdb
    
  5. gcc (optional, falls du später Assembly mit C mischen willst)

    sudo apt-get install gcc
    

Die Grundlagen

Programmaufbau

Code liegt in Sektionen:

  • section .data für initialisierte globale und statische Daten (so was wie char *msg = "hello";)
  • section .text für die ausführbaren Anweisungen
  • section .bss für nicht initialisierte globale und statische Daten

Dazu kommen Labels: Namen für Speicheradressen, mit denen du deinen Code gliederst. Für den Programmeinstieg, für Funktionen oder für Stellen, zu denen du springen oder die du aufrufen willst.

_start:
    ; instructions

loop:
    ; instructions

Anweisungen sagen der CPU, was zu tun ist. Sie sehen meist so aus (Dummy-Code, keine Sorge):

mov rax, 1
push ebx
ret

Register

Register speichern Daten und lassen sich mit Anweisungen wie mov und add bearbeiten. Reinschreiben, rauslesen. Mehr ist es im Kern nicht. Es gibt viele davon; hier die wichtigsten:

RegisterZweck
raxRückgabewert, Syscall-Nummer, Akkumulator
rcx4. Argument einer Funktion; Schleifenzähler
rdx3. Argument einer Funktion
rsi2. Argument einer Funktion
rdi1. Argument einer Funktion
rspStackpointer (Oberkante des Stacks)
r8-r15Allzweck

Der Stack

Der Stack ist ein Speicherbereich für temporäres Zeug: Funktionsparameter, lokale Variablen, so was. Er wächst nach unten: Wer Daten drauflegt (push), sieht den Stackpointer (RSP) sinken.

Mit push und pop hantierst du auf dem Stack herum:

  • push rax legt ein Element oben drauf und verringert den Stackpointer um die Größe der Daten
  • pop rbx nimmt das oberste Element runter und schiebt es ins angegebene Register. Der Stackpointer wächst entsprechend wieder.

Die .data-Sektion

Mit section .data legst du initialisierte, beschreibbare globale Daten in dein Binary.

Achtung: Große, nicht initialisierte Puffer gehören nach .bss (resb/resq benutzen). Hält das Binary schlank.

section .data

Grundlegende Direktiven

  • db definiert Byte(s) (gut für Strings und rohe Bytes)
  • dw definiert 2‑Byte-Wörter
  • dd definiert 4‑Byte-Doppelwörter
  • dq definiert 8‑Byte-Quadwörter (gut für 64‑Bit-Zahlen/Pointer)
  • $ ist die aktuelle Assemblierungsadresse
  • $ - label rechnet die Bytes vom angegebenen Label bis zur aktuellen Adresse aus
  • times n db 0 wiederholt einen Wert n‑mal

Strings und Längen:

msg:    db "Hello, world!", 0x0a     ; String mit Newline am Ende
len:    equ $ - msg                   ; Länge wird zur Assemble-Zeit berechnet

Zahlenwerte (Little-Endian beachten):

val32:  dd 0x11223344   ; liegt als 44 33 22 11 im Speicher
val64:  dq 0x1122334455667788

Arrays:

arr:    dq 1, 2, 3, 4    ; Array aus 64-Bit-Integers
zeros:  times 8 db 0     ; 8 Nullbytes

Erste Schritte mit Hello World

Dieses Beispiel gibt „Hello World“ auf der Konsole aus und liefert Exit-Code 0 zurück. Also: alles gut.

section .data
    msg db "Hello, World!", 0x0a    ; Byte-Array mit Text "Hello, World!" plus Newline (0x0a)
    len equ $ - msg                 ; ergibt 14 (Länge von "Hello, World!")

section .text
    global _start

_start:
    ; write-Syscall: rax=1, rdi=fd, rsi=buf, rdx=count
    mov rax, 1              ; Syscall write
    mov rdi, 1              ; Dateideskriptor (stdout)
    mov rsi, msg            ; Pointer auf die Nachricht
    mov rdx, len            ; Nachrichtenlänge
    syscall                 ; write(stdout, message, message_length)

    ; exit-Syscall: rax=60, rdi=code
    mov rax, 60             ; Syscall exit
    mov rdi, 0              ; Exit-Code 0 (Erfolg)
    syscall