Warum funktioniert das GCC-Pad mit NOPs?

80

Ich habe eine kurze Zeit mit C gearbeitet und vor kurzem angefangen, mich mit ASM zu beschäftigen. Wenn ich ein Programm kompiliere:

Die objdump-Demontage hat den Code, aber nops nach dem ret:

Nach dem, was ich gelernt habe, tun Nops nichts, und da nach Ret würde nicht einmal ausgeführt werden.

Meine Frage ist: Warum sich die Mühe machen? Konnte ELF (Linux-x86) nicht mit einem Textabschnitt (+ main) beliebiger Größe arbeiten?

Ich würde mich über jede Hilfe freuen, nur um zu lernen.

olly
quelle
Machen diese NOPs weiter? Wenn sie bei anhalten 80483af, ist es vielleicht ein Auffüllen, um die nächste Funktion auf 8 oder 16 Bytes auszurichten.
Mysticial
Nein, nach den 4 Nops geht es direkt zu einer Funktion: __libc_csu_fini
olly
1
Wenn die NOPs von gcc eingefügt wurden, dann denke ich nicht, dass es nur 0x90 verwenden wird, da es viele NOPs mit einer Größenvariablen von 1-9 Bytes gibt (10, wenn
Gassyntax

Antworten:

89

Erstens gccmacht das nicht immer so. Die Polsterung wird gesteuert durch -falign-functions, die automatisch aktiviert wird durch -O2und -O3:

-falign-functions
-falign-functions=n

Richten Sie den Funktionsstart auf die nächste Zweierpotenz aus, die größer als ist n, und überspringen Sie bis zu nBytes. Zum Beispiel -falign-functions=32ausrichtet Funktionen auf die nächste 32-Byte - Grenze, sondern -falign-functions=24würden auf die nächste 32-Byte - Grenze ausgerichtet nur dann , wenn dies durch das Überspringen 23 Bytes oder weniger durchgeführt werden.

-fno-align-functionsund -falign-functions=1sind äquivalent und bedeuten, dass Funktionen nicht ausgerichtet werden.

Einige Assembler unterstützen dieses Flag nur, wenn n eine Zweierpotenz ist. In diesem Fall wird es aufgerundet.

Wenn n nicht angegeben ist oder Null ist, verwenden Sie einen maschinenabhängigen Standard.

Aktiviert auf den Ebenen -O2, -O3.

Es kann mehrere Gründe dafür geben, aber der Hauptgrund für x86 ist wahrscheinlich folgender:

Die meisten Prozessoren rufen Anweisungen in ausgerichteten 16-Byte- oder 32-Byte-Blöcken ab. Es kann vorteilhaft sein, kritische Schleifeneinträge und Unterprogrammeinträge um 16 auszurichten, um die Anzahl der 16-Byte-Grenzen im Code zu minimieren. Stellen Sie alternativ sicher, dass in den ersten Anweisungen nach einem kritischen Schleifen- oder Unterprogrammeintrag keine 16-Byte-Grenze vorhanden ist.

(Zitiert aus "Optimieren von Unterprogrammen in Assemblersprache" von Agner Fog.)

edit: Hier ist ein Beispiel, das das Auffüllen demonstriert:

Beim Kompilieren mit gcc 4.4.5 mit Standardeinstellungen erhalte ich:

Angabe -falign-functionsergibt:

NPE
quelle
1
Ich habe keine -O-Flags verwendet, einfach "gcc -o test test.c".
Olly
1
@olly: Ich habe es mit gcc 4.4.5 unter 64-Bit-Ubuntu getestet und in meinen Tests gibt es standardmäßig kein Auffüllen und es gibt Auffüllen mit -falign-functions.
NPE
@aix: Ich bin auf centOS 6.0 (32-Bit) und habe ohne Flags die Auffüllung. Möchte jemand, dass ich meine vollständige Ausgabe "objdump -j .text -d ./test" ausgeben kann?
Olly
1
Beim weiteren Testen, wenn ich es als Objekt kompiliere: "gcc -c test.c". Es gibt keine Auffüllung, aber wenn ich verknüpfe: "gcc -o test test.o" erscheint es.
Olly
2
@olly: Diese Auffüllung wird vom Linker eingefügt, um die Ausrichtungsanforderungen der Funktion zu erfüllen, die mainin der ausführbaren Datei folgt (in meinem Fall ist diese Funktion __libc_csu_fini).
NPE
15

Dies geschieht, um die nächste Funktion an der 8-, 16- oder 32-Byte-Grenze auszurichten.

Aus "Optimieren von Unterprogrammen in Assemblersprache" von A.Fog:

11.5 Ausrichtung des Codes

Die meisten Mikroprozessoren rufen Code in ausgerichteten 16-Byte- oder 32-Byte-Blöcken ab. Wenn sich ein wichtiger Subroutineneintrag oder eine Sprungbezeichnung am Ende eines 16-Byte-Blocks befindet, erhält der Mikroprozessor beim Abrufen dieses Codeblocks nur wenige nützliche Codebytes. Möglicherweise müssen auch die nächsten 16 Bytes abgerufen werden, bevor die ersten Anweisungen nach dem Label dekodiert werden können. Dies kann vermieden werden, indem wichtige Unterprogrammeinträge und Schleifeneinträge um 16 ausgerichtet werden.

[...]

Das Ausrichten eines Unterprogrammeintrags ist so einfach wie das Platzieren so vieler NOPs wie nötig vor dem Unterprogrammeintrag, um die Adresse wie gewünscht durch 8, 16, 32 oder 64 teilbar zu machen.

Hamstergen
quelle
Es ist der Unterschied zwischen 25-29 Bytes (für main). Sprechen Sie von etwas Größerem? Wie im Textabschnitt habe ich durch readelf festgestellt, dass es 364 Bytes sind? Ich habe auch 14 Nops bei _start bemerkt. Warum macht "as" diese Dinge nicht? Ich bin Anfänger, entschuldige mich.
Olly
@olly: Ich habe Entwicklungssysteme gesehen, die eine Ganzprogrammoptimierung für kompilierten Maschinencode durchführen. Wenn die Adresse der Funktion foo0x1234 lautet, generiert der Code, der diese Adresse in unmittelbarer Nähe eines Literals 0x1234 verwendet, möglicherweise einen Maschinencode, mov ax,0x1234 / push ax / mov ax,0x1234 / push axden der Optimierer dann ersetzen könnte mov ax,0x1234 / push ax / push ax. Beachten Sie, dass Funktionen nach einer solchen Optimierung nicht verschoben werden dürfen, sodass das Eliminieren von Anweisungen die Ausführungsgeschwindigkeit verbessern würde, jedoch nicht die Codegröße.
Supercat
5

Soweit ich mich erinnere, werden Anweisungen in der CPU weitergeleitet, und verschiedene CPU-Blöcke (Lader, Decoder usw.) verarbeiten nachfolgende Anweisungen. Wenn RETAnweisungen ausgeführt werden, werden bereits einige nächste Anweisungen in die CPU-Pipeline geladen. Es ist eine Vermutung, aber Sie können hier anfangen zu graben und wenn Sie es herausfinden (vielleicht die spezifische Anzahl von NOPs, die sicher sind, teilen Sie bitte Ihre Ergebnisse mit.

mco
quelle
@ninjalj: Huh? Diese Frage bezieht sich auf x86, das (wie mco sagte) per Pipeline ausgeführt wird. Viele moderne x86-Prozessoren führen auch spekulativ Anweisungen aus, die "nicht" ausgeführt werden sollten, möglicherweise einschließlich dieser Nops. Vielleicht wollten Sie woanders etwas kommentieren?
David Cary
3
@ DavidCary: In x86 ist das für den Programmierer völlig transparent. Bei falsch erratenen spekulativ ausgeführten Anweisungen werden nur die Ergebnisse und Auswirkungen verworfen. Auf MIPS gibt es überhaupt keinen "spekulativen" Teil, der Befehl in einem Verzweigungsverzögerungsschlitz wird immer ausgeführt, und der Programmierer muss die Verzögerungsschlitze füllen (oder den Assembler dies tun lassen, was wahrscheinlich zu nops führen würde).
Ninjalj
@ninjalj: Ja, die Auswirkungen von falsch erratenen spekulativ ausgeführten Operationen und nicht ausgerichteten Anweisungen sind insofern transparent, als sie keinen Einfluss auf die Ausgabedatenwerte haben. Beide wirken sich jedoch auf das Timing des Programms aus. Dies kann der Grund dafür sein, dass gcc dem x86-Code Nops hinzufügt. Dies wurde in der ursprünglichen Frage gestellt.
David Cary
1
@DavidCary: Wenn das der Grund wäre, würden Sie es nur nach bedingten Sprüngen sehen, nicht nach einem bedingungslosen ret.
Ninjalj
1
Das ist nicht der Grund. Die Fallback-Vorhersage eines indirekten Sprungs (bei einem BTB-Fehler) ist die nächste Anweisung. Wenn dies jedoch kein Anweisungsmüll ist, ist die empfohlene Optimierung zum Stoppen von Fehlerspekulationen eine Anweisung wie ud2oder int3die immer fehlerhaft ist , sodass das Front-End weiß, dass die Decodierung stattdessen gestoppt werden muss divB. eine potenziell teure oder unechte TLB-Fehllast in die Pipeline einzuspeisen. Dies wird nach einem retoder einem direkten jmpTailcall am Ende einer Funktion nicht benötigt .
Peter Cordes