|
|
| (74 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) |
| Zeile 1: |
Zeile 1: |
| | <!-- /satyria.de/source/assem/ARM64Assembler --> |
| | |
| == Grundlagen == | | == Grundlagen == |
| Der ARM-Prozessor ist ein sogenannter RISC-Computer (Reduced Instruction Set Computer). Das Designprinzip von RISC-Prozessoren führt zu einem kleineren und übersichtlicheren Befehlssatz, was das Erlernen von Assembler theoretisch einfacher macht. In dieser Einführung werden wir die grundlegenden Konzepte der ARM64-Bit Assembler Programmierung verständlich und praxisorientiert erklären, sodass sie auch für Anfänger zugänglich ist. | | Der ARM-Prozessor ist ein sogenannter RISC-Computer (Reduced Instruction Set Computer). Das Designprinzip von RISC-Prozessoren führt zu einem kleineren und übersichtlicheren Befehlssatz, was das Erlernen von Assembler theoretisch einfacher macht. In dieser Einführung werden wir die grundlegenden Konzepte der ARM64-Bit Assembler Programmierung verständlich und praxisorientiert erklären, sodass sie auch für Anfänger zugänglich ist. |
| Zeile 15: |
Zeile 17: |
| ** [[Programmierumgebung unter Linux erstellen und testen#Testen der Umgebung durch Kompilieren und Ausführen von Assemblerprogrammen|Testen der Umgebung durch Kompilieren und Ausführen von Assemblerprogrammen]] | | ** [[Programmierumgebung unter Linux erstellen und testen#Testen der Umgebung durch Kompilieren und Ausführen von Assemblerprogrammen|Testen der Umgebung durch Kompilieren und Ausführen von Assemblerprogrammen]] |
| * [[Tools, die zur Programmierung benötigt werden]] | | * [[Tools, die zur Programmierung benötigt werden]] |
| ** Allgemeine Einführung in gängige Tools: Assembler (as), Linker (ld), Compiler (gcc) | | ** [[Tools, die zur Programmierung benötigt werden#Assembler (as)|Allgemeine Einführung in gängige Tools: Assembler (as), Linker (ld), Compiler (gcc) und make]] |
| ** Verwendung von Debugging-Tools wie gdb | | ** [[Tools, die zur Programmierung benötigt werden#Debugging-Tool (gdb)|Verwendung von Debugging-Tools wie gdb]] |
| ** IDEs und Texteditoren, die Assembler unterstützen | | ** [[Tools, die zur Programmierung benötigt werden#IDEs und Texteditoren, die Assembler unterstützen|IDEs und Texteditoren, die Assembler unterstützen]] |
| * Register und Speicher
| | * [[Allgemeines zu Zahlen (Dezimal, Binär, Hexadezimal)]] |
| ** Vorstellung der ARM64-Register (x0 - x30, SP, PC) | | * [[Das erste Programm "Hello World"]] |
| ** Unterschiede zwischen allgemeinen und speziellen Registern | | * [[Register und Speicher]] |
| ** Speicherorganisation und -adressierung | | * [[Laden und Speichern von Werten]] |
| * Laden und Speichern von Werten | | ** [[Laden und Speichern von Werten#Laden von konstanten Werten in Register|Laden von konstanten und variablen Werten in Register]] |
| ** Laden von konstanten und variablen Werten in Register | | ** [[Laden und Speichern von Werten#Speichern von Registerwerten im Speicher|Speichern von Registerwerten im Speicher]] |
| ** Speichern von Registerwerten im Speicher | | ** [[Laden und Speichern von Werten#Laden und Speichern: Die Befehle ldr und str|Verwendung der Befehle ldr und str]] |
| ** Verwendung der Befehle ldr und str | | * [[Addieren und Subtrahieren]] |
| * Addieren und Subtrahieren | | ** [[Addieren und Subtrahieren#Einfache arithmetische Operationen: add und sub|Einfache arithmetische Operationen: add, sub]] |
| ** Einfache arithmetische Operationen: add, sub | | ** [[Addieren und Subtrahieren#Umgang mit Überlauf und Unterlauf|Umgang mit Überlauf und Unterlauf]] |
| ** Umgang mit Überlauf und Unterlauf | | ** [[Addieren und Subtrahieren#Arbeiten mit mehreren Registern und direkten Werten|Arbeiten mit mehreren Registern und direkten Werten]] |
| ** Arbeiten mit mehreren Registern und direkten Werten | | ** [[Addieren und Subtrahieren#Umgang mit Überlauf und Unterlauf bei 128-Bit-Arithmetik in ARM64-Assembler|Umgang mit Überlauf und Unterlauf bei 128-Bit-Arithmetik in ARM64-Assembler]] |
| * Multiplizieren, Dividieren und Akkumulation
| | * [[Multiplizieren, Dividieren und Akkumulation]] |
| ** Multiplizieren und Dividieren von Werten: mul, div | | ** [[Multiplizieren, Dividieren und Akkumulation|Multiplizieren und Dividieren von Werten: mul, div]] |
| ** Erweitere Operationen wie akkumuliertes Multiplizieren: mla, mls | | ** [[Multiplizieren, Dividieren und Akkumulation#Akkumulation|Erweitere Operationen wie akkumuliertes Multiplizieren: mla, mls]] |
| * Programmablauf steuern | | * [[Shiften und Rotation]] |
| ** Bedingte und unbedingte Sprungbefehle: b, bl, cbz, cbnz | | * [[Logische Operatoren]] |
| ** Schleifen und Verzweigungen | | * [[Sprungadressen (Labels)]] |
| ** Vergleichsoperationen und bedingte Ausführung
| | * [[Programmablauf steuern]] |
| * Funktionen und Stack | | ** [[Programmablauf steuern|Bedingte und unbedingte Sprungbefehle: b, bl, cbz, cbnz]] |
| ** Funktion aufrufen und Rückkehr | | ** [[Programmablauf steuern#Schleifen|Schleifen und Verzweigungen]] |
| ** Funktionsparameter und Rückgabewerte | | ** [[Programmablauf steuern#Bedingter Sprung|Vergleichsoperationen und bedingte Ausführung]] |
| ** Verwendung des Stacks für lokale Variablen und Funktionsaufrufe | | * [[Funktionen und Stack]] |
| ** Regeln für das Aufrufen von Funktionen (AAPCS) | | ** [[Funktionen und Stack#Funktion aufrufen und Rückkehr|Funktion aufrufen und Rückkehr]] |
| * Systemaufrufe | | ** [[Funktionen und Stack#Funktionsparameter und Rückgabewerte|Funktionsparameter und Rückgabewerte]] |
| ** Einführung in Linux-Systemaufrufe | | ** [[Funktionen und Stack#Verwendung des Stacks für lokale Variablen und Funktionsaufrufe|Verwendung des Stacks für lokale Variablen und Funktionsaufrufe]] |
| ** Ausführung von grundlegenden Systemaufrufen wie exit, read, write, open, close | | ** [[Funktionen und Stack#Regeln für das Aufrufen von Funktionen (AAPCS)|Regeln für das Aufrufen von Funktionen (AAPCS)]] |
| ** Übergabe von Parametern und Entgegennahme von Rückgabewerten | | * [[Systemaufrufe]] |
| * GPIO Programmierung | | ** [[Systemaufrufe#Einführung in Linux-Systemaufrufe|Einführung in Linux-Systemaufrufe]] |
| ** Grundlagen der GPIO-Programmierung | | ** [[Systemaufrufe#Grundlegende Systemaufrufe|Ausführung von grundlegenden Systemaufrufen wie exit, read, write, open, close]] |
| ** Direkte Steuerung der GPIO-Pins | | ** [[Systemaufrufe#Übergabe von Parametern und Entgegennahme von Rückgabewerten|Übergabe von Parametern und Entgegennahme von Rückgabewerten]] |
| ** Besondere Register und Konfiguration | | ** [[Systemaufrufe#Verweis auf alle verfügbaren Linux-Systemaufrufe|Übersicht der Linux ARM64 Systemaufrufen]] |
| * Interaktion mit C | | <!--* [[GPIO Programmierung]] |
| ** C-Funktionen aus Assembler aufrufen | | ** [[GPIO Programmierung#Grundlagen der GPIO-Programmierung|Grundlagen der GPIO-Programmierung]] |
| *** Deklaration und Aufruf von C-Funktionen in Assembler
| | ** [[GPIO Programmierung#Direkte Steuerung der GPIO-Pins|Direkte Steuerung der GPIO-Pins]] |
| ** Assemblerfunktion in C aufrufen | | ** [[GPIO Programmierung#Besondere Register und Konfiguration|Besondere Register und Konfiguration]]--> |
| *** Definition und Deklaration von Assemblerfunktionen in C | | * [[Interaktion mit C]] |
| ** Inline-Assembler in C
| | ** [[Interaktion mit C#C-Funktionen aus Assembler aufrufen|C-Funktionen aus Assembler aufrufen]] |
| *** Verwendung von Inline-Assembler in C-Programmen für Leistungsoptimierung | | ** [[Interaktion mit C#Assemblerfunktionen in C aufrufen|Assemblerfunktion in C aufrufen]] |
| ** Bibliotheken verwenden
| | ** [[Interaktion mit C#Inline-Assembler in C|Inline-Assembler in C]] |
| *** Einbindung und Nutzung von C-Bibliotheken in Assembler | | ** [[Interaktion mit C#Bibliotheken verwenden|Bibliotheken verwenden]] |
| * Gleitkommaoperationen | | * [[Gleitkommaoperationen]] |
| ** FPU-Register | | ** [[Gleitkommaoperationen#FPU-Register: Überblick|FPU-Register]] |
| *** Überblick über FPU (Floating Point Unit) und ihre Register | | *** [[Gleitkommaoperationen#Registerüberblick|Überblick über FPU (Floating Point Unit) und ihre Register]] |
| ** Verwendung der Register in Funktionen | | ** [[Gleitkommaoperationen#Verwendung der FPU-Register in Funktionen|Verwendung der Register in Funktionen]] |
| *** Nutzung der FPU-Register in Assemblerfunktionen | | ** [[Gleitkommaoperationen#Hinweise zur Verwendung von FPU-Registern|Arbeiten mit FPU-Registern]] |
| ** Arbeiten mit FPU-Registern
| | *** [[Gleitkommaoperationen#Beispiele für die Verwendung|Ausführung von Gleitkommaoperationen: fadd, fsub, fmul, fdiv]] |
| *** Ausführung von Gleitkommaoperationen: fadd, fsub, fmul, fdiv | | ** [[Gleitkommaoperationen#Konvertierung von Gleitkommazahlen|Konvertierung von Gleitkommazahlen]] |
| ** Konvertierung von Gleitkommazahlen | | *** [[Gleitkommaoperationen#Konvertierung von Gleitkommazahlen|Umwandlung zwischen Ganzzahlen und Gleitkommazahlen]] |
| *** Umwandlung zwischen Ganzzahlen und Gleitkommazahlen | | ** [[Gleitkommaoperationen#Vergleich von Gleitkommazahlen und bedingte Sprungbefehle|Vergleichen]] |
| ** Vergleichen | | *** [[Gleitkommaoperationen#Vergleich von Gleitkommazahlen und bedingte Sprungbefehle|Vergleich von Gleitkommazahlen und bedingte Sprungbefehle]] |
| *** Vergleich von Gleitkommazahlen und bedingte Sprungbefehle | | * [[NEON Coprozessor]] |
| * NEON Coprozessor
| | ** [[NEON Coprozessor#Verwendung des NEON-Coprozessors im Raspberry Pi|Verwendung des NEON-Coprozessors im Raspberry Pi]] |
| ** Einführung in den NEON-Coprozessor
| | ** [[NEON Coprozessor#LANE-Prinzip: Überblick|LANE-Prinzip]] |
| ** SIMD (Single Instruction Multiple Data) Befehle | | ** [[NEON Coprozessor#Anwendung und Kontext im Raspberry Pi|Anwendung und Kontext im Raspberry Pi]] |
| ** Optimierung von Multimedia- und Signalverarbeitungsanwendungen | | <br> |
| | | * Anhang |
| | | ** [[Diassemblieren]] |
| | | ** [[Big vs. Little Endian]] |
| ---------------
| | ** [[Aliase]] |
| * Wir verwenden Linux | | ** [[Direktiven|Assembler Direktiven (as/gcc)]] |
| * Grundzustand von Rasbpian ist ausreichend | | *** [[Direktiven#4. Daten ausrichten: .align|Daten ausrichten]] |
| | | ** [[Makros in Assembler]] |
| * Zahlen | | ** [[Übersicht der Linux ARM64 Systemaufrufen]] |
| -> Dezimal, Binär, Hexadezimal
| | *** [[Übersicht der Fehlercodes|Übersicht der Fehlercodes von Systemaufrufen]] |
| * CPU-Register | |
| -> Ein 64-Bit-Programm auf einem ARM-Prozessor im Benutzermodus hat Zugriff auf 31 Allzweckregister, einen Programmzähler (PC) und eine Kombination aus Nullregister/Stapelzeiger
| |
| * x0-x30 | |
| * SP, XZR | |
| * x30, LR | |
| * PC | |
| * w0-w30, wzr: sind x-Register, die die unteren 32-Bit verwenden.
| |
| -> Zusätzliche Register Gleitkommaoperationen, Neon-Coprozessor, später
| |
| | |
| * Aufbau von Data processing instructions: | |
| | 31 | 30 | 29 | 28-24 | 23-22 | 21 | 20-16 | 15-10 | 9-5 | 4-0 |
| |
| | Bits | Opcode | Set Condition Code | Opcode | Shift | 0 | Rm | imm6 | Rn | Rd |
| |
| -> Erklärung
| |
| | |
| * Memory | |
| -> Instruktionen 32-Bit, Register 64-Bit, Memory-Adresssierung 64-Bit -> wie lösen.
| |
| | |
| * Der GCC-Assembler | |
| -> Maschinencode in lesbare Form
| |
| -> Aufbau von Befehlen, Beispiel ldr, mov
| |
| | |
| * Erstes Programm: Hello World | |
| .global _start
| |
| _start:
| |
| mov x0,#1
| |
| ldr x1,=helloworld
| |
| mov x2,#13
| |
| mov x8,#64
| |
| svc 0
| |
| mov x0,#0
| |
| mov x8,#93
| |
| svc 0
| |
| .data
| |
| helloworld:
| |
| .ascii "Hello World!\n"
| |
| helloworldlen:
| |
| .word .-helloworld
| |
| | |
| | |
| as -o HelloWorld.o HelloWorld.s
| |
| ld -o HelloWorld HelloWorld.o
| |
| | |
| * Erklärung Code | |
| -> Kommentare
| |
| -> globales Symbol _start
| |
| -> Assembler-Befehle:
| |
| --> mov, ldr, svc 0
| |
| -> data
| |
| | |
| * Linux-Systemaufrufe | |
| -> Parameter in den Registern X0–X7
| |
| -> Rückgabe x0
| |
| -> Funktionsnummer in X8
| |
| | |
| * Diassemblieren | |
| -> objdump -s -d HellowWorld.o
| |
| | |
| == Laden und Addieren ==
| |
| * mov add
| |
| * negative Zahlen
| |
| -> Beispiel: 5 + -3
| |
| 3 in 1 byte is 0x03 or 0000 0011.
| |
| Inverting the bits is
| |
| 1111 1100
| |
| Add 1 to get
| |
| 1111 1101 = 0xFD
| |
| Now add
| |
| 5 + 0xFD = 0x102 = 2
| |
| * Big vs. Little Endian
| |
| -> Reihenfolge der Bytes im Speicher
| |
| -> ARM-Prozessor erlaubt beide Versionen
| |
| -> Linux verwendet Little Endian
| |
| * Shiften und Rotation
| |
| * Carry-Flag
| |
| • Logical shift left
| |
| • Logical shift right
| |
| • Arithmetic shift right
| |
| • Rotate right
| |
| * Laden von Registern
| |
| -> mov x0,x1; Ein Alias. Gleiche ist möglich mit add x0, xzr, x1, aber tatsächlich ist es orr x0,xzr,x1
| |
| -> Verwendung von Aliase, um den Code besser zu verstehen, Problem beim debuggen, objdump, da eventuell andere Aliases verwendet werden.
| |
| * mov
| |
| -> 1. MOVK XD, #imm16{, LSL #shift}
| |
| 2. MOV XD, #imm16{, LSL #shift}
| |
| 3. MOV XD, XS
| |
| 4. MOV XD, operand2
| |
| 5. MOVN XD, operand2
| |
| * add/adc
| |
| 1. ADD{S} Xd, Xs, Operand2
| |
| 2. ADC{S} Xd, Xs, Operand2
| |
| | |
| Beispiel:
| |
| .global _start
| |
| _start: MOVN W0, #2
| |
| ADD W0, W0, #1
| |
| MOV X8, #93 // Service command code 93
| |
| SVC 0
| |
| Rückgabe in w0 -> Kann mit "echo $?" ausgegeben werden.
| |
| | |
| * Add with Carry
| |
| -> Übertrag.
| |
| -> Beispiel:
| |
| adds x1,x3,x5 //addiert untere 64-Bit
| |
| adc x0,x2,x4 //addiert obere 64-Bit mit Übertrag von zuvor
| |
| | |
| * SUB/SBC
| |
| == Tools ==
| |
| * GNU MAKE
| |
| -> make -B: erstellt neue Kompilierungen
| |
| | |
| * GDB
| |
| break (b) line Set breakpoint at line
| |
| run (r) Run the program
| |
| step (s) Single step program
| |
| continue (c) Continue running the program
| |
| quit (q or control-d) Exit gdb
| |
| control-c Interrupt the running program
| |
| info registers (i r) Print out the registers
| |
| info break Print out the breakpoints
| |
| delete n Delete breakpoint n
| |
| x /Nuf expression Show contents of memory
| |
| | |
| * Gross-Compiling
| |
| * Emulation
| |
| | |
| == Programmablauf steuern ==
| |
| * Bedingungsloser Sprung
| |
| -> b label
| |
| * Bedingungsflags
| |
| negativ: N gesetzt, wenn Ergebnis negativ ist
| |
| zero: z gesetzt wenn Ergebnis null ist
| |
| carry: c gesetzt, wenn es einen Überlauf gab. add -> wenn größer als Zahlenbereich (Überlauf), Subtraktion gesetzt, wenn Ergebnis keine Ausleihe. bei Verschieben letzte Bit herausgeschoben.
| |
| overflow: o gesetzt bei addition und subtraktion, wenn Ergebnis größer oder gleich 2^31, oder kleiner -2^31
| |
| | |
| Flags werden im NZCV-Systemregister gespeichert
| |
| | |
| * Verzweigung bei Bedingung
| |
| -> b.{condition}
| |
| {condition} Flags Meaning
| |
| EQ Z set Equal
| |
| NE Z clear Not equal
| |
| CS or HS C set Higher or same (unsigned >=)
| |
| CC or LO C clear Lower (unsigned <)
| |
| MI N set Negative
| |
| PL N clear Positive or zero
| |
| VS V set Overflow
| |
| VC V clear No overflow
| |
| HI C set and Z clear Higher (unsigned >)
| |
| LS C clear and Z set Lower or same (unsigned <=)
| |
| GE N and V the same Signed >=
| |
| LT N and V differ Signed <
| |
| GT Z clear, N and V the same Signed >
| |
| LE Z set, N and V differ Signed <=
| |
| AL Any Always (same as no suffix)
| |
| | |
| * CMP Xn,Operand2
| |
| -> subtrahiert Operand2 von Xn
| |
| | |
| * Schleifen
| |
| ** FOR NEXT
| |
| for i = 1 to 10
| |
| Mache etwas
| |
| next i
| |
| | |
| In Assembler:
| |
| mov w2,#1 //i=1
| |
| loop:
| |
| //Mache etwas
| |
| add w2,w2,#1 // i=i+1
| |
| cmp w2,#10 // if i<10
| |
| b.le loop // then goto loop
| |
| ** WHILE
| |
| while x < 10
| |
| Mache etwas
| |
| end while
| |
| | |
| In Assembler:
| |
| // w2 zuvor initialisiert -> x
| |
| loop:
| |
| cmp w2,#10
| |
| b.ge loopend
| |
| // Mache etwas
| |
| b loop
| |
| loopend:
| |
| | |
| ** if/then/else
| |
| if x < 10 then
| |
| If ist okay
| |
| else
| |
| else kommt zum zuge
| |
| end if
| |
| | |
| In Assembler:
| |
| // wert x in w2
| |
| cmp w2,#10
| |
| b.ge label_else
| |
| //If ist okay
| |
| b label_EndIf
| |
| label_else:
| |
| //else kommt zum zuge
| |
| label_EndIf:
| |
| | |
| ** do/until
| |
| do
| |
| Mache etwas
| |
| until a==0
| |
| | |
| In Assembler:
| |
| mov w0,#1
| |
| loop:
| |
| //mache etwas
| |
| cmp w0,#0
| |
| b.ne loop
| |
| | |
| * Logische Operatoren
| |
| and{s} Xd, Xs, Operand2
| |
| eor{s} Xd, Xs, Operand2
| |
| orr{s} Xd, Xs, Operand2
| |
| bic{s} Xd, Xs, Operand2
| |
| | |
| | and | eor | orr | bic |
| |
| Xs | 1100 | 1100 | 1100 | 1100 |
| |
| Operand2 | 1010 | 1010 | 1010 | 1010 |
| |
| Ergebnis | 1000 | 0110 | 1110 | 0100 |
| |
| | |
| * CMN Xn, Operand2
| |
| -> addiert Operand2 von Xn
| |
| * TST Xn, Operand2
| |
| -> Bitweise AND von Operand2 und Xn
| |
| | |
| == Speicher ==
| |
| * Speicheradressen 64 Bit lang
| |
| * .data
| |
| -> Dezimalzahl: Beginnt mit 1-9 und enthält 0-9
| |
| -> Oktalzahl: Beginnt mit 0 und enthält 0-7
| |
| -> Binärzahl: Beginnt mit 0b und enthält 0-1
| |
| -> Hexadezimal: Beginnt mit 0x und enthält 0-f
| |
| -> Gleitkommawerte: Beginnt mit 0f oder 0e und enthält die Gleitkomma-Zahl
| |
| -> Präfix: "-" nimmt das Zweierkomplement, "~" nimmt das Einerkompliment
| |
| .byte -0xa3, -22, ~0b11010010
| |
| | |
| Directive Beschreibung
| |
| .ascii Eine Zeichenfolge in doppelten Anführungszeichen
| |
| .asciz Eine mit 0 Bytes abgeschlossene ASCII-Zeichenfolge
| |
| .byte 1-byte Ganzzahl
| |
| .double Gleitkommawerte mit doppelter Genauigkeit
| |
| .float Gleitkommawerte
| |
| .octa 16-byte Ganzzahl
| |
| .quad 8-byte Ganzzahl
| |
| .short 2-byte Ganzzahl
| |
| .word 4-byte Ganzzahl
| |
| | |
| * Hilfreiche Direktiven:
| |
| .fill Anzahl, Größe, Inhalt: Erzeugt einen Speicher mit dem "Inhalt" der "Größe" und "Anzahl".
| |
| .rept Anzahl ... .endr: Wiederholt "Anzahl" den Inhalt von "..."
| |
| | |
| * ASCII-Strings
| |
| Escape Description
| |
| \b Backspace (ASCII code 8)
| |
| \f Seitenvorschub (ASCII code 12)
| |
| \n Neue Zeile (ASCII code 10)
| |
| \r Return (ASCII code 13)
| |
| \t Tabulator (ASCII code 9)
| |
| \ddd Ein Okctaler ASCII code (ex \123)
| |
| \xdd Ein Hexadezimaler ASCII code (ex \x4F)
| |
| \\ Das “\” Zeichen
| |
| \” Das Anführungszeichen
| |
| | |
| * Daten ausrichten
| |
| .align Ausrichtung: Setzt den nächsten Wert auf ein "Ausrichtung" definierten Speicher.
| |
| Beispiel:
| |
| .byte 0xef
| |
| .align 4 //Ausrichtung auf Wortbreite
| |
| .word 0x26ef43de
| |
| * ldr
| |
| * Relative Adressierung am PC
| |
| -> ldr PC-relativ Adresse 19-Bit breit (ca. +/-1MB)
| |
| Beispiel:
| |
| ldr x1,=hello
| |
| ...
| |
| .data
| |
| hello:
| |
| .ascii "Hallo"
| |
| Die Adresse zum Label "hello" wird relativ zum PC ermittelt.
| |
| -> TRICK:
| |
| ldr x1,=0x1234567890abcdef //Direkte 64-Bit Zuweisung
| |
| Der Assembler wandelt diese Anweisung:
| |
| ldr x1,#8
| |
| .quad 0x1234567890abcdef
| |
| | |
| * ldr{Typ} xt,[xa], wobei für Typ steht:
| |
| B Unsigned byte
| |
| SB Signed byte
| |
| H Unsigned halfword (16 bits)
| |
| SH Signed halfword (16 bits)
| |
| SW Signed word
| |
| | |
| Beispiel:
| |
| ldr x0,=MeineZahl //läd die Adresse meiner Variablen
| |
| ldr x0,[x0] //läd dann den Inhalt der Variablen
| |
| .data
| |
| MeineZahl: .quad 0x1234567fe
| |
| | |
| *str{Typ} xt,[xa]
| |
| | |
| *Indexsierung (Arrays)
| |
| PseudoCode:
| |
| dim a[10] as word
| |
| a[5] = 10 //setze das 5 Element mit 10
| |
| x = a[8] //Schreibe das 8 Element nach x
| |
| for i = 1 to 10
| |
| a[i] = i *8
| |
| next
| |
| | |
| In Assembler:
| |
| ldr x0,=MeinArray //Lade die Adresse von MeinArray nach x0
| |
| mov w1,#10 //Speichere 10 in w1 ab
| |
| str w1,[x0,#(4*4)] //Speichere w1 in das 5 Element (Begin ist 0) (Word = 4 Byte)
| |
| ldr w1,[x0,#(7*4)] //lade das 8 Element nach w1
| |
| mov w2,#1 //i=1
| |
| forloop:
| |
| lsl w4,w2,#3 // i * 8
| |
| str w4,[x0, w2, lsl #2] // speichere der Wert aus w4 nach Basisadresse x0 mit index aus w2, der um 4 Multipliziert wird.
| |
| add w2,w2,#1 // i=i+1
| |
| cmp w2,#10 // if i<10
| |
| b.le forloop
| |
| * Der Index kann auch negativ sein.
| |
| | |
| * Post-indexierte Adressierung
| |
| -> ldr x1,[x2], #4 //Holt den Wert aus x2 und erhöht anschließen x2 um 4
| |
| | |
| * ldp, stp -> 128-Bit
| |
| ldr x1,=GrosseZahl
| |
| ldp x2,x3,[x1]
| |
| stp x2,x3,[x1]
| |
| .data
| |
| GrosseZahl:
| |
| .octa 0x12345678901234567890123456789012
| |
| | |
| == Funktionen und Stack ==
| |
| * Stack: Unter Linux 8MB groß
| |
| Speichern auf den Stack:
| |
| str x0,[sp,#-16]!
| |
| -> Warum 16Bytes? SP muss 16Bytes ausgerichtet sein.
| |
| Laden vom Stack:
| |
| ldr x0,[sp],#16
| |
| Zwei Register gleichzeitig:
| |
| stp x0,x1,[sp,#-16]!
| |
| ldp x0,x1,[sp],#16
| |
| | |
| * bl
| |
| -> Linkregister - x30, Zeiger auf nächsten Befehl
| |
| Zurück mit ret (verwendet x30 als Rücksprungadresse)
| |
| | |
| Beispiel für eine Funktion:
| |
| ...
| |
| bl MeineFunktion
| |
| ...
| |
| ------
| |
| MeineFunktion:
| |
| ...
| |
| ret
| |
| | |
| * Nur ein LR, Problem wenn in der Funktion eine weitere aufgerufen wird.
| |
| -> Lösung, der Stapel
| |
| In der Funktion, LR sichern und vor beendigung der Funktion LR wieder herstellen
| |
| MeineFunktion:
| |
| str lr,[sp,#-16]!
| |
| ...
| |
| ldr lr,[sp],#16
| |
| ret
| |
| | |
| * Parameter übergabe und zurück
| |
| Übergabe in x0-x7, wenn mehr über stack
| |
| Rückgabe in x0. Möglich ist auch eine Ganzzahl bis 128-Bit, dann x0 und x1
| |
| Bei Rückgabe mehrere Daten werden Speicheradressen verwendet, die auch in x0 übergeben wird, in denn die Daten stehen.
| |
| | |
| * Verwaltung der Register
| |
| Regeln, wer verantwortlich ist (Aufzurufende Funktion, oder die aufgerufene Funktion, für die Register
| |
| - x0-x7 Funktionsparameter, kann beliebig geändert werden.
| |
| - x0-x18 Können frei verwendet werden, diese werden nicht gesichert.
| |
| - x19-x30 Müssen gesichert werden bevor diese verwendet werden. Dies erfolgt über Stack
| |
| | |
| * Frame Pointer
| |
| C-Programmierkonvention -> x29
| |
| Verwendung zum Beispiel für Lokale Variablen
| |
| -> Speicherbereich im Stack freigeben, und FP darauf setzen:
| |
| Subtraktion, um dem Platzbedarf für die Variablen zu erstellen (Achtung auf 16 ausgerichtet)
| |
| sub sp,sp,#16
| |
| Damit hat man Platz für 4 32-Bit Integers
| |
| str w0,[sp]
| |
| str w1,[sp,#4]
| |
| str w2,[sp,#8]
| |
| str w3,[sp,#12]
| |
| Bevor die Funktion beendet wird, muss der Stack wieder angepasst werden:
| |
| add sp,sp,#16
| |
| | |
| Soweit so gut. Aber wenn wir nun einige Dinge mit dem Stack durchführen, verlieren wir irgendwann den Überblick. Hier kommt dann unser FramePointer zum Zuge. Wir setzen einfach den FramePointer auf die Adresse des Stacks, an die Position, wo wir die Variablen abgelegt haben:
| |
| sub fp,sp,#16
| |
| sub sp,sp,#16
| |
| oder:
| |
| sub sp,sp,#16
| |
| mov fp,sp
| |
| | |
| Nun kann eindeutig auf die Variablen zugegriffen werden:
| |
| str w0,[fp]
| |
| str w1,[fp,#4]
| |
| str w2,[fp,#8]
| |
| str w3,[fp,#12]
| |
| | |
| Achtung! Das Register x29 (fp) ist ein Register, welches wir bei Funktionsstart auf den Stack sichern müssen und vor dem verlassen der Funktion wieder herstellen müssen.
| |
| | |
| * .equ
| |
| -> Definiert Werte einer lesbaren Schreibweise zu. Allerdings nur mit Zahlen möglich
| |
| | |
| * Makros
| |
| .macro Name übergabe1,übergabe2,...
| |
| Beispiel:
| |
| .macro speicher wert1,wert2
| |
| ldr x0,=\wert1
| |
| ldr x1,=\wert2
| |
| .endm
| |
| | |
| .global _start
| |
| _start:
| |
| speicher buffer1,buffer2
| |
| ...
| |
| .data
| |
| buffer1: .fill 255,1,0
| |
| buffer2: .fill 255,1,0
| |
| | |
| Beim Aufruf "speicher" wird an dieser Stelle des Codes einfach das Makro eingesetzt. Das definieren des Makros erzeugt keinen Code!
| |
| | |
| * .include
| |
| Ersetzt an dieser stelle, den Code, welcher über dem includebefehl geladen wird:
| |
| .include "ZusätzlicherSource.s"
| |
| | |
| * Labels in Makros
| |
| Problematisch wird es, wenn in einem Makro Labels definiert wurden. Wird das Makro zweimal verwendet, so wird der Assembler melden, dass die Labels doppelt vergeben wurden. Dies ist nicht erlaubt. Alternativ, was der Assembler erlaubt, sind Zahlen als Label, die auch mehrfach verwendet werden können. Um auf die richtigen Labels zu zeigen, wird bei der Sprunganweisung einfach der Zahl "f" angehängt, wenn das Label nach diesem Sprung angesprochen werden soll. Ein "b", wenn das Label zuvor angesprochen werden soll.
| |
| | |
| * Praktische Makros
| |
| .macro push1 register
| |
| str \register,[sp,#-16]!
| |
| .endm
| |
| .macro push2 register1,register2
| |
| stp \register1,\register2,[sp,#-16]!
| |
| .endm
| |
| .macro pop1 register
| |
| ldr \register,[sp],#16
| |
| .endm
| |
| .macro pop2 register1,register2
| |
| ldp \register1,\register2,[sp],#16
| |
| .endm
| |
| | |
| == Linux Systemaufrufe ==
| |
| https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
| |
| https://github.com/ilbers/linux/blob/master/arch/sh/include/uapi/asm/unistd_64.h
| |
| | |
| x0–x7: Parameterübergabe
| |
| x8: SystemCallNummer
| |
| svc 0 -> Aufruf des Systemcalls
| |
| x0: Ergebniss
| |
| | |
| Wenn x0 negativ -> negieren -> dann Fehler:
| |
| https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h
| |
| https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
| |
| | |
| In der Parameterübergabe erwarten viele SystemCalls eine Struktur. Ein einfacher SystemCall wie nanosleep wird als C-Code wie folgt aufgerufen:
| |
| int nanosleep(const struct timespec *req, struct timespec *rem);
| |
| Dieser Aufruf erwartet jeweils eine Struktur "timespec", die in x0 und x1 übergeben wird.
| |
| Diese Struktur ist wie folgt definiert:
| |
| struct timespec {
| |
| time_t tv_sec; /* seconds */
| |
| long tv_nsec; /* nanoseconds */
| |
| };
| |
| Nach Assembler:
| |
| timespec:
| |
| timespec_tv_sec: .dword 0
| |
| timespec_tv_nsec: .dword 100000000
| |
| Um es der Funktion zu übergeben:
| |
| ldr x0,=timespec
| |
| ldr x1,=timespec
| |
| | |
| * Arbeiten mit Dateien
| |
| https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h
| |
| Datei öffnen:
| |
| mov x0,#-100
| |
| ldr x1,=Dateiname
| |
| mov x2,#flags //O_RDONLY, O_WRONLY, =O_CREAT
| |
| mov x3,#666
| |
| mov x8,#323 //#define __NR_openat 323
| |
| svc 0
| |
| Rückgabe in x0 ->
| |
| adds x10,XZR,x0 //file descriptor nach x10
| |
| bpl ok //Wenn positiv, dann hat es geklappt
| |
| Fehler...
| |
| | |
| Datei schließen:
| |
| mov x0,x10 //Beispiel x10 für file descriptor
| |
| mov x8,#118 //#define __NR_fsync 118
| |
| svc 0
| |
| mov x0,x10 //Beispiel x10 für file descriptor
| |
| mov x8,#6 //#define __NR_close 6
| |
| svc 0
| |
| | |
| Lesen aus einer Datei:
| |
| mov x0,x10 //Beispiel x10 für file descriptor
| |
| ldr x1,=buffer //Hierhin sollen die Daten geladen werden
| |
| mov x2,#256 //Max Anzahl an Bytes, die geladen werden sollen (Beispiel: 256Bytes)
| |
| mov x8,#3 //#define __NR_read 3
| |
| svc 0
| |
| | |
| Speichern in eine Datei:
| |
| mov x0,x10 //Beispiel x10 für file descriptor
| |
| ldr x1,=buffer //Von hier sollen die Daten gespeichert werden
| |
| mov x2,#256 //Anzahl an Bytes, die gespeichert werden sollen (Beispiel: 256Bytes)
| |
| mov x8,#4 //#define __NR_write 4
| |
| svc 0
| |
| | |
| Ein Beispiel in ARM64-Assembler für Linux, das zeigt, wie Sie eine Datei öffnen, die Daten lesen, bearbeiten und anschließend die bearbeiteten Daten in einer anderen Datei speichern können. Ich werde dafür Systemaufrufe (syscalls) verwenden.
| |
| | |
| Überblick der Schritte:
| |
| Datei öffnen (open).
| |
| Daten aus der Datei lesen (read).
| |
| Daten bearbeiten (In diesem Beispiel ändern wir die Daten nicht, sie könnten aber leicht modifiziert werden).
| |
| Neue Datei erstellen und öffnen (open).
| |
| Daten in die neue Datei schreiben (write).
| |
| Beide Dateien schließen (close).
| |
| Vollständiges Beispiel in ARM64-Assembler:
| |
| .section .data
| |
| filename: .asciz "input.txt"
| |
| newfilename:.asciz "output.txt"
| |
| buffer: .space 1024 // Pufferspeicher zum Lesen der Datei
| |
| | |
| .section .bss
| |
| .lcomm result, 4 // Speicher für den Rückgabewert von syscalls
| |
| | |
| .section .text
| |
| .global _start
| |
| _start:
| |
| // Datei öffnen (input.txt)
| |
| mov x0, 0 // stdin als File Descriptor (0)
| |
| ldr x1, =filename // filename in x1 laden
| |
| mov x2, 0 // O_RDONLY (0)
| |
| mov x8, 2 // Syscall-Nummer für 'open' ist 2
| |
| svc 0 // Systemaufruf
| |
| | |
| mov x19, x0 // x0 enthält den File Descriptor
| |
| ldr x1, =buffer // Speicheradresse des Puffers in x1 laden
| |
| mov x2, 1024 // Maximale Anzahl von Bytes zu lesen
| |
| mov x8, 63 // Syscall-Nummer für 'read' ist 63
| |
| svc 0 // Systemaufruf
| |
| | |
| // Ergebnis der gelesenen Bytes in x0 speichern
| |
| mov x20, x0
| |
| | |
| // Datei zum Schreiben öffnen (output.txt)
| |
| ldr x1, =newfilename // filename in x1 laden
| |
| mov x2, 241 // O_WRONLY | O_CREAT | O_TRUNC (241)
| |
| mov x3, 0644 // Modus 0644 (rw-r--r--)
| |
| mov x8, 2 // Syscall-Nummer für 'open' (2)
| |
| svc 0 // Systemaufruf
| |
| | |
| mov x21, x0 // File Descriptor für die Ausgabedatei
| |
| | |
| // Daten in die neue Datei schreiben
| |
| ldr x1, =buffer // Speicheradresse des Puffers in x1 laden
| |
| mov x2, x20 // Anzahl der gelesenen Bytes
| |
| mov x0, x21 // File Descriptor der Ausgabedatei
| |
| mov x8, 64 // Syscall-Nummer für 'write' (64)
| |
| svc 0 // Systemaufruf
| |
| | |
| // Eingabedatei schließen
| |
| mov x0, x19 // File Descriptor der Eingabedatei
| |
| mov x8, 57 // Syscall-Nummer für 'close' (57)
| |
| svc 0 // Systemaufruf
| |
| | |
| // Ausgabedatei schließen
| |
| mov x0, x21 // File Descriptor der Ausgabedatei
| |
| mov x8, 57 // Syscall-Nummer für 'close' (57)
| |
| svc 0 // Systemaufruf
| |
| | |
| // Beende das Programm
| |
| mov x8, 93 // Syscall-Nummer für 'exit' (93)
| |
| mov x0, 0 // Rückgabewert 0
| |
| svc 0 // Systemaufruf
| |
| Erklärung:
| |
| Datenabschnitt:
| |
| .section .data:
| |
| | |
| filename: Speichert den Namen der Eingabedatei input.txt.
| |
| newfilename: Speichert den Namen der Ausgabedatei output.txt.
| |
| buffer: Ein Speicherbereich zum Lesen und Schreiben der Dateiinhalte.
| |
| .section .bss:
| |
| | |
| Speicher zur Speicherung des Rückgabewerts von read.
| |
| Textabschnitt:
| |
| Datei öffnen (input.txt):
| |
| | |
| mov x0, 0: Bereitet den Systemaufruf vor, um stdin als Dateideskriptor zu verwenden.
| |
| ldr x1, =filename: Lädt die Adresse des Dateinamens in x1.
| |
| mov x2, 0: Setzt den Modus auf O_RDONLY (nur lesen).
| |
| mov x8, 2: Setzt die Systemaufrufnummer für open (2).
| |
| svc 0: Führt den Systemaufruf aus. Der Dateideskriptor wird in x0 zurückgegeben und in x19 gespeichert.
| |
| Daten lesen:
| |
| | |
| ldr x1, =buffer: Lädt die Adresse des Puffers in x1.
| |
| mov x2, 1024: Gibt an, dass bis zu 1024 Bytes gelesen werden.
| |
| mov x8, 63: Setzt die Systemaufrufnummer für read (63).
| |
| svc 0: Führt den Systemaufruf aus. Die Anzahl der gelesenen Bytes wird in x0 zurückgegeben und in x20 gespeichert.
| |
| Datei öffnen (output.txt):
| |
| | |
| ldr x1, =newfilename: Lädt die Adresse des neuen Dateinamens in x1.
| |
| mov x2, 241: Setzt den Modus auf O_WRONLY | O_CREAT | O_TRUNC (schreiben, Datei erstellen, Datei kürzen).
| |
| mov x3, 0644: Setzt die Dateiberechtigungen auf rw-r--r--.
| |
| mov x8, 2: Setzt die Systemaufrufnummer für open (2).
| |
| svc 0: Führt den Systemaufruf aus. Der Dateideskriptor wird in x0 zurückgegeben und in x21 gespeichert.
| |
| Daten schreiben:
| |
| | |
| ldr x1, =buffer: Lädt die Adresse des Puffers in x1.
| |
| mov x2, x20: Setzt die Anzahl der zu schreibenden Bytes auf die Anzahl der gelesenen Bytes.
| |
| mov x0, x21: Setzt den Dateideskriptor auf die Ausgabedatei.
| |
| mov x8, 64: Setzt die Systemaufrufnummer für write (64).
| |
| svc 0: Führt den Systemaufruf aus.
| |
| Dateien schließen:
| |
| | |
| mov x0, x19: Setzt den Dateideskriptor auf die Eingabedatei.
| |
| mov x8, 57: Setzt die Systemaufrufnummer für close (57).
| |
| svc 0: Führt den Systemaufruf aus.
| |
| mov x0, x21: Setzt den Dateideskriptor auf die Ausgabedatei.
| |
| mov x8, 57: Setzt die Systemaufrufnummer für close (57).
| |
| svc 0: Führt den Systemaufruf aus.
| |
| Programm beenden:
| |
| | |
| mov x8, 93: Setzt die Systemaufrufnummer für exit (93).
| |
| mov x0, 0: Setzt den Rückgabewert auf 0.
| |
| svc 0: Führt den Systemaufruf aus und beendet das Programm.
| |
| Dieses Beispiel demonstriert die wesentlichen Systemaufrufe und die Datenbearbeitung in einem ARM64-Assemblerprogramm unter Linux.
| |
| | |
| == GPIO Programmierung ==
| |
| * Mit Linux (Syscalls):
| |
| Die Pins werden wie Dateien behandelt. Der Dateipfad ist: /sys/class/gpio
| |
| Es wird einfach ein String, der den PIN angibt nach /sys/class/gpio/export geschrieben.
| |
| Der Treiber erstellt dann vier Dateien:
| |
| * /sys/class/gpio/gpio17/direction: Wird verwendet, um anzugeben, ob der Pin für Eingabe oder Ausgabe ist
| |
| * /sys/class/gpio/gpio17/value: Wird verwendet, um den Wert des Pins festzulegen oder zu lesen
| |
| * /sys/class/gpio/gpio17/edge: Wird verwendet, um einen Interrupt festzulegen, um Wertänderungen zu erkennen
| |
| * /sys/class/gpio/gpio17/active_low: Wird verwendet, um die Bedeutung von 0 und 1 umzukehren
| |
| | |
| !!!GPIO17 ändern!!!!!!!!!!!!! und testen
| |
| Nach dem Beenden wird das Gerät mit dem File "/sys/class/gpio/unexport" und dem String des PINs geschlossen.
| |
| | |
| .text
| |
| nanosleep:
| |
| ldr x0,=timespec
| |
| ldr x1,=timespec
| |
| mov x8,#162 //#define __NR_nanosleep 162
| |
| svc 0
| |
| .data
| |
| timespec:
| |
| timespec_tv_sec: .dword 0
| |
| timespec_tv_nsec: .dword 100000000
| |
| | |
| *PIN Öffnen:
| |
| .text
| |
| mov x0,#-100
| |
| ldr x1,=gpioexp
| |
| mov x2,#1 //#define O_WRONLY 00000001
| |
| mov x3,#666 //Dateirechte (Jeder darf alles...)
| |
| mov x8,#323 //#define __NR_openat 323
| |
| svc 0
| |
| mov x10,x0 //File Descriptor
| |
| ldr x1,=pin
| |
| mov x2,#2
| |
| mov x8,#4 //#define __NR_write 4
| |
| svc 0
| |
| mov x0,x10
| |
| mov x8,#118 //#define __NR_fsync 118
| |
| svc 0
| |
| mov x0,x10
| |
| mov x8,#6 //#define __NR_close 6
| |
| svc 0
| |
| .data
| |
| gpioexp: .asciz "/sys/class/gpio/export"
| |
| pin: .asciz "17" //Ändern!!!!!!!!!!!!!
| |
| | |
| GPIODirectionOut pin17:
| |
| | |
| | |
| Muss ich probieren! Seite 180 im Buch
| |
| | |
| | |
| === Näher an die direkte Programmierung ===
| |
| In Bare Metal, siehe Kurs hier, werden Geräte über Adressen angesprochen. Diese werden laut Dokumentation des Braodcom-Chips als Register bezeichnet. Dies hat allerdings nichts mit den ARM-Registern zu tun. Hier sind die Speicheradressen gemeint.
| |
| Leider ist die Dokumentation nicht wirklich gut, so dass wir diese Adressen hier im Linux-System heraussuchen müssen. Dies ist über folgenden Befehl in der Konsole möglich:
| |
| | |
| dmesg
| |
| genauer dmesg | grep gpio //Sucht nur die Info für gpio heraus...
| |
| | |
| == Interaktion mit C ==
| |
| === C Funktion aufrufen aus Assembler ===
| |
| Das Aufrufen von C-Funktionen aus ARM64-Assembler-Code unter Verwendung des GCC-Compilers ist durchaus möglich und erfordert das Einhalten des ARM Procedure Call Standards (AAPCS). Der AAPCS legt fest, wie Argumente an Funktionen übergeben und Rückgabewerte empfangen werden.
| |
| | |
| In diesem Beispiel zeige ich Ihnen, wie Sie die printf-Funktion aus einem ARM64-Assembler-Programm heraus aufrufen können.
| |
| | |
| Schritte:
| |
| Definieren Sie den C-Standard-Header für die printf-Funktion.
| |
| Richten Sie Argumente für printf im Assembler-Code ein.
| |
| Rufen Sie printf auf.
| |
| Kompilieren und linken Sie den Assembler-Code und die C-Bibliothek.
| |
| Beispielcode: Ruf der printf-Funktion
| |
| c_program.s: ARM64-Assembler-Code
| |
| .section .data
| |
| format_string: .asciz "Hello, %s!\n" // Format-String für printf
| |
| name_string: .asciz "World" // Name-String
| |
| | |
| .section .text
| |
| .global main
| |
| main:
| |
| // Argumente für `printf` einrichten
| |
| ldr x0, =format_string // Erstes Argument: Adresse des Format-Strings in x0
| |
| ldr x1, =name_string // Zweites Argument: Adresse des Strings "World" in x1
| |
| | |
| // `printf`-Funktion aufrufen
| |
| bl printf // Branch with Link zu `printf`
| |
| | |
| // Programm beenden
| |
| mov w0, 0 // Rückgabewert 0
| |
| ret // Rückkehr aus main
| |
| Erläuterung:
| |
| Datenabschnitt:
| |
| | |
| format_string: Ein ASCIZ-String (nullterminiert) für das Format von printf.
| |
| name_string: Ein ASCIZ-String für den Namen, der in den Format-String eingefügt wird.
| |
| Textabschnitt:
| |
| | |
| main: Die Hauptfunktion, die Argumente für printf vorbereitet und die Funktion aufruft.
| |
| ldr: Lädt die Adresse des Format-Strings in das Register x0 und die Adresse des Namens-Strings in das Register x1.
| |
| bl printf: Aufruf der printf-Funktion (Branch with Link).
| |
| mov w0, 0: Setzt den Rückgabewert von main auf 0.
| |
| ret: Rückkehr von main.
| |
| Kompilieren und Linken:
| |
| Speichern Sie den obenstehende Assembler-Code in einer Datei namens c_program.s.
| |
| Kompilieren Sie das Assembler-Programm und linken Sie es mit der Standard-C-Bibliothek.
| |
| aarch64-linux-gnu-gcc -nostartfiles -o c_program c_program.s -lc
| |
| -nostartfiles: Verhindert die Verwendung der Standard-Startdateien (Programm schlägt direkt bei main auf).
| |
| -o c_program: Gibt die Ausgabedatei an.
| |
| -lc: Verlinkt das Programm mit der Standard-C-Bibliothek, die printf enthält.
| |
| Ausführen:
| |
| Nach der Kompilierung können Sie das Programm wie gewohnt ausführen:
| |
| | |
| ./c_program
| |
| Zusammenfassung:
| |
| Sie können C-Funktionen wie printf aus ARM64-Assembler aufrufen, indem Sie die Anforderungen des ARM Procedure Call Standards (AAPCS) beachten.
| |
| Argumente werden in den Registern x0, x1, x2, etc. für die ersten Passagen vorbereitet.
| |
| Sie müssen sicherstellen, dass Sie die richtige Verknüpfungsoption (-lc) verwenden, um die Standard-C-Bibliothek zu verlinken.
| |
| | |
| === Assembler-Funktion in C aufrufen ===
| |
| Wenn Sie eine Assemblerfunktion in ARM64-Assembler schreiben und diese aus einem C-Programm aufrufen möchten, ist das ebenfalls möglich. Dabei sollten Sie die ARM Procedure Call Standard (AAPCS)-Konventionen beachten, die festlegen, wie Argumente an Funktionen übergeben und Rückgabewerte empfangen werden.
| |
| | |
| Beispiel: Aufruf einer Assemblerfunktion aus C
| |
| Wir werden eine einfache Assemblerfunktion add_numbers schreiben, die zwei Ganzzahlen als Argumente nimmt, sie addiert und das Ergebnis zurückgibt.
| |
| | |
| Assembly-Datei: add_numbers.s
| |
| .section .text
| |
| .global add_numbers
| |
| | |
| add_numbers:
| |
| // x0: erstes Argument (int a)
| |
| // x1: zweites Argument (int b)
| |
| // x0: Rückgabewert (int)
| |
| | |
| add x0, x0, x1 // x0 = x0 + x1
| |
| ret // Rückkehr
| |
| C-Datei: main.c
| |
| #include
| |
| | |
| // Deklaration der Assemblerfunktion
| |
| extern int add_numbers(int a, int b);
| |
| | |
| int main() {
| |
| int a = 5;
| |
| int b = 7;
| |
| int result;
| |
| | |
| result = add_numbers(a, b);
| |
| printf("Result: %d\n", result);
| |
| | |
| return 0;
| |
| }
| |
| Schritte zum Kompilieren und Linken:
| |
| Speichern Sie den Assemblercode in einer Datei namens add_numbers.s.
| |
| Speichern Sie den C-Code in einer Datei namens main.c.
| |
| Kompilieren Sie beide Dateien und linken sie zu einem einzigen ausführbaren Programm.
| |
| aarch64-linux-gnu-gcc -c -o add_numbers.o add_numbers.s
| |
| aarch64-linux-gnu-gcc -c -o main.o main.c
| |
| aarch64-linux-gnu-gcc -o my_program main.o add_numbers.o
| |
| -c: Kompiliert die Dateien, ohne sie zu linken.
| |
| -o: Gibt die Ausgabedatei an.
| |
| Letzter Befehl: Verlinkt die Objektdateien zu einer ausführbaren Datei namens my_program.
| |
| Ausführung:
| |
| Nun können Sie das ausgeführte Programm wie gewohnt starten:
| |
| | |
| ./my_program
| |
| Erläuterung:
| |
| Assemblerfunktion:
| |
| | |
| Die Datei add_numbers.s definiert eine einfache Assemblerfunktion add_numbers, die zwei int-Werte als Parameter nimmt. Diese Parameter werden laut AAPCS-Konventionen in den Registern x0 und x1 übergeben. Das Ergebnis wird ebenfalls in x0 zurückgegeben.
| |
| Die Anweisung add x0, x0, x1 addiert die Werte der Register x0 und x1 und speichert das Ergebnis in x0.
| |
| ret: Es wird zur aufrufenden Funktion zurückgekehrt.
| |
| C-Programm:
| |
| | |
| Die Datei main.c deklariert die externe Assemblerfunktion add_numbers mit extern int add_numbers(int a, int b);.
| |
| Im main-Funktionskörper wird die Assemblerfunktion add_numbers mit den Argumenten a und b aufgerufen, und das Ergebnis wird in der Variable result gespeichert.
| |
| Das Ergebnis wird mit printf ausgegeben.
| |
| Durch das Befolgen dieser Konventionen können Sie Assemblerfunktionen nahtlos in Ihre C-Programme integrieren, was die Flexibilität und Kontrolle über speichernahe und leistungsstarke Programmierung erheblich erhöht.
| |
| === Inline-Assembler in C ===
| |
| Inline-Assembler in C ermöglicht es, Assembleranweisungen direkt in C-Code einzubetten. Dies kann nützlich sein, um spezifische CPU-Funktionen zu nutzen oder um eine effiziente Implementierung bestimmter Algorithmen zu erreichen. GCC bietet hierfür eine spezielle Syntax. Lassen Sie uns ein einfaches Beispiel durchgehen, das zeigt, wie Inline-Assembler in C verwendet wird.
| |
| | |
| Beispiel: Inline-Assembler in C
| |
| Wir werden eine einfache Funktion erstellen, die zwei Ganzzahlen addiert, indem sie Inline-Assembler verwendet.
| |
| | |
| Inline-Assembler-Syntax
| |
| Die gcc-Syntax für Inline-Assembler ist relativ komplex, aber leistungsfähig. Ein einfaches Beispiel sieht folgendermaßen aus:
| |
| | |
| #include
| |
| | |
| int add_numbers(int a, int b) {
| |
| int result;
| |
| // Inline-Assembler-Syntax: asm("assembly code" : outputs : inputs : clobbers);
| |
| asm(
| |
| "add %w[result], %w[a], %w[b]"
| |
| : [result] "=r" (result) // output operands
| |
| : [a] "r" (a), [b] "r" (b) // input operands
| |
| : // clobber list
| |
| );
| |
| return result;
| |
| }
| |
| | |
| int main() {
| |
| int a = 5;
| |
| int b = 7;
| |
| int result;
| |
| | |
| result = add_numbers(a, b);
| |
| printf("Result: %d\n", result);
| |
| | |
| return 0;
| |
| }
| |
| Erklärung des Inline-Assembler-Codes
| |
| Deklaration der Assembler-Anweisungen:
| |
| | |
| asm ist das Schlüsselwort für Inline-Assembler in gcc.
| |
| Der Assemblercode wird als String in doppelte Anführungszeichen gesetzt.
| |
| %w[result], %w[a] und %w[b] sind Platzhalter für die C-Variablen, die mit den entsprechenden Assemblerregistern verbunden werden. Das Präfix %w sorgt dafür, dass es sich um 32-Bit-Register handelt, wie es für ARM64-Ganzzahlen erforderlich ist.
| |
| Die Platzhalter in den Assembler-Anweisungen werden durch die Operanden (input, output, clobber) spezifiziert.
| |
| Output-Operands:
| |
| | |
| ": [result] "=r" (result)" spezifiziert das Ausgaberegister und gibt an, dass das Ergebnis in result gespeichert wird. Das =r bedeutet, dass das Ergebnis ein allgemeines Register (r) sein wird.
| |
| Input-Operands:
| |
| | |
| ": [a] "r" (a), [b] "r" (b)" spezifiziert die Eingaberegister, die die Werte a und b enthalten. Das r bedeutet ein allgemeines Register.
| |
| Clobber-List:
| |
| | |
| Die Clobber-Liste gibt Register oder Speicherbereiche an, die von dem Assemblercode verändert werden könnten. Hier ist sie leer.
| |
| Weitere Details zur Inline-Assembler-Syntax
| |
| Input-Operand:
| |
| | |
| : [a] "r" (a)
| |
| [a] ist der C-Name der Variablen.
| |
| "r" gibt an, dass ein allgemeines Register verwendet wird.
| |
| (a) ist der Wert, der in das Register geladen wird.
| |
| Output-Operand:
| |
| | |
| : [result] "=r" (result)
| |
| [result] ist der C-Name der Variablen.
| |
| "=r" gibt an, dass ein allgemeines Register verwendet wird und dass das Register für Ausgabe reserviert ist (=).
| |
| (result) ist die Variable, in die das Ergebnis gespeichert wird.
| |
| Kompilieren und Ausführen
| |
| Speichern Sie den Code in einer Datei namens main.c und kompilieren Sie ihn mit gcc:
| |
| | |
| gcc -o main main.c
| |
| ./main
| |
| Das Programm gibt Result: 12 aus.
| |
| | |
| Zusammenfassung
| |
| Inline-Assembler in C bietet Ihnen die Möglichkeit, spezifische Assembleranweisungen direkt in Ihren C-Code einzubetten, was in bestimmten Situationen effizientere Leistung oder Zugriff auf spezielle CPU-Funktionen ermöglicht. Die gcc-spezifische Syntax für Inline-Assembler umfasst die Verwendung von asm und die Spezifikation von Ausgaben, Eingaben und Clobbers.
| |
| | |
| === Bibliotheken verwenden ===
| |
| Die Verwendung von Bibliotheken in Assembler erfordert einige spezifische Schritte. Grundsätzlich müssen Sie:
| |
| | |
| Die zu verwendenden Funktionen korrekt deklarieren.
| |
| Die korrekten Aufrufkonventionen einhalten.
| |
| Die Assemblerdateien kompilieren und anschließend mit den Bibliotheken linken.
| |
| Hier zeige ich ein Beispiel, wie man eine C-Standardbibliothek (die printf-Funktion) aus einer Assemblerdatei heraus aufruft.
| |
| | |
| Beispiel: Aufruf der printf-Funktion aus Assembler
| |
| Assembler-Datei: main.s
| |
| .section .data
| |
| format_string: .asciz "Hello, %s!\n" // Format-String für printf
| |
| name_string: .asciz "World" // Der String, der in den Format-String eingefügt wird
| |
| | |
| .section .text
| |
| .global main
| |
| | |
| main:
| |
| // Argumente für printf vorbereiten
| |
| ldr x0, =format_string // Erstes Argument: Adresse des Format-Strings in x0
| |
| ldr x1, =name_string // Zweites Argument: Adresse des Strings "World" in x1
| |
| | |
| // printf-Funktion aufrufen
| |
| bl printf // Branch with Link zu printf
| |
| | |
| // Programm beenden (exit(0))
| |
| mov x0, 0 // Rückgabewert 0 für exit()
| |
| mov x8, 93 // Syscall-Nummer für exit(93)
| |
| svc 0 // Systemaufruf
| |
| Kompilieren und Linken
| |
| Speichern Sie den Assembler-Code in einer Datei namens main.s.
| |
| Kompilieren und linken Sie den Code mit der C-Standardbibliothek.
| |
| aarch64-linux-gnu-gcc -o main main.s -nostartfiles -lc
| |
| -nostartfiles: Verhindert die Verwendung der Standard-Startdateien, schützt die Assembler-Startfunktion vor zusätzlichen Dateien.
| |
| -lc: Linkt das Programm mit der C-Standardbibliothek, die printf enthält.
| |
| Erläuterung
| |
| Datenabschnitt:
| |
| | |
| format_string: Ein ASCIZ-String (nullterminiert) für das Format von printf.
| |
| name_string: Ein ASCIZ-String für den Namen, der in den Format-String eingefügt wird.
| |
| Textabschnitt:
| |
| | |
| main: Die Hauptfunktion, die Argumente für printf vorbereitet und die Funktion aufruft.
| |
| ldr x0, =format_string: Lädt die Adresse des Format-Strings in das Register x0 (das erste Argument für printf).
| |
| ldr x1, =name_string: Lädt die Adresse des Namens-Strings in das Register x1 (das zweite Argument für printf).
| |
| bl printf: Branch with Link zu printf. Dies ruft die printf-Funktion auf, wobei die Parameter aus den Registern x0 und x1 verwendet werden.
| |
| mov x0, 0: Setzt den Rückgabewert von main auf 0.
| |
| mov x8, 93: Setzt die Systemaufrufnummer für exit (93).
| |
| svc 0: Führt den Systemaufruf aus und beendet das Programm.
| |
| Bibliotheksfunktionen aufrufen
| |
| Beim Aufruf von Funktionen aus Bibliotheken in Assembler müssen Sie sicherstellen, dass Sie die entsprechenden Konventionen befolgen:
| |
| | |
| Argumente werden in den Registern x0 bis x7 übergeben.
| |
| Rückgabewerte werden in x0 (und x1 bei größeren Rückgaben) empfangen.
| |
| Mit bl (Branch with Link) wird die Bibliotheksfunktion aufgerufen.
| |
| Nach dem Aufruf steht das Ergebnis in x0.
| |
| Einbindung und Verwendung eigener Bibliotheken
| |
| Sie können auch eigene Bibliotheken erstellen und diese in Assembler verwenden. Hier ist ein schnelles Beispiel:
| |
| | |
| C-Datei: mylib.c
| |
| #include
| |
| | |
| void my_function(const char *msg) {
| |
| printf("Message: %s\n", msg);
| |
| }
| |
| Assembler-Datei: main.s
| |
| .section .data
| |
| msg: .asciz "Hello from Assembler!"
| |
| | |
| .section .text
| |
| .global main
| |
| | |
| main:
| |
| ldr x0, =msg // Bereite das Argument vor (Adresse des Strings)
| |
| bl my_function // Rufe die Funktion my_function auf
| |
| | |
| // Programm beenden
| |
| mov x8, 93 // Syscall-Nummer für exit (93)
| |
| svc 0 // Systemaufruf
| |
| Kompilieren und Linken
| |
| Speichern Sie die C-Datei in mylib.c.
| |
| Speichern Sie die Assembler-Datei in main.s.
| |
| Kompilieren und linken Sie beide Dateien:
| |
| aarch64-linux-gnu-gcc -c -o mylib.o mylib.c
| |
| aarch64-linux-gnu-gcc -c -o main.o main.s
| |
| aarch64-linux-gnu-gcc -o main main.o mylib.o -nostartfiles
| |
| Dieses Ergebnis ist ein ausführbares Programm, das Ihre C-Funktion my_function aus der Assembler-Datei aufruft.
| |
| | |
| Diese Beispiele zeigen, wie Bibliotheken (Standardbibliotheken und benutzerdefinierte Bibliotheken) in ARM64-Assembler verwendet und aufgerufen werden können.
| |
| | |
| == Multiplizieren, Dividieren und Akkumulation ==
| |
| === Multiplizieren ===
| |
| mul xd, xn, xm
| |
| Dies multipliziert den Wert aus "xn" mit dem Wert aus "xm". Das Ergebnis wird in "xd" zurückgegeben.
| |
| Die Mathematiker sehen hier das Problem: Es werden zwei 64-Bit Werte multipliziert, aber das Ergebnis wird in 64-Bit abgelegt.
| |
| Dies bedeutet:
| |
| * Ergebnis größer 64-Bit, wird auf 64-Bit reduziert. Damit stimmt das Ergebnis nicht
| |
| * Es werden "nur" 64-Bit Register für xn und xm akzeptiert
| |
| * Es gibt keine Anzeige des Überlaufs
| |
| | |
| Zusätzliche Befehle:
| |
| smull xd,wn,wm // Multipliziere mit Vorzeichen wn mit wm
| |
| umull xd,wn,wm // Multipliziere ohne Vorzeichen wn mit wm
| |
| | |
| Ein 128-Bit Ergebnis zu erhalten können folgende Befehle verwendet werden:
| |
| smulh xd,xn,xm // Multipliziere mit Vorzeichen xn mit xm und schreibe das obere Ergebnis (65-128 Bit) nach xd
| |
| umulh xd,xn,xm // Multipliziere ohne Vorzeichen xn mit xm und schreibe das obere Ergebnis (65-128 Bit) nach xd
| |
| | |
| In der Kombination von mul und smulh oder umulh können 128-Bit-Ergebnisse erzeugt werden.
| |
| | |
| Weitere Befehle:
| |
| mneg xd,xn,xm // xd = -(xn * xm)
| |
| smnegl xd,wn,wm // xd = -(wn * wm) mit Vorzeichen
| |
| umnegl xd,wn,wm // xd = -(wn * wm) ohne Vorzeichen
| |
| | |
| Beispiele:
| |
| mov x0, #25
| |
| mov x1, #4
| |
| mul x2,x0,x1 // x2 = 100
| |
| mneg x2,x0,x1 // x2 = -100
| |
| smull x2,w0,w1 // x2 = 100
| |
| smnegl x2,w0,w1 // x2 = -100
| |
| umull x2,w0,w1 // x2 = 100
| |
| umnegl x2,w0,w1 // x2 = -100
| |
| | |
| ldr x0,=big1
| |
| ldr x0,[x0]
| |
| ldr x1,=big2
| |
| ldr x1,[x1]
| |
| | |
| mul x2,x0,x1 // x2 = ... Untere 64-Bit
| |
| smulh x2,x0,x1 // x2 = ... Obere 64-Bit mit Vorzeichen
| |
| umulh x2,x0,x1 // x2 = ... Obere 64-Bit ohne Vorzeichen
| |
| | |
| === Division ===
| |
| | |
| sdiv xd,xn,xm // xd = xn / xm mit Vorzeichen
| |
| udiv xd,xn,xm // xd = xn / xm ohne Vorzeichen
| |
| | |
| Alle X und W Register erlaubt
| |
| | |
| Beispiel:
| |
| mov x0,#100
| |
| mov x1,#4
| |
| sdiv x2,x0,x1 //x2 = 25
| |
| udiv x2,x0,x1 //x2 = 25
| |
| | |
| Keine Möglichkeit den Rest zu bestimmen: Im Beispiel wäre es Rest = x0-(x2*x1).
| |
| Größere Zahlen als 64-Bit nicht möglich.
| |
| Division durch "0" ergibt immer Null -> Irreführend.
| |
| | |
| === Multiplizieren und Akkumulieren ===
| |
| madd xd, xn, xm, xa //xd = xa + xn * xm
| |
| msub xd, xn, xm, xa //xd = xa - xn * xm
| |
| smaddl xd, wn, wm, xa //xd = xa + wn * wm Vorzeichen
| |
| umaddl xd, wn, wm, xa //xd = xa + wn * wm ohne Vorzeichen
| |
| smsubl xd, wn, wm, xa //xd = xa - wn * wm Vorzeichen
| |
| umsubl xd, wn, wm, xa //xd = xa - wn * wm ohne Vorzeichen
| |
| | |
| xd und xa kann Identisch sein...
| |
| | |
| == Gleitkommaoperationen ==
| |
| | |
| Bits of a floating-point number
| |
| Name | Precision | Sign | Fractional | Exponent | Decimal Digits
| |
| Half | 16 bits | 1 | 10 | 5 | 3
| |
| Single | 32 bits | 1 | 23 | 8 | 7
| |
| Double | 64 bits | 1 | 52 | 11 | 16
| |
| | |
| Definition:
| |
| | |
| .single 3.1414, 2.453e10, -0.7254, -0.2345e-20
| |
| .douple 3.141592653589793, -5.83629435912e-20
| |
| | |
| Der GNU Assembler verfügt nicht über eine Anweisung für 16-Bit-Gleitkommazahlen mit halber Genauigkeit, daher müssen wir eine davon laden
| |
| und dann eine Konvertierung durchführen.
| |
| | |
| === FPU-Register ===
| |
| v0 - v31: 128-Bit
| |
| d0 - d31: 64-Bit (double floating Point)
| |
| s0 - s31: 32-Bit (single)
| |
| h0 - h31: 16-Bit (float)
| |
| | |
| * Register "d" ist ein Teil des REgister "v", das Register "s" ein Teil des Registers "d" usw. Dies ist zu vergleichen wie bei den ARM-Registern "x" und "w".
| |
| | |
| {| class="wikitable"
| |
| |-
| |
| ! Bits !! 127 - 112 !! 111 - 96 !! 95 - 80 !! 79 - 64 !! 63 - 48 !! 47 - 32 !! 31 - 16 !! 15 - 0
| |
| |-
| |
| | V-Register (NEON) || style="background-color:#ffffff;" colspan="8"| 128-Bit
| |
| |-
| |
| | D-Register (FPU) || style="background-color:#aaaaaa;" colspan="4"| || style="background-color:#ffffff;" colspan="4" | 64-Bit
| |
| |-
| |
| | S-Register (FPU) || style="background-color:#aaaaaa;" colspan="6"| || style="background-color:#ffffff;" colspan="6" | 32-Bit
| |
| |-
| |
| | H-Register (FPU) || style="background-color:#aaaaaa;" colspan="7"| || style="background-color:#ffffff;" | 16-Bit
| |
| |}
| |
| | |
| Der NEON-Coprozessor verwendet das V-Register. Allerdings kann der Coprozessor auch mit 128-Bit Ganzzahlen umgehen. Dies sind die gleichen Register, werden aber "Q"-Register genannt.
| |
| | |
| === Verwendung der Register in Funktionen ===
| |
| Auch hier müssen bestimmte Register auf dem Stack gesichert werden, da die Aufrufende Funktion davon ausgeht, dass diese nicht verloren gehen, wenn sie eine Funktion aufruft.
| |
| | |
| Davon sind die Register V8-V15 betroffen. Werden diese verwendet, müssen diese auf dem Stack gesichert werden und am ende der Funktion wieder hergestellt werden.
| |
| | |
| Beispiel:
| |
| stp q8,q9,[sp,#-32]!
| |
| str q10, [sp,#16]!
| |
| ldr q10,[sp],#16
| |
| ldp q8,q9,[sp],#32
| |
| | |
| === Arbeiten mit FPU-Registern ===
| |
| * Verwendung von ldr und str
| |
| * Kopieren von ARM-Register mit FPU-Registern: fmov
| |
| | |
| fadd rd, rn, rm // rd = rn + rm
| |
| fsub rd, rn, rm // rd = rn - rm
| |
| fmul rd, rn, rm // rd = rn * rm
| |
| fdiv rd, rn, rm // rd = rn / rm
| |
| fmadd rd, rn, rm, ra // rd = ra + rm * rn
| |
| fmsub rd, rn, rm, ra // rd = ra – rm * rn
| |
| fneg rd, rn // rd = -rn
| |
| fabs rd, rn // rd = Absuluter Wert ( rn )
| |
| fmax rd, rn, rm // rd = Max( rn, rm )
| |
| fmin rd, rn, rm // rd = Min( rn, rm )
| |
| fsqrt rd, rn // rd = Quadratwurzel( rn )
| |
| | |
| Für "r" können alle FPU-Register (d,s,h) verwendet werden
| |
| === Konvertierung von Gleitkommazahlen ===
| |
| Beispiel:
| |
| fcvt d0, s1 // s1 -> d0
| |
| fcvt s1, d0 // d0 -> s1
| |
| fcvt s1, h2 // h2 -> s1
| |
| fcvt h2, s1 // s1 -> h2
| |
| | |
| * Integer nach Gleitkommazahl:
| |
| -> (Un/Signed Convert to Floating Point)
| |
| | |
| SCVTF <Sd>, <Wn>: Konvertiert einen 32-Bit-Ganzzahlwert ('<Wn>' ) in eine 32-Bit-Single-Precision-Floatzahl ('<Sd>' ).
| |
| SCVTF <Dd>, <Wn>: Konvertiert einen 32-Bit-Ganzzahlwert ('<Wn>' ) in eine 64-Bit-Double-Precision-Floatzahl ('<Dd>' ).
| |
| SCVTF <Sd>, <Xn>: Konvertiert einen 64-Bit-Ganzzahlwert ('<Xn>' ) in eine 32-Bit-Single-Precision-Floatzahl ('<Sd>' ).
| |
| SCVTF <Dd>, <Xn>: Konvertiert einen 64-Bit-Ganzzahlwert ('<Xn>' ) in eine 64-Bit-Double-Precision-Floatzahl ('<Dd>' ).
| |
| | |
| * Gleitkommazahlen nach Integer:
| |
| FCVTxx Instruktionen
| |
| FCVTZS (Floating-point Convert to Signed integer, rounding toward Zero):
| |
| | |
| Konvertiert eine Gleitkommazahl in eine vorzeichenbehaftete Ganzzahl mittels Abrundung in Richtung Null (Truncation).
| |
| ASSEMBLY
| |
| Copy code
| |
| FCVTZS <Wd>, <Sn> // Konvertiert eine Single-Precision Gleitkommazahl (<Sn>) in eine 32-Bit Ganzzahl (<Wd>).
| |
| FCVTZS <Xd>, <Sn> // Konvertiert eine Single-Precision Gleitkommazahl (<Sn>) in eine 64-Bit Ganzzahl (<Xd>).
| |
| FCVTZS <Wd>, <Dn> // Konvertiert eine Double-Precision Gleitkommazahl (<Dn>) in eine 32-Bit Ganzzahl (<Wd>).
| |
| FCVTZS <Xd>, <Dn> // Konvertiert eine Double-Precision Gleitkommazahl (<Dn>) in eine 64-Bit Ganzzahl (<Xd>).
| |
| FCVTZS <Vd>.<T>, <Vn>.<T> // SIMD: Konvertiert Gleitkommazahlen in Ganzzahlen, z.B. <Vd>.4S, <Vn>.4S.
| |
| FCVTZU (Floating-point Convert to Unsigned integer, rounding toward Zero):
| |
| | |
| Konvertiert eine Gleitkommazahl in eine vorzeichenlose Ganzzahl durch Abrundung in Richtung Null.
| |
| ASSEMBLY
| |
| Copy code
| |
| FCVTZU <Wd>, <Sn> // Konvertiert eine Single-Precision Gleitkommazahl (<Sn>) in eine 32-Bit Unsigned Ganzzahl (<Wd>).
| |
| FCVTZU <Xd>, <Sn> // Konvertiert eine Single-Precision Gleitkommazahl (<Sn>) in eine 64-Bit Unsigned Ganzzahl (<Xd>).
| |
| FCVTZU <Wd>, <Dn> // Konvertiert eine Double-Precision Gleitkommazahl (<Dn>) in eine 32-Bit Unsigned Ganzzahl (<Wd>).
| |
| FCVTZU <Xd>, <Dn> // Konvertiert eine Double-Precision Gleitkommazahl (<Dn>) in eine 64-Bit Unsigned Ganzzahl (<Xd>).
| |
| FCVTZU <Vd>.<T>, <Vn>.<T> // SIMD: Konvertiert Gleitkommazahlen in Unsigned Ganzzahlen, z.B. <Vd>.4S, <Vn>.4S.
| |
| Verwendung und Syntax
| |
| Die FCVTZS und FCVTZU Instruktionen können sowohl auf skalare Werte als auch auf SIMD-Vektoren angewendet werden. Beispiele für die Nutzung:
| |
| | |
| ASSEMBLY
| |
| Copy code
| |
| // Skalare Konvertierungen
| |
| FCVTZS W0, S1 // Konvertiert die Single-Precision Gleitkommazahl in S1 zu einer 32-Bit Ganzzahl und speichert sie in W0.
| |
| FCVTZS X1, D2 // Konvertiert die Double-Precision Gleitkommazahl in D2 zu einer 64-Bit Ganzzahl und speichert sie in X1.
| |
| | |
| FCVTZU W0, S1 // Konvertiert die Single-Precision Gleitkommazahl in S1 zu einer 32-Bit Unsigned Ganzzahl und speichert sie in W0.
| |
| FCVTZU X1, D2 // Konvertiert die Double-Precision Gleitkommazahl in D2 zu einer 64-Bit Unsigned Ganzzahl und speichert sie in X1.
| |
| | |
| // SIMD Vektor-Konvertierungen
| |
| FCVTZS V0.4S, V1.4S // Konvertiert vier Single-Precision Gleitkommazahlen in V1 zu vier 32-Bit Ganzzahlen und speichert sie in V0.
| |
| FCVTZU V0.2D, V1.2D // Konvertiert zwei Double-Precision Gleitkommazahlen in V1 zu zwei 64-Bit Unsigned Ganzzahlen und speichert sie in V0.
| |
| Zusammenfassung:
| |
| FCVTZS und FCVTZU sind die Hauptinstruktionen für die Konvertierung von Gleitkomma- zu Ganzzahlwerten.
| |
| Diese Instruktionen unterstützen sowohl 32-Bit und 64-Bit Konvertierungen als auch SIMD-Vektoroperationen.
| |
| | |
| In der ARM64-Architektur gibt es eine Reihe von FCVTxx-Instruktionen, die Gleitkommazahlen in Ganzzahlen umwandeln, abhängig von verschiedenen Rundungsmodi. Hier ist eine detaillierte Übersicht über diese Instruktionen und ihre Funktion:
| |
| | |
| Übersicht der FCVTxx-Instruktionen
| |
| FCVTAS (Floating-point Convert to Signed integer, rounding to nearest with ties to Away from zero):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenbehaftete Ganzzahl mittels Rundung zum nächsten ganzzahligen Wert, wobei Rundungsbindungen vom Nullpunkt weg gerundet werden.
| |
| FCVTAS <Wd>, <Sn>
| |
| FCVTAS <Xd>, <Sn>
| |
| FCVTAS <Wd>, <Dn>
| |
| FCVTAS <Xd>, <Dn>
| |
| FCVTAS <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTAU (Floating-point Convert to Unsigned integer, rounding to nearest with ties to Away from zero):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenlose Ganzzahl mittels Rundung zum nächsten ganzzahligen Wert, wobei Rundungsbindungen vom Nullpunkt weg gerundet werden.
| |
| FCVTAU <Wd>, <Sn>
| |
| FCVTAU <Xd>, <Sn>
| |
| FCVTAU <Wd>, <Dn>
| |
| FCVTAU <Xd>, <Dn>
| |
| FCVTAU <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTMS (Floating-point Convert to Signed integer, rounding toward Minus infinity):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenbehaftete Ganzzahl mittels Abrundung zum nächsten kleinen Wert (Rundung gegen Minus-Unendlichkeit).
| |
| FCVTMS <Wd>, <Sn>
| |
| FCVTMS <Xd>, <Sn>
| |
| FCVTMS <Wd>, <Dn>
| |
| FCVTMS <Xd>, <Dn>
| |
| FCVTMS <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTMU (Floating-point Convert to Unsigned integer, rounding toward Minus infinity):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenlose Ganzzahl mittels Abrundung zum nächsten kleinen Wert (Rundung gegen Minus-Unendlichkeit).
| |
| FCVTMU <Wd>, <Sn>
| |
| FCVTMU <Xd>, <Sn>
| |
| FCVTMU <Wd>, <Dn>
| |
| FCVTMU <Xd>, <Dn>
| |
| FCVTMU <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTPS (Floating-point Convert to Signed integer, rounding toward Plus infinity):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenbehaftete Ganzzahl mittels Aufrundung zum nächsten größeren Wert (Rundung gegen Plus-Unendlichkeit).
| |
| FCVTPS <Wd>, <Sn>
| |
| FCVTPS <Xd>, <Sn>
| |
| FCVTPS <Wd>, <Dn>
| |
| FCVTPS <Xd>, <Dn>
| |
| FCVTPS <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTPU (Floating-point Convert to Unsigned integer, rounding toward Plus infinity):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenlose Ganzzahl mittels Aufrundung zum nächsten größeren Wert (Rundung gegen Plus-Unendlichkeit).
| |
| FCVTPU <Wd>, <Sn>
| |
| FCVTPU <Xd>, <Sn>
| |
| FCVTPU <Wd>, <Dn>
| |
| FCVTPU <Xd>, <Dn>
| |
| FCVTPU <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTZS (Floating-point Convert to Signed integer, rounding toward Zero):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenbehaftete Ganzzahl mittels Abrundung in Richtung Null (Truncation).
| |
| FCVTZS <Wd>, <Sn>
| |
| FCVTZS <Xd>, <Sn>
| |
| FCVTZS <Wd>, <Dn>
| |
| FCVTZS <Xd>, <Dn>
| |
| FCVTZS <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| FCVTZU (Floating-point Convert to Unsigned integer, rounding toward Zero):
| |
| | |
| Wandelt eine Gleitkommazahl in eine vorzeichenlose Ganzzahl durch Abrundung in Richtung Null (Truncation).
| |
| FCVTZU <Wd>, <Sn>
| |
| FCVTZU <Xd>, <Sn>
| |
| FCVTZU <Wd>, <Dn>
| |
| FCVTZU <Xd>, <Dn>
| |
| FCVTZU <Vd>.<T>, <Vn>.<T> // SIMD-Version
| |
| Zusammenfassung der Konvertierungsmodi:
| |
| Rundung zur nächsten Ganzzahl:
| |
| FCVTAS / FCVTAU: Rundung zum nächsten Wert mit Bindungen vom Nullpunkt weg.
| |
| Rundung gegen Minus-Unendlichkeit:
| |
| FCVTMS / FCVTMU: Abrundung zum nächsten kleineren Wert.
| |
| Rundung gegen Plus-Unendlichkeit:
| |
| FCVTPS / FCVTPU: Aufrundung zum nächsten größeren Wert.
| |
| Rundung gegen Null:
| |
| FCVTZS / FCVTZU: Abrundung in Richtung Null.
| |
| Diese Sammlung von FCVTxx-Instruktionen bietet eine breite Palette von Optionen für die Konvertierung von Gleitkommazahlen in Ganzzahlen, je nach den spezifischen Rundungsanforderungen.
| |
| | |
| === Vergleichen ===
| |
| FCMP Hd, Hm
| |
| FCMP Hd, #0.0
| |
| FCMP Sd, Sm
| |
| FCMP Sd, #0.0
| |
| FCMP Dd, Dm
| |
| FCMP Dd, #0.0
| |
| | |
| == NEON Coprozessor ==
| |
| Der NEON-Coprozessor, der in ARM64-Prozessoren integriert ist, bietet erweiterte SIMD (Single Instruction, Multiple Data)-Fähigkeiten. Diese ermöglichen es, mehrere Datenpunkte mit einer einzigen Anweisung zu verarbeiten, was die Leistung bei bestimmten Arten von Berechnungen erheblich steigert.
| |
| | |
| NEON-Coprozessor: Überblick
| |
| Architektur und Integration:
| |
| | |
| SIMD-Fähigkeiten: NEON erweitert die ARMv8-A-Architektur (auch bekannt als ARM64) mit fortschrittlichen SIMD-Befehlssätzen. Diese sind besonders nützlich für Multimediaverarbeitungen, Signalverarbeitungen, mathematische Berechnungen und Machine-Learning-Anwendungen.
| |
| VFP (Vector Floating Point) und NEON:
| |
| VFP bietet skalare Floating-Point-Operationen.
| |
| NEON bietet sowohl skalare Floating-Point- als auch SIMD-Operationen, was eine noch effizientere Verarbeitung von vektorisierten Daten erlaubt.
| |
| Register und Datentypen:
| |
| | |
| Register: NEON verfügt über 32 128-bit-Register (V0-V31), die je nach Bedarf in kleinere Register unterteilt werden können, z. B. in 64-bit (D0-D31), 32-bit (S0-S31), 16-bit (H0-H31) und 8-bit (B0-B31) Register.
| |
| Datentypen: NEON unterstützt eine Vielzahl von Datentypen, darunter:
| |
| 8-bit, 16-bit, 32-bit, 64-bit Integers
| |
| 32-bit, 64-bit Floating-Point-Zahlen
| |
| Polytyp-Daten für fortschrittliche kryptografische Operationen
| |
| Instruktionssatz:
| |
| | |
| NEON instruiert SIMD-Operationen wie Addition, Subtraktion, Multiplikation, Division sowie erweiterte Funktionen wie Vektorschieben, Rangieren, Laden und Speichern von Vektoren.
| |
| Beispiele für NEON-Instruktionen:
| |
| VADD (Vector Add): Addiert zwei Vektoren.
| |
| VMUL (Vector Multiply): Multipliziert zwei Vektoren.
| |
| VDUP (Vector Duplicate): Verdoppelt ein Element in allen Positionen des Zielvektors.
| |
| VLD1 (Vector Load): Lädt Daten aus dem Speicher in ein NEON-Register.
| |
| VST1 (Vector Store): Speichert Daten aus einem NEON-Register in den Speicher.
| |
| Anwendungsfälle:
| |
| | |
| Multimedia-Verarbeitung: NEON kann Videodekodierung/-codierung, Bildbearbeitung und Audioprozessoren beschleunigen. Beispiele sind JPEG-Dekodierung, H.264-Videokodierung und -decodierung sowie MP3-Dekodierung.
| |
| Digitale Signalverarbeitung (DSP): Häufige Anwendungen umfassen FFT (Fast Fourier Transform), Faltung und Filterung von Signalen.
| |
| Maschinelles Lernen: NEON kann als Beschleuniger für neuronale Netzwerke und andere maschinelle Lernalgorithmen dienen, indem es massive Mengen an Matrizenoperationen effizient ausführt.
| |
| Mathematische Berechnungen: Matrizen-Multiplikation, Vektoraddition und andere numerische Algorithmen profitieren von den parallelen Rechenfähigkeiten des NEON.
| |
| Performance:
| |
| | |
| Parallelismus: Die SIMD-Natur des NEON ermöglicht die Bearbeitung mehrerer Datenpunkte gleichzeitig, was die Berechnungsrate für Aufgaben mit massiv parallelen Anforderungen drastisch erhöht.
| |
| Effizienz: Die Kombination von NEONs parallelen Rechenfähigkeiten mit ARM64-Energieeffizienz bietet eine leistungsstarke und energieeffiziente Plattform für eine Vielzahl moderner Anwendungen.
| |
| | |
| Verwendung des NEON-Coprozessors im Raspberry Pi
| |
| Der Raspberry Pi, basierend auf einem ARM64-Prozessor, nutzt NEON, um eine Vielzahl von leistungsstarken rechnerischen Aufgaben zu erledigen. Durch die Integration von NEON ermöglicht der Raspberry Pi:
| |
| | |
| Schnellere Multimediaverarbeitung: Optimierung von Videocodecs und Grafikrendering.
| |
| Verbesserte mathematische und wissenschaftliche Berechnungen: Effiziente Ausführung von Operationen, die massive Datenmengen verarbeiten.
| |
| Höhere Leistung in Anwendungen des maschinellen Lernens: Beschleunigung von Berechnungen für neuronale Netzwerke und andere lernbasierte Algorithmen.
| |
| Der NEON-Coprozessor macht den Raspberry Pi zu einer idealen Plattform für Entwickler, die kostengünstige, leistungsstarke und energieeffiziente Lösungen für anspruchsvolle Rechenaufgaben suchen.
| |
| | |
| Das LANE-Prinzip (LANE steht für "Lane-based") ist ein Konzept, das beim Design von SIMD (Single Instruction, Multiple Data)-Architekturen, wie dem NEON-Coprozessor in ARM64-Prozessoren, Anwendung findet. Hierbei wird der SIMD-Registersatz in mehrere "Lanes" unterteilt, sodass parallele Operationen effizient ausgeführt werden können. Lassen Sie uns tiefer in das LANE-Prinzip eintauchen und seine Bedeutung und Implementierung im Kontext des NEON-Coprozessors des Raspberry Pi betrachten.
| |
| | |
| LANE-Prinzip: Überblick
| |
| Grundlagen des LANE-Prinzips:
| |
| | |
| Lanes: Eine "Lane" ist ein Kanal innerhalb eines SIMD-Registers, der unabhängig von den anderen Lanes operationell sein kann. Jede Lane kann mehrere Datenwerte gleichzeitig verarbeiten.
| |
| SIMD-Parallele Verarbeitung: SIMD ermöglicht die gleichzeitige Ausführung der gleichen Operation auf mehrere Datenpunkte. Das LANE-Prinzip unterteilt die Daten in separate Lanes, wobei jede Lane eine Teilmenge der Daten repräsentiert.
| |
| Register und Datentypen:
| |
| | |
| NEON verwendet 128-bit Register (V-Register), die in mehrere Lanes unterteilt werden können:
| |
| 16 Lanes von 8-bit Daten (Vn.16B)
| |
| 8 Lanes von 16-bit Daten (Vn.8H)
| |
| 4 Lanes von 32-bit Daten (Vn.4S)
| |
| 2 Lanes von 64-bit Daten (Vn.2D)
| |
| Jede Lane kann unabhängig operieren, wodurch parallele Berechnungen möglich werden.
| |
| Operationen im LANE-Prinzip:
| |
| | |
| Vektor-Addition: Kann so implementiert werden, dass jede Lane eines Registers parallel zur Addition genutzt wird.
| |
| Vektor-Multiplikation: Multipliziert paarweise Elemente in Lanes parallel.
| |
| Datenumschichtung: Operationen können basierend auf Lanes Daten umschichten; zum Beispiel das Vertauschen von Lanes innerhalb eines Registers.
| |
| Vorteile des LANE-Prinzips
| |
| Effizienz und Parallelisierung:
| |
| | |
| Durch das Aufteilen von Daten in Lanes und die parallele Verarbeitung kann die Effizienz und Geschwindigkeit erheblich gesteigert werden.
| |
| Parallele Berechnungen reduzieren die Ausführungszeit von Operationen, insbesondere bei datenintensiven Anwendungen wie Signalverarbeitung, Grafikbearbeitung und wissenschaftlicher Berechnungen.
| |
| Deterministische Leistung:
| |
| | |
| Jede Lane operiert unabhängig, was zu einer deterministischen und vorhersehbaren Leistungsverbesserung führt. Das bedeutet, dass die Ausführungszeit für SIMD-Operationen gut vorhersagbar ist.
| |
| Reduzierte Speicherzugriffe:
| |
| | |
| Durch die Fähigkeit, mehrere Datenpunkte gleichzeitig zu verarbeiten, werden Speicherzugriffe reduziert, was die Gesamtleistung erhöht und den Datendurchsatz verbessert.
| |
| Beispiele von NEON-Operationen unter Verwendung des LANE-Prinzips
| |
| Vektoraddition von zwei 128-Bit-Registern:
| |
| // Beispiel: Addiere zwei Vektoren von vier 32-Bit-Werten (4 Lanes)
| |
| VADD.F32 Q0, Q1, Q2 // Addiert die 4 x 32-Bit Floating-Point-Werte in Q1 und Q2, Ergebnis in Q0
| |
| In diesem Fall:
| |
| | |
| Q1 ist in 4 Lanes von 32-bit Floating-Point-Werten unterteilt.
| |
| Q2 ist ebenso unterteilt.
| |
| Jede Lane wird parallel addiert, und das Ergebnis wird in der entsprechenden Lane von Q0 gespeichert.
| |
| Vektorladen und -speichern:
| |
| // Beispiel: Lade und speichere Daten mit Lanes
| |
| VLD1.32 {Q0}, [R0] // Lade vier 32-bit-Werte von der Speicheradresse in R0 nach Q0
| |
| VST1.32 {Q0}, [R1] // Speichere vier 32-bit-Werte aus Q0 an die Speicheradresse in R1
| |
| Die Vektorinstruktionen arbeiten mit Lanes, um gleichzeitig mehrere Daten zu laden und zu speichern.
| |
| | |
| Anwendung und Kontext im Raspberry Pi
| |
| Mediensysteme und Bildverarbeitung: Anwendungen wie Videokodierung/-dekodierung, Bildverarbeitung, Audioprozessoren nutzen intensiv die parallele Verarbeitung von NEON und das LANE-Prinzip zur Beschleunigung von Algorithmen.
| |
| Maschinelles Lernen: Leistungsfähige Netzwerke und Algorithmen für maschinelles Lernen können durch die parallele Verarbeitung von NEON optimiert werden.
| |
| Digitale Signalverarbeitung (DSP): Frequenzanalyse, Filterung und andere DSP-Anwendungen profitieren von SIMD-Operationen.
| |
| Der NEON-Coprozessor, der das LANE-Prinzip nutzt, macht ARM64-Prozessoren wie die im Raspberry Pi besonders effizient für moderne, rechenintensive Anwendungen.
| |
| | |
| Beispiel, wie Sie die 4D-Vektordistanz in ARM64-Assembler mit NEON-Instruktionen berechnen können. Diese Berechnung beinhaltet typischerweise die Schritte:
| |
| | |
| Subtrahiere die entsprechenden Komponenten zweier Vektoren.
| |
| Quadriere die resultierenden Differenzen.
| |
| Addiere die quadrierten Differenzen.
| |
| Ziehe die Quadratwurzel des Sums, um die euklidische Distanz zu erhalten.
| |
| Beispiel: Berechnung der euklidischen Distanz zwischen zwei 4D-Vektoren
| |
| Angenommen, wir haben zwei 4D-Vektoren A und B, deren Komponenten in den NEON-Registern V0 und V1 gespeichert sind. Dieser Beispielcode verwendet 32-bit Gleitkommazahlen zur Darstellung der Vektorkomponenten.
| |
| | |
| Register-Setup:
| |
| V0: enthält den 4D-Vektor A mit den Komponenten [A0, A1, A2, A3]
| |
| V1: enthält den 4D-Vektor B mit den Komponenten [B0, B1, B2, B3]
| |
| V2: verwendet zur Zwischenspeicherung der Differenzen
| |
| V3: verwendet zur Zwischenspeicherung der quadratischen Differenzen
| |
| V4: verwendet zur Zwischenspeicherung der Summe der quadratischen Differenzen
| |
| ARM64-Assembly-Code:
| |
| // Nehmen wir an, V0 und V1 sind bereits mit 4D-Float-Vektoren geladen
| |
| | |
| // Schritt 1: Subtrahiere die Komponenten
| |
| FSUB V2.4S, V0.4S, V1.4S // V2 = V0 - V1
| |
| | |
| // Schritt 2: Quadriere die Differenzen
| |
| FMUL V3.4S, V2.4S, V2.4S // V3 = V2 * V2
| |
| | |
| // Schritt 3: Addiere die quadrierten Differenzen
| |
| FADDV S4, V3.4S // S4 = V3[0] + V3[1] + V3[2] + V3[3]
| |
| | |
| // Schritt 4: Ziehe die Quadratwurzel der Summe
| |
| FSQRT S4, S4 // S4 = sqrt(S4)
| |
| Erklärung:
| |
| Subtraktion der Komponenten:
| |
| | |
| FSUB V2.4S, V0.4S, V1.4S
| |
| FSUB V2.4S, V0.4S, V1.4S: Subtrahiert die jeweiligen Elemente der Vektoren V0 und V1 und speichert das Ergebnis in V2. Dies führt zu V2 = [A0-B0, A1-B1, A2-B2, A3-B3].
| |
| Quadrieren der Differenzen:
| |
| | |
| FMUL V3.4S, V2.4S, V2.4S
| |
| FMUL V3.4S, V2.4S, V2.4S: Quadriert die Differenzen und speichert sie in V3. Dies führt zu V3 = [(A0-B0)², (A1-B1)², (A2-B2)², (A3-B3)²].
| |
| Addition der quadratischen Differenzen:
| |
| | |
| FADDV S4, V3.4S
| |
| FADDV S4, V3.4S: Addiert alle Elemente in V3 und speichert das Ergebnis in S4. Dies führt zu S4 = (A0-B0)² + (A1-B1)² + (A2-B2)² + (A3-B3)².
| |
| Quadratwurzel der Summe:
| |
| | |
| FSQRT S4, S4
| |
| FSQRT S4, S4: Berechnet die Quadratwurzel des Ergebnisses in S4, wodurch die euklidische Distanz zwischen den Vektoren A und B entsteht.
| |
| Zusammenfassung:
| |
| Diese Assembly-Sequenz berechnet die euklidische Distanz zwischen zwei 4D-Vektoren effizient mit NEON-Instruktionen, indem sie die parallele Verarbeitung von Vektordaten in SIMD-Architekturen nutzt.
| |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| * Interaktion mit anderen Programmiersprachen
| |
| * Zugriff auf Hardwaregeräte
| |
| * Anweisungen für den Gleitkommaprozessor
| |
| * Anweisungen für den NEON-Prozessor
| |