Programmieren mit ARM64 Assembler: Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
Zeile 527: Zeile 527:
* Mit Linux (Syscalls):
* Mit Linux (Syscalls):
nanosleep:
nanosleep:
   ldr x0,=
   ldr x0,=timespec
  ldr x1,=timespec
  mov x8,




.data
timespec:
timespec_tv_sec:  .dword 0
timespec_tv_nsec: .dword 100000000


Adressen finden:
Adressen finden:

Version vom 8. November 2024, 12:42 Uhr

Der ARM ist ein sogenannter RISC-Computer, was das Erlernen von Assembler theoretisch einfacher macht.

  • Wir verwenden Linux
  • Grundzustand von Rasbpian ist ausreichend
  • Zahlen

-> Dezimal, Binär, Hexadezimal

  • 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"


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


GPIO Programmierung

  • Mit Linux (Syscalls):

nanosleep:

 ldr x0,=timespec
 ldr x1,=timespec
 mov x8,


.data
timespec:
timespec_tv_sec:  .dword 0
timespec_tv_nsec: .dword 100000000

Adressen finden: dmesg genauer dmesg | grep gpio









  • Interaktion mit anderen Programmiersprachen
  • Zugriff auf Hardwaregeräte
  • Anweisungen für den Gleitkommaprozessor
  • Anweisungen für den NEON-Prozessor