Unterschied in der Arbeitsweise von Assembler & Compiler?

Stannis

Lieutenant
Registriert
Juli 2011
Beiträge
601
Ich hätte gerne mal eine Verständnisfrage gestellt, weil mir immer noch einige Dinge nicht ganz klar sind, was die Arbeitsweise von Compilern angeht.

Assembler ist klar:
Die Assemblersprache wird beim assemblieren nahezu unmittelbar in entsprechende Binärsequenzen umgewandelt, die dann von einer CPU abgearbeitet werden, entsprechende Schaltwerke setzen usw. Juhu.

Aber was nun das compilieren angeht...
Meinem Verständnis nach muss es doch so sein, dass bspw. ein C-Compiler den C-Code runter in den entsprechenden Assemblercode wandelt, der dann wiederum assembliert werden müsste.
Dem scheint aber nicht so zu sein, oder? Die Compiler, die Ich so kenne, erzeugen unmittelbar Maschinencode, bspw. als Hex-file, falls man das für den µ-Controller will. Ist in einem Compiler nun ein Assembler mit eingebaut?

Meinem Verständnis nach geht es nicht wirklich, diesen Zwischenschritt zu überspringen, da ja jede CPU-Familie eine spezifische Assemblersprache hat, in die der Code gewandelt werden muss.
 
Du hast hier schon einige logische Annahmen getätigt. Vor allem geht es darum eine für Menschen komfortablere Schreibweise eines Programms letztendlich in maschinenverständliche Form zu wandeln. Dabei überläßt man dem Compiler, je höher die Sprsche angesiedelt ist, viele Dinge selbst zu entscheiden (wie z.B. Registerwahl und aritmethische Verfahren). Das gelingt mehr oder weniger efizient. Darum wurden in früheren Jahren selbst im kommerziellen Bereich zeitkritische Rouinen oder solche, die sehr oft genutz wurden (wie z.B. das Sortierverfahren) ausschlißlich nicht in Hochsprachen programmiert, sondern eben in Assembler. Im weitesten Sinne ist auch schon Assembler eine höhere Sprache, aber dennoch schon sehr maschinenennah. Du siehst in beiden Fällen ist eine Umwandlung in Maschinencode erforderlich. Daraus ersiehst du die Tatsache, dass es nicht wirklich zielführend wäre eine doppelte Umwandlung durch zu führen. Also gibt es keinen Grund ein Hochspracheprogramm zuerst in ein Assemblerprogramm zu wandeln. Das wäre "Doppelt Gemoppelt". Ich hoffe etwas Licht in aller Kürze in die Sache gebracht zu haben.
 
Die kürzeste Erklärung dazu: Assembler ist eine sehr Maschinennahe Sprache und wird von der CPU quasi ohne zu tun und automatisch weiter übersetzt, so dass ausführbarer Code zur Verfügung steht. Dass Compilen ist der Vorgang, aus einer Hochsprache per Programm Assembler Code zu erzeugen. Je nach Wahl des Compilers kann man aus ein und denselben Code Hochsprache Assembler für verschiedene CPUs erzeugen.
 
Aber was nun das compilieren angeht...
Meinem Verständnis nach muss es doch so sein, dass bspw. ein C-Compiler den C-Code runter in den entsprechenden Assemblercode wandelt, der dann wiederum assembliert werden müsste.
Dem scheint aber nicht so zu sein, oder?
Doch, genau so ist es. Hab den Schalter nicht mehr im Kopf, aber dem gcc kann man sagen, er soll vor dem Assembler aufhören und den Assemblercode ausgeben.

Je nach Sprache wird aber auch nicht direkt Assembler erzeugt, sondern eine Zwischensprache, meist in Form von Bytecode (z.B. bei Java oder .net). Dieser Bytecode wird dann von der passenden Laufzeitumgebung interpretiert und dabei in Maschinencode umgesetzt (... zumindest grob in der Art).
 
KillerCow's Aussage scheint der von Ost-Ösi zu widersprechen. Letzterer scheint zu verkünden, dass Quellcode -> Assembler -> Maschinencode nicht ausgeführt wird, sondern unmittelbar Quellcode -> Maschinencode.

edit:
Und woher weiß nun bspw. gcc, in welche Assemblersprache er runterbasteln muss?
Könnte Ich ihm das auch vorgeben?
 
Zuletzt bearbeitet:
edit:
Und woher weiß nun bspw. gcc, in welche Assemblersprache er runterbasteln muss?
Könnte Ich ihm das auch vorgeben?
Naja gcc erkennt ja die Plattform auf der er läuft und nutzt diese als default:
http://stackoverflow.com/questions/11727855/obtaining-current-gcc-architecture
google sonst mal nach 'gcc default architecture'
Wenn du aber einen Crosscompiler installierst (zB den hier https://launchpad.net/gcc-arm-embedded für µC oder den hier für andere ARMs: http://askubuntu.com/questions/250696/cross-compile-for-arm) kannst du auch unter x86 ARM generieren.. standardmäßig ist das nicht alles mit installiert.
Über weitere flags wie -O3 oder was es da noch so gibt (https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html) kann man auch festlegen, welche spezielleren Bereiche einer CPU verwendet werden sollen (MMX, AVX, SSE2, ...)
Mit -O3 und -march native entscheidet gcc selbst und holt passend für deine CPU schon nahezu alles raus.
 
Streng genommen ist ein Compiler erst mal alles, was ein Programm von einer Sprache in eine andere übersetzt. So gesehen ist ein Assembler einfach nur ein besonderer Compiler, bei dem es ein 1:1 Mapping zwischen Befehlen der Quellesprachen (Assembler) und der Zielsprache (Binärer Maschinencode) gibt.

Für einen z.B. C++-Compiler ist es insofern trivial je nach Bedarf entweder Assembler oder Binärcode zu erzeugen (er muss einfach nur das "Wörterbuch" austauschen), ich bezweifle daher, dass moderne Compiler wirklich erst Assemblercode erzeugen und dann den Assembler als extra Tool einsetzen.

Die meisten Compiler übersetzen den Quellcode übrigens Erstmal in eine Platformunabhängige "Zwischensprache". Diese Repräsentation wird dann genutzt um das Programm zu optimieren und den endgültigen Maschinencode zu generieren. Die Zielarchitektur hängt übrigens vom gewählten Compiler (genauer gesagt dem Backend ab). Es gibt z.B. einen GCC für x86, einen für ARM, einen für MIPS etc. Diese teilen sich natürlich große Teile des Codes wie z.B. das Frontend (also der Teil, der z.B. c++ code in die Zwischenrepräsentation umwandelt) und Platformunabhängige Optimierungstechniken.
 
Das klingt für mich auch einleuchtend, Miuwa.

Wenn wir nun also davon ausgehen, dass ein Hochsprachencompiler fertigen Maschinencode ausspuckt, dann entspricht das ja ohnehin mehr oder weniger dem Assemblercode - letzterer ist ja 1:1 Maschinencode, nur noch binär aufgeschlüsselt, sondern durch Codewörter und Hexzahlen aufgebaut.
An der Assembler-Struktur würde beim assemblieren also sowieso nichts mehr geändert werden wie es beim compilen der Fall ist.

Ich bin begeistert :] Ich danke euch für das informative Gespräch.
 
Und eben weil die Assemblersprache quasi eine 1:1-Repräsentation vom Bytecode der jeweiligen Maschine ist, muss der Compiler auch gar nicht damit arbeiten, sondern kann eine handlichere Form verwenden, die aber auch nicht dem Bytecode entsprechen muss. Im Grunde kannst du die Instruktionen als einfache Struktur aus einem Operationstyp und Operanden begreifen. Mal ein ganz einfaches Beispiel:

Code:
enum Reg {
  r0, r1, /* ... */, r15 // normale Register
}

struct Mem {
  base  : Reg,
  idx   : Reg,
  shift : u32,
  ofs   : u32,
}

enum Instruction {
  Add(Reg, Reg, Reg),  // op0 := op1 + op2
  Sub(Reg, Reg, Reg),  // op0 := op1 - op2
  Load (Reg, Mem),  // op0 := Mem[op1.base + (op1.idx << op1.shift) + op1.ofs]
  Store(Mem, Reg),  // Mem[op0.base + (op0.idx << op0.shift) + op0.ofs] := op1
}

Wenn du dir jetzt Bytecode ausgeben lassen willst:
Code:
fn op_id(r : Reg) {
  match r {
    Reg::r0 => 0x00,
    // ...
    Reg::r15 => 0x0F,
  }
}

fn encode_simple_op(opCode : u32, op1 : Reg, op2 : Reg, op3 : Reg) -> u32 {
  opCode
    | (op_id(op1) << 4)
    | (op_id(op2) << 8)
    | (op_id(op3) << 12)
}

fn encode(ins : Instruction) -> u32 {
  match (ins) {
    Add(r1, r2, r3) => encode_simple_op(0x00, r1, r2, r3),  // 0x00 Op-Code für Addition
    Sub(r1, r2, r3) => encode_simple_op(0x01, r1, r2, r3),  // 0x01 Op-Code für Subtraktion
  }
}

Wenn du dir Text (also in Assemblersprache) ausgeben lassen willst:
Code:
fn encode_reg(r : Reg) -> String {
  format!("r{}", op_id(r).to_string())  // z.B. "r5" für r5
}

fn encode_simple_op(opCode : String, op1 : Reg, op2 : Reg, op3 : Reg) -> u32 {
  format!("{} {} {} {}", opCode, encode_reg(op1), encode_reg(op2), encode_reg(op3))
}

fn encode_ins(ins : Instruction) -> String {
  match ins {
    Add(r1, r2, r3) => encode_simple_op("add", r1, r2, r3),
    Sub(r1, r2, r3) => encode_simple_op("sub", r1, r2, r3),
  }
}

Oder falls du das ganze interpretieren, also sofort ausführen möchtest:
Code:
struct Context {
  regs : u32[16],
}

fn execute_ins(context : &mut Context, ins : Instruction) {
  match ins {
    Add(r1, r2, r3) => { context.regs[op_id(r1)] = context.regs[op_id(r2)] + context.regs[op_id(r3)]; },
    Sub(r1, r2, r3) => { context.regs[op_id(r1)] = context.regs[op_id(r2)] - context.regs[op_id(r3)]; },
  }
}

Stichwort Abstrakter Syntaxbaum. Aus dem Baum eines C-Programms kannst du auch relativ einfach den entsprechenden Baum für die Maschinensprache generieren. Du musst dir nur überlegen, wie du die einzelnen Features der Sprache in eben jener Maschinensprache umsetzt (also z.B. Integer-Addition, if-Anweisungen, ...)
 
Zuletzt bearbeitet:
Zurück
Oben