Dlaczego pad GCC działa z NOP?

81

Pracuję z C przez krótki czas i bardzo niedawno zacząłem zajmować się ASM. Kiedy kompiluję program:

Demontaż objdump ma kod, ale nops po ret:

Z tego, czego się dowiedziałem, nops nic nie robi, a ponieważ po ret nie zostałby nawet stracony.

Moje pytanie brzmi: po co się przejmować? Czy ELF (linux-x86) nie może działać z sekcją .text (+ main) dowolnego rozmiaru?

Byłbym wdzięczny za każdą pomoc, po prostu próbując się nauczyć.

olly
źródło
Czy te NOP nadal działają? Jeśli zatrzymają się na 80483af, być może jest to wypełnienie, aby wyrównać następną funkcję do 8 lub 16 bajtów.
Mysticial
nie po 4 nopsach przechodzi prosto do funkcji: __libc_csu_fini
olly
1
Jeśli NOP zostały wstawione przez gcc, to nie sądzę, że użyje tylko 0x90, ponieważ istnieje wiele NOPów ze zmiennymi rozmiaru od 1-9 bajtów (10, jeśli używasz składni gazu )
phuclv

Odpowiedzi:

89

Przede wszystkim gccnie zawsze to robi. Wypełnienie jest kontrolowane przez -falign-functions, które jest automatycznie włączane przez -O2i -O3:

-falign-functions
-falign-functions=n

Wyrównaj początek funkcji do następnej potęgi dwa większej niż n, przeskakując do nbajtów. Na przykład -falign-functions=32wyrównuje funkcje do następnej 32-bajtowej granicy, ale wyrówna -falign-functions=24ją do następnej 32-bajtowej granicy tylko wtedy, gdy można to zrobić, pomijając 23 bajty lub mniej.

-fno-align-functionsi -falign-functions=1są równoważne i oznaczają, że funkcje nie zostaną wyrównane.

Niektóre asemblery obsługują tę flagę tylko wtedy, gdy n jest potęgą dwóch; w takim przypadku jest zaokrąglana w górę.

Jeśli n nie jest określone lub wynosi zero, użyj wartości domyślnej zależnej od komputera.

Włączone na poziomach -O2, -O3.

Może być wiele powodów, dla których warto to zrobić, ale główny na x86 jest prawdopodobnie taki:

Większość procesorów pobiera instrukcje w wyrównanych blokach 16-bajtowych lub 32-bajtowych. Korzystne może być wyrównanie krytycznych wpisów pętli i podprogramów o 16 w celu zminimalizowania liczby 16-bajtowych granic w kodzie. Alternatywnie, upewnij się, że w pierwszych kilku instrukcjach po wejściu w pętli krytycznej lub wpisie podprogramu nie ma granicy 16-bajtowej.

(Cytat z „Optimizing podprogramów w języku asemblera” autorstwa Agner Fog.)

edycja: Oto przykład, który demonstruje dopełnienie:

Po skompilowaniu przy użyciu gcc 4.4.5 z domyślnymi ustawieniami otrzymuję:

Określenie -falign-functionsdaje:

NPE
źródło
1
Nie użyłem żadnych flag -O, po prostu "gcc -o test test.c".
olly
1
@olly: Testowałem to z gcc 4.4.5 na 64-bitowym Ubuntu iw moich testach domyślnie nie ma dopełnienia, a jest dopełnienie -falign-functions.
NPE
@aix: Jestem na centOS 6.0 (32-bitowy) i bez żadnych flag mam dopełnienie. Czy ktoś chce, żebym zrzucił moje pełne wyjście „objdump -j .text -d ./test”?
olly
1
Podczas dalszych testów, kiedy kompiluję go jako obiekt: "gcc -c test.c". Nie ma dopełnienia, ale kiedy linkuję: "gcc -o test test.o" pojawia się.
olly
2
@olly: to wypełnienie jest wstawiane przez konsolidator, aby spełnić wymagania dotyczące wyrównania funkcji, która następuje mainw pliku wykonywalnym (w moim przypadku jest to funkcja __libc_csu_fini).
NPE
15

Ma to na celu wyrównanie następnej funkcji do granicy 8, 16 lub 32 bajtów.

Z „Optimizing podprogramów w języku asemblera” autorstwa A. Fog:

11.5 Wyrównanie kodu

Większość mikroprocesorów pobiera kod w wyrównanych blokach 16-bajtowych lub 32-bajtowych. Jeśli ważny wpis podprogramu lub etykieta skoku znajduje się blisko końca 16-bajtowego bloku, wówczas mikroprocesor otrzyma tylko kilka użytecznych bajtów kodu podczas pobierania tego bloku kodu. Może być konieczne pobranie kolejnych 16 bajtów, zanim zdekoduje pierwsze instrukcje po etykiecie. Można tego uniknąć, wyrównując ważne wpisy podprogramów i pętli o 16.

[…]

Wyrównanie wpisu podprogramu jest tak proste, jak umieszczenie tylu NOP, ile potrzeba, przed wpisem podprogramu, aby adres był podzielny przez 8, 16, 32 lub 64, zależnie od potrzeb.

hamstergen
źródło
To różnica między 25-29 bajtów (dla głównego), czy mówisz o czymś większym? Podobnie jak w sekcji tekstowej, za pomocą readelf stwierdziłem, że ma 364 bajty? Zauważyłem również 14 nops na _start. Dlaczego „jako” nie robi tych rzeczy? Jestem debiutantem, przepraszam.
olly
@olly: Widziałem systemy programistyczne, które wykonują optymalizację całego programu na skompilowanym kodzie maszynowym. Jeśli adres funkcji footo 0x1234, to kod, który używa tego adresu w bliskim sąsiedztwie literału 0x1234, może w końcu wygenerować kod maszynowy, taki, mov ax,0x1234 / push ax / mov ax,0x1234 / push axjaki optymalizator mógłby następnie zastąpić mov ax,0x1234 / push ax / push ax. Należy pamiętać, że po takiej optymalizacji funkcji nie można przenosić, więc eliminacja instrukcji poprawiłaby szybkość wykonywania, ale nie zwiększyłaby rozmiaru kodu.
supercat
5

O ile pamiętam, instrukcje są przetwarzane potokowo w procesorze i różne bloki procesora (program ładujący, dekoder itp.) Przetwarzają kolejne instrukcje. Kiedy RETinstrukcje są wykonywane, kilka następnych instrukcji jest już ładowanych do potoku procesora. To przypuszczenie, ale możesz zacząć szukać tutaj, a jeśli się dowiesz (może konkretna liczba NOPbezpiecznych, podziel się swoimi odkryciami).

mco
źródło
@ninjalj: Hę? To pytanie dotyczy procesora x86, który jest potokowy (jak powiedział mco). Wiele nowoczesnych procesorów x86 również spekulacyjnie wykonuje instrukcje, które „nie powinny” być wykonywane, być może włączając te nopsy. Może chciałeś skomentować gdzie indziej?
David Cary
3
@DavidCary: w x86, to jest całkowicie przezroczyste dla programisty. W przypadku błędnie odgadniętych spekulatywnych instrukcji wykonanych po prostu ich wyniki i efekty są odrzucane. W MIPS nie ma w ogóle części "spekulatywnej", instrukcja w gałęzi opóźnienia jest zawsze wykonywana, a programista musi wypełnić szczeliny opóźnienia (lub pozwolić na to asemblerowi, co prawdopodobnie dałoby nops).
ninjalj
@ninjalj: Tak, efekt błędnie odgadniętych, spekulacyjnie wykonanych operacji i nie wyrównanych instrukcji jest przezroczysty, w tym sensie, że nie mają one wpływu na wartości danych wyjściowych. Jednak oba mają wpływ na czas działania programu, co może być powodem, dla którego gcc dodaje nops do kodu x86, o co zadawano pierwotne pytanie.
David Cary
1
@DavidCary: gdyby to był powód, zobaczyłbyś to tylko po skokach warunkowych, a nie po bezwarunkowym ret.
ninjalj
1
To nie jest powód. Przewidywanie awaryjnego skoku pośredniego (przy chybieniu BTB) jest następną instrukcją, ale jeśli jest to śmieci niebędące instrukcją, zalecana optymalizacja, aby zatrzymać błędne spekulacje, to instrukcje takie jak ud2lub, int3które zawsze zawierają błędy, więc front-end wie, aby zamiast tego zatrzymać dekodowanie na przykład zasilania divrurociągu potencjalnie kosztownym lub fałszywym ładunkiem nieudanym TLB. Nie jest to potrzebne po retlub bezpośrednim jmpwywołaniu końcowym na końcu funkcji.
Peter Cordes,