Die Startadresse wird normalerweise von einem Linker-Skript gesetzt.
Betrachten Sie beispielsweise unter GNU/Linux /usr/lib/ldscripts/elf_x86_64.x
wir sehen:
...
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); \
. = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
Der Wert 0x400000
ist der Standardwert für SEGMENT_START()
Funktion auf dieser Plattform.
Sie können mehr über Linker-Skripte erfahren, indem Sie das Linker-Handbuch durchsuchen:
% info ld Scripts
ld
Das Standard-Linker-Skript von hat diesen 0x400000
Wert für ausführbare Nicht-PIE-Dateien eingebrannt.
PIEs (Position Independent Executables) haben keine Standard-Basisadresse; sie werden immer vom Kernel verschoben, mit den kernels Standard ist 0x0000555...
plus etwas ASLR-Offset, es sei denn, ASLR ist für diesen Prozess oder systemweit deaktiviert. ld
hat darauf keinen Einfluss. Beachten Sie, dass die meisten modernen Systeme GCC so konfigurieren, dass es -fPIE -pie
verwendet standardmäßig, also übergibt es -pie
bis ld
, und verwandelt C in asm, das positionsunabhängig ist. Handgeschriebenes asm muss den gleichen Regeln folgen, wenn Sie es auf diese Weise verlinken.
Aber was macht 0x400000
(4 MiB) ein guter Standard?
Es muss über mmap_min_addr
liegen =65536 =64 KB standardmäßig.
Und weit weg von 0 gibt es viel mehr Raum, um sich vor NULL-Deref mit einem Offset zu schützen, der .text
lautet oder .data
/.bss
Speicher (array[i]
wobei array
ist Null). Auch ohne Erhöhung von mmap_min_addr
(für die dies Platz lässt, ohne ausführbare Dateien zu beschädigen), normalerweise mmap
wählt nach dem Zufallsprinzip hohe Adressen aus, sodass wir in der Praxis mindestens 4 MB Schutz vor NULL-Deref haben.
2M-ausgerichtet ist gut
Dadurch wird es an den Anfang eines Seitenverzeichnisses in der nächsthöheren Ebene der Seitentabellen gesetzt, was bedeutet, dass die gleiche Anzahl von 4K-Seitentabelleneinträgen auf weniger 2M-Seitenverzeichniseinträge aufgeteilt wird, wodurch Kernel-Seitentabellenspeicher und Hilfsseiten gespart werden -walk Hardware-Cache besser. Für große statische Arrays ist es auch gut, nahe am Anfang eines 1G-Teilbaums der nächsthöheren Ebene zu sein.
IDK, warum 4 MiB statt 2 MiB, oder was die Argumentation der Entwickler war. 4 MiB ist die 32-Bit-Largepage-Größe ohne PAE (4-Byte-PTEs, also 10 Bits pro Ebene statt 9), aber eine CPU muss x86-64-Seitentabellen verwenden, um im 64-Bit-Modus zu sein.
Eine niedrige Startadresse erlaubt fast 2 GiB statischer Arrays
(Ohne die Verwendung eines größeren Codemodells, bei dem zumindest große Arrays auf manchmal weniger effiziente Weise adressiert werden müssen. Siehe Abschnitt 3.5.1 Architectural Constraints im x86-64 System V ABI-Dokument für Details zu Codemodellen.)
Das standardmäßige Codemodell für ausführbare Nicht-PIE-Dateien („klein“) lässt Programme davon ausgehen, dass sich jede statische Adresse im niedrigen 2-GiB-Bereich des virtuellen Adressraums befindet. Also jede absolute Adresse in .text
/.rodata
, .data
, .bss
kann als zeichenerweitertes 32-Bit-Immediate im Maschinencode verwendet werden, wo dies effizienter ist.
(Dies ist in einem PIE oder einer gemeinsam genutzten Bibliothek nicht der Fall:siehe Absolute 32-Bit-Adressen in x86-64-Linux nicht mehr zulässig? für die Dinge, die Sie / der Compiler infolgedessen in x86-64 asm nicht tun können, insbesondere addss xmm0, [foo + rdi*4]
erfordert stattdessen ein RIP-relatives LEA, um die Array-Startadresse in ein Register zu bekommen. Der einzige RIP-relative Adressierungsmodus von x86-64 ist [RIP+rel32], ohne Allzweckregister.)
Wenn die Abschnitte/Segmente der ausführbaren Datei am unteren Rand des virtuellen Adressraums beginnen, bleiben fast die gesamten 2 GiB für Text+Daten+BSS verfügbar, um so groß zu sein. (Es wäre möglich gewesen, einen höheren Standardwert zu haben und große ausführbare Dateien dazu zu bringen, dass ld eine niedrigere Adresse wählt, um sie passend zu machen, aber das wäre ein komplizierteres Linker-Skript.)
Dazu gehören nullinitialisierte Arrays in der .bss-Datei, die die ausführbare Datei nicht machen riesig, nur das Prozessabbild im Speicher. In der Praxis stoßen Fortran-Programmierer häufiger darauf als auf C und C++, da dort statische Arrays beliebt sind. Zum Beispiel gfortran für Dummies:Was macht mcmodel=medium genau? hat eine gute Erklärung für einen Build-Fehler mit dem Standardwert small
-Modell und die resultierende x86-64-ASM-Differenz für medium
(wobei Objekte über einer bestimmten Größenschwelle nicht als in den niedrigen 2G oder innerhalb von +-2G des Codes angenommen werden. Aber Code und kleinere statische Daten sind es immer noch, so dass die Geschwindigkeitseinbußen gering sind.)
Zum Beispiel static float arr[1UL<<28];
ist ein 1-GiB-Array. Wenn Sie 3 davon hätten, könnten sie nicht alle starten innerhalb der niedrigen 2 GiB (die möglicherweise alles sind, was Sie für handschriftliche asm benötigen), geschweige denn, dass jedes Element zugänglich ist.
gcc -fno-pie
erwartet, float *p = &arr[size-1];
kompilieren zu können bis mov $arr+1073741820, %edi
, ein 5-Byte mov $imm32
. RIP-relativ funktioniert auch nicht, wenn die Zieladresse mehr als 2 GiB von dem Code entfernt ist, der die Adresse generiert (oder von dort mit movss arr+1073741820(%rip), %xmm0
lädt).; RIP-relativ ist die normale Methode zum Laden/Speichern statischer Daten, selbst in einem Nicht-PIE, wenn es keinen Index für Laufzeitvariablen gibt Lücken zwischen Segmenten):Alle statischen Daten und Codes müssen innerhalb von 2 GiB von allen anderen liegen, die sie möglicherweise erreichen möchten.
Wenn Ihr Code immer nur auf High-Elemente oder ihre Adressen über Indizes von Laufzeitvariablen zugreift, müssen Sie nur den Anfang jedes Arrays, das Symbol selbst, in den niedrigen 2 GiB haben. Ich vergesse, ob der Linker erzwingt, dass das Ende des BSS innerhalb der niedrigen 2GiB liegt; es könnte sein, da das Linker-Skript dort ein Symbol platziert, auf das ein CRT-Startcode verweisen könnte.
Fußnote 1 :Es gibt keine sinnvollen kleineren Größen für ein Codemodell kleiner als 2 GiB. x86-64-Maschinencode verwendet entweder 8 oder 32 Bit für Sofortnachrichten und den Adressierungsmodus. 8-Bit (256 Bytes) ist zu klein, um verwendbar zu sein, und viele wichtige Anweisungen wie call rel32
, mov r32, imm32
, und [rip+rel32]
Adressierung, sind ohnehin nur mit 4-Byte- und nicht mit 1-Byte-Konstanten verfügbar.
Die Beschränkung auf die niedrigen 2 GiB (statt 4) bedeutet, dass Adressen wie bei mov edi, OFFSET arr
sicher durch Nullen erweitert werden können , oder vorzeichenerweitert, wie bei mov eax, [arr + rdi*4]
. Denken Sie daran, dass Adressen nicht der einzige Anwendungsfall für [reg + disp32]
sind Adressierungsmodi; [rbp - 256]
kann oft sinnvoll sein, daher ist es gut, dass x86-64-Maschinencodezeichen disp8 und disp32 auf 64-Bit erweitern und nicht auf Null erweitern.
Eine implizite Nullerweiterung auf 64-Bit erfolgt beim Schreiben eines 32-Bit-Registers, wie bei mov
-unmittelbar, um eine Adresse in ein Register zu schreiben, wobei die 32-Bit-Operandengröße eine kleinere Maschinencodeanweisung ist als die 64-Bit-Operandengröße. Siehe So laden Sie die Adresse einer Funktion oder eines Labels in ein Register (das auch RIP-relative LEA abdeckt).
Verwandt für 32-Bit-Windows
Raymond Chen hat einen Artikel darüber geschrieben, warum das gleiche 0x400000
ist Basisadresse ist die Standardeinstellung für 32-Bit-Windows .
Er erwähnt, dass DLLs standardmäßig an hohen Adressen geladen werden und eine niedrige Adresse weit davon entfernt ist. Gemeinsam genutzte x86-64-SysV-Objekte können überall dort geladen werden, wo eine ausreichend große Lücke im Adressraum vorhanden ist, wobei der Kernel standardmäßig nahe am oberen Rand des virtuellen Adressraums des Benutzerraums liegt, d. h. am oberen Rand des kanonischen Bereichs. Aber gemeinsam genutzte ELF-Objekte müssen vollständig verschiebbar sein, also würden sie überall gut funktionieren.
Die Wahl von 4 MB für 32-Bit-Windows wurde auch durch die Vermeidung der niedrigen 64 KB (NULL-Deref) und durch die Auswahl des Beginns eines Seitenverzeichnisses für ältere 32-Bit-Seitentabellen motiviert. (Wo die "Largepage"-Größe 4 MB beträgt, nicht 2 MB für x86-64 oder PAE.) Mit einer Reihe von Win95- und Win3.1-Legacy-Memory-Map-Gründen, warum mindestens 1 MiB oder 4 MiB teilweise erforderlich waren, und Sachen wie das Arbeiten um die CPU herum Fehler.