Kurs:Wie funktioniert eigentlich ein Computer/Hauptthemen/Assembler

Aus Wikiversity

Diese Seite gibt eine Einfürung in die Programmierung in Assembler anhand des Tutorials Crashkurs ARM Assembler (thinkingeek.com).

Definition[Bearbeiten]

Was ist Assembler? Assembler ist eine sehr hardwarenahe Programmiersprache. Die Assemblersprache ist je nach Befehlssatz der CPU (Central Processing Unit oder auch Prozessor) individuell zugeschnitten. Durch seine Hardwarenähe kann Assembler helfen ein Verständnis für die Rechnerarchitektur und seine Arbeitsweise zu bekommen. Zudem ist vom Menschen geschriebener Assemblercode um ein Vielfaches schneller als von einem Compiler generierter. Im Assemblercode werden die programmierten Befehle und Operanden grundlegend verständlich für den Menschen dargestellt. Fundamental für Assembler-Programmierung sind sogenannte Labels. Labels sind nur symbolische Namen für Speicheradressen. Diese Adressen können sowohl Daten als auch Programmcode enthalten. Ein Label bezeichnet immer nur die Adresse, nie ihren Inhalt. Labels ermöglichen dem Programmierer im Programmcode zu springen, bzw. an bestimmten Stellen bestimmte Adressen aufzurufen.

Einführung[Bearbeiten]

Assembler ist ein Begriff mit mehreren Bedeutungen. Es steht einmal für die Assembler-Sprache(Syntax), aber ursprünglich mient es das Programm, das diesen Code "assembled", also zusammenbaut. Assembler-Code ist eine sehr hardwarenahe Sprache, die direkt auf die Prozessorbefehle zugreifen kann.

Das erste Programm[Bearbeiten]

/* -- first.s */
.global main /* 'main' ist der Einstigspunkt und mussglobal sein */
.func main   /* 'main' ist eine Funktion*/
 
main:          /* main wird definiert */
    mov r0, #2 /* in Register 0 wird eine 2 geschrieben */
    bx lr      /* Programm beenden */

Zuerst wird ein globales Label "main" definiert und als Funktion markiert. Dann wird definiert, welche Befehle die Funktion ausführt. Der "mov" Befehl transportiert Werte zwischen Registern hin und her. In diesem Fall wird eine 2 in das Register "r0" geschrieben. Der Befehl "bx lr" ist ein Sprungbefehl, der an die Stelle im Register "lr" springt. Diese gibt standardmäßig die Stelle zum Aussteigen aus dem Programm an.
Um das Programm ausführen zu können, muss es in zwei Schritten kompiliert werden. Zuerst wird mit dem Konsolenbefehl "as first.s -o first.o" der Assemblercode assembled und in die Datei "first.o" geschrieben. Damit das Programmm nun auf dem Betriebssystem funktioniert, muss es mit "gcc first.o -o first" erneut kompiliert werden und ist nun mit "./first" ausführbar. Wenn man "./first; echo $?" ausführt, wird der Fehlercode des Programms ausgegeben. Der Fehlercode ist immer im Register "r0" gespeichert und weil wir gerade eine 2 dort hineingeschrieben haben, wird eine 2 ausgegeben.

Ein Assembler-Programm, dass als Fehlercode 2 ausgibt

Grundlegende Arithmetik[Bearbeiten]

Jeder Prozessor ist in der Lage, einige logische und Arithmetische (durch das Rechenwerk) Operationen durchzuführen. Dazu benutzt er die Informationen bzw. Daten, welche in den Registern gespeichert sind. Die Register sind zumindest im ARM Prozessor kleine Speichereinheiten, in denen, 32 Bits gespeichert werden können. Einige Register haben Sonderfunktionen, wie zum Beispiel r15, der als Programm Counter (siehe Referat: Leitwerk) fungiert. Grundsätzlich kann man die Register jedoch für alles und jede Information gebrauchen.

Eine Beispielhafte Operation, die der Prozessor durchführt, ist die Addition zweier Register.

Unterschiede zur Hochsprache C[Bearbeiten]

Worin liegt der Unterschied zwischen Assembler-Programmierung und der Programmierung in C? C ist eine Hochsprache, d.h. eine höhere Programmiersprache. Programmiersprachen dienen lediglich dazu, dem Programmierer eine Möglichkeit zu bieten, den Code für den Menschen verständlich zu schreiben. Der Computer würde mit einem C-Programm nichts anfangen können. Assemblercode befindet sich bereits sehr nahe am Maschinencode. Er kann daher auch direkt in Maschinenbefehle übersetzt werden, wohingegen die Hochsprachen komplexere Kompiliervorgänge benötigen. Kompilieren meint das Übersetzen von menschenlesbarem Code zu maschinenlesbarem Code, ein Compiler ist also ein Programm, welches den C- oder Assemblercode in Binärcodes übersetzt. Beim Kompilieren eines Programms in C entsteht mehr Binärcode als beim Kompilieren des Assemblers. Der Grund dafür ist zudem einer der wesentlichen Unterschiede zwischen Assembler und C. Ein Compiler, welcher für eine höhere Programmiersprache geschrieben wird, muss stark generalisiert geschrieben werden. C-Programmcode enthält zum Beispiel Variablen, diese sind deklariert und i.d.R. initialisiert, d.h. sie besitzen einen Bezeichner, einen Wert und sind von einem bestimmten Typ, im Assemblercode jedoch wird jeder Variable zusätzlich eigene Speicheradresse zugewiesen.

Branches[Bearbeiten]

Beim ARM Prozessor im Raspberry Pi ist das 16 Register (r15) der Programmcounter. Normalerweise wird er immer um 4 (da 32bit Architektur) erhöht, dadurch wird ein Befehl nach dem anderem ausgeführt(implicit sequencing).

Unconditional Branches[Bearbeiten]

Mit einer Instruktion springt man immer zu einem bestimmten Label. Beispiel

.text
.global main
main:
    mov r0, #2 
    b end      /* branch to 'end' */
    mov r0, #3 
end:
    bx lr

Das Register r0 bleibt bei dem Wert 2, da der zweite move Befehl übersprungen wird.

Conditional Branches[Bearbeiten]

Bevor wir bedingte Branches machen können, müssen wir zunächst wissen, was das Current Program Status Register (cprs) ist. Mit dem Befehl cmp (compare) und zwei Parametern (Registern) werden die beiden Zahlen verglichen. Es kann folgende "condition code flags" darstellen:

  • N (negative)
Ist das Ergebnis negativ ?
  • Z (zero)
Ist das Ergebnis 0 ?
  • C (carry)
lässt sich die Zahl nichtmehr mit 32bit darstellen?
  • V (overflow):
hat man ein Problem mit vorzeichenbehaften Bits?

Bedingungen: //keine Ahnung welche man davon zur Übersichtlichkeit streichen sollte :)

  • EQ (equal)
  • NEQ (not equal)
  • GE (greater or equal than, in two’s complement)
  • LT (lower than, in two’s complement)
  • GT (greater than, in two’s complement)
  • LE (lower or equal than, in two’s complement)
  • MI (minus/negative)
  • PL (plus/positive or zero)
  • OS (overflow set)
  • OC (overflow clear)
  • HI (higher)
  • LS (lower or same)
  • CS (carry set)
  • CC (carry clear)

Diese Bedingungen lassen sich mit dem Branch-Befehl kombinieren, dadurch lässt sich dann ein conditional Branch ausführen. Beispiel :

.text
.global main
main:
    mov r1, #2       
    mov r2, #2       
    cmp r1, r2       /* update cpsr condition codes with the value of r1-r2 */
    beq case_equal   /* branch to case_equal only if Z = 1 */
case_different :
    mov r0, #2       
    b end            /* branch to end */
case_equal:
    mov r0, #1       
end:
    bx lr

Man muss am Ende von case_different b_end ausführen, da man sonst als nächstes das Label case_equal ausführen würde und der wert in r0 wieder überschrieben wird

Einordnung in den Prozess des Kompilierens[Bearbeiten]

Wo steht der Assembler im Prozess des Kompilierens? Programme werden heutzutage überwiegend in einer Hochsprache geschrieben, dies ist für den Menschen sehr viel einfacher / verständlicher als die Assemblerprogrammierung. Allerdings übersetzen viele Hochsprachencompiler den Code zuerst in Assemblercode, bzw. können diesen optimal ausgeben. Assemblercode kann dann von einem speziellen Compiler (Assembler) direkt in maschinenlesbaren Code übersetzt werden. Die Umsetzung von ausführbarer Maschinensprache zurück in Assembler ist ebenfalls möglich, dieser Vorgang wird Disassemblierung genannt. Im Prozess des Kompilierens befindet sich der Assembler also zwischen Hochsprache und Maschinencode. Er ist nicht zwingend nötig, kann dem Programmierer allerdings helfen ein sehr hardwarenahes Verständnis für die Funktionsweise eines Computers zu bekommen.

Beispiel: Addition zweier Zahlen[Bearbeiten]

Um Assembler zu verstehen kann das folgende Beispiel helfen. Als sehr einfaches Beispiel benutzen wir allein die Addition zweier Zahlen. (siehe Abbildung 1.1)

Abbildung 1.1

Erklärung: Im linken Teil der Abbildung ist das bereits genannte Programm zum Addieren zweier Zahlen in C geschrieben, im rechten Teil in Assemblersprache. Im C-Programm deklarieren wir die Variablen a und b vom Typ Integer und weißen ihnen die Werte 1 und 2 zu, ausgegeben wird dann a, welches sich aus der Addition von a und b ergibt. Der Code ist für den Menschen also sehr einfach zu verstehen. Der Assemblercode kann ebenfalls verstanden werden, ist allerdings sehr viel hardwarenäher. Die Zahl 1 wird durch den 'move'-Befehl in Register r1 geladen, die Zahl 2 in Register r2. Die Inhalte des Registers r1 und r2 werden dann addiert und in Register r1 geschrieben. Dieses banale Beispiel zeigt bereits wie die Assemblersprache einen kleinen Einblick in die Arbeitsweise eines Computers geben kann. Noch deutlicher wird dies, wenn man die zentralen 'load'-Befehle betrachtet.

Abbildung 1.2

In Abbildung 1.2 werden den Variablen myvar1 und myvar2 (also a und b) die Werte 1 und 2 zugewiesen. Danach lädt der 'load'-Befehl (ldr) die beiden Variablen in die Register r1 und r2. Was folgt ist die Addition. Interessante Quellen für weitere Informationen: http://thinkingeek.com/2013/01/09/arm-assembler-raspberry-pi-chapter-1/

Beispiel: der ARM-Assembler[Bearbeiten]

Im Kurs der Deutschen Schülerakademie Rostock 2013 programmierten wir den ARM-Assembler, eine 32-Bit-Prozessorarchitektur der Firma ARM. Diese hat sich beim ARM-Assembler ein zentrales Ziel gesetzt: maximale Flexibilität.