Jaka jest funkcja instrukcji push / pop używanych w rejestrach w asemblerze x86?


105

Czytając o assemblerze często spotykam ludzi, którzy piszą, że wciskają pewien rejestr procesora, a później ponownie go wyskakują , aby przywrócić jego poprzedni stan.

  • Jak możesz pushować rejestr? Gdzie jest popychany? Dlaczego jest to potrzebne?
  • Czy sprowadza się to do pojedynczej instrukcji procesora, czy jest to bardziej złożone?

4
Ostrzeżenie: wszystkie aktualne odpowiedzi są podane w składni zespołu Intel; push-pop w AT & T składni przykład używa post-fix jak b, w, l, lub qdo określenia rozmiaru pamięci manipulowany. Np .: pushl %eaxipopl %eax
Hawken,

5
@hawken W większości asemblerów zdolnych do połknięcia składni AT&T (szczególnie gas), przyrostek rozmiaru można pominąć, jeśli rozmiar operandu można wywnioskować z rozmiaru operandu. Tak jest w przypadku podanych przykładów, ponieważ %eaxzawsze ma rozmiar 32-bitowy.
Gunther Piez

Odpowiedzi:


155

przesunięcie wartości (niekoniecznie przechowywanej w rejestrze) oznacza zapisanie jej na stosie.

popping oznacza przywrócenie do rejestru wszystkiego, co znajduje się na szczycie stosu . To są podstawowe instrukcje:

push 0xdeadbeef      ; push a value to the stack
pop eax              ; eax is now 0xdeadbeef

; swap contents of registers
push eax
mov eax, ebx
pop ebx

5
Jawnym operandem dla push i pop jest r/mnie tylko register, więc możesz push dword [esi]. Lub nawet pop dword [esp]załadować, a następnie zapisać tę samą wartość z powrotem pod tym samym adresem. ( github.com/HJLebbink/asm-dude/wiki/POP ). Wspominam o tym tylko dlatego, że mówisz „niekoniecznie rejestr”.
Peter Cordes

2
Możesz także popwejść w obszar pamięci:pop [0xdeadbeef]
SS Anne

Cześć, jaka jest różnica między push / pop a pushq / popq? Jestem na macos / intel
SteakOverflow

47

Oto jak wypychasz rejestr. Zakładam, że mówimy o x86.

push ebx
push eax

Jest odkładany na stos. Wartość ESPrejestru jest zmniejszana do rozmiaru przekazanej wartości, gdy stos rośnie w dół w systemach x86.

Konieczne jest zachowanie wartości. Ogólne użycie to

push eax           ;   preserve the value of eax
call some_method   ;   some method is called which will put return value in eax
mov  edx, eax      ;    move the return value to edx
pop  eax           ;    restore original eax

A pushto pojedyncza instrukcja w x86, która wewnętrznie wykonuje dwie rzeczy.

  1. Zmniejsz ESPrejestr o rozmiar przekazanej wartości.
  2. Przechowuj wypchniętą wartość pod aktualnym adresem ESPrejestru.

40

Gdzie jest popychany?

esp - 4. Dokładniej:

  • esp jest odejmowany przez 4
  • wartość jest przenoszona do esp

pop odwraca to.

ABI Systemu V mówi Linuksowi, aby rspwskazał rozsądną lokalizację stosu, gdy program zacznie działać: Jaki jest domyślny stan rejestru podczas uruchamiania programu (asm, linux)? który jest tym, czego zwykle powinieneś używać.

Jak możesz pushować rejestr?

Przykład minimalnego GNU GAS:

.data
    /* .long takes 4 bytes each. */
    val1:
        /* Store bytes 0x 01 00 00 00 here. */
        .long 1
    val2:
        /* 0x 02 00 00 00 */
        .long 2
.text
    /* Make esp point to the address of val2.
     * Unusual, but totally possible. */
    mov $val2, %esp

    /* eax = 3 */
    mov $3, %ea 

    push %eax
    /*
    Outcome:
    - esp == val1
    - val1 == 3
    esp was changed to point to val1,
    and then val1 was modified.
    */

    pop %ebx
    /*
    Outcome:
    - esp == &val2
    - ebx == 3
    Inverses push: ebx gets the value of val1 (first)
    and then esp is increased back to point to val2.
    */

Powyższe na GitHubie z działającymi asercjami .

Dlaczego jest to potrzebne?

Prawdą jest, że instrukcje te mogą być łatwo realizowane za pośrednictwem mov, addi sub.

Powodem ich istnienia jest to, że te kombinacje instrukcji są tak częste, że Intel zdecydował się je nam dostarczyć.

Powodem, dla którego te kombinacje są tak częste, jest to, że ułatwiają tymczasowe zapisywanie i przywracanie wartości rejestrów do pamięci, aby nie zostały nadpisane.

Aby zrozumieć problem, spróbuj ręcznie skompilować kod C.

Główną trudnością jest podjęcie decyzji, gdzie będzie przechowywana każda zmienna.

Idealnie, wszystkie zmienne pasowałyby do rejestrów, które są najszybszymi dostępnymi pamięcią (obecnie około 100x szybciej niż pamięć RAM).

Ale oczywiście możemy z łatwością mieć więcej zmiennych niż rejestrów, szczególnie dla argumentów funkcji zagnieżdżonych, więc jedynym rozwiązaniem jest zapisywanie w pamięci.

Moglibyśmy pisać na dowolny adres pamięci, ale ponieważ zmienne lokalne i argumenty wywołań funkcji i zwrotów pasują do ładnego wzorca stosu, który zapobiega fragmentacji pamięci , jest to najlepszy sposób radzenia sobie z tym. Porównaj to z szaleństwem pisania alokatora sterty.

Następnie pozwalamy kompilatorom zoptymalizować alokację rejestrów za nas, ponieważ jest to NP zakończone i jedna z najtrudniejszych części pisania kompilatora. Ten problem nazywa się alokacją rejestrów i jest izomorficzny do kolorowania grafów .

Kiedy alokator kompilatora jest zmuszony do przechowywania rzeczy w pamięci, a nie tylko w rejestrach, jest to znane jako wyciek .

Czy sprowadza się to do pojedynczej instrukcji procesora, czy jest to bardziej złożone?

Jedyne, co wiemy na pewno, to to, że Intel dokumentuje pushi popinstrukcję, więc są one jedną instrukcją w tym sensie.

Wewnętrznie można go rozszerzyć na wiele mikrokodów, jeden do modyfikacji, espa drugi do wykonywania operacji we / wy pamięci i wykonywania wielu cykli.

Ale możliwe jest również, że pojedyncza pushinstrukcja jest szybsza niż równoważna kombinacja innych instrukcji, ponieważ jest bardziej szczegółowa.

Jest to głównie nieudokumentowane:


4
Nie musisz zgadywać, jak push/ popdekodować na Uops. Dzięki licznikom wydajności możliwe są testy eksperymentalne, a firma Agner Fog to zrobiła i opublikowała tabele instrukcji . Procesory Pentium-M i nowsze mają tryb single-uop push/ popdzięki silnikowi stosu (patrz plik pdf mikroarch firmy Agner). Obejmuje to najnowsze procesory AMD, dzięki umowie o współużytkowaniu patentów Intel / AMD.
Peter Cordes

@PeterCordes awesome! Czyli liczniki wydajności są dokumentowane przez firmę Intel w celu zliczania mikrooperacji?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Ponadto zmienne lokalne przenoszone z regów będą zazwyczaj nadal aktywne w pamięci podręcznej L1, jeśli którykolwiek z nich jest faktycznie używany. Ale odczyt z rejestru jest w rzeczywistości bezpłatny, zero latencji. Jest więc nieskończenie szybszy niż pamięć podręczna L1, w zależności od tego, jak chcesz definiować terminy. W przypadku lokalizacji lokalnych tylko do odczytu, które zostały przeniesione na stos, głównym kosztem są po prostu dodatkowe obciążenia podczas ładowania (czasami operandy pamięci, czasami z oddzielnymi movładowaniami). W przypadku rozlanych zmiennych niebędących stałymi, cykliczne przekazywanie do sklepu wiąże się z dużym opóźnieniem (dodatkowe ~ 5c w porównaniu z przekazywaniem bezpośrednio, a instrukcje sklepu nie są tanie).
Peter Cordes

Tak, istnieją liczniki całkowitych błędów na kilku różnych etapach potoku (wydanie / wykonanie / wycofanie), więc można policzyć domenę połączoną lub domenę nieużywaną. Zobacz na przykład tę odpowiedź . Gdybym teraz przepisał tę odpowiedź, ocperf.pyużyłbym skryptu opakowującego, aby uzyskać łatwe symboliczne nazwy dla liczników.
Peter Cordes

26

Wypychanie i otwieranie rejestrów jest odpowiednikiem tego:

push reg   <= same as =>      sub  $8,%rsp        # subtract 8 from rsp
                              mov  reg,(%rsp)     # store, using rsp as the address

pop  reg    <= same as=>      mov  (%rsp),reg     # load, using rsp as the address
                              add  $8,%rsp        # add 8 to the rsp

Zauważ, że jest to składnia x86-64 At & t.

Używany jako para, pozwala to zapisać rejestr na stosie i przywrócić go później. Są też inne zastosowania.


5
Tak, te sekwencje poprawnie emulują push / pop. (z wyjątkiem push / pop nie wpływa na flagi).
Peter Cordes

2
Lepiej użyj lea rsp, [rsp±8]zamiast add/, subaby lepiej emulować efekt push/ popna flagach.
Ruslan

13

Prawie wszystkie procesory używają stosu. Stos programu jest techniką LIFO z zarządzaniem obsługiwanym sprzętowo.

Stos to ilość pamięci programu (RAM) normalnie alokowana na szczycie stosu pamięci procesora i rośnie (w instrukcji PUSH wskaźnik stosu jest zmniejszany) w przeciwnym kierunku. Standardowym terminem wstawiania do stosu jest PUSH, a usuwania ze stosu - POP .

Stos jest zarządzany przez odpowiedni rejestr procesora stosu, zwany także wskaźnikiem stosu, więc gdy procesor wykonuje POP lub PUSH, wskaźnik stosu załaduje / zapisze rejestr lub stałą w pamięci stosu, a wskaźnik stosu zostanie automatycznie zmniejszony x lub zwiększony zgodnie z liczbą wprowadzonych słów lub wskoczył do (z) stosu.

Za pomocą instrukcji asemblera możemy przechowywać w stosie:

  1. Rejestry procesora, a także stałe.
  2. Adresy zwrotne funkcji lub procedur
  3. Funkcje / procedury zmienne wejścia / wyjścia
  4. Funkcje / procedury zmienne lokalne.
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.