Cel instrukcji NOP i instrukcji align w zestawie x86

15

Minęło około roku, odkąd ostatni raz brałem udział w zajęciach montażowych. W tej klasie używaliśmy MASM z bibliotekami Irvine, aby ułatwić programowanie.

Po przejściu większości instrukcji powiedział, że instrukcja NOP zasadniczo nic nie zrobiła i nie martwi się o jej użycie. W każdym razie chodziło o okres śródokresowy, a on ma przykładowy kod, który nie działałby poprawnie, więc powiedział nam, abyśmy dodali instrukcję NOP i działało dobrze. Zapytałem, że jestem po zajęciach, dlaczego i co to właściwie zrobiło, a on odpowiedział, że nie wie.

Ktoś wie

alvonellos
źródło
NOP nic nie robi, ale zużywa cykle. Nie sądzę, aby na twoje pytanie można było odpowiedzieć, bez kodu możemy tylko zgadywać. Domyślam się, że to slajd NOP ...
yannis
11
NOP faktycznie coś robi. Zwiększa wskaźnik instrukcji.
EricSchaefer,

Odpowiedzi:

37

Często czasy NOPsłużą do wyrównania adresów instrukcji. Zazwyczaj występuje to na przykład podczas pisania Shellcode w celu wykorzystania luki w zabezpieczeniach polegającej na przepełnieniu bufora lub sformatowaniu łańcucha .

Załóżmy, że masz względny skok do 100 bajtów do przodu i dokonaj pewnych modyfikacji kodu. Możliwe, że twoje modyfikacje zepsują adres celu skoku i jako taki będziesz musiał również zmienić wyżej wspomniany skok względny. Tutaj możesz dodać NOPs, aby przesunąć adres docelowy do przodu. Jeśli masz wiele NOPs między adresem docelowym a instrukcją skoku, możesz usunąć NOPs, aby wyciągnąć adres docelowy do tyłu.

Nie stanowiłoby to problemu, jeśli pracujesz z asemblerem obsługującym etykiety. Możesz po prostu zrobić JXX someLabel(gdzie JXX to jakiś skok warunkowy), a asembler zastąpi someLabeladres tej etykiety. Jeśli jednak po prostu ręcznie zmodyfikujesz zmontowany kod maszynowy (rzeczywiste kody operacyjne) (jak to czasami bywa przy pisaniu kodu powłoki), musisz również ręcznie zmienić instrukcję skoku. Możesz go zmodyfikować lub przenieść docelowy adres kodu za pomocą NOPs.

Innym przypadkiem użycia NOPinstrukcji może być coś, co nazywa się zaprzęgiem NOP . W istocie chodzi o stworzenie wystarczająco dużej tablicy instrukcji, które nie powodują żadnych skutków ubocznych (takich jakNOPlub zwiększanie, a następnie zmniejszanie rejestru), ale zwiększ wskaźnik instrukcji. Jest to przydatne na przykład, gdy chcemy przejść do określonego fragmentu kodu, którego adres nie jest znany. Sztuczka polega na tym, aby umieścić wspomniane sanki NOP przed kodem docelowym, a następnie wskoczyć gdzieś na te sanki. Mamy nadzieję, że wykonanie będzie kontynuowane z tablicy, która nie wywołuje żadnych skutków ubocznych, i przesuwa dalej instrukcje według instrukcji, aż trafi na pożądany fragment kodu. Technikę tę stosuje się powszechnie we wspomnianych exploitach przepełnienia bufora, a zwłaszcza w celu przeciwdziałania środkom bezpieczeństwa, takim jak ASLR .

Jeszcze innym szczególnym zastosowaniem NOPinstrukcji jest modyfikowanie kodu jakiegoś programu. Na przykład można zastąpić części skoków warunkowych na NOPs i jako takie ominąć warunek. Jest to często stosowana metoda podczas „ łamania ” ochrony oprogramowania przed kopiowaniem. Najprościej chodzi tylko o usunięcie konstrukcji kodu asemblera dla if(genuineCopy) ...wiersza kodu i zastąpienie instrukcji NOPs i .. Voilà! Nie dokonuje się żadnych kontroli i działa nieoryginalna kopia!

Zauważ, że w zasadzie oba przykłady kodu powłoki i crackowania robią to samo; modyfikować istniejący kod bez aktualizacji względnych adresów operacji opartych na względnym adresowaniu.

zxcdw
źródło
2
To była wspaniała odpowiedź, dziękuję za poświęcenie czasu na wyjaśnienie tego! Wreszcie rozumiem!
alvonellos
Pewne systemy czasu rzeczywistego (PLC przychodzą na myśl) pozwalają „wstawić” nową logikę do istniejącego programu podczas jego działania. Te systemy pozostawiają NOP przed każdym małym elementem logiki, więc możesz zastąpić NOP skokiem do nowej logiki, którą wstawiasz. Pod koniec nowej logiki przeskoczy na koniec oryginalnej logiki, którą zastępujesz. Nowa logika będzie również miała NOP z przodu, więc możesz ją również zastąpić.
Scott Whitlock,
10

W szczelinie opóźniającej może być użyty nop, gdy nie można zmienić kolejności innych instrukcji w celu umieszczenia w nim.

lw   v0,4(v1)
jr   v0

W MIPS byłby to błąd, ponieważ w czasie, gdy jr czytał rejestr v0, rejestr v0 nie został jeszcze załadowany wartością z poprzedniej instrukcji.

Sposobem na rozwiązanie tego byłoby:

lw   v0,4(v1)
nop
jr   v0
nop

Wypełnia to zajmowane miejsca po słowie ładowania i instrukcjach rejestru skoku znakiem nop, dzięki czemu instrukcja słowa ładowania jest zakończona przed wykonaniem polecenia rejestru skoku.

Dalsza lektura - trochę o wypełnianiu SPARC szczelin opóźniających . Z tego dokumentu:

Co można umieścić w gnieździe opóźnienia?

  • Kilka przydatnych instrukcji, które należy wykonać niezależnie od tego, czy rozgałęziasz się, czy nie.
  • Niektóre instrukcje, które przydają się, działają tylko wtedy, gdy rozgałęziasz się (lub gdy nie rozgałęziasz się), ale nie wyrządzają żadnej szkody, jeśli zostaną wykonane w innym przypadku.
  • Gdy wszystko inne zawiedzie, instrukcja NOP

Czego NIE MOŻNA umieścić w gnieździe opóźnienia?

  • Wszystko, co określa CC, od którego zależy decyzja oddziału. Instrukcja rozgałęzienia natychmiast podejmuje decyzję, czy rozgałęzić, czy nie, ale tak naprawdę nie wykonuje rozgałęzienia, dopóki nie pojawi się instrukcja opóźnienia. (Tylko oddział jest opóźniony, a nie decyzja.)
  • Kolejna instrukcja oddziału. (Co się stanie, jeśli to zrobisz, nie zostanie nawet zdefiniowane! Wynik jest nieprzewidywalny!)
  • Instrukcja „ustawiania”. To tak naprawdę dwie instrukcje, a nie jedna, a tylko połowa będzie w polu opóźnienia. (Asembler ostrzeże cię przed tym.)

Zwróć uwagę na trzecią opcję w tym, co umieścić w gnieździe opóźnienia. Błąd, który widziałeś, prawdopodobnie spowodował, że ktoś wypełnił jedną z rzeczy, których nie wolno umieszczać w szczelinie opóźnienia. Umieszczenie nop w tym miejscu naprawiłoby błąd.

Uwaga: po ponownym przeczytaniu pytania, dotyczyło to x86, która nie ma slotów opóźniających (rozgałęzienie zamiast tego zatrzymuje tylko potok). To nie byłaby przyczyna / rozwiązanie błędu. W systemach RISC może to być odpowiedź.


źródło
4
Zauważ, że pytanie jest oznaczone jako x86, a x86 nie ma przedziałów opóźnienia. Nigdy też nie będzie, ponieważ jest to przełomowa zmiana.
MSalters
6

co najmniej jednym powodem użycia NOP jest wyrównanie. Procesory x86 odczytują dane z pamięci głównej w dość dużych blokach, a początek bloku do odczytu jest zawsze wyrównany, więc jeśli ktoś ma blok kodu, który będzie czytany dużo, ten blok powinien zostać wyrównany. Spowoduje to niewielkie przyspieszenie.

permeakra
źródło
Blok nie musi być wyrównywany, nie trzeba pobierać ostatnich kilku bajtów poprzedniego bloku. Więc dobrze jest skoczyć do 0x1002, ponieważ nadal jest 14 bajtów instrukcji w wyrównanym bloku 16B, który zawiera adres docelowy, ale nie jest dobrze, aby przejść do 0x099D.
Peter Cordes,
3

Jednym z celów NOP (w ogólnym zgromadzeniu, nie tylko x86) jest wprowadzenie opóźnień czasowych. Na przykład chcesz zaprogramować mikrokontroler, który musi wysyłać sygnały do ​​niektórych diod LED z opóźnieniem 1 s. To opóźnienie można zrealizować za pomocą NOP (i gałęzi). Oczywiście możesz użyć ADD lub czegoś innego, ale to uczyniłoby kod bardziej nieczytelnym; a może potrzebujesz wszystkich rejestrów.

m3th0dman
źródło
1
Zwykle dla długich ram czasowych, takich jak 1 sekunda, używane są timery. NOPS są używane w epokach rzędu rzędu zegara - nano i mikro sekund.
mattnz
Ma to sens tylko w przypadku mikrokontrolera, a nie nowoczesnego x86. Większość kodu x86 nie nasyca szerokości potoku nowoczesnych superskalarnych niedziałających procesorów, więc dodanie NOP między każdą instrukcją w większości kodów miałoby tylko niewielki wpływ (przypuszczam, że liczba dla „przeciętnego” kodu może być 5 do 20% na podwojenie liczby instrukcji, z pewnym kodem wykazujące żadnego spowolnienia, ale kilka ciasne pętle pokazujące prawie jedną 2x spowolnienie.) w każdym razie, chrupiący stary kod x86 tradycyjnie używane w loopinstrukcji dla pętli opóźniających , a nie NoPS.
Peter Cordes,
3

Ogólnie na 80x86 instrukcje NOP nie są wymagane do poprawności programu, chociaż czasami na niektórych komputerach strategicznie rozmieszczone NOP mogą powodować szybsze działanie kodu. Na przykład w 8086 kod byłby pobierany w dwubajtowych porcjach, a procesor miał wewnętrzny bufor „wstępnego pobierania”, który mógł pomieścić trzy takie porcje. Niektóre instrukcje wykonałyby się szybciej, niż mogłyby zostać pobrane, podczas gdy inne instrukcje zajęłyby trochę czasu. Podczas powolnych instrukcji procesor próbowałby zapełnić bufor pobierania wstępnego, aby jeśli kilka kolejnych instrukcji było szybkich, można je szybko wykonać. Jeśli instrukcja następująca po instrukcji powolnej rozpoczyna się na granicy parzystego słowa, instrukcje o wartości następnych sześciu bajtów zostaną pobrane wstępnie; jeśli zaczyna się na granicy nieparzystych bajtów, pobieranych jest tylko pięć bajtów.

Takie problemy z wyrównaniem pamięci mogą mieć wpływ na szybkość programu, ale ogólnie nie wpływają na poprawność. Z drugiej strony istnieją pewne problemy związane ze wstępnym pobieraniem na starszych procesorach, w których NOP może wpłynąć na poprawność. Jeśli instrukcja zmieni bajt kodu, który został już pobrany z wyprzedzeniem, 8086 (i myślę, że 80286 i 80386) wykona instrukcję z wyprzedzeniem, mimo że nie pasuje już do tego, co jest w pamięci. Dodanie NOP lub dwóch między instrukcją zmieniającą pamięć a zmienionym bajtem kodu może uniemożliwić pobranie bajtu kodu, dopóki nie zostanie zapisany. Należy przy okazji zauważyć, że wiele schematów ochrony przed kopiowaniem wykorzystuje takie zachowanie; należy jednak pamiętać, że takie zachowanie nie jest gwarantowane. Różne odmiany procesorów mogą inaczej obsługiwać pobieranie wstępne, niektóre mogą unieważnić wstępnie pobrane bajty, jeśli pamięć, z której zostały odczytane, jest zmodyfikowana, a przerwania zwykle unieważniają bufor pobierania wstępnego; kod zostanie ponownie pobrany, gdy przerwania powrócą.

supercat
źródło
3

Istnieje konkretny przypadek x86, który nie został jeszcze opisany w innych odpowiedziach: obsługa przerwań. W przypadku niektórych stylów mogą istnieć sekcje kodu, gdy przerwania są wyłączone, ponieważ kod główny działa z niektórymi danymi współdzielonymi z programami obsługi przerwań, ale uzasadnione jest zezwolenie na przerwania między takimi sekcjami. Jeśli ktoś naiwnie pisze


    STI
    CLI

nie przetworzy to oczekujących przerwań, ponieważ powołując się na Intel:

Po ustawieniu flagi IF procesor zaczyna reagować na zewnętrzne, dające się maskować przerwania po wykonaniu następnej instrukcji.

należy to przepisać co najmniej jako:


    STI
    NOP
    CLI

W drugim wariancie wszystkie oczekujące przerwania będą przetwarzane tylko między NOP a CLI. (Oczywiście istnieje wiele alternatywnych wariantów, takich jak podwojenie instrukcji STI. Ale wyraźne NOP jest bardziej oczywiste, przynajmniej dla mnie.)

Netch
źródło
-2

NOP oznacza brak operacji

Zwykle służy do wstawiania lub usuwania kodu maszynowego lub do opóźniania wykonania określonego kodu.

Używany również przez crackerów i debugerów do ustawiania punktów przerwania.

Prawdopodobnie więc zrobienie czegoś takiego: XCHG BX, BX również da to samo.

Brzmi dla mnie tak, jakby było kilka operacji, które wciąż były w toku i dlatego spowodowało błąd.

Jeśli znasz VB, mogę podać przykład:

Jeśli utworzysz system logowania w VB i załadujesz 3 strony razem - Facebook, YouTube i Twitter w 3 różnych zakładkach.

I użyj 1 przycisku logowania dla wszystkich. Może to powodować błąd, jeśli twoje połączenie internetowe jest wolne. Co oznacza, że ​​jedna ze stron nie została jeszcze załadowana. Dlatego zastosowaliśmy Application.DoEvents, aby temu zaradzić. W zespole można zastosować ten sam sposób.

Całkowite zanurzenie w anime
źródło