Vor- und Nachteile verschiedener Transportarten. Transportlogistiktransport. Internationaler Straßentransport

Um gcc, den Standard-C-Compiler für Linux, richtig zu verwenden, müssen Sie die Befehlszeilenoptionen kennen. Darüber hinaus erweitert gcc die Sprache C. Auch wenn Sie beabsichtigen, Quellcode nach dem ANSI-Sprachstandard zu schreiben, sind einige gcc-Erweiterungen für das Verständnis von Linux-Headerdateien unerlässlich.

Die meisten Befehlszeilenoptionen sind dieselben wie in C-Compilern. Für einige Optionen gibt es keine Standards. In diesem Kapitel gehen wir auf die wichtigsten Optionen ein, die im Programmieralltag verwendet werden.

Das Streben nach Einhaltung des ISO-C-Standards ist sinnvoll, aber da es sich bei C um eine Low-Level-Sprache handelt, gibt es Situationen, in denen die Standardtools nicht ausdrucksstark genug sind. Es gibt zwei Bereiche, in denen gcc-Erweiterungen häufig verwendet werden: die Schnittstelle zu Assemblercode (siehe http://www.delorie.com/djgpp/doc/brennan/) und die Erstellung gemeinsam genutzter Bibliotheken (siehe Kapitel 8). Da Header-Dateien Teil gemeinsam genutzter Bibliotheken sind, erscheinen einige Erweiterungen auch in System-Header-Dateien.

Natürlich gibt es noch viele weitere Erweiterungen, die bei jeder anderen Art der Programmierung nützlich sind und sogar beim Codieren sehr hilfreich sein können. Weitere Informationen zu diesen Erweiterungen finden Sie in der gcc-Dokumentation im Texinfo-Format.

5.1. gcc-Optionen

gcc akzeptiert viele Befehlsoptionen. Glücklicherweise gibt es nicht viele Optionen, über die Sie wirklich Bescheid wissen müssen, und wir werden sie uns in diesem Kapitel ansehen.

Die meisten Optionen sind die gleichen oder ähneln denen anderer Compiler, und gcc enthält eine umfangreiche Dokumentation zu seinen Optionen, die über info gcc verfügbar ist (man gcc stellt diese Informationen ebenfalls bereit, aber die Manpages werden nicht so häufig aktualisiert wie die Texinfo-Dokumentation).

-o Dateiname Gibt den Namen der Ausgabedatei an. Dies ist normalerweise nicht erforderlich, wenn in eine Objektdatei kompiliert wird, d. h. standardmäßig wird file_name.c durch file_name.o ersetzt. Wenn Sie jedoch eine ausführbare Datei erstellen, wird diese standardmäßig (aus historischen Gründen) unter dem Namen a.out erstellt. Dies ist auch nützlich, wenn Sie die Ausgabedatei in einem anderen Verzeichnis ablegen möchten.
-Mit Kompiliert, ohne die für die Befehlszeile angegebene Quelldatei zu verknüpfen. Als Ergebnis wird für jede Quelldatei eine Objektdatei erstellt. Bei Verwendung von make wird der gcc-Compiler normalerweise für jede Objektdatei aufgerufen; Auf diese Weise lässt sich im Fehlerfall leichter feststellen, welche Datei nicht kompiliert werden konnte. Wenn Sie Befehle jedoch manuell eingeben, ist es nicht ungewöhnlich, dass in einem einzigen gcc-Aufruf mehrere Dateien angegeben werden. Wenn bei der Angabe mehrerer Dateien in der Befehlszeile Unklarheiten auftreten, ist es besser, nur eine Datei anzugeben. Anstelle von gcc -c -o a.o a.c b.c ist es beispielsweise sinnvoll, gcc -c -o a.o b.c zu verwenden.
-D foo Definiert Präprozessormakros in der Befehlszeile. Möglicherweise müssen Zeichen deaktiviert werden, die von der Shell als Sonderzeichen behandelt werden. Wenn Sie beispielsweise eine Zeichenfolge definieren, sollten Sie die Verwendung der Zeichenfolgenabschlusszeichen „ vermeiden. Die beiden gebräuchlichsten Methoden sind „-Dfoo="bar"" und -Dfoo=\"bar\" . Die erste Methode funktioniert viel besser, wenn Die Zeichenfolge enthält Leerzeichen, da die Shell Leerzeichen anders behandelt.
-I-Verzeichnis Fügt ein Verzeichnis zur Liste der Verzeichnisse hinzu, in denen nach Include-Dateien gesucht werden soll.
-L Verzeichnis Fügt ein Verzeichnis zur Liste der Verzeichnisse hinzu, um nach Bibliotheken zu suchen. Gcc bevorzugt gemeinsam genutzte Bibliotheken gegenüber statischen, sofern nicht anders angegeben.
-l foo Links zur Bibliothek lib foo. Sofern nicht anders angegeben, bevorzugt gcc die Verknüpfung mit gemeinsam genutzten Bibliotheken (lib foo .so) gegenüber statischen Bibliotheken (lib foo .a). Der Linker sucht in allen aufgelisteten Bibliotheken nach Funktionen in der Reihenfolge, in der sie aufgelistet sind. Die Suche endet, wenn alle benötigten Funktionen gefunden wurden.
-statisch Links mit nur statischen Bibliotheken. Siehe Kapitel 8.
-g , -ggdb Enthält Debugging-Informationen. Die Option -g zwingt gcc, Standard-Debugging-Informationen einzubeziehen. Die Option -ggdb gibt an, ob aktiviert werden soll riesige Menge Informationen, die nur der GDB-Debugger verstehen kann.
Wenn der Speicherplatz begrenzt ist oder Sie einige Funktionen für die Verbindungsgeschwindigkeit opfern möchten, sollten Sie -g verwenden. In diesem Fall müssen Sie möglicherweise einen anderen Debugger als gdb verwenden. Für maximales Debugging müssen Sie -ggdb angeben. In diesem Fall wird gcc so viel wie möglich vorbereiten genaue Information für gdb. Es ist zu beachten, dass gcc im Gegensatz zu den meisten Compilern einige Debugging-Informationen in den optimierten Code einfügt. Allerdings kann die Verfolgung optimierten Codes im Debugger eine Herausforderung darstellen, da bei der Ausführung Teile des Codes, deren Ausführung erwartet wurde, übersprungen und übersprungen werden können. Sie können jedoch bekommen Gute Show darüber, wie optimierende Compiler die Art und Weise verändern, wie Code ausgeführt wird.
-O , -O n Zwingt gcc, den Code zu optimieren. Standardmäßig führt gcc eine kleine Optimierung durch; Bei der Angabe der Anzahl (n) erfolgt die Optimierung auf einem bestimmten Niveau. Die häufigste Optimierungsstufe ist 2; Derzeit ist in der Standardversion von gcc die höchste Optimierungsstufe 3. Wir empfehlen die Verwendung von -O2 oder -O3; -O3 kann die Größe der Anwendung erhöhen. Wenn das wichtig ist, probieren Sie beide Optionen aus. Wenn Arbeitsspeicher und Festplattenspeicher für Ihre Anwendung wichtig sind, können Sie auch die Option -Os verwenden, die die Codegröße auf Kosten einer längeren Ausführungszeit minimiert. gcc aktiviert integrierte Funktionen nur, wenn mindestens eine minimale Optimierung (-O) angewendet wird.
-ansi Unterstützung in C-Programmen für alle ANSI-Standards (X3.159-1989) oder deren ISO-Äquivalent (ISO/IEC 9899:1990) (üblicherweise als C89 oder seltener als C90 bezeichnet). Es ist zu beachten, dass dies keine vollständige Konformität mit dem ANSI/ISO-Standard gewährleistet.
Die Option -ansi deaktiviert GCC-Erweiterungen, die normalerweise mit ANSI/ISO-Standards in Konflikt stehen. (Aufgrund der Tatsache, dass diese Erweiterungen von vielen anderen C-Compilern unterstützt werden, stellt dies in der Praxis kein Problem dar.) Es definiert auch das Makro __STRICT_ANSI__ (wie später in diesem Buch beschrieben), das Header-Dateien zur Unterstützung einer ANSI/ISO-Konformität verwenden Umfeld.
-pedantisch Zeigt alle vom ANSI/ISO C-Sprachstandard geforderten Warnungen und Fehler an. Dies gewährleistet keine vollständige Konformität mit dem ANSI/ISO-Standard.
-Wand Ermöglicht die Generierung aller GCC-Warnungen, was normalerweise nützlich ist. Dies umfasst jedoch nicht Optionen, die in bestimmten Fällen nützlich sein können. Für den Lint-Parser Ihres Quellcodes wird ein ähnlicher Granularitätsgrad festgelegt. Mit gcc können Sie jede Compiler-Warnung manuell ein- und ausschalten. Das gcc-Handbuch erläutert alle Warnungen ausführlich.
5.2. Header-Dateien
5.2.1. lang Lang

Der Typ long long gibt an, dass der Speicherblock mindestens so groß ist wie long . Auf Intel i86 und anderen 32-Bit-Plattformen beträgt long 32 Bit und long long 64 Bit. Auf 64-Bit-Plattformen belegen Zeiger und Long Long 64 Bit, und Long kann je nach Plattform 32 oder 64 Bit belegen. Der Long-Long-Typ wird im C99-Standard (ISO/IEC 9899:1999) unterstützt und ist eine seit langem von gcc bereitgestellte C-Erweiterung.

5.2.2. Integrierte Funktionen

Einige Teile der Linux-Header-Dateien (insbesondere diejenigen, die systemspezifisch sind) nutzen in großem Umfang integrierte Funktionen. Sie sind so schnell wie Makros (kein Overhead bei Funktionsaufrufen) und bieten alle Arten von Überprüfungen, die bei einem normalen Funktionsaufruf verfügbar sind. Code, der integrierte Funktionen aufruft, muss mit mindestens aktivierter minimaler Optimierung (-O) kompiliert werden.

5.2.3. Alternative erweiterte Schlüsselwörter

In gcc hat jedes erweiterte Schlüsselwort (Schlüsselwörter, die nicht im ANSI/ISO-Standard spezifiziert sind) zwei Versionen: sich selbst Stichwort und ein Schlüsselwort, das auf beiden Seiten von zwei Unterstrichen flankiert wird. Wenn der Compiler im Standardmodus verwendet wird (normalerweise, wenn die Option -ansi aktiviert ist), werden normale erweiterte Schlüsselwörter nicht erkannt. So sollte beispielsweise das Attribute-Schlüsselwort in der Header-Datei als __attribute__ geschrieben werden.

5.2.4. Attribute

Das Schlüsselwort „extended attribute“ wird verwendet, um mehr Informationen über eine Funktion, Variable oder einen deklarierten Typ an gcc zu übergeben, als ANSI/ISO-C-Code zulässt. Beispielsweise teilt das Attribut „aligned“ gcc genau mit, wie eine Variable oder ein Typ ausgerichtet werden soll. Das gepackte Attribut gibt an, dass keine Auffüllung verwendet wird. noreturn gibt an, dass eine Funktion niemals zurückkehrt, wodurch gcc eine bessere Optimierung durchführen und falsche Warnungen vermeiden kann.

Funktionsattribute werden deklariert, indem man sie der Funktionsdeklaration hinzufügt, zum Beispiel:

void die_die_die(int, char*) __attribute__ ((__noreturn__));

Eine Attributdeklaration wird zwischen Klammern und einem Semikolon platziert und enthält das Schlüsselwort Attribut, gefolgt von den Attributen in doppelten Klammern. Bei vielen Attributen sollte eine durch Kommas getrennte Liste verwendet werden.

int printm(char*, ...)

Attribut__((const,

format(printf, 1, 2)));

Dieses Beispiel zeigt, dass printm keine anderen als die angegebenen Werte berücksichtigt und keine Nebenwirkungen im Zusammenhang mit der Codegenerierung (const) hat. printm gibt an, dass gcc Funktionsargumente auf die gleiche Weise wie printf()-Argumente prüfen soll. Das erste Argument ist die Formatzeichenfolge und das zweite ist der erste Ersetzungsparameter (Format).

Einige Attribute werden im Verlauf des Materials behandelt (z. B. während der Diskussion über den Aufbau gemeinsam genutzter Bibliotheken in Kapitel 8). Ausführliche Informationen zu Attributen finden Sie in der gcc-Dokumentation im Texinfo-Format.

Von Zeit zu Zeit kann es sein, dass Sie sich Linux-Headerdateien ansehen. Sie werden wahrscheinlich eine Reihe von Designs finden, die nicht ANSI/ISO-konform sind. Einige davon sind es wert, verstanden zu werden. Alle in diesem Buch besprochenen Konstrukte werden in der gcc-Dokumentation ausführlicher behandelt.

Von Zeit zu Zeit kann es sein, dass Sie sich Linux-Headerdateien ansehen. Sie werden wahrscheinlich eine Reihe von Designs finden, die nicht ANSI/ISO-konform sind. Einige davon sind es wert, verstanden zu werden. Alle in diesem Buch besprochenen Konstrukte werden in der gcc-Dokumentation ausführlicher behandelt.

Nachdem Sie nun etwas über den C-Standard wissen, werfen wir einen Blick auf die Optionen, die der gcc-Compiler bietet, um die Einhaltung des C-Standards in der von Ihnen geschriebenen Sprache sicherzustellen. Es gibt drei Möglichkeiten, um sicherzustellen, dass Ihr C-Code standardkonform und fehlerfrei ist: Optionen, die die Version des Standards steuern, dem Sie entsprechen möchten, Definitionen, die Header-Dateien steuern, und Warnoptionen, die eine strengere Codeprüfung auslösen.

gcc bietet eine große Auswahl an Optionen, und hier werden wir nur diejenigen betrachten, die wir für die wichtigsten halten. Eine vollständige Liste der Optionen finden Sie in den Online-Manpages von gcc. Wir werden auch kurz einige der #define-Anweisungsoptionen besprechen, die verwendet werden können; Normalerweise sollten sie in Ihrem Quellcode vor allen #include-Zeilen angegeben oder in der gcc-Befehlszeile definiert werden. Sie werden überrascht sein, wie viele Möglichkeiten es gibt, den zu verwendenden Standard auszuwählen, anstatt einfach ein Flag zu aktivieren, um die Verwendung des aktuellen Standards zu erzwingen. Der Grund dafür ist, dass viele ältere Programme auf dem historischen Compilerverhalten basieren und erhebliche Arbeit erfordern würden, um sie auf die neuesten Standards zu aktualisieren. Selten, wenn überhaupt, werden Sie Ihren Compiler aktualisieren wollen, um laufenden Code zu beschädigen. Da sich Standards ändern, ist es wichtig, mit einem bestimmten Standard arbeiten zu können, auch wenn es sich nicht um die aktuellste Version des Standards handelt.

Selbst wenn Sie ein kleines Programm für den persönlichen Gebrauch schreiben, bei dem die Einhaltung von Standards möglicherweise nicht so wichtig ist, ist es oft sinnvoll, zusätzliche GCC-Warnungen einzufügen, um den Compiler zu zwingen, vor der Ausführung des Programms nach Fehlern in Ihrem Code zu suchen. Dies ist immer effektiver, als den Code Schritt für Schritt im Debugger auszuführen und sich zu fragen, wo das Problem liegen könnte. Der Compiler verfügt über viele Optionen, die über die einfache Überprüfung von Standards hinausgehen, z. B. die Möglichkeit, Code zu erkennen, der dem Standard entspricht, aber möglicherweise eine fragwürdige Semantik aufweist. Beispielsweise kann ein Programm eine Ausführungsreihenfolge haben, die den Zugriff auf eine Variable vor der Initialisierung ermöglicht.

Wenn Sie ein Programm zur gemeinsamen Nutzung schreiben müssen, ist es angesichts des Grads der Konformität mit dem Standard und der Arten von Compiler-Warnungen, die Sie für ausreichend halten, sehr wichtig, etwas mehr Aufwand zu betreiben und dafür zu sorgen, dass Ihr Code ohne Warnungen kompiliert wird alle. Wenn Sie das Erscheinen einiger Warnungen zulassen und sich angewöhnen, sie zu ignorieren, kann eines Tages eine ernstere Warnung erscheinen, die Sie möglicherweise übersehen. Wenn Ihr Code immer ohne Warnmeldungen kompiliert wird, wird eine neue Warnung unweigerlich Ihre Aufmerksamkeit erregen. Es ist eine gute Angewohnheit, Code ohne Warnungen zu kompilieren.

Compiler-Optionen für die Standardverfolgung

Ansi ist die wichtigste Standardoption und zwingt den Compiler, gemäß dem Sprachstandard ISO C90 zu agieren. Es deaktiviert einige nicht standardkonforme gcc-Erweiterungen, deaktiviert Kommentare im C++-Stil (//) in C-Programmen und ermöglicht die Verarbeitung von ANSI-Trigraphen (dreistellige Sequenzen). Darüber hinaus enthält es das Makro __ STRICT_ANSI__, das einige Erweiterungen in Header-Dateien deaktiviert, die nicht mit dem Standard kompatibel sind. Der übernommene Standard kann sich in nachfolgenden Versionen des Compilers ändern.

Std= – Diese Option bietet eine genauere Kontrolle über den verwendeten Standard und stellt einen Parameter bereit, der genau den erforderlichen Standard angibt. Im Folgenden sind die wichtigsten möglichen Optionen aufgeführt:

C89 – unterstützt den C89-Standard;

Iso9899:1999 – unterstützt die neueste Version des ISO-Standards, C90;

Gnu89 – Behält den C89-Standard bei, erlaubt aber einige GNU-Erweiterungen und einige C99-Funktionalität. In Version 4.2 von gcc ist diese Option die Standardeinstellung.

Optionen für die Standardverfolgung in Definitionsanweisungen

Es gibt Konstanten (#defines), die als Optionen in der Befehlszeile oder als Definitionen im Quellcode des Programms angegeben werden können. Im Allgemeinen stellen wir uns diese als Verwendung der Compiler-Befehlszeile vor.

STRICT_ANSI__ – erzwingt die Verwendung des ISO-C-Standards. Wird bestimmt, wenn die Option -ansi in der Compiler-Befehlszeile angegeben wird.

POSIX_C_SOURCE=2 – Aktiviert die in IEEE Std 1003.1 und 1003.2 definierte Funktionalität. Wir werden später in diesem Kapitel auf diese Standards zurückkommen.

BSD_SOURCE – Aktiviert die Funktionalität von BSD-Systemen. Bei Konflikten mit POSIX-Definitionen haben die BSD-Definitionen Vorrang.

GNU_SOURCE – Ermöglicht eine Vielzahl von Eigenschaften und Funktionen, einschließlich GNU-Erweiterungen. Wenn diese Definitionen mit POSIX-Definitionen in Konflikt stehen, haben letztere Vorrang.

Compiler-Optionen für die Ausgabe von Warnungen

Diese Optionen werden über die Befehlszeile an den Compiler übergeben. Und wieder werden wir nur die wichtigsten auflisten, volle Liste finden Sie im gcc-Online-Referenzhandbuch.

Pedantic ist die leistungsstärkste Option zur Überprüfung der Reinheit von C-Code. Zusätzlich zur Aktivierung der C-Standard-Prüfoption deaktiviert es einige traditionelle C-Konstrukte, die vom Standard verboten sind, und macht alle GNU-Erweiterungen des Standards ungültig. Diese Option sollte verwendet werden, um die Portabilität Ihres C-Codes zu maximieren. Der Nachteil ist, dass der Compiler sehr auf die Sauberkeit Ihres Codes bedacht ist und Sie sich manchmal den Kopf zerbrechen müssen, um die wenigen verbleibenden Warnungen loszuwerden.

Wformat – prüft die Richtigkeit der Argumenttypen von Funktionen der printf-Familie.

Wparentheses – sucht nach Klammern, auch wenn diese nicht benötigt werden. Diese Option ist sehr nützlich, um zu überprüfen, ob komplexe Strukturen wie beabsichtigt initialisiert werden.

Wswitch-default – prüft, ob in Switch-Anweisungen eine Standardoption vorhanden ist, was im Allgemeinen als guter Programmierstil gilt.

Wunused – prüft eine Vielzahl von Fällen, zum Beispiel deklarierte, aber nicht beschriebene statische Funktionen, nicht verwendete Parameter, verworfene Ergebnisse.

Wall – Aktiviert die meisten GCC-Warnungstypen, einschließlich aller vorherigen -W-Optionen (nur -pedantic wird nicht abgedeckt). Mit seiner Hilfe ist es einfach, sauberen Code zu erreichen.

Notiz

Es stehen viele weitere erweiterte Warnoptionen zur Verfügung. Weitere Informationen finden Sie auf den GCC-Webseiten. Im Allgemeinen empfehlen wir die Verwendung von -Wall ; Dies ist ein guter Kompromiss zwischen der Verifizierung und Sicherstellung des Programmcodes Gute Qualität und die Notwendigkeit für den Compiler, eine Menge trivialer Warnungen auszugeben, die nur schwer auf Null reduziert werden können.

GCC ist in jeder Distribution enthalten Linux und wird normalerweise standardmäßig installiert. Die GCC-Schnittstelle ist eine Standard-Compiler-Schnittstelle auf der UNIX-Plattform aus den späten 60er und frühen 70er Jahren des letzten Jahrhunderts – eine Befehlszeilenschnittstelle. Seien Sie nicht beunruhigt; in der letzten Zeit wurde der Interaktionsmechanismus mit dem Benutzer auf die in diesem Fall mögliche Perfektion verfeinert, und die Arbeit mit GCC (mit mehreren zusätzlichen und nützlichen Dienstprogrammen) Texteditor) einfacher als mit jeder der modernen visuellen IDEs. Die Autoren des Sets versuchten, den Prozess der Kompilierung und Zusammenstellung von Anwendungen so weit wie möglich zu automatisieren. Der Benutzer ruft das Steuerungsprogramm auf gcc, interpretiert es die übergebenen Befehlszeilenargumente (Optionen und Dateinamen) und führt für jede Eingabedatei, entsprechend der verwendeten Programmiersprache, seinen Compiler aus und führt dann, falls erforderlich, gcc Ruft automatisch den Assembler und Linker (Linker) auf.

Interessanterweise gehören Compiler zu den wenigen UNIX-Anwendungen, die Dateierweiterungen berücksichtigen. Durch die Erweiterung bestimmt GCC, welche Art von Datei sich vor ihm befindet und was damit gemacht werden muss (kann). Sprachquelldateien C muss in der Sprache die Erweiterung .c haben C++, alternativ .cpp , Header-Dateien in der Sprache C.h-, .o-Objektdateien usw. Wenn Sie die falsche Erweiterung verwenden, gcc wird nicht richtig funktionieren (sofern Sie zustimmen, überhaupt etwas zu tun).

Kommen wir zum Üben. Lassen Sie uns ein einfaches Programm schreiben, kompilieren und ausführen. Seien wir nicht originell, da die Quelldatei ein Beispielprogramm in der Sprache ist C Erstellen wir eine Datei mit folgendem Inhalt:

/* Hallo c */

#enthalten

Hauptsächlich( Leere )
{

Printf("Hallo Welt \n " );

zurückkehren 0 ;

Nun geben wir im Verzeichnis c hello.c den Befehl aus:

$ gcc hallo.c

Nach wenigen Sekundenbruchteilen erscheint die a.out-Datei im Verzeichnis:

$ls
a.out hallo.c

Dies ist die fertige ausführbare Datei unseres Programms. Default gcc gibt der ausführbaren Ausgabedatei den Namen a.out (einmal bedeutete dieser Name Assembler-Ausgabe).

$file a.out
a.out: ELF 64-Bit-LSB-ausführbare Datei, x86-64, Version 1 (SYSV), dynamisch verknüpft (verwendet gemeinsam genutzte Bibliotheken), für GNU/Linux 2.6.15, nicht entfernt

Lassen Sie uns das Ergebnis ausführen Software:

$./a.out
Hallo Welt


Warum muss im Ausführungsbefehl explizit der Pfad zur Datei angegeben werden, um eine Datei aus dem aktuellen Verzeichnis auszuführen? Wenn der Pfad zur ausführbaren Datei nicht explizit angegeben wird, sucht die Shell bei der Interpretation von Befehlen in den durch die Systemvariable PATH angegebenen Verzeichnissen nach der Datei.

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

Verzeichnisse in der Liste werden durch einen Doppelpunkt getrennt. Bei der Suche nach Dateien durchsucht die Shell die Verzeichnisse in der Reihenfolge, in der sie aufgelistet sind. Aus Sicherheitsgründen ist das aktuelle Verzeichnis standardmäßig . ist nicht in der Liste enthalten, daher sucht die Shell darin nicht nach ausführbaren Dateien.

Warum wird es nicht empfohlen, einen Beitrag zu leisten? im PFAD? Es wird davon ausgegangen, dass es in einem echten Mehrbenutzersystem immer eine böse Person gibt, die in einem öffentlichen Verzeichnis ein Schadprogramm mit einem ausführbaren Dateinamen ablegt, der mit dem Namen eines Befehls übereinstimmt, der häufig von einem lokalen Administrator mit Superuser-Rechten aufgerufen wird ... Die Verschwörung wird erfolgreich sein, wenn . steht am Anfang der Verzeichnisliste.


Dienstprogramm Datei Zeigt Informationen über den Typ (aus Systemsicht) der auf der Befehlszeile übergebenen Datei an; für einige Dateitypen werden alle möglichen Informationen angezeigt Weitere Informationenüber den Inhalt der Datei.

$file hallo.c
hello.c: ASCII-C-Programmtext
$file annotation.doc
annotation.doc: CDF V2-Dokument, Little Endian, Betriebssystem: Windows, Version 5.1, Codepage: 1251, Autor: MIH, Vorlage: Normal.dot, zuletzt gespeichert von: MIH, Revisionsnummer: 83, Name der erstellenden Anwendung: Microsoft Office Word, Gesamtbearbeitungszeit: 09:37:00, Letzter Druck: Do, 22. Januar 07:31:00 2009, Erstellungszeit/-datum: Mo, 12. Januar 07:36:00 2009, Letzte Speicherungszeit/-datum: Do, 22. Januar 07:34:00 2009, Anzahl der Seiten: 1, Anzahl der Wörter: 3094, Anzahl der Zeichen: 17637, Sicherheit: 0

Das ist alles, was der Benutzer für eine erfolgreiche Nutzung benötigt gcc :)

Der Name der ausführbaren Ausgabedatei (sowie aller anderen generierten Dateien). gcc) kann mit geändert werden Optionen -o:

$ gcc -o hallo hallo.c
$ls
hallo hallo.c
$./Hallo
Hallo Welt


In unserem Beispiel gibt die Funktion main() den scheinbar unnötigen Wert 0 zurück. In UNIX-ähnlichen Systemen ist es üblich, nach Abschluss eines Programms eine Ganzzahl an die Befehlsshell zurückzugeben – Null, wenn der Abschluss erfolgreich war, andernfalls jede andere Zahl. Der Shell-Interpreter weist den resultierenden Wert automatisch einer Umgebungsvariablen namens ? zu. . Sie können den Inhalt mit echo $? anzeigen. :

$./Hallo
Hallo Welt
$echo$?
0

Das wurde oben gesagt gcc ist ein Steuerprogramm zur Automatisierung des Kompilierungsprozesses. Sehen wir uns an, was tatsächlich passiert, wenn der Befehl gcc hello.c ausgeführt wird.

Der Kompilierungsprozess kann in vier Hauptphasen unterteilt werden: Verarbeitung durch einen Präprozessor, Kompilierung selbst, Assemblierung, Verknüpfung.

Optionen gcc ermöglichen es Ihnen, den Vorgang in jeder dieser Phasen zu unterbrechen.

Der Präprozessor bereitet die Quelldatei für die Kompilierung vor – schneidet Kommentare aus, fügt den Inhalt von Header-Dateien hinzu (Präprozessor-Direktive #include ), implementiert die Erweiterung von Makros (symbolische Konstanten, Präprozessor-Direktive #define ).

Vorteil nehmen mit Option -E Weitere Maßnahmen gcc Sie können den Inhalt der vom Präprozessor verarbeiteten Datei unterbrechen und anzeigen.

$ gcc -E -o hallo.i hallo.c
$ls
hallo.c hallo.i
$ weniger hallo.i
. . .
# 1 „/usr/include/stdio.h“ 1 3 4
# 28 „/usr/include/stdio.h“ 3 4
# 1 „/usr/include/features.h“ 1 3 4
. . .
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
. . .
extern int printf (__const char *__restrict __format, ...);
. . .
#4 „Hallo.c“ 2
main (nichtig)
{
printf("Hallo Welt\n");
0 zurückgeben;
}

Nach der Verarbeitung durch den Präprozessor schwoll der Quelltext unseres Programms an und nahm eine unleserliche Form an. Der Code, den wir einmal mit unseren eigenen Händen eingegeben haben, wurde auf wenige Zeilen ganz am Ende der Datei reduziert. Der Grund ist die Einbindung der Header-Datei der Standardbibliothek C. Die Header-Datei stdio.h selbst enthält viele verschiedene Dinge und erfordert auch die Einbindung anderer Header-Dateien.

Beachten Sie die Dateierweiterung hello.i . Nach Vereinbarung gcc Die Erweiterung .i entspricht Dateien mit Quellcode in der Sprache C erfordert keine Präprozessorverarbeitung. Solche Dateien werden unter Umgehung des Präprozessors kompiliert:

$ gcc -o hallo hallo.i
$ls
hallo hallo.c hallo.i
$./Hallo
Hallo Welt

Nach der Vorverarbeitung kommt die Kompilierung. Der Compiler wandelt den Quellcode des Programms in einer Hochsprache in Code in Assemblersprache um.

Die Bedeutung des Wortes Zusammenstellung ist vage. Wikipedianer denken zum Beispiel darüber nach, sich darauf zu beziehen internationale Standards Diese Kompilierung ist eine „Transformation durch ein Compilerprogramm“. Quellentext jedes Programm, das in einer höheren Programmiersprache, in einer maschinencodeähnlichen Sprache oder in Objektcode geschrieben ist.“ Im Prinzip passt diese Definition zu uns; Assemblersprache ist wirklich näher an Maschinensprache als C. Aber im Alltag wird unter Kompilierung meist einfach jeder Vorgang verstanden, der den Quellcode eines Programms in einer beliebigen Programmiersprache in ausführbaren Code umwandelt. Das heißt, ein Prozess, der alle vier oben genannten Phasen umfasst, kann auch als Kompilierung bezeichnet werden. Eine ähnliche Mehrdeutigkeit ist im vorliegenden Text vorhanden. Andererseits kann der Vorgang der Konvertierung des Quelltextes eines Programms in Code in Assemblersprache auch mit dem Wort Übersetzung bezeichnet werden – „Konvertieren eines in einer der Programmiersprachen dargestellten Programms in ein Programm in einer anderen Sprache und, in gewisser Weise gleichbedeutend mit dem ersten.“

Sie können die Erstellung einer ausführbaren Datei stoppen, nachdem die Kompilierung abgeschlossen ist. Optionen:

$ gcc -S hallo.c
$ls
hallo.c hallo.s
$file hallo.s
hello.s: ASCII-Assembler-Programmtext
$ weniger hallo.s
.file „hello.c“
.Abschnitt .rodata
.LC0:
.string „Hallo Welt“
.Text
.globl main
.type main, @function
hauptsächlich:
pushl %ebp
movl %esp, %ebp
andl $-16, %bes
subl $16, %esp
movl $.LC0, (%esp)
Call-Puts
movl $0, %eax
verlassen
im Ruhestand
.size main, .-main


Im Verzeichnis erschien eine Datei hello.s, die eine Implementierung des Programms in Assemblersprache enthielt. Bitte beachten Sie, dass Sie den Namen der Ausgabedatei mit angeben müssen Optionen -o in diesem Fall war es nicht notwendig, gcc hat es automatisch generiert, indem die Erweiterung .c im Namen der Quelldatei durch .s ersetzt wurde. Für die meisten Grundoperationen gcc Durch eine solche Ersetzung wird der Name der Ausgabedatei gebildet. Die Erweiterung .s ist Standard für Assembler-Quellcodedateien.

Natürlich können Sie den ausführbaren Code auch aus der Datei hello.s erhalten:

$ gcc -o hallo hallo.s
$ls
hallo hallo.c hallo.s
$./Hallo
Hallo Welt

Der nächste Schritt des Assemblervorgangs ist die Übersetzung des Assemblersprachencodes in Maschinencode. Das Ergebnis der Operation ist eine Objektdatei. Eine Objektdatei enthält Blöcke mit ausführbarem Maschinencode, Datenblöcke und eine Liste von Funktionen und externen Variablen, die in der Datei definiert sind ( Symboltabelle ), enthält jedoch keine absoluten Adressen von Links zu Funktionen und Daten. Eine Objektdatei kann nicht direkt zur Ausführung gestartet werden, sie kann jedoch später (in der Verknüpfungsphase) mit anderen Objektdateien kombiniert werden (in diesem Fall werden anhand der Symboltabellen die Adressen vorhandener Querverweise zwischen Dateien berechnet). und gefüllt). Möglichkeit gcc-c stoppt den Prozess, wenn die Montagephase abgeschlossen ist:

$ gcc -c hallo.c
$ls
hallo.c hallo.o
$file hallo.o
hello.o: ELF 64-Bit LSB verschiebbar, x86-64, Version 1 (SYSV), nicht entfernt

Die Standarderweiterung für Objektdateien ist .o.

Wenn die resultierende Objektdatei hello.o an den Linker übergeben wird, berechnet dieser die Linkadressen, fügt Programmstart- und -beendigungscode sowie Code zum Aufrufen von Bibliotheksfunktionen hinzu und als Ergebnis erhalten wir eine fertige ausführbare Programmdatei.

$ gcc -o hallo hallo.o
$ls
hallo hallo.c hallo.o
$./Hallo
Hallo Welt

Was wir jetzt getan haben (bzw gcc für uns erledigt) und ist Inhalt der letzten Stufe - der Verlinkung (Verlinkung, Anordnung).

Na ja, vielleicht über die Zusammenstellung und so. Kommen wir nun zu einigen meiner Meinung nach wichtigen Optionen. gcc.

Option -I Pfad/zum/Verzeichnis/mit/Header/Dateien – fügt das angegebene Verzeichnis zur Liste der Suchpfade für Header-Dateien hinzu. Verzeichnis per Option hinzugefügt -ICH zunächst angezeigt, dann wird die Suche in den Standard-Systemkatalogen fortgesetzt. Wenn Optionen -ICH Bei mehreren werden die von ihnen angegebenen Verzeichnisse von links nach rechts angezeigt, während Optionen angezeigt werden.

Option -Wand- Zeigt Warnungen an, die durch mögliche Fehler im Code verursacht werden, die die Kompilierung des Programms nicht verhindern, aber nach Ansicht des Compilers zu bestimmten Problemen bei der Ausführung führen können. Eine wichtige und nützliche Option, Entwickler gcc Ich empfehle, es immer zu verwenden. Beispielsweise werden viele Warnungen ausgegeben, wenn versucht wird, eine Datei wie diese zu kompilieren:

1 /* Bemerkung.c */
2
3 statisch int k = 0 ;
4 statisch int l( int A);
5
6 main()
7 {
8
9 int A;
10
11 int b, c;
12
13 b + 1 ;
14
15 b = c;
16
17 int*P;
18
19 b = *p;
20
21 }


$ gcc -o Bemerkung Bemerkung.c
$ gcc -Wall -o Bemerkung Bemerkung.c
Bemerkung.c:7: Warnung: Der Rückgabetyp ist standardmäßig „int“.

Bemerkung.c:13: Warnung: Anweisung ohne Wirkung
Bemerkung.c:9: Warnung: unbenutzte Variable 'a'
Bemerkung.c:21: Warnung: Die Steuerung erreicht das Ende der Nicht-Void-Funktion
Bemerkung.c: Auf oberster Ebene:
Bemerkung.c:3: Warnung: „k“ definiert, aber nicht verwendet
Bemerkung.c:4: Warnung: „l“ wurde als „statisch“ deklariert, aber nie definiert
Bemerkung.c: In der Funktion 'main':
Bemerkung.c:15: Warnung: „c“ wird in dieser Funktion nicht initialisiert verwendet
Bemerkung.c:19: Warnung: „p“ wird in dieser Funktion nicht initialisiert verwendet

Option -Werror- Wandelt alle Warnungen in Fehler um. Wenn eine Warnung erscheint, wird der Kompilierungsvorgang unterbrochen. Wird in Verbindung mit verwendet Option -Wand.

$ gcc -Werror -o Bemerkung Bemerkung.c
$ gcc -Werror -Wall -o Bemerkung Bemerkung.c
cc1: Warnungen werden als Fehler behandelt
Bemerkung.c:7: Fehler: Der Rückgabetyp ist standardmäßig „int“.
Bemerkung.c: In der Funktion 'main':
Bemerkung.c:13: Fehler: Anweisung ohne Wirkung
Bemerkung.c:9: Fehler: unbenutzte Variable 'a'

Option -g– Platziert Informationen, die für die Arbeit des Debuggers erforderlich sind, in einem Objekt oder einer ausführbaren Datei gdb. Beim Zusammenstellen eines Projekts zum Zweck des anschließenden Debuggens Option -g muss sowohl in der Kompilierungs- als auch in der Verknüpfungsphase einbezogen werden.

Optionen -O1, -O2, -O3- Legen Sie den Optimierungsgrad des vom Compiler generierten Codes fest. Mit zunehmender Anzahl steigt der Optimierungsgrad. Die Wirkung der Optionen ist in diesem Beispiel zu sehen.

Originaldatei:

/*circle.c */

Hauptsächlich( Leere )
{

int ich;

für(i = 0; ich< 10 ; ++i)
;

zurückkehren ich;

Kompilieren mit Standardoptimierungsstufe:

$ gcc -S kreis.c
$ weniger Kreis.s
.Datei „circle.c“
.Text
.globl main
.type main, @function
hauptsächlich:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $0, -4(%ebp)
jmp .L2
.L3:
addl $1, -4(%ebp)
.L2:
cmpl $9, -4(%ebp)
jle .L3
movl -4(%ebp), %eax
verlassen
im Ruhestand
.size main, .-main
.ident „GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3“
.section .note.GNU-stack,"",@progbits

Kompilierung mit maximalem Optimierungsgrad:

$ gcc -S -O3 Kreis.c
$ weniger Kreis.s
.Datei „circle.c“
.Text
.p2align 4.15
.globl main
.type main, @function
hauptsächlich:
pushl %ebp
movl $10, %eax
movl %esp, %ebp
popl %ebp
im Ruhestand
.size main, .-main
.ident „GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3“
.section .note.GNU-stack,"",@progbits

Im zweiten Fall weist der resultierende Code nicht einmal einen Hinweis auf einen Zyklus auf. Tatsächlich kann der Wert von i in der Kompilierungsphase berechnet werden, die durchgeführt wurde.

Leider ist der Leistungsunterschied bei verschiedenen Optimierungsstufen bei realen Projekten praktisch nicht wahrnehmbar ...

Option -O0– bricht jegliche Codeoptimierung ab. Die Option ist in der Anwendungs-Debugging-Phase erforderlich. Wie oben gezeigt, kann die Optimierung dazu führen, dass sich die Struktur des Programms bis zur Unkenntlichkeit verändert; der Zusammenhang zwischen der ausführbaren Datei und dem Quellcode wird nicht offensichtlich sein; dementsprechend ist ein schrittweises Debuggen des Programms nicht möglich. Wenn Sie die Option aktivieren -G, wird empfohlen, und zu aktivieren -O0.

-Os-Option– Legt die Optimierung nicht auf Code-Effizienz fest, sondern auf die Größe der resultierenden Datei. Die Leistung des Programms sollte mit der Leistung des Codes vergleichbar sein, der während der Kompilierung mit der standardmäßig eingestellten Optimierungsstufe erhalten wird.

Option -march= Architektur– Gibt die Zielprozessorarchitektur an. Die Liste der unterstützten Architekturen ist beispielsweise für Prozessoren der Familie umfangreich Intel/AMD kann eingestellt werden i386, Pentium, Prescott, opteron-sse3 usw. Benutzer von Binärdistributionen sollten bedenken, dass es für die ordnungsgemäße Funktion von Programmen mit dieser Option wünschenswert ist, dass alle enthaltenen Bibliotheken mit derselben Option kompiliert werden.

Die an den Linker übergebenen Optionen werden im Folgenden erläutert.

Kleine Ergänzung:

Das wurde oben gesagt gcc ermittelt den Typ (Programmiersprache) der übertragenen Dateien anhand ihrer Erweiterung und führt entsprechend dem erratenen Typ (Sprache) Aktionen an ihnen aus. Der Benutzer ist verpflichtet, die Erweiterungen der erstellten Dateien zu überwachen und diese gemäß den Vereinbarungen auszuwählen gcc. In der Wirklichkeit gcc Sie können Dateien mit beliebigen Namen einfügen. gcc -x-Option ermöglicht es Ihnen, die Programmiersprache der kompilierten Dateien explizit anzugeben. Die Wirkung der Option gilt für alle nachfolgenden im Befehl aufgeführten Dateien (bis zum Erscheinen der nächsten Option). -X). Mögliche Optionsargumente:

c C-Header c-cpp-Ausgabe

c++ c++-Header c++-cpp-Ausgabe

Objective-C Objective-C-Header Objective-C-CPP-Ausgabe

Objective-C++ Objective-C++-Header Objective-C++-CPP-Ausgabe

Assembler Assembler-mit-CPP

ada

f77 f77-cpp-input

f95 f95-cpp-input

Java

Der Zweck der Argumente sollte aus ihrer Niederschrift klar hervorgehen (hier). cpp hat nichts zu tun mit C++, dies ist eine von einem Präprozessor vorverarbeitete Quellcodedatei). Lass uns das Prüfen:

$ mv hallo.c hallo.txt
$ gcc -Wall -x c -o hallo hallo.txt
$./Hallo
Hallo Welt

Separate Zusammenstellung

Stärke der Sprachen C/C++ ist die Möglichkeit, den Quellcode des Programms in mehrere Dateien aufzuteilen. Man kann sogar noch mehr sagen: Die Möglichkeit der separaten Kompilierung ist die Grundlage der Sprache, ohne sie effiziente Nutzung C undenkbar. Es handelt sich um eine Multidateiprogrammierung, die Ihnen die Implementierung ermöglicht C Großprojekte, wie z Linux(hier unter dem Wort Linux Dies bezieht sich sowohl auf den Kern als auch auf das System als Ganzes. Was bringt die separate Kompilierung einem Programmierer?

1. Ermöglicht Ihnen, den Programmcode (Projektcode) lesbarer zu machen. Es wird fast unmöglich, die Quelldatei für mehrere Dutzend Bildschirme abzudecken. Wenn Sie es gemäß einer (vorab durchdachten) Logik in viele kleine Fragmente (jeweils in einer separaten Datei) aufteilen, wird es viel einfacher sein, die Komplexität des Projekts zu bewältigen.

2. Ermöglicht Ihnen, die Zeit zu reduzieren, die zum Neukompilieren eines Projekts benötigt wird. Wenn Änderungen an einer Datei vorgenommen werden, macht es keinen Sinn, das gesamte Projekt neu zu kompilieren; es reicht aus, nur diese geänderte Datei neu zu kompilieren.

3. Ermöglicht Ihnen, die Arbeit an einem Projekt auf mehrere Entwickler zu verteilen. Jeder Programmierer erstellt und debuggt seinen eigenen Teil des Projekts, aber es wird jederzeit möglich sein, alle resultierenden Entwicklungen zum Endprodukt zusammenzusetzen (wieder zusammenzusetzen).

4. Ohne separate Kompilierung gäbe es keine Bibliotheken. Durch Bibliotheken wird die Wiederverwendung und Verteilung von Code implementiert. C/C++, und Binärcode, der es einerseits Entwicklern ermöglicht, einen einfachen Mechanismus zum Einbinden in ihre Programme bereitzustellen und andererseits bestimmte Implementierungsdetails vor ihnen zu verbergen. Bei der Arbeit an einem Projekt sollte man immer darüber nachdenken, ob man von dem, was bereits erledigt wurde, irgendwann einmal etwas brauchen wird? Vielleicht lohnt es sich, einen Teil des Codes im Voraus zu trennen und als Bibliothek zu organisieren? Meiner Meinung nach vereinfacht dieser Ansatz das Leben erheblich und spart viel Zeit.

GCC Natürlich unterstützt es die separate Kompilierung und erfordert keine besonderen Anweisungen des Benutzers. Im Allgemeinen ist alles sehr einfach.

Hier ist ein praktisches Beispiel (wenn auch sehr, sehr bedingt).

Satz Quellcodedateien:

/* Haupt c */

#enthalten

#include „first.h“
#include „second.h“

int hauptsächlich( Leere )
{

Erste();
zweite();

Printf("Hauptfunktion... \n " );

zurückkehren 0 ;


/* first.h */

Leere Erste( Leere );


/* first.c */

#enthalten

#include „first.h“

Leere Erste( Leere )
{

Printf("Erste Funktion... \n " );


/* Sekunde.h */

Leere zweite( Leere );


/* second.c */

#enthalten

#include „second.h“

Leere zweite( Leere )
{

Printf("Zweite Funktion... \n " );

Im Allgemeinen haben wir Folgendes:

$ls
erstes.c erstes.h Haupt.c zweites.c zweites.h

All diese Dinge können in einem Befehl zusammengefasst werden:

$ gcc -Wall -o main main.c first.c second.c
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Dies wird uns jedoch praktisch keine Vorteile bringen, mit Ausnahme eines strukturierteren und lesbareren Codes, der auf mehrere Dateien verteilt ist. Bei diesem Kompilierungsansatz ergeben sich alle oben aufgeführten Vorteile:

$ gcc -Wall -c main.c
$ gcc -Wall -c first.c
$ gcc -Wall -c second.c
$ls
first.c first.h first.o main.c main.o second.c second.h second.o
$ gcc -o main main.o first.o second.o
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Was haben wir getan? Aus jeder Quelldatei (Kompilieren mit der Option -C) hat eine Objektdatei erhalten. Anschließend wurden die Objektdateien in die endgültige ausführbare Datei verknüpft. Natürlich Teams gcc Es gibt noch mehr, aber niemand stellt Projekte manuell zusammen. Dafür gibt es Assembly-Dienstprogramme (die beliebtesten). machen). Bei der Verwendung von Assembly-Dienstprogrammen kommen alle oben genannten Vorteile der separaten Kompilierung zum Tragen.

Es stellt sich die Frage: Wie schafft es der Linker, Objektdateien zusammenzufügen und gleichzeitig die Adressierung von Aufrufen korrekt zu berechnen? Woher weiß er überhaupt, dass die Datei second.o den Code für die Funktion second() enthält und der Code in der Datei main.o ihren Aufruf enthält? Es stellt sich heraus, dass alles einfach ist – die Objektdatei enthält das sogenannte Symboltabelle , einschließlich der Namen einiger Codepositionen (Funktionen und externe Variablen). Der Linker durchsucht die Symboltabelle jeder Objektdatei, sucht nach gemeinsamen (mit übereinstimmenden Namen) Positionen, auf deren Grundlage er Rückschlüsse auf die tatsächliche Position des Codes der verwendeten Funktionen (oder Datenblöcke) zieht und dementsprechend die neu berechnet Aufrufadressen in der ausführbaren Datei.

Sie können die Symboltabelle mit dem Dienstprogramm anzeigen nm.

$nm main.o
du zuerst
00000000 T Haupt
Du setzt
Du Zweiter
$nm zuerst.o
00000000 T zuerst
Du setzt
$nmsecond.o
Du setzt
00000000 T Sekunde

Das Aussehen des puts-Aufrufs wird durch die Verwendung der Standardbibliotheksfunktion printf() erklärt, die sich zur Kompilierungszeit in puts() verwandelte.

Die Symboltabelle wird nicht nur in die Objektdatei, sondern auch in die ausführbare Datei geschrieben:

$nm main
08049f20d_DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
080484fc R_IO_stdin_used
w_Jv_RegisterClasses
08049f10 d __CTOR_END__
08049f0c d __CTOR_LIST__
08049f18 D __DTOR_END__
08049f14 d __DTOR_LIST__
08048538 r __FRAME_END__
08049f1c d __JCR_END__
08049f1c d __JCR_LIST__
0804a014 Ein __bss_start
0804a00c D __data_start
080484b0 t __do_global_ctors_aux
08048360 t __do_global_dtors_aux
0804a010 D __dso_handle
w __gmon_start__
080484aa T __i686.get_pc_thunk.bx
08049f0c d __init_array_end
08049f0c d __init_array_start
08048440 T __libc_csu_fini
08048450 T __libc_csu_init
U __libc_start_main@@GLIBC_2.0
0804a014 A_edata
0804a01c A_Ende
080484dc T_fini
080484f8 R_fp_hw
080482b8 T_init
08048330 T_start
0804a014 b abgeschlossen.7021
0804a00c W data_start
0804a018 b dtor_idx.7023
0804840c T zuerst
080483c0 t frame_dummy
080483e4 T-Haupt
U setzt@@GLIBC_2.0
08048420 T Sekunde

Das Einbinden einer Symboltabelle in die ausführbare Datei ist insbesondere zur Vereinfachung des Debuggens erforderlich. Im Prinzip ist es nicht unbedingt erforderlich, die Anwendung auszuführen. Für ausführbare Dateien realer Programme mit vielen Funktionsdefinitionen und externen Variablen, die eine Reihe verschiedener Bibliotheken verwenden, wird die Symboltabelle recht umfangreich. Um die Größe der Ausgabedatei zu reduzieren, kann sie mit entfernt werden mit der Option gcc -s.

$ gcc -s -o main main.o first.o second.o
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...
$nm main
nm: main: keine Symbole

Es ist zu beachten, dass der Linker beim Verknüpfen keine Kontextprüfungen für Funktionsaufrufe durchführt; er überwacht weder den Typ des Rückgabewerts noch den Typ und die Anzahl der empfangenen Parameter (und er hat keinen Ort, an dem er solche Informationen abrufen kann). . Alle Überprüfungen der Richtigkeit von Aufrufen müssen in der Kompilierungsphase erfolgen. Bei der Mehrdateiprogrammierung ist es erforderlich, hierfür den Sprachheaderdateimechanismus zu verwenden. C.

Bibliotheken

Bibliothek – in der Sprache C, eine Datei mit Objektcode, der in der Verknüpfungsphase mithilfe der Bibliothek an ein Programm angehängt werden kann. Tatsächlich handelt es sich bei einer Bibliothek um eine Reihe speziell angeordneter Objektdateien.

Der Zweck von Bibliotheken besteht darin, dem Programmierer einen Standardmechanismus zur Wiederverwendung von Code zur Verfügung zu stellen, und dieser Mechanismus ist einfach und zuverlässig.

Aus Sicht des Betriebssystems und der Anwendungssoftware sind es Bibliotheken statisch Und geteilt (dynamisch ).

Der Code statischer Bibliotheken wird bei deren Verknüpfung in die ausführbare Datei eingebunden. Es stellt sich heraus, dass die Bibliothek „fest in der Datei verankert“ ist, der Bibliothekscode wird mit dem Rest des Dateicodes „zusammengeführt“. Ein Programm, das statische Bibliotheken verwendet, wird eigenständig und kann auf fast jedem Computer mit geeigneter Architektur und Betriebssystem ausgeführt werden.

Der Shared-Library-Code wird vom Betriebssystem auf Anforderung des Programms während seiner Ausführung geladen und mit dem Programmcode verbunden. Die ausführbare Datei des Programms enthält keinen dynamischen Bibliothekscode; nur der Link zur Bibliothek ist in der ausführbaren Datei enthalten. Dadurch ist ein Programm, das gemeinsam genutzte Bibliotheken verwendet, nicht mehr eigenständig und kann nur auf einem System erfolgreich gestartet werden, auf dem die beteiligten Bibliotheken installiert sind.

Das Shared-Library-Paradigma bietet drei wesentliche Vorteile:

1. Die Größe der ausführbaren Datei wird um ein Vielfaches reduziert. In einem System, das viele Binärdateien enthält, die denselben Code verwenden, ist es nicht erforderlich, für jede ausführbare Datei eine Kopie dieses Codes zu speichern.

2. Der von mehreren Anwendungen verwendete Shared-Library-Code wird in einer Kopie im RAM gespeichert (das ist tatsächlich nicht so einfach...), wodurch der Bedarf des Systems an verfügbarem RAM reduziert wird.

3. Es ist nicht erforderlich, jede ausführbare Datei neu zu erstellen, wenn Änderungen am Code der gemeinsam genutzten Bibliothek vorgenommen werden. Änderungen und Korrekturen am dynamischen Bibliothekscode werden automatisch in allen Programmen widergespiegelt, die ihn verwenden.

Ohne das Shared-Library-Paradigma gäbe es keine vorkompilierten (binären) Distributionen Linux(Ja, es gibt keine). Stellen Sie sich die Größe der Verteilung vor, in der in jeder Binärdatei der Code der Standardbibliothek platziert würde C(und alle anderen enthaltenen Bibliotheken). Stellen Sie sich vor, was Sie tun müssten, um das System zu aktualisieren, nachdem eine kritische Schwachstelle in einer der weit verbreiteten Bibliotheken beseitigt wurde ...

Jetzt etwas Übung.

Zur Veranschaulichung verwenden wir den Satz Quelldateien aus dem vorherigen Beispiel. In unserer hausgemachten Bibliothek werden wir den Code (Implementierung) der Funktionen first() und second() platzieren.

Linux hat das folgende Benennungsschema für Bibliotheksdateien (obwohl es nicht immer befolgt wird): Der Name der Bibliotheksdatei beginnt mit dem lib-Präfix, gefolgt vom Bibliotheksnamen selbst und endet mit der Erweiterung .a ( Archiv ) – für eine statische Bibliothek, .so ( gemeinsames Objekt ) – für gemeinsam genutzte (dynamische) Bibliotheken werden nach der Erweiterung die Ziffern der Versionsnummer durch einen Punkt aufgelistet (nur für eine dynamische Bibliothek). Der Name der Header-Datei, die der Bibliothek entspricht, besteht (wiederum in der Regel) aus dem Bibliotheksnamen (ohne Präfix und Version) und der Erweiterung .h. Zum Beispiel: libogg.a, libogg.so.0.7.0, ogg.h.

Lassen Sie uns zunächst eine statische Bibliothek erstellen und verwenden.

Die Funktionen first() und second() bilden den Inhalt unserer libhello-Bibliothek. Der Name der Bibliotheksdatei lautet dementsprechend libhello.a. Die Header-Datei hello.h ist mit der Bibliothek vergleichbar.

/* Hallo h */

Leere Erste( Leere );
Leere zweite( Leere );

Natürlich die Zeilen:

#include „first.h“


#include „second.h“

in den Dateien main.c , first.c und second.c müssen ersetzt werden durch:

#include „hello.h“

Nun geben wir die folgende Befehlsfolge ein:

$ gcc -Wall -c first.c
$ gcc -Wall -c second.c
$ ar crs libhello.a first.o second.o
$file libhello.a
libhello.a: aktuelles AR-Archiv

Wie bereits erwähnt, ist eine Bibliothek eine Sammlung von Objektdateien. Mit den ersten beiden Befehlen haben wir diese Objektdateien erstellt.

Als nächstes müssen Sie die Objektdateien in einem Satz anordnen. Hierzu wird ein Archiver verwendet ar- Das Dienstprogramm „fügt“ mehrere Dateien zu einer zusammen; das resultierende Archiv enthält die Informationen, die zum Wiederherstellen (Extrahieren) jeder einzelnen Datei erforderlich sind (einschließlich ihrer Eigentums-, Zugriffs- und Zeitattribute). Es erfolgt keine „Komprimierung“ der Archivinhalte oder sonstige Transformation der gespeicherten Daten.

Option Autoname- Erstellen Sie ein Archiv. Wenn kein Archiv mit dem Namen arname existiert, wird es erstellt, andernfalls werden die Dateien dem vorhandenen Archiv hinzugefügt.

Option r- legt den Archivaktualisierungsmodus fest; wenn eine Datei mit dem angegebenen Namen bereits im Archiv vorhanden ist, wird sie gelöscht und eine neue Datei wird am Ende des Archivs hinzugefügt.

Optionen- Fügt den Archivindex hinzu (aktualisiert ihn). In diesem Fall ist der Archivindex eine Tabelle, in der jedem in den Archivdateien definierten symbolischen Namen (Funktionsname oder Datenblock) der entsprechende Objektdateiname zugeordnet ist. Der Archivindex ist notwendig, um die Arbeit mit der Bibliothek zu beschleunigen – um die gewünschte Definition zu finden, ist es nicht notwendig, die Symboltabellen aller Archivdateien einzusehen; Sie können sofort zu der Datei gehen, die den gesuchten Namen enthält. Sie können den Archivindex mit dem bereits bekannten Dienstprogramm anzeigen nm es auszunutzen mit der Option -s(Es werden auch die Symboltabellen aller Objektdateien des Archivs angezeigt):

$ nm -s libhello.a
Archivindex:
erster in erster.o
Sekunde in Sekunde.o

first.o:
00000000 T zuerst
Du setzt

zweite.o:
Du setzt
00000000 T Sekunde

Um einen Archivindex zu erstellen, gibt es ein spezielles Dienstprogramm ranlib. Die Bibliothek libhello.a hätte wie folgt erstellt werden können:

$ ar cr libhello.a first.o second.o
$ ranlib libhello.a

Allerdings funktioniert die Bibliothek ohne einen Archivindex einwandfrei.

Nutzen wir nun unsere Bibliothek:

$ gcc -Wall -c main.c
$
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Funktioniert...

Nun zu den Kommentaren... Es sind zwei neue Optionen aufgetaucht gcc:

Option -l Name– an den Linker übergeben, was darauf hinweist, dass die libname-Bibliothek in die ausführbare Datei aufgenommen werden muss. Verbinden bedeutet anzugeben, dass bestimmte Funktionen (externe Variablen) in dieser oder jener Bibliothek definiert sind. In unserem Beispiel ist die Bibliothek statisch; alle symbolischen Namen verweisen auf den Code, der sich direkt in der ausführbaren Datei befindet. Bitte in der Option vermerken -l Der Bibliotheksname wird als Name ohne lib-Präfix angegeben.

Option -L /Pfad/zum/Verzeichnis/mit/Bibliotheken – wird an den Linker übergeben und gibt den Pfad zum Verzeichnis an, das die verbundenen Bibliotheken enthält. In unserem Fall ist der Punkt gegeben . , sucht der Linker zunächst im aktuellen Verzeichnis nach Bibliotheken und dann in den im System definierten Verzeichnissen.

Hier muss eine kleine Anmerkung gemacht werden. Tatsache ist, dass es eine Reihe von Optionen gibt gcc Die Reihenfolge, in der sie in der Befehlszeile angezeigt werden, ist wichtig. Auf diese Weise sucht der Linker nach Code, der mit den in der Symboltabelle der Datei angegebenen Namen in den in der Befehlszeile aufgeführten Bibliotheken übereinstimmt nach der Name dieser Datei. Die Inhalte der vor dem Dateinamen aufgeführten Bibliotheken werden vom Linker ignoriert:

$ gcc -Wall -c main.c
$ gcc -o main -L. -lhallo main.o
main.o: In der Funktion „main“:
main.c:(.text+0xa): undefinierter Verweis auf „first“
main.c:(.text+0xf): undefinierter Verweis auf „second“

$ gcc -o main main.o -L. -lHallo
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Dieses Verhaltensmerkmal gcc Aufgrund des Wunsches der Entwickler, dem Benutzer die Möglichkeit zu geben, Dateien auf unterschiedliche Weise mit Bibliotheken zu kombinieren, verwenden Sie sich überschneidende Namen... Meiner Meinung nach ist es nach Möglichkeit besser, sich damit nicht zu beschäftigen. Im Allgemeinen sollten eingebundene Bibliotheken nach dem Namen der Datei aufgeführt werden, die auf sie verweist.

Existiert alternativer Weg Angabe des Speicherorts von Bibliotheken im System. Abhängig von der Distribution kann die Umgebungsvariable LD_LIBRARY_PATH oder LIBRARY_PATH eine durch Doppelpunkte getrennte Liste von Verzeichnissen speichern, in denen der Linker nach Bibliotheken suchen soll. In der Regel ist diese Variable standardmäßig überhaupt nicht definiert, aber nichts hindert Sie daran, sie zu erstellen:

$ echo $LD_LIBRARY_PATH

/usr/lib/gcc/i686-pc-linux-gnu/4.4.3/../../../../i686-pc-linux-gnu/bin/ld: -lhello kann nicht gefunden werden
Collect2: ld-Ausführung mit Rückkehrcode 1 abgeschlossen
$ export LIBRARY_PATH=.
$ gcc -o main main.o -lhello
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Das Bearbeiten von Umgebungsvariablen ist nützlich, wenn Sie Ihre eigenen Bibliotheken erstellen und debuggen sowie wenn die Notwendigkeit besteht, eine nicht standardmäßige (veraltet, aktualisiert, geändert – im Allgemeinen anders als die in der Distribution enthaltene) gemeinsam genutzte Bibliothek mit der Anwendung zu verbinden.

Lassen Sie uns nun eine dynamische Bibliothek erstellen und verwenden.

Der Satz der Quelldateien bleibt unverändert. Wir geben die Befehle ein, schauen, was passiert, lesen die Kommentare:

$ gcc -Wall -fPIC -c first.c
$ gcc -Wall -fPIC -c second.c
$ gcc -shared -o libhello.so.2.4.0.5 -Wl,-soname,libhello.so.2 first.o second.o

Was haben Sie als Ergebnis erhalten?

$-Datei libhello.so.2.4.0.5
libhello.so.2.4.0.5: Gemeinsames ELF-64-Bit-LSB-Objekt, x86-64, Version 1 (SYSV), dynamisch verknüpft, nicht entfernt

Die Datei ist libhello.so.2.4.0.5, dies ist unsere gemeinsam genutzte Bibliothek. Im Folgenden besprechen wir die Verwendung.

Nun die Kommentare:

Option -fPIC- erfordert, dass der Compiler beim Erstellen von Objektdateien generiert Positionsunabhängiger Code (PIC – Positionsunabhängiger Code ), der Hauptunterschied liegt in der Art und Weise, wie Adressen dargestellt werden. Anstatt feste (statische) Positionen anzugeben, werden alle Adressen basierend auf den in angegebenen Offsets berechnet globale Offset-Tabelle (globale Offsettabelle - GOT ). Das positionsunabhängige Codeformat ermöglicht es Ihnen, ausführbare Module zum Zeitpunkt des Ladens mit dem Hauptprogrammcode zu verbinden. Dementsprechend besteht der Hauptzweck von ortsunabhängigem Code in der Erstellung dynamischer (gemeinsam genutzter) Bibliotheken.

-shared-Option- zeigt an gcc, dass als Ergebnis keine ausführbare Datei kompiliert werden sollte, sondern ein gemeinsam genutztes Objekt – eine dynamische Bibliothek.

Option -Wl,-soname,libhello.so.2- Sätze soname Bibliotheken. Wir werden im nächsten Absatz ausführlich über Soname sprechen. Lassen Sie uns nun das Format der Option besprechen. Dieses auf den ersten Blick seltsame Design mit Kommas ist für die direkte Interaktion zwischen Benutzer und Linker gedacht. Während der Kompilierung gcc Der Linker ruft automatisch, automatisch und nach eigenem Ermessen auf. gccübergibt ihm die Optionen, die für den erfolgreichen Abschluss der Aufgabe erforderlich sind. Wenn der Benutzer selbst in den Verknüpfungsprozess eingreifen muss, kann er eine spezielle Option nutzen gcc -Wl, -option, Wert1, Wert2 .... Was bedeutet es, an den Linker zu übergeben ( -Wl) Möglichkeit -Möglichkeit mit Argumenten Wert1, Wert2 usw. In unserem Fall wurde dem Linker die Option gegeben -soname mit Argument libhello.so.2.

Nun zu Soname. Beim Erstellen und Verteilen von Bibliotheken entsteht das Problem der Kompatibilität und Versionskontrolle. Damit das System, insbesondere der dynamische Bibliothekslader, eine Vorstellung davon hat, welche Version der Bibliothek beim Kompilieren der Anwendung verwendet wurde und dementsprechend für den erfolgreichen Betrieb erforderlich ist, wurde eine spezielle Kennung bereitgestellt – soname , sowohl in der Bibliotheksdatei selbst als auch in der ausführbaren Datei der Anwendung platziert. Der soname-Bezeichner ist eine Zeichenfolge, die den Bibliotheksnamen mit dem Präfix lib, einen Punkt, die Erweiterung so, wiederum einen Punkt, und eine oder zwei (durch Punkte getrennte) Ziffern der Bibliotheksversion enthält – lib-Name .so. X. j. Das heißt, soname stimmt mit dem Namen der Bibliotheksdatei bis zur ersten oder zweiten Ziffer der Versionsnummer überein. Der Name der ausführbaren Datei unserer Bibliothek sei libhello.so.2.4.0.5, dann könnte der Soname der Bibliothek libhello.so.2 lauten. Wenn Sie die Schnittstelle einer Bibliothek ändern, muss deren Soname geändert werden! Jede Änderung des Codes, die zu einer Inkompatibilität mit früheren Versionen führt, muss mit dem Erscheinen eines neuen Sonamens einhergehen.

Wie funktioniert das Ganze? Angenommen, für die erfolgreiche Ausführung einer Anwendung ist eine Bibliothek mit dem Namen „hello“ erforderlich. Es sei eine solche im System vorhanden. Der Name der Bibliotheksdatei lautet „libhello.so.2.4.0.5“ und der darin geschriebene Soname der Bibliothek lautet „libhello.so.2“. In der Anwendungskompilierungsphase wird der Linker gemäß der Option verwendet -l Hallo, durchsucht das System nach einer Datei namens libhello.so . Auf einem realen System ist libhello.so ein symbolischer Link zur Datei libhello.so.2.4.0.5. Nachdem er Zugriff auf die Bibliotheksdatei erhalten hat, liest der Linker den darin geschriebenen Soname-Wert und platziert ihn zusammen mit anderen Dingen in der ausführbaren Datei der Anwendung. Wenn die Anwendung gestartet wird, erhält der dynamische Bibliothekslader eine Anfrage, die aus der ausführbaren Datei gelesene Bibliothek mit soname einzuschließen, und versucht, auf dem System eine Bibliothek zu finden, deren Dateiname mit soname übereinstimmt. Das heißt, der Loader versucht, die Datei libhello.so.2 zu finden. Wenn das System richtig konfiguriert ist, sollte es einen symbolischen Link libhello.so.2 zur Datei libhello.so.2.4.0.5 enthalten, der Bootloader hat Zugriff auf die benötigte Bibliothek und wird dies dann ohne zu zögern (und ohne etwas anderes zu überprüfen) tun Verbinden Sie es mit der Anwendung. Stellen Sie sich nun vor, dass wir die so kompilierte Anwendung auf ein anderes System übertragen haben, wo nur die vorherige Version der Bibliothek mit dem Sonamen libhello.so.1 bereitgestellt wird. Der Versuch, das Programm auszuführen, führt zu einem Fehler, da auf diesem System keine Datei mit dem Namen libhello.so.2 vorhanden ist.

Daher muss der Linker in der Kompilierungsphase eine Bibliotheksdatei (oder einen symbolischen Link zu einer Bibliotheksdatei) mit dem Namen lib name .so bereitstellen. Zur Laufzeit benötigt der Loader eine Datei (oder einen symbolischen Link) mit dem Namen lib name .so . X. j. Was hat der lib-Name .so damit zu tun? X. y muss mit der Soname-Zeichenfolge der verwendeten Bibliothek übereinstimmen.

In Binärdistributionen werden in der Regel die Bibliotheksdatei libhello.so.2.4.0.5 und ein Link dazu libhello.so.2 in das libhello-Paket eingefügt, und der Link libhello.so, der nur für die Kompilierung notwendig ist, zusammen mit Die Bibliotheks-Header-Datei hello.h wird im libhello-devel-Paket gepackt (das devel-Paket enthält auch eine Datei für die statische Version der libhello.a-Bibliothek; die statische Bibliothek kann ebenfalls nur in der Kompilierungsphase verwendet werden). . Beim Entpacken des Pakets befinden sich alle aufgelisteten Dateien und Links (außer hello.h) im selben Verzeichnis.

Stellen wir sicher, dass die angegebene Soname-Zeile tatsächlich in unserer Bibliotheksdatei geschrieben ist. Lassen Sie uns das Mega-Dienstprogramm verwenden objdump mit Option -P :

$ objdump -p libhello.so.2.4.0.5 | grep SONAME
SONAME libhello.so.2


Dienstprogramm objdump- ein leistungsstarkes Tool, mit dem Sie umfassende Informationen über den internen Inhalt (und die Struktur) eines Objekts oder einer ausführbaren Datei erhalten können. Das steht in der Manpage des Dienstprogramms objdump Erstens wird es für Programmierer nützlich sein, die Debugging- und Kompilierungstools erstellen und nicht nur einige Anwendungsprogramme schreiben :) Insbesondere mit der Option -D Dies ist ein Disassembler. Wir haben die Option genutzt -P- verschiedene Metainformationen zur Objektdatei anzeigen.

Im obigen Beispiel zum Erstellen einer Bibliothek haben wir uns strikt an die Prinzipien der separaten Kompilierung gehalten. Natürlich könnte die Bibliothek mit einem Aufruf auch so kompiliert werden gcc:

$ gcc -shared -Wall -fPIC -o libhello.so.2.4.0.5 -Wl,-soname,libhello.so.2 first.c second.c

Versuchen wir nun, die resultierende Bibliothek zu verwenden:

$ gcc -Wall -c main.c
$
/usr/bin/ld: -lhello kann nicht gefunden werden
Collect2: ld hat 1 Exit-Status zurückgegeben

Der Linker flucht. Erinnern wir uns daran, was oben über symbolische Links gesagt wurde. Erstellen Sie libhello.so und versuchen Sie es erneut:

$ ln -s libhello.so.2.4.0.5 libhello.so
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.

Jetzt sind alle glücklich. Starten Sie die erstellte Binärdatei:

Fehler... Der Loader beschwert sich und kann die Bibliothek libhello.so.2 nicht finden. Stellen wir sicher, dass die ausführbare Datei tatsächlich einen Link zu libhello.so.2 enthält:

$ objdump -p main | grep ERFORDERLICH
BENÖTIGT libhello.so.2
BENÖTIGT libc.so.6

$ ln -s libhello.so.2.4.0.5 libhello.so.2
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Es hat funktioniert... Jetzt Kommentare zu den neuen Optionen gcc.

Option -Wl,-rpath,.- bereits bekannte Konstruktion, übergeben Sie die Option an den Linker -rpath mit Argument . . Mit Hilfe -rpath In die ausführbare Datei des Programms können Sie zusätzliche Pfade schreiben, entlang derer der Shared Library Loader nach Bibliotheksdateien sucht. In unserem Fall ist der Pfad geschrieben . - Die Suche nach Bibliotheksdateien beginnt im aktuellen Verzeichnis.

$ objdump -p main | grep RPATH
RPATH.

Dank dieser Option ist es nicht erforderlich, Umgebungsvariablen beim Start des Programms zu ändern. Es ist klar, dass, wenn Sie das Programm in ein anderes Verzeichnis verschieben und versuchen, es auszuführen, die Bibliotheksdatei nicht gefunden wird und der Loader eine Fehlermeldung anzeigt:

$mv main..
$ ../main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Mit dem Dienstprogramm können Sie auch herausfinden, welche gemeinsam genutzten Bibliotheken Ihre Anwendung benötigt ldd:

$ldd main
linux-vdso.so.1 => (0x00007fffaddff000)
libhello.so.2 => ./libhello.so.2 (0x00007f9689001000)
libc.so.6 => /lib/libc.so.6 (0x00007f9688c62000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9689205000)

In der Ausgabe ldd Für jede erforderliche Bibliothek werden ihr Soname und der vollständige Pfad zur Bibliotheksdatei angegeben, der gemäß den Systemeinstellungen festgelegt wird.

Jetzt ist es an der Zeit, darüber zu sprechen, wo Bibliotheksdateien im System abgelegt werden sollen, wo der Loader versucht, sie zu finden und wie dieser Prozess verwaltet wird.

Gemäß den Vereinbarungen FHS (Filesystem Hierarchy Standard) Das System muss über zwei (mindestens) Verzeichnisse zum Speichern von Bibliotheksdateien verfügen:

/lib – hier sind die wichtigsten Distributionsbibliotheken, die für den Betrieb von Programmen aus /bin und /sbin erforderlich sind;

/usr/lib – Bibliotheken, die von Anwendungsprogrammen aus /usr/bin und /usr/sbin benötigt werden, werden hier gespeichert;

Die den Bibliotheken entsprechenden Header-Dateien müssen sich im Verzeichnis /usr/include befinden.

Der Loader sucht standardmäßig in diesen Verzeichnissen nach Bibliotheksdateien.

Zusätzlich zu den oben aufgeführten muss das System über ein Verzeichnis /usr/local/lib verfügen – dieses sollte vom Benutzer unabhängig bereitgestellte Bibliotheken unter Umgehung des Paketverwaltungssystems enthalten (nicht in der Distribution enthalten). In diesem Verzeichnis befinden sich beispielsweise standardmäßig Bibliotheken, die aus Quellen kompiliert wurden (aus Quellen installierte Programme werden in /usr/local/bin und /usr/local/sbin abgelegt, wir sprechen natürlich von binären Distributionen). Die Bibliotheks-Header-Dateien werden in diesem Fall in /usr/local/include abgelegt.

In einer Reihe von Distributionen (in Ubuntu) Der Loader ist nicht für die Anzeige des Verzeichnisses /usr/local/lib konfiguriert. Wenn der Benutzer die Bibliothek daher von der Quelle installiert, wird sie vom System nicht angezeigt. Dies wurde von den Autoren der Distribution speziell getan, um dem Benutzer die Installation beizubringen Software nur über das Paketverwaltungssystem. Was in diesem Fall zu tun ist, wird im Folgenden beschrieben.

Um die Suche nach Bibliotheksdateien zu vereinfachen und zu beschleunigen, sucht der Loader nicht bei jedem Zugriff auf die oben genannten Verzeichnisse, sondern verwendet die in der Datei /etc/ld.so.cache (Bibliothekscache) gespeicherte Datenbank ). Dies enthält Informationen darüber, wo im System sich die Bibliotheksdatei befindet, die einem bestimmten Sonamen entspricht. Nachdem der Loader eine Liste der von einer bestimmten Anwendung benötigten Bibliotheken erhalten hat (eine Liste der in der ausführbaren Datei des Programms angegebenen Soname-Bibliotheken), verwendet er /etc/ld.so.cache, um den Pfad zur Datei jeder erforderlichen Bibliothek zu ermitteln und zu laden es ins Gedächtnis einprägen. Darüber hinaus kann der Bootloader Verzeichnisse anzeigen, die in den Systemvariablen LD_LIBRARY_PATH, LIBRARY_PATH und im RPATH-Feld der ausführbaren Datei aufgeführt sind (siehe oben).

Verwenden Sie das Dienstprogramm, um den Bibliothekcache zu verwalten und auf dem neuesten Stand zu halten ldconfig. Wenn du läufst ldconfig Ohne Optionen prüft das Programm die in der Befehlszeile angegebenen Verzeichnisse, die vertrauenswürdigen Verzeichnisse /lib und /usr/lib sowie die in der Datei /etc/ld.so.conf aufgeführten Verzeichnisse. Für jede in den angegebenen Verzeichnissen gefundene Bibliotheksdatei wird der Soname gelesen, ein symbolischer Link basierend auf dem Soname erstellt und die Informationen in /etc/ld.so.cache aktualisiert.

Lassen Sie uns sicherstellen, was gesagt wurde:

$ls
hallo.h libhello.so libhello.so.2.4.0.5 main.c
$
$ sudo ldconfig /full/path/to/catalog/with/example
$ls
hello.h libhello.so libhello.so.2 libhello.so.2.4.0.5 main main.c
$./main
Erste Funktion...
Zweite Funktion...
Hauptfunktion...

Erster Aufruf ldconfig Wir haben unsere Bibliothek zum Cache hinzugefügt und sie beim zweiten Aufruf ausgeschlossen. Beachten Sie, dass die Option beim Kompilieren von main weggelassen wurde -Wl,-rpath,. Daher suchte der Loader nur im Cache nach den erforderlichen Bibliotheken.

Jetzt sollte klar sein, was zu tun ist, wenn das System die Bibliothek nach der Installation aus der Quelle nicht sieht. Zunächst müssen Sie in der Datei /etc/ld.so.conf den vollständigen Pfad zum Verzeichnis mit den Bibliotheksdateien eingeben (standardmäßig /usr/local/lib). Format /etc/ld.so.conf – Die Datei enthält eine durch Doppelpunkt, Leerzeichen, Tabulator oder Zeilenumbruch getrennte Liste von Verzeichnissen, in denen nach Bibliotheken gesucht wird. Dann ruf an ldconfig ohne Optionen, aber mit Superuser-Rechten. Alles sollte funktionieren.

Lassen Sie uns zum Schluss darüber sprechen, wie statische und dynamische Versionen von Bibliotheken zusammenpassen. Was genau ist die Frage? Oben wurde bei der Erörterung der akzeptierten Namen und Speicherorte von Bibliotheksdateien gesagt, dass die Dateien der statischen und dynamischen Versionen der Bibliothek im selben Verzeichnis gespeichert sind. Wie denn gcc findet heraus, welche Art von Bibliothek wir verwenden möchten? Standardmäßig wird die dynamische Bibliothek bevorzugt. Wenn der Linker eine dynamische Bibliotheksdatei findet, verknüpft er diese ohne zu zögern mit der ausführbaren Datei des Programms:

$ls
hello.h libhello.a libhello.so libhello.so.2 libhello.so.2.4.0.5 main.c
$ gcc -Wall -c main.c
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.
$ldd main
linux-vdso.so.1 => (0x00007fffe1bb0000)
libhello.so.2 => ./libhello.so.2 (0x00007fd50370b000)
libc.so.6 => /lib/libc.so.6 (0x00007fd50336c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd50390f000)
$ du -h main
12K Hauptteil

Achten Sie auf die Größe der ausführbaren Datei des Programms. Es ist das Minimum, das möglich ist. Alle verwendeten Bibliotheken werden dynamisch verknüpft.

Existiert gcc -static-Option- Anweisung des Linkers, nur statische Versionen aller von der Anwendung benötigten Bibliotheken zu verwenden:

$ gcc -static -o main main.o -L. -lHallo
$file main
main: ELF 64-Bit-LSB-ausführbare Datei, x86-64, Version 1 (GNU/Linux), statisch verknüpft, für GNU/Linux 2.6.15, nicht entfernt
$ldd main
ist keine dynamische ausführbare Datei
$ du -h main
728K Haupt

Die Größe der ausführbaren Datei ist 60-mal größer als im vorherigen Beispiel – Standardsprachbibliotheken sind in der Datei enthalten C. Jetzt kann unsere Anwendung sicher von Verzeichnis zu Verzeichnis und sogar auf andere Maschinen übertragen werden, der Hallo-Bibliothekscode befindet sich in der Datei, das Programm ist völlig autonom.

Was tun, wenn Sie nur einen Teil der verwendeten Bibliotheken statisch verknüpfen müssen? Mögliche Variante Lösungen – Machen Sie den Namen der statischen Version der Bibliothek anders als den Namen der gemeinsam genutzten Version und geben Sie beim Kompilieren der Anwendung an, welche Version wir dieses Mal verwenden möchten:

$ mv libhello.a libhello_s.a
$ gcc -o main main.o -L. -lhello_s
$ldd main
linux-vdso.so.1 => (0x00007fff021f5000)
libc.so.6 => /lib/libc.so.6 (0x00007fd0d0803000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd0d0ba4000)
$ du -h main
12K Hauptteil

Da die Codegröße von libhello vernachlässigbar ist,

$ du -h libhello_s.a
4.0K libhello.a

Die Größe der resultierenden ausführbaren Datei entspricht praktisch der Größe der Datei, die mithilfe der dynamischen Verknüpfung erstellt wurde.

Nun, das ist wahrscheinlich alles. Vielen Dank an alle, die an dieser Stelle mit dem Lesen fertig waren.

Es wird allgemein angenommen, dass GCC hinsichtlich der Leistung anderen Compilern hinterherhinkt. In diesem Artikel werden wir versuchen herauszufinden, welche grundlegenden Optimierungen des GCC-Compilers angewendet werden sollten, um eine akzeptable Leistung zu erzielen.

Was sind die Standardoptionen in GCC?

(1) Die Standardoptimierungsstufe in GCC ist „-O0“. Aus Leistungssicht ist es eindeutig nicht optimal und wird nicht für die Kompilierung des Endprodukts empfohlen.
GCC erkennt die Architektur, auf der die Kompilierung ausgeführt wird, erst, wenn die Option „-march=native“ übergeben wird. Standardmäßig verwendet GCC die während der Konfiguration angegebene Option. Um die GCC-Konfiguration herauszufinden, führen Sie einfach Folgendes aus:

Das bedeutet, dass GCC „-march=corei7“ zu Ihren Optionen hinzufügt (sofern keine andere Architektur angegeben ist).
Die meisten GCC-Compiler für x86 (Basislinie für 64-Bit-Linux) fügen den angegebenen Optionen Folgendes hinzu: „-mtune=generic -march=x86-64“, da in der Konfiguration keine Optionen angegeben wurden, die die Architektur definieren. Mit dem folgenden Befehl können Sie jederzeit alle beim Start von GCC übergebenen Optionen sowie die internen Optionen herausfinden:

Daher häufig verwendet:

Die Angabe der zu verwendenden Architektur ist für die Leistung wichtig. Die einzige Ausnahme bilden Programme, bei denen der Aufruf von Bibliotheksfunktionen fast die gesamte Startzeit in Anspruch nimmt. GLIBC kann zur Laufzeit die optimale Funktion für eine bestimmte Architektur auswählen. Es ist wichtig zu beachten, dass bei statischer Verknüpfung einige GLIBC-Funktionen keine Versionen für verschiedene Architekturen haben. Das heißt, dynamische Assemblierung ist besser, wenn die Geschwindigkeit der GLIBC-Funktionen wichtig ist..
(2) Standardmäßig verwenden die meisten GCC-Compiler für x86 im 32-Bit-Modus das x87-Gleitkommamodell, da sie ohne „-mfpmath=sse“ konfiguriert wurden. Nur wenn die GCC-Konfiguration „--with-mfpmath=sse“ enthält:

Der Compiler verwendet standardmäßig das SSE-Modell. In allen anderen Fällen ist es besser, die Option „-mfpmath=sse“ zum Build im 32-Bit-Modus hinzuzufügen.
Also, oft verwendet:

Das Hinzufügen der Option „-mfpmath=sse“ ist im 32-Bit-Modus wichtig! Die Ausnahme ist der Compiler, der „--with-mfpmath=sse“ in seiner Konfiguration hat.

32-Bit-Modus oder 64-Bit?

Der 32-Bit-Modus wird normalerweise verwendet, um den verwendeten Speicher zu reduzieren und dadurch die Arbeit damit zu beschleunigen (mehr Daten passen in den Cache).
Im 64-Bit-Modus (im Vergleich zum 32-Bit-Modus) erhöht sich die Anzahl der verfügbaren öffentlichen Register von 6 auf 14, die der XMM-Register von 8 auf 16. Außerdem unterstützen alle 64-Bit-Architekturen die SSE2-Erweiterung, also im 64-Bit-Modus Es ist nicht erforderlich, die Option „-mfpmath“ =sse“ hinzuzufügen.
Es wird empfohlen, für Zählaufgaben den 64-Bit-Modus und für mobile Anwendungen den 32-Bit-Modus zu verwenden.

Wie erreiche ich maximale Leistung?

Es gibt keine spezifischen Optionen, um die beste Leistung zu erzielen, aber GCC bietet viele Optionen, die einen Versuch wert sind. Nachfolgend finden Sie eine Tabelle mit empfohlenen Optionen und Wachstumsprognosen für Intel Atom- und Intel Core i7-Prozessoren der 2. Generation im Vergleich zur Option „-O2“. Die Vorhersagen basieren auf dem geometrischen Mittel der Ergebnisse einer bestimmten Reihe von Problemen, die von GCC Version 4.7 zusammengestellt wurden. Es wird außerdem davon ausgegangen, dass die Compilerkonfiguration für x86-64 generisch durchgeführt wurde.
Prognostizierte Produktivitätssteigerung um mobile Anwendungen zu „-O2“ (nur im 32-Bit-Modus, da es der Hauptmodus für das Mobilfunksegment ist):

Prognose für eine höhere Leistung bei Rechenaufgaben im Vergleich zu „-O2“ (im 64-Bit-Modus):
-m64 -Ofast -flto ~17%
-m64 -Ofast -flto -march=native ~21%
-m64 -Ofast -flto -march=native -funroll-loops ~22%

Der Vorteil des 64-Bit-Modus gegenüber 32-Bit für Rechenaufgaben mit den Optionen „-O2 -mfpmath=sse“ beträgt etwa ~5 %
Bei allen Daten im Artikel handelt es sich um Prognosen, die auf den Ergebnissen einer bestimmten Reihe von Benchmarks basieren.
Nachfolgend finden Sie eine Beschreibung der im Artikel verwendeten Optionen. Vollständige Beschreibung (auf Englisch): http://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/Optimize-Options.html "
  • „-Ofast“ ermöglicht ähnlich wie „-O3 -ffast-math“ ein höheres Maß an Optimierungen und aggressivere Optimierungen für arithmetische Berechnungen (z. B. reale Neuzuordnung).
  • „-flto“-Optimierungen zwischen Modulen
  • „-m32“ 32-Bit-Modus
  • „-mfpmath=sse“ ermöglicht die Verwendung von XMM-Registern in der echten Arithmetik (anstelle des echten Stacks im x87-Modus)
  • „-funroll-loops“ ermöglicht das Abrollen der Schleife

Transportlogistik(Analyse verschiedener Transportarten: Vor- und Nachteile)

Der Transport ist ein Zweig der materiellen Produktion, der Menschen und Güter transportiert. in der Struktur soziale Produktion Der Transport gehört zum Bereich der Produktion materieller Dienstleistungen.

Es wird darauf hingewiesen, dass ein erheblicher Teil der Logistikvorgänge auf dem Weg des Materialflusses von der primären Rohstoffquelle bis zum Endverbrauch mit verschiedenen Mitteln abgewickelt wird Fahrzeug. Die Kosten für die Durchführung dieser Vorgänge betragen bis zu 50 % der gesamten Logistikkosten.

Zweckmäßig gibt es zwei Hauptverkehrsgruppen: Öffentlicher Verkehr – Industrie nationale Wirtschaft, das den Bedarf aller Sektoren der Volkswirtschaft und Bevölkerung an der Beförderung von Gütern und Personen befriedigt. Der öffentliche Verkehr dient der Verkehrssphäre und der Bevölkerung. Sie wird oft als Hauptlinie bezeichnet (die Hauptlinie ist in manchen Systemen die Hauptlinie, in diesem Fall im Kommunikationsroutensystem). Das Konzept des öffentlichen Verkehrs umfasst Schienenverkehr, Wassertransport (See- und Flusstransport), Straßen-, Luft- und Pipelinetransport).

Nichtöffentlicher Verkehr – innerindustrieller Verkehr sowie Fahrzeuge aller Art, die nicht-verkehrsbezogenen Organisationen gehören.

Die Organisation des Warenverkehrs im nichtöffentlichen Verkehr ist Gegenstand des Studiums der Produktionslogistik. Das Problem der Wahl der Vertriebskanäle wird im Bereich der Vertriebslogistik gelöst.

Es gibt also folgende Haupttransportarten:

Eisenbahn

Binnenwasserfluss

Automobil

Luft

Pipeline

Jede Transportart weist aus Sicht des Logistikmanagements spezifische Merkmale sowie Vor- und Nachteile auf, die die Einsatzmöglichkeiten im Logistiksystem bestimmen. Den Verkehrskomplex bilden verschiedene Verkehrsträger. Der Transportkomplex Russlands besteht aus juristischen und natürlichen Personen, die auf seinem Territorium registriert sind – Unternehmer, die Transport- und Speditionstätigkeiten für alle Arten von Transport, Planung, Bau, Reparatur und Wartung von Eisenbahnen, Autobahnen und darauf befindlichen Bauwerken, Pipelines und Arbeiten durchführen im Zusammenhang mit der Instandhaltung von schiffbaren Wasserbauwerken, Wasser und Atemwege Nachrichten, Dirigieren wissenschaftliche Forschung und Personalschulung, Unternehmen des Transportsystems, die Fahrzeuge herstellen, sowie Organisationen, die andere Arbeiten im Zusammenhang mit dem Transportprozess durchführen. Das russische Verkehrsgesetz umfasst über 160.000 km Haupteisenbahnen und Zufahrtsstraßen, 750.000 km asphaltierte Straßen, 1,0 Millionen km Seeschifffahrtslinien und 101.000 km Binnenstraßen Wasserstraßen, 800.000 km Fluglinien. Allein der öffentliche Verkehr transportiert über diese Kommunikation täglich etwa 4,7 Millionen Tonnen Fracht (Stand 2000), in der TC arbeiten über 4 Millionen Menschen und der Anteil des Verkehrs am Bruttoinlandsprodukt des Landes beträgt etwa 9 %. Somit ist der Verkehr ein wesentlicher Bestandteil der Infrastruktur der Wirtschaft und des gesamten Sozial- und Produktionspotenzials unseres Landes.

In der Tabelle 1 (4, 295) Die vergleichenden logistischen Eigenschaften verschiedener Transportarten werden angegeben.

Tabelle 1 Merkmale der Verkehrsträger

Art des Transports

Vorteile

Mängel

Eisenbahn

Hohe Tragfähigkeit und Durchsatz. Unabhängigkeit von klimatischen Bedingungen, Jahres- und Tageszeit.

Hohe Regelmäßigkeit des Transports. Relativ niedrige Tarife; erhebliche Rabatte für Transitsendungen. Hohe Geschwindigkeit Lieferung von Waren über große Entfernungen.

Begrenzte Anzahl von Trägern. Große Kapitalinvestitionen in die Produktions- und technische Basis. Hoher Material- und Energieverbrauch beim Transport. Geringe Erreichbarkeit der Endverkaufsstellen (Konsum).

Unzureichende Ladungssicherung.

Möglichkeit des interkontinentalen Transports. Niedrige Kosten für den Ferntransport. Hohe Trag- und Durchsatzkapazität. Geringe Kapitalintensität des Transports.

Begrenzter Transport.

Geringe Liefergeschwindigkeit (lange Frachtlaufzeit).

Abhängigkeit von geografischen, Navigations- und Wetterbedingungen.

Die Notwendigkeit, eine komplexe Hafeninfrastruktur zu schaffen.

Binnengewässer (Fluss)

Hohe Transportmöglichkeiten auf Tiefwasserflüssen und Stauseen.

Niedrige Transportkosten. Geringe Kapitalintensität.

Begrenzter Transport. Geringe Geschwindigkeit der Frachtlieferung.

Abhängigkeit von ungleichmäßigen Tiefen von Flüssen und Stauseen, Schifffahrtsbedingungen. Saisonalität. Unzureichende Zuverlässigkeit des Transports und Sicherheit der Ladung.

Automobil

Hohe Verfügbarkeit.

Möglichkeit der Lieferung von Fracht von Tür zu Tür

Hohe Manövrierfähigkeit, Flexibilität, Dynamik. Hohe Liefergeschwindigkeit. Möglichkeit der Nutzung verschiedener Routen und Lieferschemata.

Hohe Ladungssicherheit. Möglichkeit, Fracht in kleinen Mengen zu versenden.

Schwache Leistung. Abhängigkeit von Wetter- und Straßenverhältnissen. relativ hohe Transportkosten über große Entfernungen.

Unzureichende Umweltsauberkeit.

Luft

Die höchste Geschwindigkeit der Frachtlieferung. Hohe Zuverlässigkeit.

Höchste Ladungssicherheit.

Die kürzesten Transportwege.

Hohe Transportkosten, die höchsten Tarife unter anderen Transportarten. Hohe Kapitalintensität, Material- und Energieintensität des Transports. Abhängigkeit von den Wetterbedingungen. Unzureichende geografische Erreichbarkeit.

Pipeline

Niedrige Kosten. Hohe Leistung (Durchsatz). Hohe Ladungssicherheit. Geringe Kapitalintensität.

Begrenzte Ladungsarten (Gas, Ölprodukte, Emulsionen). rohes Material). Unzureichende Verfügbarkeit kleiner Transportmengen.

Daher muss der Logistikmanager zunächst entscheiden, ob er einen eigenen Fuhrpark aufbaut oder gemietete Transportmittel (öffentliche oder private) nutzt. Bei der Auswahl einer Alternative gehen sie in der Regel von einem bestimmten Kriteriensystem aus, zu dem unter anderem gehören: Kosten für den Aufbau und Betrieb eines eigenen Fuhrparks. Kosten für die Bezahlung der Dienstleistungen von Transportunternehmen, Speditionen und anderen Logistikvermittlern im Bereich Transportgeschwindigkeit

Qualität des Transports (Zuverlässigkeit der Lieferung, Sicherheit der Ladung usw.)

In den meisten Fällen greifen produzierende Unternehmen auf die Dienste spezialisierter Transportunternehmen zurück.