Linux kann ohne GCC-Optimierungen nicht kompiliert werden. Implikationen? [geschlossen]

9

Man kann im Internet mehrere Threads finden, wie zum Beispiel:

http://www.gossamer-threads.com/lists/linux/kernel/972619

Wenn Leute sich beschweren, dass sie Linux nicht mit -O0 erstellen können, wird ihnen gesagt, dass dies nicht unterstützt wird. Linux verlässt sich auf GCC-Optimierungen, um Funktionen automatisch zu integrieren, toten Code zu entfernen und ansonsten Dinge zu tun, die für den Erfolg des Builds erforderlich sind.

Ich habe dies selbst für mindestens einige der 3.x-Kernel überprüft. Diejenigen, die ich versucht habe, nach ein paar Sekunden Build-Zeit zu beenden, wenn sie mit -O0 kompiliert wurden.

Wird dies allgemein als akzeptable Codierungspraxis angesehen? Sind Compiler-Optimierungen wie automatisches Inlining vorhersehbar genug, um sich darauf zu verlassen? Zumindest wenn es sich nur um einen Compiler handelt? Wie wahrscheinlich ist es, dass zukünftige Versionen von GCC Builds aktueller Linux-Kernel mit Standardoptimierungen (dh -O2 oder -Os) beschädigen?

Und noch pedantischer: Da 3.x-Kernel nicht ohne Optimierungen kompiliert werden können, sollten sie als technisch inkorrekter C-Code betrachtet werden?

DanL4096
quelle
4
Diese Frage ist hier nicht zum Thema, wäre aber eine gute Frage zu Programmierern. SE, denke ich.
Kyle Jones
Vielleicht könnte es auf Stack Overflow oder so verschoben werden, wenn das besser wäre?
DanL4096
2
Ich bin anderer Meinung, da diese Frage die GCC-Compiler-Einstellungen betrifft, die zwischen den Distributionen unterschiedlich abgestimmt sind. Das OP fragt, wie die Stimmungen allgemein geändert werden können.
eyoung100
1
Die Frage zu den Codierungspraktiken passt zu Programmers.SE. Die Frage, welche zukünftige Version von GCC Linux-Kernel-Builds beschädigen könnte, wäre meinungsbasiert, daher sollte diese Frage fallen gelassen werden.
Kyle Jones
Wie zu versionieren
Ciro Santilli 法轮功 病毒 审查 六四 事件 法轮功

Antworten:

13

Sie haben verschiedene (aber verwandte) Fragen miteinander kombiniert. Einige von ihnen sind hier nicht wirklich thematisch (z. B. Codierungsstandards), daher werde ich diese ignorieren.

Ich werde damit beginnen, ob der Kernel "technisch inkorrekter C-Code" ist. Ich fange hier an, weil die Antwort die spezielle Position erklärt, die ein Kernel einnimmt, was für das Verständnis des Restes entscheidend ist.

Ist der Kernel technisch falscher C-Code?

Die Antwort ist definitiv "falsch".

Es gibt einige Möglichkeiten, wie ein C-Programm als falsch bezeichnet werden kann. Lassen Sie uns zuerst ein paar einfache aus dem Weg räumen:

  • Ein Programm, das nicht der C-Syntax folgt (dh einen Syntaxfehler aufweist), ist falsch. Der Kernel verwendet verschiedene GNU-Erweiterungen der C-Syntax. Dies sind nach C-Standard Syntaxfehler. (Für GCC natürlich nicht. Versuchen Sie, mit -std=c99 -pedanticoder ähnlichem zu kompilieren ...)
  • Ein Programm, das nicht das tut, wofür es entwickelt wurde, ist falsch. Der Kernel ist ein riesiges Programm, und wie selbst eine schnelle Überprüfung seiner Änderungsprotokolle beweisen wird, ist dies sicherlich nicht der Fall. Oder, wie wir allgemein sagen würden, es hat Fehler.

Was Optimierung in C bedeutet

[HINWEIS: Dieser Abschnitt enthält eine sehr lose Anpassung der tatsächlichen Regeln. Einzelheiten finden Sie im Standard und suchen Sie nach Stapelüberlauf.]

Nun zu dem, der mehr Erklärung braucht. Der C-Standard besagt, dass bestimmter Code ein bestimmtes Verhalten erzeugen muss. Es heißt auch, dass bestimmte Dinge, die syntaktisch gültig sind, "undefiniertes Verhalten" haben; Ein (leider häufiges!) Beispiel ist der Zugriff über das Ende eines Arrays hinaus (z. B. ein Pufferüberlauf).

Undefiniertes Verhalten ist mächtig. Wenn ein Programm es enthält, auch nur ein kleines bisschen, kümmert sich der C-Standard nicht mehr darum, welches Verhalten das Programm zeigt oder welche Ausgabe ein Compiler erzeugt, wenn er damit konfrontiert wird.

Aber selbst wenn das Programm nur definiertes Verhalten enthält, lässt C dem Compiler dennoch viel Spielraum. Als triviales Beispiel (Anmerkung: In meinen Beispielen lasse ich der #includeKürze halber Zeilen usw. weg ):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}

Das sollte natürlich 5 gefolgt von einer neuen Zeile drucken. Das verlangt der C-Standard.

Wenn Sie dieses Programm kompilieren und die Ausgabe zerlegen, erwarten Sie, dass malloc aufgerufen wird, um Speicher zu erhalten. Der zurückgegebene Zeiger wird irgendwo gespeichert (wahrscheinlich ein Register), der Wert 3 wird in diesem Speicher gespeichert und 2 wird in diesem Speicher hinzugefügt (möglicherweise) sogar das Laden, Hinzufügen und Speichern erforderlich), dann der auf den Stapel kopierte Speicher und die ebenfalls "%i\n"auf den Stapel gesetzte Punktzeichenfolge, dann die printfaufgerufene Funktion. Ein gutes Stück Arbeit. Stattdessen sehen Sie möglicherweise Folgendes, als hätten Sie geschrieben:

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }

und hier ist die Sache: Der C-Standard erlaubt das. Der C-Standard kümmert sich nur um die Ergebnisse , nicht um die Art und Weise, wie sie erreicht werden.

Darum geht es bei der Optimierung in C. Der Compiler bietet eine intelligentere Methode (im Allgemeinen entweder kleiner oder schneller, abhängig von den Flags), um die vom C-Standard geforderten Ergebnisse zu erzielen. Es gibt einige Ausnahmen, wie beispielsweise die -ffast-mathOption von GCC , aber ansonsten ändert die Optimierungsstufe nicht das Verhalten technisch korrekter Programme (dh solche, die nur definiertes Verhalten enthalten).

Können Sie einen Kernel nur mit definiertem Verhalten schreiben?

Lassen Sie uns unser Beispielprogramm weiter untersuchen. Die Version, die wir geschrieben haben, nicht die, in die der Compiler sie geschrieben hat. Das erste, was wir tun, ist anzurufen malloc, um etwas Gedächtnis zu bekommen. Der C-Standard sagt uns, was er malloctut, aber nicht, wie er es tut.

Wenn wir uns eine Implementierung ansehen, mallocdie auf Klarheit abzielt (im Gegensatz zu Geschwindigkeit), werden wir sehen, dass es einen gewissen Systemaufruf (wie z. B. mmapmit MAP_ANONYMOUS) macht, um einen großen Teil des Speichers zu erhalten. Intern werden einige Datenstrukturen beibehalten, die angeben, welche Teile dieses Blocks im Vergleich zu frei verwendet werden. Es findet einen freien Block, der mindestens so groß ist wie das, wonach Sie gefragt haben, schneidet den Betrag heraus, nach dem Sie gefragt haben, und gibt einen Zeiger darauf zurück. Es ist auch vollständig in C geschrieben und enthält nur definiertes Verhalten. Wenn es threadsicher ist, kann es einige pthread-Aufrufe enthalten.

Nun endlich, wenn wir uns was ansehen mmaptut, wir sehen alle Arten von interessanten Sachen. Zunächst werden einige Überprüfungen durchgeführt, um festzustellen, ob das System über genügend freien RAM und / oder Swap für die Zuordnung verfügt. Als Nächstes wird ein freier Adressraum zum Einfügen des Blocks gefunden. Anschließend wird eine Datenstruktur namens Seitentabelle bearbeitet, und es werden wahrscheinlich mehrere Inline-Assembly-Aufrufe ausgeführt. Möglicherweise werden tatsächlich einige freie Seiten des physischen Speichers gefunden (dh tatsächliche Bits in tatsächlichen DRAM-Modulen) - ein Prozess, bei dem möglicherweise auch andere Speicher zum Austauschen gezwungen werden müssen -. Wenn dies nicht für den gesamten angeforderten Block der Fall ist, werden stattdessen die Dinge so eingerichtet, dass dies geschieht, wenn zum ersten Mal auf den Speicher zugegriffen wird. Ein Großteil davon wird durch Inline-Assemblierung, Schreiben an verschiedene magische Adressen usw. erreicht. Beachten Sie auch, dass große Teile des Kernels verwendet werden, insbesondere wenn ein Austausch erforderlich ist.

Die Inline-Baugruppe, das Schreiben an magische Adressen usw. liegt außerhalb der C-Spezifikation. Das ist nicht überraschend. C läuft über viele verschiedene Maschinenarchitekturen - einschließlich einer Reihe, die in den frühen 1970er Jahren, als C erfunden wurde, kaum vorstellbar waren. Das Ausblenden dieses maschinenspezifischen Codes ist ein zentraler Bestandteil dessen, wofür ein Kernel (und in gewissem Maße eine C-Bibliothek) gedacht ist.

Wenn Sie zum Beispielprogramm zurückkehren, wird natürlich klar, printfdass es ähnlich sein muss. Es ist ziemlich klar, wie alle Formatierungen usw. in Standard C ausgeführt werden. aber tatsächlich auf den Monitor bekommen? Oder an ein anderes Programm weitergeleitet? Wieder einmal viel Magie vom Kernel (und möglicherweise X11 oder Wayland).

Wenn Sie an andere Dinge denken, die der Kernel tut, befinden sich viele außerhalb von C. Beispielsweise liest der Kernel Daten von Festplatten (C kennt keine Festplatten, PCIe-Busse oder SATA) in den physischen Speicher (C kennt nur Malloc, nicht von DIMMs, MMUs usw.), macht es ausführbar (C weiß nichts über Prozessorausführungsbits) und ruft es dann als Funktionen auf (nicht nur außerhalb von C, sehr unzulässig).

Die Beziehung zwischen einem Kernel und seinen Compilern

Wenn Sie sich von früher erinnern, wenn ein Programm undefiniertes Verhalten enthält, was den C-Standard betrifft, sind alle Wetten ungültig. Aber ein Kernel muss wirklich undefiniertes Verhalten enthalten. Es muss also eine Beziehung zwischen dem Kernel und seinem Compiler bestehen, zumindest genug, damit die Kernelentwickler sicher sein können, dass der Kernel trotz Verstoßes gegen den C-Standard funktioniert. Zumindest im Fall von Linux schließt dies ein, dass der Kernel einige Kenntnisse darüber hat, wie GCC intern funktioniert.

Wie wahrscheinlich ist es zu brechen?

Zukünftige GCC-Versionen werden wahrscheinlich den Kernel beschädigen. Ich kann das ziemlich sicher sagen, wie es schon mehrmals passiert ist. Natürlich haben Dinge wie die strengen Aliasing-Optimierungen in GCC neben dem Kernel auch viele andere Dinge kaputt gemacht.

Beachten Sie auch, dass das Inlining, von dem der Linux-Kernel abhängt, kein automatisches Inlining ist, sondern ein Inlining, das die Kernel-Entwickler manuell angegeben haben. Es gibt verschiedene Leute, die den Kernel mit -O0 kompiliert haben und berichten, dass er im Grunde funktioniert, nachdem einige kleinere Probleme behoben wurden. (Einer ist sogar in dem Thread, mit dem Sie verlinkt haben). Meistens sehen die Kernel-Entwickler keinen Grund zum Kompilieren -O0, und wenn eine Optimierung als Nebeneffekt erforderlich ist, funktionieren einige Tricks, und niemand testet damit -O0, sodass sie nicht unterstützt werden.

Dies kompiliert und verknüpft beispielsweise mit -O1oder höher, jedoch nicht mit -O0:

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}

Mit der Optimierung kann gcc herausfinden, dass dies f()niemals aufgerufen wird, und es weglassen. Ohne Optimierung verlässt gcc den Aufruf und der Linker schlägt fehl, weil es keine Definition von gibt f(). Die Kernel-Entwickler verlassen sich auf ein ähnliches Verhalten, um das Lesen / Schreiben des Kernel-Codes zu vereinfachen.

derobert
quelle
0

Aus dem Gentoo GCC Optimization Wiki

Abschnitt 2.3: Das Flag -O

-O Als nächstes kommt die Variable -O. Dies steuert den gesamten Optimierungsgrad. Dadurch nimmt die Codekompilierung etwas mehr Zeit in Anspruch und kann viel mehr Speicherplatz beanspruchen, insbesondere wenn Sie den Optimierungsgrad erhöhen.

Es gibt sieben -O-Einstellungen: -O0, -O1, -O2, -O3, -Os, -Og und -Ofast. Sie sollten nur eine davon in /etc/portage/make.conf verwenden.

Mit Ausnahme von -O0 aktivieren die -O-Einstellungen jeweils mehrere zusätzliche Flags. Lesen Sie daher unbedingt das Kapitel des GCC-Handbuchs zu Optimierungsoptionen, um zu erfahren, welche Flags auf jeder -O-Ebene aktiviert sind, sowie einige Erläuterungen zu deren Flags tun.

Lassen Sie uns jede Optimierungsstufe untersuchen:

-O0: Diese Stufe (das ist der Buchstabe "O", gefolgt von einer Null) deaktiviert die Optimierung vollständig und ist die Standardeinstellung, wenn in CFLAGS oder CXXFLAGS keine -O-Stufe angegeben ist. Dies verkürzt die Kompilierungszeit und kann die Debugging-Informationen verbessern. Einige Anwendungen funktionieren jedoch ohne aktivierte Optimierung nicht ordnungsgemäß. Diese Option wird nur zu Debugging-Zwecken empfohlen.
-O1: Dies ist die grundlegendste Optimierungsstufe. Der Compiler versucht, schnelleren, kleineren Code zu erstellen, ohne viel Kompilierungszeit in Anspruch zu nehmen. Es ist ziemlich einfach, aber es sollte die ganze Zeit die Arbeit erledigen.
-O2: Ein Schritt von -O1. Dies ist die empfohlene Optimierungsstufe, sofern Sie keine besonderen Anforderungen haben. -O2 aktiviert zusätzlich zu den durch -O1 aktivierten Flags einige weitere Flags. Mit -O2 versucht der Compiler, die Codeleistung zu steigern, ohne Kompromisse bei der Größe einzugehen und ohne zu viel Kompilierungszeit in Anspruch zu nehmen.
-O3: Dies ist die höchstmögliche Optimierungsstufe. Es ermöglicht Optimierungen, die hinsichtlich Kompilierungszeit und Speichernutzung teuer sind. Das Kompilieren mit -O3 ist keine garantierte Möglichkeit, die Leistung zu verbessern, und kann in vielen Fällen ein System aufgrund größerer Binärdateien und erhöhter Speichernutzung verlangsamen. -O3 ist auch dafür bekannt, mehrere Pakete zu zerbrechen. Daher wird die Verwendung von -O3 nicht empfohlen.
-Os: Diese Option optimiert Ihren Code hinsichtlich der Größe. Es aktiviert alle -O2-Optionen, die den generierten Code nicht vergrößern. Dies kann nützlich sein für Computer mit extrem begrenztem Speicherplatz und / oder CPUs mit kleinen Cache-Größen.
-Og: In GCC 4.8 wurde eine neue allgemeine Optimierungsstufe -Og eingeführt. Es befasst sich mit der Notwendigkeit einer schnellen Kompilierung und einer überlegenen Debugging-Erfahrung bei gleichzeitiger Bereitstellung einer angemessenen Laufzeitleistung. Die Gesamterfahrung für die Entwicklung sollte besser sein als die Standardoptimierungsstufe -O0. Beachten Sie, dass -Og nicht -g impliziert, sondern lediglich Optimierungen deaktiviert, die das Debuggen beeinträchtigen können.
-Ofast: Neu in GCC 4.7, besteht aus -O3 plus -ffast-math, -fno-protected-parens und -fstack-arrays. Diese Option verstößt gegen strenge Standards und wird nicht zur Verwendung empfohlen. Wie bereits erwähnt, ist -O2 die empfohlene Optimierungsstufe. Wenn die Paketkompilierung fehlschlägt und Sie -O2 nicht verwenden, versuchen Sie es mit dieser Option neu. Versuchen Sie als Fallback-Option, Ihre CFLAGS und CXXFLAGS auf eine niedrigere Optimierungsstufe einzustellen, z. B. -O1 oder sogar -O0 -g2 -ggdb (zur Fehlerberichterstattung und zur Überprüfung auf mögliche Probleme).

Sie haben speziell nach -O0 gefragt, was keine Optimierung ist. Lesen Sie oben, dass O0 nur zum Debuggen verwendet werden sollte. Wenn Sie schon einmal menuconfig verwendet haben, werden Sie feststellen, dass es eine Option zum Aktivieren oder Deaktivieren des Kernel-Debuggens gibt. Wenn diese Option aktiviert ist, werden Debugging-Informationen auf die gleiche Weise ausgegeben, wie O0 Ihnen die Informationen geben würde. Ich denke auch, dass Sie möglicherweise den Punkt verpassen, an dem das gesamte System mit einer und nur einer Optimierungseinstellung erstellt oder kompiliert wird, dh Sie können keinen Kernel bei O0 und den Rest Ihres Systems bei O2 kompilieren


In Bezug auf die Abwärtskompatibilität zwischen GCC-Versionen bleibt GCC zwischen den Versionen immer kompatibel, da das Aktivieren eines -O-Flags in einer Version dieselbe -O-Einstellung in der neuen Version ist. Beachten Sie den obigen Hinweis zu GCC4.7 und der Option -Ofast, da die Option nur ab Version 4.7 verfügbar ist, in jeder Version jedoch -O2 ab 4.7 = -O2

eyoung100
quelle
2
Sind Sie sicher, dass das gesamte System mit derselben Optimierungsstufe kompiliert werden muss? -Os Kernel funktionieren meiner Erfahrung nach gut mit einem -O2-Benutzerbereich, und -Os deaktiviert viele in -O2 aktivierte Optimierungen.
DanL4096
Abgesehen vom Debuggen ist -O0 nicht das kanonische Verhalten des Compilers, wie im Sprachstandard angegeben? dh die Optimierung kann Code "reparieren", der semantisch falsch ist; Wo wird das Kompilieren ohne Optimierung nicht?
DanL4096
@ DanL4096 Zumindest in einem Gentoo-System wird dies nicht auf Paketebene empfohlen, um die Situation zu vermeiden, nach der das OP fragt. Auf einem binären System wie Debian wird die -O-Ebene von den Betriebssystembetreuern festgelegt und kann nicht AFAIK geändert werden. -O0 ist die Basislinie, aber -O2 ist die empfohlene Einstellung, da, wie im Wiki angegeben, nicht alle Programme mit -O0 kompiliert werden
eyoung100