Was macht eigentlich das Kompilieren von WITH_PIC (-DWITH_PIC, --with-pic)?

8

Was sind beim Erstellen von Binärdateien aus der Quelle die tatsächlichen Unterschiede zwischen dem Generieren von PIC-Objekten oder nicht? An welcher Stelle würde jemand sagen: "Ich hätte PIC-Objekte generieren oder verwenden sollen, als ich MySQL kompiliert habe." Oder nicht?

Ich habe Gentoos Einführung in Position Independent Code gelesen, Institutionen für den unabhängigen Standortcode , HOWTO-Fehler beheben -fPIC , Erstellen von Objektdateien von Libtool und Positionsunabhängiger Code .

Aus PHP ./configure --help :

  

- mit Bild: Versuchen Sie, nur PIC / Nicht-PIC-Objekte zu verwenden [Standard = beide verwenden].

Von MySQL cmake -LAH . :

  

-DWITH_PIC: Generiere PIC-Objekte

Diese Info ist ein guter Anfang, aber hinterlässt viele Fragen.

Soweit ich weiß, wird -fPIC im Compiler aktiviert, was wiederum PIC-Objekte in den resultierenden Binärdateien / Bibliotheken generiert. Warum sollte ich das tun wollen? Oder umgekehrt. Vielleicht ist es riskanter oder könnte die Binärdatei möglicherweise weniger stabil machen? Vielleicht sollte es vermieden werden, wenn man auf bestimmten Architekturen kompiliert (amd64 / x86_64 in meinem Fall)?

Der Standard-MySQL-Build setzt PIC = OFF. Die offizielle Version von MySQL release setzt PIC = ON. Und PHP "versucht beide zu verwenden." In meinen Tests führt -DWITH_PIC=ON zu geringfügig größeren Binärdateien:

%Vor%     
Jeff 02.08.2013, 21:20
quelle

4 Antworten

12

Es gibt zwei Konzepte, die man nicht verwechseln sollte:

  1. Verschiebbare Binärdateien
  2. Position unabhängiger Code

Beide befassen sich mit ähnlichen Problemen, aber auf einer anderen Ebene.

Das Problem

Die meisten Prozessorarchitekturen haben zwei Arten der Adressierung: absolut und relativ. Die Adressierung wird normalerweise für zwei Arten von Zugriffen verwendet: Zugriff auf Daten (Lesen, Schreiben usw.) und Ausführen eines anderen Teils des Codes (Sprung, Aufruf usw.). Beides kann absolut erfolgen (Aufruf des Codes an einer festen Adresse, Daten an einer festen Adresse lesen) oder relativ (springe zu fünf Anweisungen zurück, gelesen relativ zu einem Zeiger).

Relative Adressierung kostet normalerweise sowohl Geschwindigkeit als auch Speicher. Geschwindigkeit, weil der Prozessor die absolute Adresse aus dem Zeiger und dem relativen Wert berechnen muss, bevor er auf den realen Speicherort oder den realen Befehl zugreifen kann. Speicher, weil ein zusätzlicher Zeiger gespeichert werden muss (in der Regel in einem Register, das ist sehr schnell, aber auch sehr knapp Speicher).

Absolute Adressierung ist nicht immer machbar, denn wenn man naiv implementiert, muss man alle Adressen zur Kompilierzeit kennen. In vielen Fällen ist dies unmöglich. Beim Aufrufen von Code aus einer externen Bibliothek weiß man möglicherweise nicht, an welchem ​​Speicherort das Betriebssystem die Bibliothek lädt. Beim Adressieren von Daten auf dem Heap weiß man nicht im Voraus, welcher Heap-Block das Betriebssystem für diesen Vorgang reserviert.

Dann gibt es viele technische Details. Z.B. die Prozessorarchitektur erlaubt nur relative Sprünge bis zu einer bestimmten Grenze; Alle weiteren Sprünge müssen dann absolut sein. Oder bei Architekturen mit einem sehr breiten Adressbereich (zB 64 Bit oder sogar 128 Bit) führt die relative Adressierung zu einem kompakteren Code (weil man für relative Adressen 16 Bit oder 8 Bit verwenden kann, aber absolute Adressen immer 64 Bit oder 128 Bit).

Verschiebbare Binärdateien

Wenn Programme absolute Adressen verwenden, treffen sie sehr starke Annahmen über das Layout des Adressraums. Das Betriebssystem ist möglicherweise nicht in der Lage, all diese Annahmen zu erfüllen. Um dieses Problem zu beheben, können die meisten Betriebssysteme einen Trick verwenden: Die Binärdateien werden mit zusätzlichen Metadaten angereichert. Das Betriebssystem verwendet dann diese Metadaten, um die Binärdatei zur Laufzeit zu ändern, sodass die geänderten Annahmen der aktuellen Situation entsprechen. Normalerweise beschreiben die Metadaten die Position von Anweisungen in der Binärdatei, die absolute Positionierung verwenden. Wenn das Betriebssystem dann die Binärdatei lädt, ändert es bei Bedarf die absoluten Adressen, die in diesen Anweisungen gespeichert sind.

Ein Beispiel für diese Metadaten sind die "Relocation Tables" im ELF-Dateiformat.

Einige Betriebssysteme verwenden einen Trick, daher müssen sie nicht immer jede Datei vor der Ausführung verarbeiten: Sie verarbeiten die Dateien vor und ändern die Daten, so dass ihre Annahmen sehr wahrscheinlich zur Laufzeit zur Situation passen (und daher sind keine Änderungen erforderlich). . Dieser Vorgang wird auf Mac OS X als "Prebinding" und auf Linux als "Prelink" bezeichnet.

Relozierbare Binärdateien werden auf Linkerebene erstellt.

Positionsunabhängiger Code (PIC)

Der Compiler kann Code erzeugen, der nur relative Adressierung verwendet. Dies könnte eine relative Adressierung für Daten und Code oder nur für eine dieser Kategorien bedeuten. Die Option "-fPIC" auf gcc, z.B. eine relative Adressierung für Code wird erzwungen (d. h. nur relative Sprünge und Aufrufe). Der Code kann dann ohne Änderung an einer beliebigen Speicheradresse ausgeführt werden. Auf einigen Prozessorarchitekturen wird ein solcher Code nicht immer möglich sein, z. Wenn relative Sprünge in ihrem Umfang begrenzt sind (z. B. maximal 128 Befehle sind relative Sprünge erlaubt).

Positionsunabhängiger Code wird auf der Compiler-Ebene behandelt. Ausführbare Dateien, die nur PIC-Code enthalten, benötigen keine Verlagerungsinformationen.

Wann ist PIC-Code erforderlich?

In einigen speziellen Fällen benötigt man unbedingt den PIC-Code, da das Umladen während des Ladens nicht möglich ist. Einige Beispiele:

  1. Einige eingebettete Systeme können Binärdateien direkt aus dem Dateisystem ausführen, ohne sie zuvor in den Speicher zu laden. Dies ist normalerweise der Fall, wenn das Dateisystem bereits im Speicher ist, z. im ROM- oder FLASH-Speicher. Die Executalbane starten dann viel schneller und benötigen keinen zusätzlichen Teil des (normalerweise knappen) RAMs. Diese Funktion heißt " vor Ort ausführen ".
  2. Sie verwenden ein spezielles Plugin-System. Ein Extremfall wäre ein sogenannter "Shell-Code", d. H. Code, der unter Verwendung eines Sicherheitslochs injiziert wird. Sie werden dann normalerweise nicht wissen, wo sich Ihr Code zur Laufzeit befindet, und die betreffende ausführbare Datei wird keinen Relocation-Service für Ihren Code bereitstellen.
  3. Das Betriebssystem unterstützt nicht verschiebbare Binärdateien (normalerweise aufgrund knapper Ressourcen, z. B. auf einer eingebetteten Plattform)
  4. Das Betriebssystem kann allgemeine Speicherseiten zwischen laufenden Programmen zwischenspeichern.Wenn Binärdateien während der Verschiebung geändert werden, funktioniert dieses Zwischenspeichern nicht mehr (da jede Binärdatei eine eigene Version des verschobenen Codes hat).

Wenn PIC vermieden werden sollte

  1. In einigen Fällen kann es für den Compiler unmöglich sein, alles positionsunabhängig zu machen (z. B. weil der Compiler nicht "clever" genug ist oder weil die Prozessorarchitektur zu beschränkt ist)
  2. Der positionsunabhängige Code kann aufgrund der vielen Zeigeroperationen zu langsam oder zu groß sein.
  3. Der Optimierer hat möglicherweise Probleme mit den vielen Zeigeroperationen, so dass die notwendigen Optimierungen nicht angewendet werden und die ausführbare Datei wie molase läuft.

Beratung / Schlussfolgerung

PIC-Code ist möglicherweise aufgrund bestimmter Einschränkungen erforderlich. In allen anderen Fällen bleiben Sie bei den Standardeinstellungen. Wenn Sie solche Beschränkungen nicht kennen, brauchen Sie nicht "-fPIC".

    
stefan.schwetschke 12.08.2013, 11:18
quelle
1

Es gibt wirklich zwei Gründe, die Sie auf diese Weise kompilieren möchten.

Eins, wenn Sie eine gemeinsame Bibliothek erstellen möchten. Im Allgemeinen müssen Shared Libraries PIC unter Linux sein.

Zweitens möchten Sie möglicherweise die Hauptdatei "PIE" kompilieren, die im Grunde PIC für ausführbare Dateien ist. PIE ist eine Sicherheitsfunktion, mit der Adressraum-Randomisierung auf die Hauptdatei angewendet werden kann.

    
Tom Tromey 04.08.2013 01:53
quelle
1

Gemeinsam genutzte Bibliotheken und ausführbare Dateien können mit aktiviertem und deaktiviertem PIC-Code erstellt werden. I.e. Wenn Sie sie ohne PIC erstellen, können sie weiterhin von anderen Apps verwendet werden. Nicht-PIC-Bibliotheken werden jedoch nicht überall unterstützt. Unter Linux gibt es jedoch einige Einschränkungen.

=== Dies ist eine kurze Erklärung, die Sie nicht brauchen ;-) ===

Was PIC macht, ist, dass es die Code-Position unabhängig macht. Jede gemeinsam genutzte Bibliothek wird an einer bestimmten Position im Speicher geladen - aus Sicherheitsgründen ist dieser Platz oft randomisiert - und somit können "absolute" Speicherreferenzen im Code nicht wirklich "absolut" sein - sie sind vielmehr relativ zum Speichersegmentstart der Bibliothek Adresse. Nachdem die Bibliothek geladen wurde, müssen sie angepasst werden.

Dies kann getan werden, indem Sie alle von ihnen gehen (ihre Adressen werden in der Dateikopfzeile gespeichert) und korrigiert werden. Aber das ist langsam, und ein "korrigiertes" Bild kann nicht zwischen Prozessen ausgetauscht werden, wenn die Basisadresse anders ist.

Daher wird normalerweise eine andere Methode verwendet. Jeder Verweis auf einen Speicher erfolgt über ein spezielles Register (normalerweise ebx). Wenn eine Funktion aufgerufen wird, springt sie zu Beginn zu einem speziellen Codeblock, der den ebx-Wert an die Speichersegmentadresse der Bibliothek anpasst. Dann greift die Funktion auf ihre Daten mit [ebx + wissen Offset].

Also muss für jedes Programm nur dieser Codeblock angepasst werden, nicht jede Funktion und Speicherreferenz.

Beachten Sie, dass der Compiler / Linker die PIC-Register (ebx) -Anpassung auslassen kann, wenn bekannt ist, dass die Funktion bereits über den korrekten Wert verfügt. In einigen Architekturen (vor allem x86_64) können Programme auf Daten zugreifen, die sich auf den IP (den aktuellen Befehlszeiger) beziehen, der bereits absolut eingestellt ist und somit die Notwendigkeit für ein spezielles Register wie ebx und seine Anpassung eliminiert.

=== Hier ist das Ende des Abschnitts, der ohne gelesen werden kann ===

Warum sollten Sie also etwas ohne PIC bauen?

Nun, zuerst verlangsamt es Ihr Programm um einige Prozente, weil zu Beginn jeder Funktion ein zusätzlicher Code ausgeführt wird, um das Register anzupassen, und ein wertvolles Register ist nicht für den Optimierer verfügbar (nur x86). Oft kann die Funktion nicht wissen, ob sie von derselben Bibliothek oder von einer anderen aufgerufen wird, und daher leiden sogar interne Anrufe unter der Strafe. Wenn Sie also auf Geschwindigkeit optimieren möchten, versuchen Sie, ohne PIC zu kompilieren.

Dann ist die Code-Größe ein bisschen größer, wie Sie bemerkt haben, weil jede Funktion ein paar mehr Anweisungen enthält, um das PIC-Register einzurichten.

Dies kann bis zu einem gewissen Grad vermieden werden, wenn wir die Link-Zeit-Optimierung (--lto switch) und die Sichtbarkeit geschützter Funktionen verwenden, damit der Compiler weiß, welche Funktionen überhaupt nicht extern aufgerufen werden und somit keinen PIC-Code benötigen. Aber ich habe das (noch) nicht versucht.

Und warum sollten Sie PIC verwenden? Weil es sicherer ist (dies ist für die Adressraum-Randomisierung erforderlich); weil nicht alle Systeme Nicht-PIC-Bibliotheken unterstützen; weil die Ladezeit des Starts für Nicht-PIC-Bibliotheken langsamer sein kann (das gesamte Code-Segment muss an absolute Adressen angepasst werden und nicht nur an Tabellen-Stubs); und geladene Bibliothekssegmente können nicht geteilt werden, wenn sie in einen anderen Raum geladen werden (d. h., es kann mehr Speicher verwendet werden). Dann sind nicht alle Compiler- / Linker-Flags mit Nicht-PIC-Bibliotheken kompatibel (von dem, was ich mich erinnere, gibt es etwas über thread-lokale Unterstützung), so dass manchmal überhaupt kein PIC-Code erstellt werden kann.

Nicht-PIC-Code ist also ein bisschen riskanter (weniger sicher) und Sie können es nicht immer bekommen, aber wenn Sie es brauchen (z. B. für Geschwindigkeit) - warum nicht.

    
queen3 05.08.2013 20:56
quelle
1

Der Hauptgrund, warum ich PIC unter Linux verwendet habe, ist, wenn Sie ein Objekt erstellen, das von einem anderen System oder von einer Software verwendet wird (zB eine Systembibliothek oder eine Bibliothek, die Teil einer Software-Suite wie MySQL ist).

Zum Beispiel können Sie Module für PHP, Apache und wahrscheinlich MySQL schreiben, und diese Module müssen von diesen Tools geladen werden, und das geschieht an einer "zufälligen" Adresse und sie können ihren Code mit minimalem Aufwand ausführen arbeite an dem Code. Tatsächlich überprüfen diese Systeme in den meisten Fällen, ob Ihr Modul ein PIC (position independent code, wie queen3 unterstrichen) Modul ist und wenn nicht, lehnen sie es ab, Ihr Modul zu laden.

Damit können Sie den Großteil Ihres Codes ausführen, ohne die so genannten Umlagerungen ausführen zu müssen. Eine Verschiebung ist eine Ergänzung zu einer Adresse der Basisadresse, wo der Code geladen wurde und die den Code der Bibliothek ändert (es ist jedoch vollkommen sicher). Dies ist wichtig für dynamische Bibliotheken, da sie jedes Mal von einem anderen Prozess geladen werden Sie erhalten möglicherweise eine andere Adresse (beachten Sie, dass dies nichts mit Sicherheit zu tun hat, sondern nur Adressraum, der für Ihren Prozess zur Verfügung steht.) Allerdings bedeuten Umlagerungen, dass jede Version anders ist, da Sie, wie ich gerade sagte, den Code ändern für jeden Prozess geladen und so hat jeder Prozess eine andere Version im Speicher (was bedeutet, dass die Tatsache, dass die Bibliothek dynamisch geladen wird, nicht so viel wie es sonst möglich wäre!)

Der PIC-Mechanismus erstellt eine Tabelle, wie von anderen erwähnt, die für Ihren Prozess spezifisch ist, ebenso wie der von diesen Bibliotheken verwendete Lese- / Schreibspeicher (.data), aber der Rest der Bibliothek (der .text und .rodata) Abschnitte) bleibt intakt, was bedeutet, dass es von vielen Prozessen von diesem einen Ort aus verwendet werden kann (obwohl die Adresse dieser Bibliothek von der Sichtweise jedes Prozesses verschieden sein kann), ist dies ein Nebeneffekt dessen, was als MMU: Memory bezeichnet wird Verwaltungseinheit, die jeder physischen Adresse eine virtuelle Adresse zuweisen kann.)

Früher wurde unter Systemen wie dem berühmten IRIX-System von SGI eine Basisadresse für jede dynamische Bibliothek vorab zugewiesen. Das war eine Vor-Verlagerung, so dass jeder Prozess diese dynamische Bibliothek an diesem einen bestimmten Ort finden würde, wodurch er wirklich gemeinsam genutzt werden könnte. Aber wenn Sie Hunderte von gemeinsam genutzten Bibliotheken haben, würde die Vorabzuweisung einer virtuellen Adresse zu jedem von ihnen es nahezu unmöglich machen, große Systeme wie heute zu betreiben. Und ich werde nicht einmal über die Tatsache sprechen, dass eine Bibliothek aktualisiert werden kann, und nun die, der die Adresse zugewiesen wurde, direkt nach ... Nur die MMU der Zeit waren weniger vielseitig als die von heute und PIC war es noch nicht als eine gute Lösung angesehen.

Um Ihre Frage in Bezug auf mysql zu beantworten, ist die -DWITH_PIC wahrscheinlich eine gute Idee, da viele Tools die ganze Zeit laufen und alle diese Bibliotheken einmal geladen und von allen Tools wiederverwendet werden. Zur Laufzeit wird es also schneller gehen. Ohne die PIC-Funktion muss diese Bibliothek sicherlich immer wieder neu geladen werden, wobei viel Zeit verschwendet wird. Also ein paar mehr Mb können Sie Millionen von Zyklen pro Sekunde sparen und wenn Sie einen Prozess 24/7 laufen lassen, ist das ziemlich viel Zeit!

Ich denke, dass vielleicht ein kleines Beispiel in der Versammlung besser erklären könnte, worüber wir hier reden ...

Wenn Ihr Code an einen bestimmten Ort springen muss, verwenden Sie am einfachsten einen Sprungbefehl:

%Vor%

In diesem Fall wird $ someplace als absolute Adresse bezeichnet. Dies ist ein Problem, da, wenn Sie Ihren Code an einem anderen Ort laden (eine andere Basisadresse), ändert sich auch $ someplace. Um zu lindern, haben wir Umzüge. Dies ist eine Tabelle, die das System anweist, die Basisadresse zu $ ​​someplace hinzuzufügen, so dass der JMP tatsächlich wie erwartet funktioniert.

Wenn PIC verwendet wird, wird diese Sprunganweisung mit einer absoluten Adresse auf eine von zwei Arten transformiert: durch eine Tabelle springen oder mit relativen Adressen springen.

%Vor%

Wie Sie hier sehen können, benutze ich den speziellen Anweisungs-BH (Zweig) anstelle eines Sprungs, um den relativen Sprung zu erhalten. Dies ist möglich, wenn Sie zu einem anderen Ort innerhalb des gleichen Codeabschnitts springen, obwohl bei einigen Prozessoren dieses Springen sehr begrenzt ist (dh -128 bis +127 Bytes!), Aber bei neueren Prozessoren beträgt das Limit im Allgemeinen +/- 2 GB.

Der jmp (oder jsr für den Sprung zur Unterroutine, bei INTEL ist es die Aufrufanweisung) wird jedoch im Allgemeinen verwendet, wenn zu einer anderen Funktion oder außerhalb desselben Abschnittscodes gesprungen wird. Das ist viel sauberer für Interfunktionsaufrufe.

In vielerlei Hinsicht ist der Großteil Ihres Codes bereits in PIC, außer:

  • wenn Sie eine andere Funktion aufrufen (andere als Inline- oder intrinsische Funktionen)
  • wenn Sie auf Daten zugreifen

Für Daten haben wir ein ähnliches Problem, wir wollen einen Wert von einer Adresse mit einem mov laden:

%Vor%

Hier wäre% my_data eine absolute Adresse, die eine Verlagerung erfordern würde (d. h.Der Compiler würde den Versatz von $ my_data im Vergleich zum Anfang der Abschnitte speichern, und beim Laden würde die Basisadresse, unter der die Bibliothek geladen wird, zum Speicherort der Adresse in der Anweisung mov hinzugefügt.)

Hier kommt unsere Tabelle mit dem% ebx-Register ins Spiel. Der Anfang der Adresse wird an einem bestimmten Offset in der Tabelle gefunden und kann abgerufen werden, um auf die Daten zuzugreifen. Dies erfordert zwei Anweisungen:

%Vor%

Wir laden zuerst den Zeiger auf den Anfang des Datenpuffers, dann laden wir die Daten selbst von diesem Zeiger. Es ist etwas langsamer, aber die erste Ladung wird vom Prozessor zwischengespeichert, so dass der erneute Zugriff immer wieder sofort erfolgt (kein tatsächlicher Speicherzugriff).

    
Alexis Wilke 06.08.2013 00:01
quelle

Tags und Links