Warum funktioniert GCC-Pad mit NOPs?

Lesezeit: 7 Minuten

ollys Benutzeravatar
Olli

Ich arbeite seit kurzer Zeit mit C und habe vor kurzem begonnen, mich mit ASM zu beschäftigen. Wenn ich ein Programm kompiliere:

int main(void)
  {
  int a = 0;
  a += 1;
  return 0;
  }

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

...
08048394 <main>:
 8048394:       55                      push   %ebp
 8048395:       89 e5                   mov    %esp,%ebp
 8048397:       83 ec 10                sub    $0x10,%esp
 804839a:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%ebp)
 80483a1:       83 45 fc 01             addl   $0x1,-0x4(%ebp)
 80483a5:       b8 00 00 00 00          mov    $0x0,%eax
 80483aa:       c9                      leave  
 80483ab:       c3                      ret    
 80483ac:       90                      nop
 80483ad:       90                      nop
 80483ae:       90                      nop
 80483af:       90                      nop
...

Nach dem, was ich gelernt habe, tun Nops nichts, und da nach ret nicht einmal hingerichtet würde.

Meine Frage ist: warum sich die Mühe machen? Könnte ELF (linux-x86) nicht mit einem .text-Abschnitt (+ main) beliebiger Größe funktionieren?

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

  • Gehen diese NOPs weiter? Wenn sie anhalten 80483afdann ist es vielleicht eine Auffüllung, um die nächste Funktion auf 8 oder 16 Bytes auszurichten.

    – Mystisch

    27. Oktober 2011 um 6:51 Uhr


  • nein nach den 4 Nops geht es direkt zu einer Funktion: __libc_csu_fini

    – Olli

    27. Oktober 2011 um 6:55 Uhr

  • Wenn die NOPs von gcc eingefügt wurden, wird es meiner Meinung nach nicht nur 0x90 verwenden, da es viele NOPs mit Größenvariablen von gibt 1-9 Byte (10 bei Verwendung der Gassyntax)

    – phuklv

    10. Februar 2014 um 3:35 Uhr

Benutzeravatar von NPE
NPE

Zuerst, gcc tut dies nicht immer. Die Polsterung wird durch gesteuert -falign-functionsdie automatisch durch eingeschaltet wird -O2 und -O3:

-falign-functions
-falign-functions=n

Richten Sie den Beginn von Funktionen an der nächsten Zweierpotenz größer als aus nbis zu springen n Bytes. Zum Beispiel,
-falign-functions=32 richtet Funktionen an der nächsten 32-Byte-Grenze aus, aber -falign-functions=24 würde sich nur dann an der nächsten 32-Byte-Grenze ausrichten, wenn dies durch Überspringen von 23 Bytes oder weniger erfolgen kann.

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

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

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

Aktiviert bei Niveaus -O2, -O3.

Es könnte mehrere Gründe dafür geben, aber der wichtigste auf x86 ist wahrscheinlich dieser:

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

(Zitiert aus „Optimizing subroutines in assembly language“ von Agner Fog.)

bearbeiten: Hier ist ein Beispiel, das die Polsterung demonstriert:

// align.c
int f(void) { return 0; }
int g(void) { return 0; }

Beim Kompilieren mit gcc 4.4.5 mit Standardeinstellungen erhalte ich:

align.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <f>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   b8 00 00 00 00          mov    $0x0,%eax
   9:   c9                      leaveq 
   a:   c3                      retq   

000000000000000b <g>:
   b:   55                      push   %rbp
   c:   48 89 e5                mov    %rsp,%rbp
   f:   b8 00 00 00 00          mov    $0x0,%eax
  14:   c9                      leaveq 
  15:   c3                      retq   

Angabe -falign-functions gibt:

align.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <f>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   b8 00 00 00 00          mov    $0x0,%eax
   9:   c9                      leaveq 
   a:   c3                      retq   
   b:   eb 03                   jmp    10 <g>
   d:   90                      nop
   e:   90                      nop
   f:   90                      nop

0000000000000010 <g>:
  10:   55                      push   %rbp
  11:   48 89 e5                mov    %rsp,%rbp
  14:   b8 00 00 00 00          mov    $0x0,%eax
  19:   c9                      leaveq 
  1a:   c3                      retq   

  • Ich habe keine -O-Flags verwendet, einfach “gcc -o test test.c”.

    – Olli

    27. Oktober 2011 um 7:11 Uhr

  • @olly: Ich habe es mit gcc 4.4.5 auf 64-Bit-Ubuntu getestet und in meinen Tests gibt es standardmäßig keine Auffüllung und Auffüllung mit -falign-functions.

    – NPE

    27. Oktober 2011 um 7:13 Uhr

  • @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 von “objdump -j .text -d ./test” ablege?

    – Olli

    27. Oktober 2011 um 7:17 Uhr

  • Beim weiteren Testen, wenn ich es als Objekt kompiliere: “gcc -c test.c”. Es gibt keine Auffüllung, aber wenn ich verlinke: “gcc -o test test.o” erscheint es.

    – Olli

    27. Oktober 2011 um 7:42 Uhr

  • @olly: Diese Auffüllung wird vom Linker eingefügt, um die Ausrichtungsanforderungen der folgenden Funktion zu erfüllen main in der ausführbaren Datei (in meinem Fall ist das die Funktion __libc_csu_fini).

    – NPE

    27. Oktober 2011 um 9:54 Uhr

Benutzeravatar von hamstergene
Hamstergen

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

Aus „Optimierung 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 ein wichtiger Unterroutineneintrag oder eine Sprungmarke zufällig nahe dem Ende eines 16-Byte-Blocks ist, erhält der Mikroprozessor nur ein paar nützliche Codebytes, wenn er diesen Codeblock abruft. Es kann sein, dass es auch die nächsten 16 Bytes holen muss, bevor es die ersten Anweisungen nach dem Etikett decodieren kann. Dies kann vermieden werden, indem wichtige Unterprogrammeinträge und Schleifeneinträge an 16 ausgerichtet werden.

[…]

Das Ausrichten eines Subroutineneintrags ist so einfach wie das Setzen so vieler NOPs wie nötig vor den Subroutineneintrag, um die Adresse je nach Wunsch durch 8, 16, 32 oder 64 teilbar zu machen.

  • Es ist der Unterschied zwischen 25-29 Bytes (für main), sprichst du von etwas Größerem? Wie beim Textabschnitt habe ich durch Readelf festgestellt, dass es 364 Bytes sind? Ich habe auch 14 nops auf _start bemerkt. Warum tut “als” diese Dinge nicht? Ich bin Anfänger, Entschuldigung.

    – Olli

    27. Oktober 2011 um 7:02 Uhr

  • @olly: Ich habe Entwicklungssysteme gesehen, die eine Optimierung des gesamten Programms für kompilierten Maschinencode durchführen. Wenn die Adresse der Funktion foo 0x1234 ist, dann könnte Code, der diese Adresse in unmittelbarer Nähe zu einem wörtlichen 0x1234 verwendet, möglicherweise Maschinencode wie generieren mov ax,0x1234 / push ax / mov ax,0x1234 / push ax durch die 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 die Eliminierung von Anweisungen die Ausführungsgeschwindigkeit, aber nicht die Codegröße verbessern würde.

    – Superkatze

    30. Juli 2015 um 18:30 Uhr

Benutzeravatar von mco
mco

Soweit ich mich erinnere, werden Anweisungen in der CPU geleitet und verschiedene CPU-Blöcke (Loader, Decoder und dergleichen) verarbeiten nachfolgende Anweisungen. Wann RET Anweisungen ausgeführt werden, sind 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 genaue Anzahl von NOPs, die sicher sind, teilen Sie bitte Ihre Ergebnisse mit.

  • @ninjalj: Hä? Diese Frage bezieht sich auf x86, das Pipeline ist (wie mco sagte). 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 an anderer Stelle kommentieren?

    – David Cary

    6. Oktober 2013 um 21:07 Uhr

  • @DavidCary: In x86 ist das für den Programmierer völlig transparent. Bei falsch erratenen spekulativ ausgeführten Anweisungen werden ihre Ergebnisse und Effekte einfach verworfen. Auf MIPS gibt es überhaupt keinen “spekulativen” Teil, die Anweisung 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 dazu führen würde nops).

    – ninjalj

    6. Oktober 2013 um 21:27 Uhr

  • @ninjalj: Ja, die Auswirkung von falsch erratenen spekulativ ausgeführten Operationen und nicht ausgerichteten Anweisungen sind transparent, in dem Sinne, dass sie keine Auswirkung auf die Ausgabedatenwerte haben. Beide wirken sich jedoch auf das Timing des Programms aus, was möglicherweise der Grund dafür ist, dass gcc dem x86-Code nops hinzufügt, was in der ursprünglichen Frage gestellt wurde.

    – David Cary

    7. Oktober 2013 um 18:35 Uhr


  • @DavidCary: Wenn das der Grund wäre, würdest du es nur nach bedingten Sprüngen sehen, nicht nach unbedingten Sprüngen ret.

    – ninjalj

    7. Oktober 2013 um 19:25 Uhr

  • Das ist nicht der Grund. Die Fallback-Vorhersage eines indirekten Sprungs (bei einem BTB-Fehltreffer) ist die nächste Anweisung, aber wenn das kein Anweisungsmüll ist, ist die empfohlene Optimierung, um Fehlspekulationen zu stoppen, eine Anweisung wie ud2 oder int3 das schlägt immer fehl, sodass das Front-End weiß, dass es die Dekodierung stoppen muss, anstatt ein potenziell teures zu füttern div oder zum Beispiel falsches TLB-Miss-Laden in die Pipeline. Dies wird nach a nicht benötigt ret oder direkt jmp Tailcall am Ende einer Funktion.

    – Peter Cordes

    6. Januar 2020 um 0:04 Uhr

1419260cookie-checkWarum funktioniert GCC-Pad mit NOPs?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy