web-dev-qa-db-de.com

Wofür kompilieren C und Assembler eigentlich?

Also fand ich heraus, dass C (++) - Programme tatsächlich nicht zu "Binär" kompilieren sind (ich habe hier möglicherweise einige Probleme, in diesem Fall tut es mir leid: D), sondern zu einer Reihe von Dingen (Symboltabelle) , os-bezogene Sachen, ...) aber ...

  • Kompiliert Assembler zu reinem Binärcode? Das bedeutet keine zusätzlichen Dinge außer Ressourcen wie vordefinierte Zeichenfolgen usw.

  • Wenn C zu etwas anderem als einer reinen Binärdatei kompiliert, wie kann dieser kleine Assembler-Bootloader dann einfach die Anweisungen von der Festplatte in den Speicher kopieren und ausführen? Ich meine, wenn der OS-Kernel, der wahrscheinlich in C geschrieben ist, zu etwas anderem kompiliert als zu reinem Binär - wie geht der Bootloader damit um?

edit: Ich weiß, dass der Assembler nicht "kompiliert", weil er nur den Befehlssatz Ihres Computers enthält - ich habe kein gutes Wort für das gefunden, was der Assembler "zusammensetzt". Wenn Sie einen haben, lassen Sie ihn hier als Kommentar und ich werde ihn ändern.

44
lamas

C kompiliert normalerweise zum Assembler, nur weil dies dem armen Compiler-Writer das Leben leicht macht.

Assembler-Code wird immer zusammengestellt (nicht "kompiliert") zu verschiebbarem Objektcode . Sie können sich dies als binären Maschinencode und binäre Daten vorstellen, jedoch mit viel Dekoration und Metadaten. Die wichtigsten Teile sind:

  • Code und Daten werden in benannten "Abschnitten" angezeigt.

  • Verschiebbare Objektdateien können Definitionen von labels enthalten, die sich auf Positionen innerhalb der Abschnitte beziehen.

  • Versetzbare Objektdateien können "Löcher" enthalten, die mit den an anderer Stelle definierten Werten von Etiketten gefüllt werden sollen. Der offizielle Name für ein solches Loch ist ein Umzugseintrag .

Zum Beispiel, wenn Sie dieses Programm kompilieren und zusammenbauen (aber nicht verknüpfen)

int main () { printf("Hello, world\n"); }

sie werden wahrscheinlich mit einer verschiebbaren Objektdatei mit

  • Ein text-Abschnitt, der den Maschinencode für main enthält

  • Eine Etikettendefinition für main, die auf den Anfang des Textabschnitts zeigt

  • Ein Abschnitt mit rodata (schreibgeschützte Daten), der die Bytes des Zeichenfolgenliteral "Hello, world\n" enthält.

  • Ein Umzugseintrag, der von printf abhängt und auf eine "Lücke" in einer Aufrufanweisung in der Mitte eines Textabschnitts verweist.

Wenn Sie sich auf einem Unix-System befinden, wird eine verschiebbare Objektdatei im Allgemeinen als .o-Datei bezeichnet, wie in hello.o. Sie können die Etikettendefinitionen und -verwendungen mit einem einfachen Tool namens nm untersuchen und detailliertere Informationen dazu erhalten kompliziertes Werkzeug namens objdump.

Ich unterrichte eine Klasse, die sich mit diesen Themen befasst, und ich habe Studenten, die einen Assembler und einen Linker schreiben, was einige Wochen dauert, aber wenn sie das getan haben, haben die meisten von ihnen einen ziemlich guten Umgang mit dem verschiebbaren Objektcode. Es ist nicht so einfach.

40
Norman Ramsey

Nehmen wir ein C-Programm.

Wenn Sie "gcc" oder "cl" für das Programm "c" ausführen, durchläuft es folgende Schritte:

  1. Preprozessor-Lexing (#include, #ifdef, Trigrafeanalyse, Kodierung von Übersetzungen, Kommentarverwaltung, Makros ...)
  2. Lexikalische Analyse (Erzeugung von Token und lexikalischen Fehlern).
  3. Syntaktische Analyse (Syntaxanalyse und Syntaxfehler).
  4. Semantische Analyse (Erstellung einer Symboltabelle, Informationen zum Umfang und Angaben zu Fehlern/Tippfehlern).
  5. Ausgabe in Assembly (oder ein anderes Zwischenformat)
  6. Optimierung der Montage (wie oben). Wahrscheinlich noch in ASM-Strings.
  7. Assembling der Assembly in ein binäres Objektformat.
  8. Verknüpfen der Assembly mit den benötigten statischen Bibliotheken sowie bei Bedarf Verschieben.
  9. Ausgabe der endgültigen ausführbaren Datei im Elf- oder Coff-Format.

In der Praxis können einige dieser Schritte gleichzeitig ausgeführt werden, dies ist jedoch die logische Reihenfolge.

Beachten Sie, dass es um die eigentliche ausführbare Binärdatei einen 'Container' mit Elf- oder Coff-Format gibt.

Sie werden feststellen, dass ein Buch über Compiler (ich empfehle das Dragon Buch, das Standard-Einführungsbuch im Feld) all die Informationen, die Sie benötigen, und vieles mehr.

Wie Marco kommentierte, ist das Verknüpfen und Laden ein großer Bereich und das Dragon-Buch bleibt mehr oder weniger an der Ausgabe der ausführbaren Binärdatei stehen. Von dort tatsächlich auf ein Betriebssystem zu gehen, ist ein ziemlich komplexer Prozess, den Levine in Linkers und Loaders abdeckt.

Ich habe diese Antwort wiki, damit die Leute Fehler korrigieren/Informationen hinzufügen können. 

35
Paul Nathan

Es gibt verschiedene Phasen bei der Übersetzung von C++ in eine binäre ausführbare Datei. Die Sprachspezifikation gibt die Übersetzungsphasen nicht explizit an. Ich werde jedoch die üblichen Übersetzungsphasen beschreiben. 

Quellc ++ zur Assembler- oder Zwischensprache

Einige Compiler übersetzen den C++ - Code tatsächlich in eine Assembly- oder eine Zwischensprache. Dies ist keine erforderliche Phase, aber hilfreich beim Debuggen und Optimieren. 

Assembly zu Objektcode

Der nächste gemeinsame Schritt ist das Übersetzen der Assembler-Sprache in einen Objektcode. Der Objektcode enthält Assemblycode mit relativen Adressen und offenen Referenzen zu externen Unterprogrammen (Methoden oder Funktionen). Im Allgemeinen gibt der Übersetzer so viele Informationen wie möglich in eine Objektdatei ein. Alles andere ist nicht gelöst .

Objektcode (s) verknüpfen

Die Verknüpfungsphase kombiniert einen oder mehrere Objektcodes, löst Verweise auf und eliminiert doppelte Unterprogramme. Die endgültige Ausgabe ist eine ausführbare -Datei. Diese Datei enthält Informationen zum Betriebssystem und relative Adressen.

Ausführen von Binary Files

Das Betriebssystem lädt die ausführbare Datei, normalerweise von einer Festplatte, und speichert sie im Arbeitsspeicher. Das Betriebssystem kann relative Adressen in physische Standorte konvertieren. Das Betriebssystem kann auch Ressourcen (wie DLLs und GUI-Widgets) vorbereiten, die von der ausführbaren Datei benötigt werden (was in der ausführbaren Datei angegeben sein kann).

Direktes Kompilieren in Binärdateien Einige Compiler, z. B. diejenigen, die in eingebetteten Systemen verwendet werden, können von C++ direkt in einen ausführbaren Binärcode kompilieren. Dieser Code hat physische Adressen anstelle der relativen Adresse und erfordert nicht das Laden eines Betriebssystems.

Vorteile

Einer der Vorteile dieser Phasen ist, dass C++ - Programme in Stücke aufgeteilt, einzeln zusammengestellt und zu einem späteren Zeitpunkt verknüpft werden können. Sie können sogar mit Teilen anderer Entwickler (a.k.a.-Bibliotheken) verknüpft werden. Dies ermöglicht es Entwicklern, nur Compilerstücke in der Entwicklung zu kompilieren und in Stücken zu verknüpfen, die bereits validiert sind. Im Allgemeinen ist die Übersetzung von C++ in object der zeitaufwändige Teil des Prozesses. Außerdem möchte eine Person nicht warten, bis alle Phasen abgeschlossen sind, wenn ein Fehler im Quellcode vorliegt.

Seien Sie offen und erwarten Sie immer die Dritte Alternative (Option) .

18
Thomas Matthews

Um Ihre Fragen zu beantworten, beachten Sie bitte, dass dies subjektiv ist, da es verschiedene Prozessoren, verschiedene Plattformen, verschiedene Assembler und C-Compiler gibt. In diesem Fall werde ich über die Intel x86-Plattform sprechen.

  1. Assembler kompilieren nicht zu reinen Binärdateien. Sie sind roher Maschinencode, definiert mit Segmenten wie Daten, Text und bss, um nur einige zu nennen. Dies wird als Objektcode bezeichnet. Der Linker führt die Segmente aus und passt sie an, um sie ausführbar zu machen, das heißt, sie kann ausgeführt werden. Übrigens ist die Standardausgabe beim Kompilieren mit gcc 'a.out', das ist eine Abkürzung für Assembler-Ausgabe.
  2. Bootloader haben eine spezielle Direktive definiert, die zu Zeiten von DOS definiert wurde. Normalerweise würde man eine Direktive wie .Org 100h finden, die den Assembler-Code als alte .COM-Sorte definiert, bevor die .EXE-Popularität an Bedeutung gewonnen hat. Sie brauchten auch keinen Assembler, um eine .COM-Datei zu erstellen. Dabei wurde die alte, mit MSDOS mitgelieferte debug.exe verwendet, die den Trick für kleine, einfache Programme ausführte. Die .COM-Dateien benötigten keinen Linker und waren sofort einsatzbereit. Binärformat ausführen. Hier ist eine einfache Sitzung mit DEBUG.
 1: * a 0100 
 2: * mov AH, 07 
 3: * int 21 
 4: * cmp AL, 00 
 5 : * jnz 010c 
 6: * mov AH, 07 
 7: * int 21 
 8: * mov AH, 4C 
 9: * int 21 
 10: * 
 11: * r CX 
 12: * 10 
 13: * n respons.com 
 14: * w 
 15: * q 

Daraufhin wird ein einsatzbereites .COM-Programm namens "respons.com" erstellt, das auf einen Tastendruck wartet und diesen nicht auf dem Bildschirm anzeigt. Beachten Sie am Anfang die Verwendung von 'a 100h', was zeigt, dass der Instruction-Zeiger bei 100h beginnt, was das Merkmal einer .COM ist. Dieses alte Skript wurde hauptsächlich in Batch-Dateien verwendet, die auf eine Antwort warteten und diese nicht echo. Das Original-Skript finden Sie hier .

Bei Bootloadern werden diese wiederum in ein binäres Format konvertiert. Es gab ein Programm, das früher mit DOS kam, als EXE2BIN . Das war die Aufgabe, den Code des Rohobjekts in ein Format zu konvertieren, das zum Booten auf eine startfähige Festplatte kopiert werden kann. Denken Sie daran, dass kein Linker für den zusammengesetzten Code ausgeführt wird, da der Linker für die Laufzeitumgebung ist und den Code so konfiguriert, dass er lauffähig und ausführbar ist.

Das BIOS erwartet beim Booten, dass sich der Code im Segment befindet: offset, 0x7c00. Wenn mein Arbeitsspeicher richtig ist, wird der Code (nachdem er EXE2BIN ist) gestartet, und der Bootloader verlagert sich im Speicher nach unten und fährt mit dem Laden fort Geben Sie zum Lesen von der Festplatte int 0x13 aus, schalten Sie das A20-Gatter ein, aktivieren Sie den DMA, schalten Sie in den geschützten Modus, da sich das BIOS im 16-Bit-Modus befindet. Dann werden die von der Festplatte gelesenen Daten in den Speicher geladen, und der Bootloader gibt einen großen Sprung in den Datencode (wahrscheinlich in C geschrieben). Auf diese Weise bootet das System. 

Ok, der vorige Absatz klingt abstrakt und einfach, ich habe vielleicht etwas übersehen, aber so ist es auf den Punkt gebracht.

Ich hoffe, das hilft. Mit freundlichen Grüßen Tom.

3
t0mm13b

Es gibt zwei Dinge, die Sie hier mischen können. Im Allgemeinen gibt es zwei Themen:

Letzteres kann sich im Rahmen der Versammlung mit dem Erstgenannten kompilieren. Einige Zwischenformate werden nicht zusammengestellt, sondern von einer virtuellen Maschine ausgeführt. Im Falle von C++ kann er may in CIL kompiliert werden, das zu einer .NET-Assembly zusammengefügt wird, daher gibt es einige Verwirrung.

Im Allgemeinen werden C und C++ jedoch in der Regel binär oder mit anderen Worten in ein ausführbares Dateiformat kompiliert.

1

Sie haben viele Antworten, die ich durchlesen kann, aber ich denke, ich kann mich kurz fassen.

"Binärcode" bezieht sich auf die Bits, die die Schaltungen des Mikroprozessors durchlaufen. Der Mikroprozessor lädt jede Anweisung nacheinander aus dem Speicher und tut, was immer sie sagen. Unterschiedliche Prozessorfamilien haben unterschiedliche Formate für Anweisungen: x86, ARM, PowerPC usw. Sie richten den Prozessor auf die gewünschte Anweisung, indem Sie ihm die Adresse der Anweisung im Speicher angeben, und dann fährt er fröhlich durch den Rest des Programms.

Wenn Sie ein Programm in den Prozessor laden möchten, müssen Sie zuerst den Binärcode im Speicher zugänglich machen, damit er an erster Stelle eine Adresse hat. Der C-Compiler gibt im Dateisystem eine Datei aus, die in einen neuen virtuellen Adressraum geladen werden muss. Daher muss diese Datei zusätzlich zum Binärcode die Informationen enthalten, die sie hat Binärcode und wie der Adressraum aussehen soll.

Ein Bootloader hat andere Anforderungen, daher kann das Dateiformat unterschiedlich sein. Die Idee ist jedoch die gleiche: Binärcode ist immer eine Nutzlast in einem größeren Dateiformat, das mindestens eine Überprüfung der Fehlerfreiheit beinhaltet, um sicherzustellen, dass er im richtigen Befehlssatz geschrieben wird.

C-Compiler und -Assembler sind normalerweise so konfiguriert, dass sie statische Bibliotheksdateien erzeugen. Bei eingebetteten Anwendungen ist es wahrscheinlicher, dass Sie einen Compiler finden, der so etwas wie ein einfaches Speicherabbild mit Anweisungen ab Adresse 0 erzeugt. Ansonsten können Sie einen Linker schreiben, der die Ausgabe des C-Compilers in ein beliebiges anderes konvertiert.

1
Potatoswatter

Sie werden zu einer Datei in einem bestimmten Format (COFF für Windows usw.) kompiliert, das aus Headern und Segmenten besteht, von denen einige "plain binary" -Op-Codes haben. Assembler und Compiler (z. B. C) erzeugen dieselbe Ausgabe. Einige Formate, wie z. B. die alten * .COM-Dateien, hatten keine Kopfzeilen, hatten jedoch bestimmte Annahmen (z. B. wo sie im Speicher geladen würden oder wie groß sie sein könnten).

Auf Windows-Computern befindet sich der Boostrapper des Betriebssystems in einem vom BIOS geladenen Plattensektor, in dem beide "normal" sind. Sobald das Betriebssystem seinen Loader geladen hat, kann es Dateien lesen, die Header und Segmente enthalten.

Hilft das?

1
Steven Sudit

Um den Assembly-Teil der Frage zu beantworten, wird Assembly nicht binär kompiliert, wie ich es verstehe. Assembly === binär. Es übersetzt direkt. Jede Assembly-Operation hat eine binäre Zeichenfolge, die direkt dazu passt. Jede Operation hat einen Binärcode und jede Registervariable hat eine Binäradresse.

Das heißt, es sei denn Assembler! = Assembly und ich missverstehe Ihre Frage.

1
Daniel Bingham

Nach meinem Verständnis wird ein Chipsatz (CPU usw.) einen Satz von Registern zum Speichern von Daten haben und einen Satz von Anweisungen zum Manipulieren dieser Register verstehen. Die Anweisungen sind beispielsweise "diesen Wert in diesem Register speichern", "diesen Wert verschieben" oder "diese beiden Werte vergleichen". Diese Anweisungen werden oft in kurzen, vom Menschen erfaßbaren alphabetischen Codes (Assembler oder Assembler) ausgedrückt, die den vom Chipsatz erkannten Zahlen zugeordnet werden. Diese Zahlen werden dem Chip binär (Maschinencode) angezeigt.

Diese Codes sind die niedrigste Stufe, auf die die Software zurückgreift. Wenn man tiefer geht, greift man in die Architektur des eigentlichen Chips ein, woran ich mich nicht beteiligt habe.

0
Laizer

Es gibt viele Antworten, die Sie sich ansehen können, aber ich dachte, ich würde diese Ressourcen hinzufügen, die Ihnen einen Eindruck davon vermitteln, was passiert. Grundsätzlich hat jemand unter Windows und Linux versucht, die kleinstmögliche ausführbare Datei zu erstellen. in Linux, ELF, Windows, PE.

Beide durchlaufen, was und warum entfernt wird, und Sie verwenden Assembler zum Erstellen von ELF-Dateien, ohne die -felf-ähnlichen Optionen zu verwenden, die dies für Sie tun.

Hoffentlich hilft das.

Bearbeiten - Sie können auch die Assembly für einen Bootloader wie in truecrypt http://www.truecrypt.org oder "stage1" von grub (das Bit, das tatsächlich in das MDR geschrieben wird) sehen.

0
user257111

Die ausführbaren Dateien (PE-Format unter Windows) können nicht zum Starten des Computers verwendet werden, da sich der PE-Loader nicht im Speicher befindet. 

Das Bootstrapping funktioniert so, dass der Master-Boot-Record auf der Festplatte einen Fleck von einigen hundert Byte Code enthält. Das BIOS des Computers (in ROM auf der Hauptplatine) lädt diesen Blob in den Speicher und setzt den CPU-Befehlszeiger auf den Anfang dieses Startcodes. 

Der Bootcode lädt dann einen "Second Stage" -Loader mit dem Namen NTLDR (ohne Erweiterung) aus dem Stammverzeichnis. Dies ist ein unformatierter Maschinencode, der wie der MBR-Loader kalt in den Speicher geladen und ausgeführt wird. 

NTLDR kann PE-Dateien einschließlich DLLs und Treiber vollständig laden.

0
Tyler Durden