Dlaczego potrzebujemy „nop”, tj. Instrukcji obsługi w mikroprocesorze 8085?

19

W instrukcji mikroprocesora 8085 występuje operacja sterowania maszyną „nop” (brak operacji). Moje pytanie brzmi: dlaczego potrzebujemy operacji bez? Mam na myśli, że jeśli musimy zakończyć program, użyjemy HLT lub RST 3. Lub jeśli chcemy przejść do następnej instrukcji, podamy kolejne instrukcje. Ale dlaczego nie ma operacji? Jaka jest potrzeba?

Demietra95
źródło
5
NOP jest często używany do debugowania i aktualizacji programu. Jeśli później chcesz dodać kilka wierszy do swojego programu, możesz po prostu zastąpić NOP. W przeciwnym razie będziesz musiał wstawić linie, a wstawienie oznacza przesunięcie całego programu. Również błędne instrukcje (niepoprawne) mogą być zastąpione (po prostu nadpisane) przez NOP po tych samych argumentach.
Przemytnik plutonu
Okej Ale użycie nop zwiększa również przestrzeń. Podczas gdy naszym głównym celem jest sprawienie, aby zajęło to niewiele miejsca.
Demietra95
* Mam na myśli, że naszym celem jest zmniejszenie problemu. Czy to też nie staje się problemem?
Demietra95
2
Dlatego należy go mądrze wykorzystywać. W przeciwnym razie cały twój program będzie tylko garstką NOP.
Przemytnik plutonu

Odpowiedzi:

34

Jednym z zastosowań instrukcji NOP (lub NOOP, brak operacji) w procesorach i MCU jest wstawienie niewielkiego, przewidywalnego opóźnienia w kodzie. Chociaż NOP nie wykonują żadnej operacji, ich przetworzenie zajmuje trochę czasu (procesor musi pobrać i zdekodować kod operacyjny, więc zajmuje to trochę czasu). Tylko 1 cykl CPU jest „marnowany” na wykonanie instrukcji NOP (zwykle dokładną liczbę można wywnioskować z arkusza danych CPU / MCU), dlatego ustawianie N NOP w sekwencji jest łatwym sposobem na wstawienie przewidywalnego opóźnienia:

tremilzay=N.T.dolodokK.

gdzie K jest liczbą cykli (najczęściej 1) potrzebnych do przetworzenia instrukcji NOP, a jest okresem zegara.T.dolodok

Dlaczego chcesz to zrobić? Przydatne może być wymuszenie na procesorze trochę czasu, aż zewnętrzne (być może wolniejsze) urządzenia zakończą pracę i przekażą dane do CPU, tzn. NOP jest przydatny do celów synchronizacji.

Zobacz także powiązaną stronę Wikipedii na NOP .

Innym zastosowaniem jest wyrównanie kodu pod pewnymi adresami w pamięci i innymi „sztuczkami asemblera”, jak wyjaśniono również w tym wątku na Programmers.SE oraz w tym drugim wątku na StackOverflow .

Kolejny interesujący artykuł na ten temat .

Ten link do strony książki Google dotyczy w szczególności procesora 8085. Fragment:

Każda instrukcja NOP wykorzystuje cztery zegary do pobierania, dekodowania i wykonywania.

EDYCJA (aby rozwiązać problem wyrażony w komentarzu)

Jeśli martwisz się szybkością, pamiętaj, że wydajność (czasowa) to tylko jeden parametr do rozważenia. Wszystko zależy od zastosowania: jeśli chcesz obliczyć 10-miliardową liczbę , być może Twoim jedynym problemem może być prędkość. Z drugiej strony, jeśli chcesz dane z czujników temperatury podłączonych do MCU przez ADC zalogować, prędkość nie jest zwykle tak ważne, ale czeka odpowiednią ilość czasu, aby umożliwić ADC, aby prawidłowo zakończyć każdy odczyt jest niezbędna . W tym przypadku, jeśli MCU nie zaczeka wystarczająco długo, ryzykuje to uzyskaniem całkowicie niewiarygodnych danych (przyznaję, że szybciej uzyska te dane : o).π

Lorenzo Donati wspiera Monikę
źródło
3
Wiele rzeczy (szczególnie wyjścia sterujące układami zewnętrznymi poza uC) podlegają ograniczeniom czasowym, takim jak „minimalny czas między D jest stabilny a krawędzią zegara wynosi 100 us” lub „dioda IR musi migać przy 1 MHz”. Dlatego często wymagane są (dokładne) opóźnienia.
Wouter van Ooijen
6
Procesory NOP mogą być przydatne do uzyskania właściwej synchronizacji podczas bit-bangu protokołu szeregowego. Mogą być również przydatne do wypełnienia nieużywanej przestrzeni kodu, a następnie przejścia do wektora zimnego startu w rzadkich sytuacjach, gdy licznik programu ulegnie uszkodzeniu (np. Usterka zasilacza, uderzenie rzadkiego zdarzenia promieniowania gamma itp.) I rozpocznie wykonywanie kodu w w przeciwnym razie pusta część przestrzeni kodu.
Techydude
6
W systemie komputerowym wideo Atari 2600 (druga konsola do gier do uruchamiania programów przechowywanych na kartridżach) procesor wykonałby dokładnie 76 cykli na każdej linii skanowania, a wiele operacji musiałoby zostać wykonanych pewną dokładną liczbę cykli po rozpoczęciu linia skanowania. Na tym procesorze udokumentowana instrukcja „NOP” zajmuje dwa cykle, ale kod często korzysta z bezużytecznej instrukcji trzech cykli, aby uzupełnić opóźnienie do określonej liczby cykli. Szybsze uruchamianie kodu dałoby całkowicie zniekształcony obraz.
supercat
1
Używanie NOP do opóźnień może mieć sens nawet w systemach nie działających w czasie rzeczywistym, w przypadkach, gdy urządzenie I / O narzuca minimalny czas między kolejnymi operacjami, ale nie maksymalny. Na przykład na wielu kontrolerach przesunięcie bajtu poza port SPI zajmie osiem cykli procesora. Kod, który pobiera tylko bajty z pamięci i wysyła je do portu SPI, może działać nieco za szybko, ale dodanie logiki w celu sprawdzenia, czy port SPI jest gotowy na każdy bajt, spowodowałoby, że byłby on niepotrzebnie wolny. Dodanie NOP lub dwóch może pozwolić kodowi na osiągnięcie maksymalnej dostępnej prędkości ...
supercat
1
... w przypadku, gdy nie ma przerw. Jeśli dojdzie do przerwania, NOP niepotrzebnie marnują czas, ale czas zmarnowany przez jeden lub dwa NOP byłby krótszy niż czas wymagany do ustalenia, czy przerwanie czyni je niepotrzebnymi.
supercat
8

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.

Luaan
źródło
Właściwie, NOPto zwykle aliasem MOV ax, ax, ADD ax, 0lub podobne instrukcje. Dlaczego miałbyś zaprojektować dedykowaną instrukcję, która nic nie robi, gdy jest jej dużo.
Dmitrij Grigoriew
@DmitryGrigoryev To naprawdę wchodzi w projekt samego języka procesora (no, mikroarchitektury). Większość procesorów (i kompilatorów) będzie dążyła do optymalizacji działania MOV ax, ax; NOPzawsze będzie mieć określoną liczbę cykli wykonania. Ale i tak nie rozumiem, jak to ma związek z tym, co napisałem w mojej odpowiedzi.
Luaan
Procesory nie mogą tak naprawdę zoptymalizować MOV ax, axodejścia, ponieważ zanim się dowiedzą, że MOVinstrukcja jest już w przygotowaniu.
Dmitrij Grigoriew
@DmitryGrigoryev To naprawdę zależy od rodzaju procesora, o którym mówisz. Nowoczesne procesory do komputerów stacjonarnych wykonują wiele czynności, nie tylko potokowanie instrukcji. Na przykład, procesor wie, że nie musi unieważniać linii pamięci podręcznej itp., Wie, że tak naprawdę nie musi nic robić (bardzo ważne dla HyperThreading, a nawet ogólnie dla wielu potoków). Nie byłbym zaskoczony, gdyby miało to również wpływ na przewidywanie gałęzi (choć prawdopodobnie byłoby tak samo dla NOPi MOV ax, ax). Nowoczesne procesory są znacznie bardziej złożone niż
stare
1
+1 za optymalizację nikt do noöne! Myślę, że wszyscy powinniśmy współpracować i wrócić do tego stylu pisowni! Z80 (i z kolei 8080) ma 7 LD r, rinstrukcji, gdzie rjest dowolny pojedynczy rejestr, podobny do twojej MOV ax, axinstrukcji. Powodem jest 7, a nie 8, ponieważ jedna z instrukcji jest przeciążona HALT. Więc 8080 i Z80 mają co najmniej 7 innych instrukcji, które robią to samo co NOP! Co ciekawe, mimo że instrukcje te nie są logicznie powiązane wzorcem bitowym, wszystkie wykonują 4 stany T, więc nie ma powodu, aby korzystać z LDinstrukcji!
CJ Dennis
5

W późnych latach 70. mieliśmy (wtedy byłem młodym studentem badań) mały system deweloperów (8080, jeśli pamięć służy), który działał w 1024 bajtach kodu (tj. W jednym UVEPROM) - miał tylko cztery polecenia do załadowania (L ), zapisz (S), wydrukuj (P) i coś innego, czego nie pamiętam. Był napędzany prawdziwym teletypem i taśmą dziurkowaną. To było ściśle zakodowane!

Jednym z przykładów użycia NOOP była procedura obsługi przerwań (ISR), która była rozmieszczona w odstępach 8-bajtowych. Ta procedura zakończyła się na 9 bajtach kończących się (długim) skokiem na adres nieco dalej w przestrzeni adresowej. Oznaczało to, biorąc pod uwagę małą kolejność bajtów endian, że wysoki bajt adresu wynosił 00h i był wstawiany do pierwszego bajtu następnego ISR, co oznaczało, że (następny ISR) zaczynał się od NOOP, tak aby „pasowaliśmy” kod w ograniczonej przestrzeni!

Więc NOOP jest przydatny. Poza tym podejrzewam, że najłatwiej było kodować go w ten sposób - prawdopodobnie mieli listę instrukcji, które chcieli zaimplementować i zaczęło się od „1”, jak to robią wszystkie listy (to były dni FORTRAN), więc zero Kod NOOP stał się wypadnięciem. (Nigdy nie widziałem artykułu, w którym argumentowałby, że NOOP jest istotną częścią teorii informatyki (to samo pytanie: czy matematycy mają zerową wartość, w odróżnieniu od zera teorii grup?)

Philip Oakley
źródło
Nie wszystkie procesory mają kodowanie NOP na 0x00 (chociaż 8085, 8080 i procesor, z którym jestem najbardziej zaznajomiony, Z80, wszyscy tak robią). Gdybym jednak projektował procesor, to właśnie tam go umieściłem! Przydaje się również to, że pamięć jest zwykle inicjowana na wszystkie 0x00, więc wykonanie tego, ponieważ kod nic nie zrobi, dopóki procesor nie osiągnie niezerowanej pamięci.
CJ Dennis
@CJDennis mam wyjaśnić dlaczego procesory x86 nie używać 0x00na nopmoim odpowiedź. Krótko mówiąc, oszczędza na dekodowaniu instrukcji - xchg ax, axwypływa naturalnie ze sposobu, w jaki działa dekodowanie instrukcji, i robi coś „noppy”, więc dlaczego by tego nie użyć i nazwać nop, prawda? :) Używane, aby zaoszczędzić sporo na krzemie do dekodowania instrukcji ...
Luaan
5

W niektórych architekturach NOPsłuży do zajmowania nieużywanych miejsc na opóźnienia . Na przykład, jeśli instrukcja rozgałęzienia nie wyczyści potoku, to mimo to kilka instrukcji po jego wykonaniu:

 JMP     .label
 MOV     R2, 1    ; these instructions start execution before the jump
 MOV     R2, 2    ; takes place so they still get executed

Ale co, jeśli nie masz żadnych przydatnych instrukcji, które pasowałyby po JMP? W takim przypadku będziesz musiał użyć NOPs.

Automaty do opóźnień nie są ograniczone do skoków. W niektórych architekturach zagrożenia danych w potoku procesora nie są rozwiązywane automatycznie. Oznacza to, że po każdej instrukcji modyfikującej rejestr jest miejsce, w którym nowa wartość rejestru nie jest jeszcze dostępna. Jeśli następna instrukcja wymaga tej wartości, miejsce powinno być zajęte przez NOP:

 ADD     R1, R1
 NOP               ; The new value of R1 is not ready yet
 ADD     R1, R3

Ponadto niektóre instrukcje wykonania warunkowego ( jeśli-prawda-fałsz i podobne) używają gniazd dla każdego warunku, a gdy dany warunek nie jest powiązany z żadnymi działaniami, jego miejsce powinno być zajęte przez NOP:

CMP     R0, R1       ; Compare R0 and R1, setting flags
ITF     GT           ; If-True-False on GT flag 
MOV     R3, R2       ; If 'greater than', move R2 to R3
NOP                  ; Else, do nothing
Dmitrij Grigoriew
źródło
+1. Oczywiście, pojawiają się one tylko na architekturach, które nie dbają o kompatybilność wsteczną - jeśli x86 spróbowałby czegoś takiego podczas wprowadzania potoku instrukcji, prawie wszyscy po prostu nazwaliby to źle (w końcu po prostu zaktualizowali swój procesor i swoje aplikacje przestały działać!). Więc x86 musi upewnić się, że aplikacja nie zauważy, gdy takie ulepszenia zostaną dodane do procesora - dopóki nie
przejdziemy
2

Kolejny przykład zastosowania dwubajtowego NOP: http://blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

Instrukcja MOV EDI, EDI to dwubajtowy NOP, który jest wystarczającą ilością miejsca do łatania instrukcji skoku, aby funkcja mogła być aktualizowana w locie. Intencją jest, aby instrukcja MOV EDI, EDI została zastąpiona dwubajtową instrukcją JMP $ -5 w celu przekierowania sterowania do pięciu bajtów miejsca łatki, które pojawia się bezpośrednio przed uruchomieniem funkcji. Pięć bajtów wystarcza na instrukcję pełnego skoku, która może wysłać kontrolę do funkcji zastępczej zainstalowanej gdzie indziej w przestrzeni adresowej.

pjc50
źródło