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 RETi 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 NOPwykonania. 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 NOPs - na przykład Windows używa MOV EDI, EDIdo ł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, AXzrobią 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ą
NOPdość 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, EDIma najlepszą wydajność jako 2-bajtowy NOP na x86. Jeśli użyłeś dwóch NOPs, 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 NOPinstrukcji, NOPsłowo kluczowe (alias / mnemonic) byłoby nadal przydatne i byłoby do niego przypisane.
- Instrukcje takie jak
MOVmapują wiele różnych kodów operacyjnych, ponieważ oszczędza to czas i przestrzeń - na przykład mov al, 42jest „przenieś natychmiastowy bajt do alrejestru”, co przekłada się na 0xB02A( 0xB0bycie opcode, 0x2Abycie „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, rmbprzeciąż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, axpochodzi? Musisz tylko spojrzeć na kody innych xhcginstrukcji. Zobaczysz 0x86, 0x87i wreszcie, 0x91- 0x97. Wydaje się więc, nopże jest 0x90to całkiem dobre dopasowanie xchg ax, ax(co znowu nie jest xchg„przeciążeniem” - trzeba by użyć xchg rb, rmbdwóch bajtów). I faktycznie jestem całkiem pewien, że był to miły efekt uboczny 0x90-0x97mikrotechniki tamtych czasów - jeśli dobrze pamiętam, łatwo było zmapować cały zakres na „xchg, działający na rejestry axi 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- bxto 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, immediatezestawu. 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 nopmoże być uważane za prawdziwą instrukcję, czy nie. Oryginalna mikroarchitektura traktowała to jako wariant, xchgjeśli dobrze pamiętam, ale tak naprawdę została nazwana nopw specyfikacji. A ponieważ xchg ax, axtak 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 0x90mapuje 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 ajest 0x2Yi 0xX8oznacza „rejestr 0 bezpośredni”, tak 0x28jest 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.