Czy instrukcje x86 wymagają własnego kodowania, a także wszystkich argumentów znajdujących się w pamięci w tym samym czasie?

64

Próbuję dowiedzieć się, czy można uruchomić maszynę wirtualną z systemem Linux, której pamięć RAM jest obsługiwana tylko przez jedną fizyczną stronę.

Aby to zasymulować, zmodyfikowałem moduł obsługi błędów zagnieżdżonej strony w KVM, aby usunąć obecny bit ze wszystkich pozycji zagnieżdżonej tablicy stron (NPT), oprócz tego odpowiadającego aktualnie przetwarzanej usterce strony.

Podczas próby uruchomienia gościa z systemem Linux zaobserwowałem instrukcje montażu, które używają operandów pamięci, takich jak

add [rbp+0x820DDA], ebp

prowadzić do pętli błędów strony, dopóki nie przywrócę obecnego bitu dla strony zawierającej instrukcję, a także dla strony, do której odwołuje się operand (w tym przykładzie [rbp+0x820DDA]).

Zastanawiam się, dlaczego tak jest. Czy CPU nie powinien uzyskiwać dostępu do stron pamięci sekwencyjnie, tzn. Najpierw przeczytać instrukcję, a następnie uzyskać dostęp do operandu pamięci? Czy też x86 wymaga, aby strona instrukcji, jak również wszystkie strony argumentów były dostępne w tym samym czasie?

Testuję na AMD Zen 1.

savvybug
źródło
2
Dlaczego chcesz to zrobić?
SS Anne
11
Po prostu z technicznego zainteresowania :)
savvybug
14
Pozytywne nastawienie do zabawnego pomysłu na projekt.
rura
10
Jest to szalone na poziomie „bootowania Linuksa na emulatorze 486 działającym w JavaScript w przeglądarce”. Kocham to.
Chrylis
3
Hej, najwyraźniej podniosłem to pytanie do tego samego logicznego wniosku, który już myślałeś, o minimalnym zestawie roboczym dla gwarantowanego postępu. Odpowiedziałem już na to, zanim dodałeś ten nowy akapit do pytania. : PI dodało kilka linków i więcej szczegółów w kilku miejscach (np. Walker strony może wewnętrznie buforować niektóre wpisy katalogu strony gościa), ponieważ to pytanie zyskuje o wiele więcej uwagi, niż się spodziewałem, dzięki jakimś sposobem dotarcia do HNQ.
Peter Cordes

Odpowiedzi:

56

Tak, wymagają kodu maszynowego i wszystkich operandów pamięci.

Czy CPU nie powinien uzyskiwać dostępu do stron pamięci sekwencyjnie, tzn. Najpierw przeczytać instrukcję, a następnie uzyskać dostęp do operandu pamięci?

Tak, logicznie, co się dzieje, ale wyjątek błędu strony przerywa ten 2-etapowy proces i odrzuca wszelkie postępy. Procesor nie ma sposobu na zapamiętanie instrukcji, która była w środku, kiedy wystąpił błąd strony.

Gdy moduł obsługi błędów strony powróci po przetworzeniu prawidłowego błędu strony, RIP = adres instrukcji powodującej błąd, więc procesor próbuje wykonać ją od nowa .

Dopuszczalne byłoby, aby system operacyjny zmodyfikował kod maszynowy instrukcji powodującej błąd i oczekiwał, że wykona inną instrukcję później iretniż program obsługi błędów stronicowania (lub innego wyjątku lub procedury obsługi przerwań). AFAIK wymaga architektonicznie, aby CPU powtórzył pobieranie kodu z CS: RIP w przypadku, o którym mówisz. (Zakładając, że nawet wraca do błędu CS: RIP zamiast planować inny proces podczas oczekiwania na dysk z uszkodzeniem strony twardej lub dostarczanie SIGSEGV do procedury obsługi sygnału z powodu błędu nieprawidłowej strony).

Prawdopodobnie jest to również wymagane architektonicznie do wejścia / wyjścia hiperwizora. I nawet jeśli nie jest to wyraźnie zabronione na papierze, nie tak działają procesory.

@torek komentuje, że niektóre mikroprocesory (CISC) częściowo dekodują instrukcje i zrzucają stan mikroprocesora na błąd strony , ale x86 nie jest taki.


Kilka instrukcji jest przerywalnych i może dokonywać częściowych postępów, takich jak rep movs(memcpy w puszce) i innych instrukcji łańcuchowych, lub gromadzić ładunki / sklepy rozproszone. Ale jedynym mechanizmem jest aktualizacja rejestrów architektonicznych, takich jak RCX / RSI / RDI dla operacji łańcuchowych, lub rejestrów docelowych i masek dla gromadzeń (np. Instrukcja dla AVX2vpgatherdd ). Nieprzestrzeganie kodu operacji / dekodowania powoduje ukryty rejestr wewnętrzny i ponowne uruchomienie go po przejściu do procedury obsługi błędów strony. Są to instrukcje, które wykonują wiele różnych dostępu do danych.

Należy również pamiętać, że x86 (jak większość ISA) gwarantuje, że instrukcje są niepodzielne na wrt. przerwania / wyjątki: albo w pełni się zdarzają, albo w ogóle nie zdarzają się przed przerwaniem. Przerwanie instrukcji montażu podczas jej działania . Tak więc na przykład add [mem], regkonieczne byłoby odrzucenie ładunku, jeśli część sklepu uległa awarii, nawet bez lockprefiksu.


Najgorszy przypadek liczby stron w przestrzeni użytkownika gościa, które mogą wykonać postęp, może wynosić 6 (plus osobne poddrzewa tabeli stron jądra gościa dla każdego z nich):

  • movsqlub movswinstrukcja 2-bajtowa obejmująca granicę strony, więc obie strony są potrzebne do jej odkodowania.
  • operand źródłowy qword [rsi]również podział strony
  • qword operand docelowy [rdi]również podział strony

Jeśli którakolwiek z tych 6 stron ulegnie awarii, wrócimy do pierwszej.

rep movsdjest także instrukcją 2-bajtową, a zrobienie postępu na jednym jej etapie wymagałoby takich samych wymagań. Podobne przypadki, takie jak push [mem]lub pop [mem]mogą być skonstruowane z niewłaściwie wyrównanym stosem.

Jednym z powodów (lub korzyści dodatkowych) dla / spowodowania, aby gromadzenie ładunków / rozproszenie sklepów było „przerywalne” (aktualizowanie wektora maski wraz z ich postępem), jest unikanie zwiększania tego minimalnego poziomu zajmowanego miejsca w celu wykonania pojedynczej instrukcji. Również w celu poprawy wydajności obsługi wielu błędów podczas jednego zbierania lub rozpraszania.


@Brandon wskazuje w komentarzach, że gość będzie potrzebował swoich tabel stron w pamięci , a podziały stron w przestrzeni użytkownika mogą być również podziałami 1GiB, więc obie strony znajdują się w różnych poddrzewach PML4 najwyższego poziomu. Aby przejść do następnego kroku, przejdź do strony sprzętowej. Sytuacja, w której ta patologia nie wystąpi przypadkowo.

TLB (i elementy wewnętrzne walkera) mogą buforować niektóre dane tabeli stron i nie są wymagane do ponownego uruchomienia przejścia strony od zera, chyba że system operacyjny to zrobił invlpglub nie ustawił nowego katalogu stron najwyższego poziomu CR3. Żadne z nich nie jest konieczne przy zmianie strony z nieobecnej na prezentowaną; x86 na papierze gwarantuje, że nie jest potrzebne (więc „negatywne buforowanie” nieobecnych PTE jest niedozwolone, przynajmniej niewidoczne dla oprogramowania). Dlatego procesor może nie VMexit, nawet jeśli niektóre strony fizycznej strony tabeli gościa nie są faktycznie obecne.

Liczniki wydajności PMU można włączyć i skonfigurować tak, że instrukcja wymaga również zdarzenia perf do zapisu w buforze PEBS dla tej instrukcji. Gdy maska ​​licznika jest skonfigurowana do liczenia tylko instrukcji przestrzeni użytkownika, a nie jądra, może być tak, że próbuje przepełnić licznik i przechowywać próbkę w buforze za każdym razem, gdy wracasz do przestrzeni użytkownika, powodując błąd strony.

Peter Cordes
źródło
15
Najgorszym przypadkiem pojedynczej instrukcji może być coś w rodzaju „ push dword [foo” (lub nawet po prostu call [foo]), gdy wszystko jest źle wyrównane w „granicy tabeli wskaźników stronicowania” (dodając do 6 stron, 6 tabel stron, 6 katalogów stron, 6 PDPT i jednego PML4); z włączoną i skonfigurowaną funkcją „precyzyjnego próbkowania opartego na zdarzeniach z buforem PEBS”, która pushpowoduje dodanie danych monitorowania wydajności do bufora PEBS. Dla konserwatywnych „minimalnych stron dostarczonych przez gospodarza, aby gość mógł robić postępy w przypadkach patologicznych” chciałbym mieć co najmniej 16 stron.
Brendan
4
Zauważ, że tego rodzaju rzeczy zawsze były powszechne w architekturach CISC-y. Niektóre mikroprocesory częściowo dekodują instrukcje i zrzucają stan mikroprocesora na błąd strony, ale inne nie wymagają i / lub wymagają, aby argumenty adresowe dla instrukcji „loop-y” (DBRA na m68k, MOVC3 / MOVC5 na Vax itp.) Znajdowały się w rejestrach podobnych do twojego przykładu REP MOVS.
torek
1
@Brendan: ktoś policzył najgorszy przypadek instrukcji VAX jako około 50 stron. Zapominam o szczegółach, ale oczywiście umieściłbyś samą instrukcję na granicy strony, użyj czegoś takiego jak wyszukiwanie w tabeli translacji z tabelą obejmującą granicę strony, użyj (rX) [rY] z pośrednimi na granicach strony i wkrótce. Najbardziej włochate instrukcje zajmowały do ​​6 operandów (ładowanie ich do r0-r5) i wszystkie sześć może być podwójnymi pośrednimi, jak sądzę.
torek
3
System operacyjny może zmienić instrukcję, ale może również zmienić EIP. Jest więc logiczne pytanie uzupełniające. Jaka jest minimalna potrzebna liczba stron przy założeniu inteligentnego schematu łatek instrukcji? Np. Skopiuj niezrównaną wartość do wyrównanego bufora scratch, emuluj instrukcję i IRET do następnej instrukcji.
MSalters
1
Strona zawierająca instrukcję systemu operacyjnego iretrównież musi znajdować się w pamięci. Jest to instrukcja jednobajtowa, więc jedna dodatkowa strona. Adres przerwania procedury obsługi błędów strony również musi znajdować się w pamięci, ale może to być ta sama strona, co powyżej.
Stig Hemmer