Inne odpowiedzi dotyczą tylko NOP, który faktycznie wykonuje się w pewnym momencie - jest to dość powszechne, ale nie jest to jedyne użycie NOP.
Niewykonujący się NOP jest również bardzo użyteczny podczas pisania kodu, który można załatać - w zasadzie po funkcji RET
(lub podobnej instrukcji) wypełnisz funkcję kilkoma NOP . Kiedy musisz załatać plik wykonywalny, możesz łatwo dodać więcej kodu do funkcji, zaczynając od oryginału RET
i używając tyle NOP, ile potrzebujesz (np. Do skoków w dal lub nawet kodu wbudowanego), a kończąc na innym RET
.
W takim przypadku noöne nigdy nie oczekuje NOP
wykonania. Jedyną rzeczą jest umożliwienie łatania pliku wykonywalnego - w teoretycznym nie wypełnionym pliku wykonywalnym trzeba by było zmienić kod samej funkcji (czasem może pasować do pierwotnych granic, ale dość często i tak potrzebujesz skoku ) - jest to o wiele bardziej skomplikowane, zwłaszcza biorąc pod uwagę ręcznie napisany zestaw lub optymalizujący kompilator; musisz szanować skoki i podobne konstrukcje, które mogły wskazywać na jakiś ważny fragment kodu. W sumie dość trudne.
Oczywiście, to było dużo większym stopniu wykorzystywane w dawnych czasach, kiedy to było przydatne do łatki jak te małe i internetowych . Dzisiaj będziesz po prostu dystrybuował skompilowany plik binarny i skończysz z nim. Nadal są tacy, którzy używają łatania NOP (wykonując lub nie, i nie zawsze dosłownie NOP
s - na przykład Windows używa MOV EDI, EDI
do łatania online - w ten sposób możesz aktualizować bibliotekę systemową, podczas gdy system faktycznie działa, bez potrzeby restartowania).
Ostatnie pytanie brzmi: po co mieć dedykowaną instrukcję dla czegoś, co tak naprawdę nic nie robi?
- Jest to rzeczywista instrukcja - ważna podczas debugowania lub ręcznego kodowania zestawu. Instrukcje takie
MOV AX, AX
zrobią dokładnie to samo, ale nie sygnalizują tak wyraźnie intencji.
- Padding - „kod”, który służy tylko poprawie ogólnej wydajności kodu, która zależy od wyrównania. To nigdy nie jest przeznaczone do wykonania. Niektóre debuggery po prostu ukrywają wypełnianie NOP podczas ich demontażu.
- Daje to więcej miejsca na optymalizację kompilatorów - nadal używany wzorzec polega na tym, że masz dwa etapy kompilacji, pierwszy jest raczej prosty i generuje dużo niepotrzebnego kodu asemblującego, podczas gdy drugi czyści, ponownie łączy odniesienia do adresów i usuwa obce instrukcje. Jest to często widoczne również w językach kompilowanych w JIT - zarówno IL .NET, jak i kod bajtowy JVM używają
NOP
dość dużo; rzeczywisty skompilowany kod zestawu już go nie ma. Należy jednak zauważyć, że nie są to x86 NOP
.
- Ułatwia debugowanie online zarówno do odczytu (wstępnie wyzerowana pamięć będzie wszystkim
NOP
, co znacznie ułatwia demontaż), jak i do łatania na gorąco (choć zdecydowanie wolę Edytuj i kontynuuj w Visual Studio: P).
Do wykonania NOP jest oczywiście jeszcze kilka punktów:
- Wydajność, oczywiście - nie dlatego miało to miejsce w 8085, ale nawet 80486 już miał potokowe wykonywanie instrukcji, co sprawia, że „nic nie robić” jest nieco trudniejsze.
- Jak widać z
MOV EDI, EDI
, istnieją inne skuteczne NOP niż dosłowne NOP
. MOV EDI, EDI
ma najlepszą wydajność jako 2-bajtowy NOP na x86. Jeśli użyłeś dwóch NOP
s, byłyby to dwie instrukcje do wykonania.
EDYTOWAĆ:
W rzeczywistości dyskusja z @DmitryGrigoryev zmusiła mnie do zastanowienia się nad tym trochę i uważam, że jest to cenny dodatek do tego pytania / odpowiedzi, więc dodaję kilka dodatkowych bitów:
Po pierwsze, rzecz jasna - dlaczego miałaby istnieć instrukcja, która coś takiego robi mov ax, ax
? Na przykład spójrzmy na przypadek kodu maszynowego 8086 (starszy niż nawet kod maszynowy 386):
- Istnieje specjalna instrukcja NOP z opcode
0x90
. To wciąż czas, kiedy wiele osób pisało na zgromadzeniu, pamiętajcie. Nawet gdyby nie było dedykowanej NOP
instrukcji, NOP
słowo kluczowe (alias / mnemonic) byłoby nadal przydatne i byłoby do niego przypisane.
- Instrukcje takie jak
MOV
mapują wiele różnych kodów operacyjnych, ponieważ oszczędza to czas i przestrzeń - na przykład mov al, 42
jest „przenieś natychmiastowy bajt do al
rejestru”, co przekłada się na 0xB02A
( 0xB0
bycie opcode, 0x2A
bycie „bezpośrednim” argumentem). To zajmuje dwa bajty.
- Nie ma kodu opcji skrótu dla
mov al, al
(ponieważ jest to głupota, w zasadzie), więc będziesz musiał użyć mov al, rmb
przeciążenia (rmb to „rejestr lub pamięć”). To faktycznie zajmuje trzy bajty. (chociaż prawdopodobnie użyłby mniej specyficznego mov rb, rmb
, który powinien zająć tylko dwa bajty mov al, al
- bajt argumentu służy do określenia zarówno rejestru źródłowego, jak i docelowego; teraz wiesz, dlaczego 8086 miał tylko 8 rejestrów: D). Porównaj z NOP
, która jest instrukcją jednobajtową! Oszczędza to pamięć i czas, ponieważ czytanie pamięci w 8086 było wciąż dość drogie - nie wspominając o ładowaniu tego programu z taśmy, dyskietki czy czegoś takiego.
Więc skąd xchg ax, ax
pochodzi? Musisz tylko spojrzeć na kody innych xhcg
instrukcji. Zobaczysz 0x86
, 0x87
i wreszcie, 0x91
- 0x97
. Wydaje się więc, nop
że jest 0x90
to całkiem dobre dopasowanie xchg ax, ax
(co znowu nie jest xchg
„przeciążeniem” - trzeba by użyć xchg rb, rmb
dwóch bajtów). I faktycznie jestem całkiem pewien, że był to miły efekt uboczny 0x90-0x97
mikrotechniki tamtych czasów - jeśli dobrze pamiętam, łatwo było zmapować cały zakres na „xchg, działający na rejestry ax
i ax
- di
” ( operand jest symetryczny, to dał ci pełny zakres, w tym NOP xchg ax, ax
; Zauważ, że kolejność jest ax, cx, dx, bx, sp, bp, si, di
- bx
to podx
,ax
; pamiętaj, że nazwy rejestrów są mnemoniczne, a nie uporządkowane - akumulator, licznik, dane, baza, wskaźnik stosu, wskaźnik bazy, indeks źródłowy, indeks docelowy). To samo podejście zastosowano również w przypadku innych operandów, na przykład mov someRegister, immediate
zestawu. W pewnym sensie można by pomyśleć o tym, jakby kod operacyjny nie był w rzeczywistości pełnym bajtem - kilka ostatnich bitów stanowi „argument” dla „prawdziwego” argumentu.
Wszystko to powiedziane na x86 nop
może być uważane za prawdziwą instrukcję, czy nie. Oryginalna mikroarchitektura traktowała to jako wariant, xchg
jeśli dobrze pamiętam, ale tak naprawdę została nazwana nop
w specyfikacji. A ponieważ xchg ax, ax
tak naprawdę nie ma to sensu jako instrukcja, możesz zobaczyć, jak projektanci 8086 zapisali na tranzystorach i ścieżkach podczas dekodowania instrukcji, wykorzystując fakt, że 0x90
mapuje naturalnie na coś, co jest całkowicie „noppy”.
Z drugiej strony i8051 ma całkowicie zaprojektowany kod operacji dla nop
- 0x00
. Trochę praktyczne. Projekt instrukcji w zasadzie używa wysokiej wartości dla operacji i niskiej wartości dla wyboru argumentów - na przykład add a
jest 0x2Y
i 0xX8
oznacza „rejestr 0 bezpośredni”, tak 0x28
jest add a, r0
. Dużo oszczędza na krzemie :)
Nadal mogę kontynuować, ponieważ projektowanie procesorów (nie wspominając o projektowaniu kompilatora i projektowaniu języka) jest dość szerokim tematem, ale myślę, że pokazałem wiele różnych punktów widzenia, które weszły w projekt całkiem niezły.