015
18.04.2007, 01:40 Uhr
Pablo
Supertux (Operator)
|
Aaaaalso, ich finde viele Antworten hier grottenschlecht, wenn ich das so sagen darf "naja weil halt" bringt nicht weiter.
Diese Frage ist nicht leicht zu beantworten, zumindest in einem Forum, weil sie meiner Meinung nach recht lang sein kann. Ich hab zwar zu tun, aber diese Frage in wenig Worten zu beantworten ist ne schöne Herausforderung.
Also der Tipp mit dem Rechnerarchitektur Buch ist nicht so schlecht, denn man kann auch damit viel verstehen.
Der heutiger Rechner ist kein magisches Wesen, dass 'halt so' funktioniert sondern nach der Von-Neumann Architektur gebaut wird.
Rechner kennen im Prinzip 2 Zustände, wahr und falsch, oder 0 oder 1. Das heißt aber nicht, dass 0 "0 Volt" und 1 "5 Volt" entsprechen, sondern ja nach Architektur hat man Voltbereiche, die 0 bzw. 1. entsprechen.
Heutige Rechner haben im Prinzip die selben Grundbausteine: Speicher (RAM), CPU (wo die Operationen ausgeführt werden) und Register (wo man Werte zwischenspeichern kann). Einer solcher Register wird in der Regel PC (Program counter) genannt (oder auch instruction pointer). Dieser Register hat den Wert der Speichers, an der Stelle, wo der nächste Befehl geholt werden soll.
Es gibt die sogenannten RISC (Reduced Instruction Set Computing) und CISC (Complex Instruction Set Computing) Architekturen. RISC Prominente sind bspweise SPARC oder ARM Prozessoren, CISC intel x86 (wobei man sagen muss, dass es kaum reine RISC/CISC Rechner gibt). Der bedeutenste Unterschied zwischen beiden ist, dass RISC in der Regel eine feste Länge der Befehle hat (bsp: 4 bytes in der ARM Architektur) während CISC unterchiedliche Längen der Befehle haben kann, deswegen ist Pipelining auf CISC so gut wie unmöglich, aber das ist ein anderes Thema.
Ich will jetzt an dem Beispiel RISC bleiben, weil das viel einfacher zu erklären und zu verstehen ist, da man eine feste Länge für die Befehle festlegen kann. Sagen wir mal 4 Bytes pro Befehl. Damit Rechner vernünftig arbeiten, müssen sie Befehlen folgen. Welche Befehle einem zur Verfügung stehen, hängt von der Architektur ab, aber es gibt Befehle, die in den meisten Architekturen vorkommen, wie aritmetische Operationen oder Befehle zum Kopieren von Werten von den Registern in den Speicher und umgekehrt, usw. Sowas nennt man den Befehlsatz.
Da die Rechner an sich nur 0 und 1 verstehen können, muss man die Befehle kodieren, bsp. mit 32 Bits (oder 4 Bytes). Sagen wir man, wir haben folgende Befehle:
PabloAssembler: |
ADD r0 r1 r2 := r0=r1+r2 SUB ... MUL ... DIV ... MOVR rx n := rx=memory[n] MOVM rx n := memory[n]=rx STORE r0 <value> := r0=<value>
|
wobei rx Register sind und memory[n] ist der Wert im Speicher an der Stelle n.
Das wäre ein Beispiel für eine minimale Assembler Sprache. Aber diese Befehle müssen vom Rechner verstanden werden, kaum als ADD r0 r1 r1 sondern als eine Bitfolge von 0 und 1, d.h. wir müssen alle Befehle als Folgen von 0 und 1 kodieren. Wenn wir 4 Bytes pro Befehl zur Verfügung haben, dann können wir die Befehle wie folgt kodieren:
- die ersten 8 Bits (0, ..., 7) entsprechen den Befehlen (ADD, SUB, MUL, usw) - die nächsten 8 Bits (8, ... 15) entsprechen der Adresse des Registers rx - die nächsten 8 Bits (16, ... 23) ebenso, bei MOV*/STORE Befehlen die low Bits von n und <value> - die nächsten 8 Bits (24, ... 31) ebenso, bei MOV*/STORE Befehlen die high Bits von n und <value>
Dann sagen wir mal
PabloAssembler: |
ADD := 0000 0001 ... SUB := 0000 0010 ... MUL := 0000 0011 ... DIV := 0000 0100 ... MOVR := 1000 0001 ... MOVM := 1000 0010 ... STORE := 1100 0000 ....
|
Dann sagen wir mal, wir haben folgenden Code zum Ausführen
PabloAssembler: |
MOVR r1 16 ; r1 = memory[16]; MOVR r2 20 ; r2 = memory[20]; ADD r0 r1 r2 ; r0 = r1 + r2 SUB r3 r1 r2 ; r3 = r1 - r2 MUL r0 r0 r3 ; r0 = r0 * r3 MOVM r0 24 ; memory[24] = r0
|
Das Programm macht nichts anders als (a+b)*(a-b). Dieser Block befindet sich irgendwo im Speicher. Der Speicher kann nur 0 und 1 aufnehmen, d.h. die Befehle sind im Speicher kodiert (nach der obigen Kodierung), sagen wir mal PC:=k und die k. Stelle des Speicher ist der Anfang des Codes
Speicher: |
memory[k] = 1000 0001 0000 0001 0001 0000 0000 0000 (MOVR r1 16) memory[k+4] = 1000 0001 0000 0010 0001 0100 0000 0000 (MOVR r2 20) memory[k+8] = 0000 0001 0000 0000 0000 0001 0000 0010 (ADD r0 r1 r2) usw.
|
Der Rechner arbeitet in Zyklen, da er nicht alles auf einmal machen kann, d.h. es gibt ein Zyklus, wo der Rechner der Befehl aus dem Speicher holt (durch den Wert von PC) (fetch phase), dann muss der Rechner den geholten Befehl "verstehen", also dekodieren (decode phase). Die nächste Phase holt sich beispielsweise vom Befehl abhängig die Register Werte und legt sie der CPU vor (operand fetch phase). In der nächste Phase wird der Befehl tatsächlich von der CPU ausgeführt (execute phase). Die letzte Phase werden Register/Speicher geschrieben, wenn nötig (write-back phase).
Ich habe mir also einen Rechner mit 5 Zyklen (Phasen) gebaut. Also PC=k
Phase 1: fetch phase Kopiere den Wert des Speichers an der Stelle PC ( =k ) in den IR (Instruction register) --> IR = 1000 0001 0000 0001 0001 0000 0000 0000
Phase 2: decode Phase Der Rechner wird jetzt den Befehl dekodieren. Der Rechner weiß welche Bitmuster er suchen soll. Die ersten 8 Bits enstprechen dem Befehl. In diesem Fall 1000 0001. Der Rechner weiß, dass dieses Muster dem Befehl MOVR entsrpicht und schaltet gewiesse MUX Gatter an den Bussen auf 1. Der Rechner weiß auch, dass die nächsten 8 Bits den Registern entsprechen: 0000 0001 also r1. Die nächsten 8 Bits sind die low Bits von n: (siehe Tabelle oben) 0001 0000 = 16. Die nächsten Bits sind die high Bits von n: 0000 0000 = 0. Also ist n = 16 + 0*2^16 = 16. D.h. der Rechner weiß, dass an der Stelle k des Speichers der Befehl MOVR r1 16 steht.
Phase 3: operand fetch Jetzt holt er die nötige Werte für die Register und vom Speicher und legt sie der CPU vor. In diesem Fall den gespeicherten Wert des Speichers an der Stelle 16.
Phase 4: execute Naja, hier muss die CPU nicht viel tun. Wäre der Befehl bspweise ADD, dann würde die CPU die vorgelegten Werten addieren.
Phase 5: write-back Speicher und Register, die beschrieben werden müssen, werden beschrieben, in diesem Fall r1 mit dem Wert des Speichers an der Stelle 16.
Der Rechner inkrementiert den PC um 4 (weil die Befehle 4 Byte breit sind), somit zeigt PC auf k+4 und Phase 1 startet noch einmal, usw. Nach 24 Zyklen ist der gesamte Code ausgeführt.
Ich hoffe, dass du (Suba Esel) es einigermassen verstanden hast, wie ein Rechner arbeitet.
So, da du jetzt ungefähr weißt, wie ein Rechner arbeitet, kann man verstehen, was es mit den compilierten Codes auf sich hat: Der Compiler einer Sprache muss den Code in Maschinencode übersetzen. Dabei muss der Compiler den Befehlsatz des Zielprozessors wissen, denn der Compiler/Linker macht im Prinzip nicht anders [1] als den Code in Assembler zu übersetzen und eine Binary Datei auszugeben, die die Assembler Befehle als Folgen von 0 und 1 speichert. Wenn man das Programm ausführen willst, kopiert das OS den binary Code, sprich diese Folge von 0 und 1, in den Speicher und setzt den PC auf die Adresse, wo der Code anfängt. Und du weißt schon, was passiert, wenn der PC auf den Anfang eines Codes zeigt: der Code wird ausgeführt, sprich dein Programm läuft.
So, ich hoffe, ich konnte es einigermassen verständlich erklären. Ich habe einige Aspekte oben vernachlässigt oder einfach ignoriert (Caches, Byte alignment bei MOVM/STORE Befehle und solche Geschichten)
[1] In Wirklichkeit kommt noch mehr dazu, aber das würde dich jetzt mehr verwirren als helfen -- A! Elbereth Gilthoniel! silivren penna míriel o menel aglar elenath, Gilthoniel, A! Elbereth! Dieser Post wurde am 18.04.2007 um 01:50 Uhr von Pablo editiert. |