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?
Antworten:
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:
-std=c99 -pedantic
oder ähnlichem zu kompilieren ...)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
#include
Kürze halber Zeilen usw. weg ):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 dieprintf
aufgerufene Funktion. Ein gutes Stück Arbeit. Stattdessen sehen Sie möglicherweise Folgendes, als hätten Sie geschrieben: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-math
Option 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 ermalloc
tut, aber nicht, wie er es tut.Wenn wir uns eine Implementierung ansehen,
malloc
die auf Klarheit abzielt (im Gegensatz zu Geschwindigkeit), werden wir sehen, dass es einen gewissen Systemaufruf (wie z. B.mmap
mitMAP_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
mmap
tut, 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,
printf
dass 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
-O1
oder höher, jedoch nicht mit-O0
: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 gibtf()
. Die Kernel-Entwickler verlassen sich auf ein ähnliches Verhalten, um das Lesen / Schreiben des Kernel-Codes zu vereinfachen.quelle
Aus dem Gentoo GCC Optimization Wiki
Abschnitt 2.3: Das Flag -O
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
quelle