język asemblera x86 - x86 assembly language


asembler x86 to rodzina wstecznie kompatybilnych języków montażowych , które zapewniają pewien poziom zgodności z powrotem całą drogę do Intel 8008 wprowadzono w kwietniu 1972. x86 języki montażowe są wykorzystywane do produkcji kodu wynikowego dla x86 klasy procesorów. Jak wszystkie języki asemblerowe, używa krótkich mnemoników do reprezentowania podstawowych instrukcji, które procesor w komputerze może zrozumieć i wykonać. Kompilatory czasami tworzą kod asemblera jako etap pośredni podczas tłumaczenia programu wysokiego poziomu na kod maszynowy . Uważany za język programowania , kodowanie w asemblerze jest specyficzne dla maszyny i na niskim poziomie . Języki asemblera są częściej używane do szczegółowych i krytycznych czasowo aplikacji, takich jak małe systemy wbudowane czasu rzeczywistego lub jądra systemu operacyjnego i sterowniki urządzeń .

Mnemoniki i kody operacyjne

Każda instrukcja asemblera x86 jest reprezentowana przez mnemonik, który często w połączeniu z jednym lub większą liczbą operandów tłumaczy się na jeden lub więcej bajtów nazywanych opcode ; NOP instrukcja przekłada się 0x90, na przykład, a HLT instrukcja przekłada się 0xF4. Istnieją potencjalne kody operacji bez udokumentowanych mnemoników, które różne procesory mogą interpretować w różny sposób, przez co program korzystający z nich zachowuje się niespójnie lub nawet generuje wyjątek na niektórych procesorach. Te kody operacyjne często pojawiają się w konkursach pisania kodu jako sposób na zmniejszenie kodu, szybsze, bardziej eleganckie lub po prostu pokazanie umiejętności autora.

Składnia

asembler x86 ma dwa główne składniowe oddziały: Intel składni i AT & T składni . Składnia Intela dominuje w świecie DOS i Windows , a składnia AT&T dominuje w świecie Unix , ponieważ Unix został stworzony w AT&T Bell Labs . Oto podsumowanie głównych różnic między składnią Intela a składnią AT&T :

AT&T Intel
Kolejność parametrów Źródło przed miejscem docelowym.
movl $5, %eax
Miejsce docelowe przed źródłem.
mov eax, 5
Rozmiar parametru Mnemoniki są poprzedzone literą wskazującą rozmiar argumentów: q dla qword, l dla long (dword), w dla słowa i b dla bajtu.
addl $4, %esp
Pochodzi od nazwy rejestru, który jest używany (np. rax, eax, ax, al oznaczają odpowiednio q, l, w, b ).
add esp, 4
pieczęć Wartości bezpośrednie poprzedzone „$”, rejestry poprzedzone „%”. Asembler automatycznie wykrywa rodzaj symboli; tzn. czy są to rejestry, stałe czy coś innego.
Skuteczne adresy Ogólna składnia DISP(BASE,INDEX,SCALE) . Przykład:
movl mem_location(%ebx,%ecx,4), %eax
Wyrażenia arytmetyczne w nawiasach kwadratowych; dodatkowo, słowa kluczowe rozmiaru, takie jak byte , słowo lub dword muszą być użyte, jeśli rozmiar nie może być określony z operandów. Przykład:
mov eax, [ebx + ecx*4 + mem_location]

Wiele asemblerów x86 używa składni Intela , w tym NASM , FASM , MASM , TASM i YASM . GAS , który pierwotnie używał składni AT&T , obsługuje obie składnie od wersji 2.10 poprzez dyrektywę .intel_syntax . Dziwactwo w składni AT&T dla x86 polega na tym, że operandy x87 są odwrócone, co jest błędem odziedziczonym po oryginalnym asemblerze AT&T.

Składnia AT&T jest prawie uniwersalna dla wszystkich innych architektur o tej samej movkolejności; pierwotnie była to składnia dla asemblera PDP-11. Składnia Intela jest specyficzna dla architektury x86 i jest używana w dokumentacji platformy x86.

Rejestry

Procesory x86 mają zbiór rejestrów dostępnych do wykorzystania jako magazyny danych binarnych. Łącznie rejestry danych i adresów nazywane są rejestrami ogólnymi. Każdy rejestr ma specjalny cel oprócz tego, co każdy z nich może zrobić:

  • Mnożenie/dzielenie AX, ładowanie i przechowywanie sznurka
  • Rejestr indeksów BX dla MOVE
  • Liczba CX dla operacji strunowych i zmian
  • Adres portu DX dla IN i OUT
  • SP wskazuje na szczyt stosu
  • BP wskazuje na podstawę ramy stosu
  • SI wskazuje źródło w operacjach strumieniowych
  • DI wskazuje miejsce docelowe w operacjach strumieniowych

Wraz z rejestrami ogólnymi znajdują się dodatkowo:

  • Wskaźnik instrukcji IP
  • FLAGI
  • rejestry segmentowe (CS, DS, ES, FS, GS, SS), które określają, gdzie zaczyna się segment 64k (brak FS i GS w 80286 i wcześniejszych)
  • rejestry dodatkowy przedłużacz ( MMX , 3DNow! , SSE , itd.) (Pentium i później tylko).

Rejestr IP wskazuje na przesunięcie pamięci następnej instrukcji w segmencie kodu (wskazuje na pierwszy bajt instrukcji). Programista nie ma bezpośredniego dostępu do rejestru IP.

Rejestry x86 mogą być używane za pomocą instrukcji MOV . Na przykład w składni Intela:

mov ax, 1234h ; copies the value 1234hex (4660d) into register AX
mov bx, ax    ; copies the value of the AX register into the BX register

Adresowanie segmentowe

Architektury x86 w realnym i wirtualnym trybie 8086 wykorzystuje proces zwany segmentacji do pamięci adresowej, a nie płaski model pamięci stosowanych w wielu innych środowiskach. Segmentacja polega na skomponowaniu adresu pamięci z dwóch części, segmentu i przesunięcia ; segment wskazuje początek 64 KB (64×2 10 ) grupy adresów, a przesunięcie określa, jak daleko od tego adresu początkowego znajduje się żądany adres. W adresowaniu segmentowym do pełnego adresu pamięci wymagane są dwa rejestry. Jeden do utrzymania segmentu, drugi do utrzymania przesunięcia. Aby przetłumaczyć z powrotem na płaski adres, wartość segmentu jest przesuwana o cztery bity w lewo (co odpowiada mnożeniu przez 2 4 lub 16), a następnie dodawana do przesunięcia w celu utworzenia pełnego adresu, co pozwala przełamać barierę 64k poprzez sprytny dobór adresów , choć znacznie komplikuje to programowanie.

W trybie rzeczywistym /tylko chronionym, na przykład, jeśli DS zawiera liczbę szesnastkową 0xDEAD, a DX zawiera liczbę 0xCAFE, będą razem wskazywać adres pamięci 0xDEAD * 0x10 + 0xCAFE = 0xEB5CE. Dlatego procesor może adresować do 1 048 576 bajtów (1 MB) w trybie rzeczywistym. Łącząc wartości segmentu i przesunięcia otrzymujemy 20-bitowy adres.

Oryginalny IBM PC ograniczał programy do 640 KB, ale specyfikacja pamięci rozszerzonej została wykorzystana do zaimplementowania schematu przełączania banków, który wyszedł z użycia, gdy późniejsze systemy operacyjne, takie jak Windows, używały większych zakresów adresów nowszych procesorów i implementowały własną pamięć wirtualną schematy.

Tryb chroniony, począwszy od Intel 80286, był wykorzystywany przez OS/2 . Kilka niedociągnięć, takich jak brak dostępu do BIOS-u i niemożność przełączenia się z powrotem do trybu rzeczywistego bez resetowania procesora, uniemożliwiło powszechne użycie. 80286 był również nadal ograniczony do adresowania pamięci w 16-bitowych segmentach, co oznacza, że ​​jednocześnie można było uzyskać dostęp tylko do 2 16 bajtów (64 kilobajtów ). Aby uzyskać dostęp do rozszerzonej funkcjonalności 80286, system operacyjny ustawi procesor w tryb chroniony, umożliwiając adresowanie 24-bitowe, a tym samym 2 24 bajty pamięci (16 megabajtów ).

W trybie chronionym selektor segmentu można podzielić na trzy części: 13-bitowy indeks, bit wskaźnika tabeli, który określa, czy wpis jest w GDT czy LDT, oraz 2-bitowy żądany poziom uprawnień ; zobacz segmentacja pamięci x86 .

Odnosząc się do adresu z segmentem i offsetem , używany jest zapis segment : offset , więc w powyższym przykładzie płaski adres 0xEB5CE może być zapisany jako 0xDEAD:0xCAFE lub jako segment i para rejestrów offsetowych; DS:DX.

Istnieje kilka specjalnych kombinacji rejestrów segmentowych i rejestrów ogólnych, które wskazują na ważne adresy:

  • CS:IP (CS to Code Segment , IP to Instruction Pointer ) wskazuje adres, pod którym procesor pobierze następny bajt kodu.
  • SS:SP (SS to Stack Segment , SP to Stack Pointer ) wskazuje na adres wierzchołka stosu, tj. ostatnio odsunięty bajt.
  • DS:SI (DS to segment danych , SI to indeks źródłowy ) jest często używany do wskazywania ciągów danych, które mają zostać skopiowane do ES:DI.
  • ES:DI (ES to Extra Segment , DI to Destination Index ) jest zwykle używane do wskazywania miejsca docelowego kopii ciągu, jak wspomniano powyżej.

Intel 80386 posiadał trzy tryby pracy: tryb rzeczywisty, tryb chroniony i tryb wirtualny. Chroniony tryb , który zadebiutował w 80286 został przedłużony, aby umożliwić 80386 zająć maksymalnie 4 GB pamięci, wszystkie nowe wirtualne 8086 (tryb vm86 ) pozwoliła uruchomić jeden lub więcej rzeczywistych programów trybu w chronionym środowisku, które w dużej mierze emulowane w trybie rzeczywistym, chociaż niektóre programy nie były kompatybilne (zwykle w wyniku sztuczek adresowania pamięci lub używania nieokreślonych kodów operacyjnych).

32-bitowy model pamięci płaski z 80386 „s rozszerzony tryb chroniony może być najważniejszą zmianą cecha dla rodziny procesorów x86, aż AMD wydany x86-64 w 2003 roku, jak to pomogło kierowania dużą skalę przyjęcie Windows 3.1 (która oparła się na w trybie chronionym), ponieważ system Windows może teraz uruchamiać wiele aplikacji jednocześnie, w tym aplikacje DOS, przy użyciu pamięci wirtualnej i prostej wielozadaniowości.

Tryby wykonania

Procesory x86 obsługują pięć trybów działania kodu x86: Real Mode , Protected Mode , Long Mode , Virtual 86 Mode , System Management Mode , w których niektóre instrukcje są dostępne, a inne nie. 16-bitowy podzbiór instrukcji jest dostępny na 16-bitowych procesorach x86, czyli 8086, 8088, 80186, 80188 i 80286. Instrukcje te są dostępne w trybie rzeczywistym na wszystkich procesorach x86 oraz w 16-bitowym trybie chronionym (od 80286 ) dostępne są dodatkowe instrukcje dotyczące trybu chronionego. W 80386 i nowszych, 32-bitowe instrukcje (w tym późniejsze rozszerzenia) są również dostępne we wszystkich trybach, w tym w trybie rzeczywistym; w tych procesorach dodano tryb V86 i 32-bitowy tryb chroniony wraz z dodatkowymi instrukcjami zawartymi w tych trybach w celu zarządzania ich funkcjami. SMM, z niektórymi własnymi specjalnymi instrukcjami, jest dostępny w niektórych procesorach Intel i386SL, i486 i nowszych. Wreszcie, w trybie długim (od AMD Opteron ) dostępne są również instrukcje 64-bitowe i więcej rejestrów. Zestaw instrukcji jest podobny w każdym trybie, ale adresowanie pamięci i rozmiar słowa różnią się, co wymaga różnych strategii programowania.

Tryby, w których kod x86 może być wykonywany, to:

  • Tryb rzeczywisty (16-bitowy)
    • 20-bitowa segmentowana przestrzeń adresowa pamięci (co oznacza, że można zaadresować tylko 1 MB pamięci — w rzeczywistości nieco więcej), bezpośredni dostęp programowy do sprzętu peryferyjnego i brak koncepcji ochrony pamięci lub wielozadaniowości na poziomie sprzętowym. Komputery korzystające z systemu BIOS uruchamiają się w tym trybie.
  • Tryb chroniony (16-bitowy i 32-bitowy)
    • Rozszerza adresowalną pamięć fizyczną do 16 MB, a adresowalną pamięć wirtualną do 1 GB . Zapewnia poziomy uprawnień i chronioną pamięć , co zapobiega wzajemnemu uszkadzaniu programów. 16-bitowy tryb chroniony (używany pod koniec ery DOS ) wykorzystywał złożony, wielosegmentowy model pamięci. 32-bitowy tryb chroniony wykorzystuje prosty, płaski model pamięci.
  • Tryb długi (64-bitowy)
    • Przeważnie rozszerzenie zestawu instrukcji 32-bitowych (tryb chroniony), ale w przeciwieństwie do przejścia z 16 do 32 bitów, wiele instrukcji zostało usuniętych w trybie 64-bitowym. Pionierski przez AMD .
  • Tryb wirtualny 8086 (16-bitowy)
    • Specjalny hybrydowy tryb pracy, który umożliwia działanie programów i systemów operacyjnych w trybie rzeczywistym pod kontrolą systemu operacyjnego nadzorcy trybu chronionego
  • Tryb zarządzania systemem (16-bitowy)
    • Obsługuje funkcje ogólnosystemowe, takie jak zarządzanie energią, kontrola sprzętu systemowego i zastrzeżony kod zaprojektowany przez producentów OEM. Jest przeznaczony do użytku tylko przez oprogramowanie systemowe. Całe normalne wykonywanie, w tym system operacyjny , jest zawieszone. Alternatywny system oprogramowania (który zwykle znajduje się w oprogramowaniu układowym komputera lub w debugerze wspomaganym sprzętowo ) jest następnie uruchamiany z wysokimi uprawnieniami.

Przełączanie trybów

Procesor działa w trybie rzeczywistym natychmiast po włączeniu zasilania, więc jądro systemu operacyjnego lub inny program musi jawnie przełączyć się w inny tryb, jeśli chce działać w trybie innym niż rzeczywisty. Przełączanie trybów jest realizowane poprzez modyfikację pewnych bitów rejestrów kontrolnych procesora po pewnym przygotowaniu, a po przełączeniu może być wymagana dodatkowa konfiguracja.

Przykłady

W przypadku komputera ze starszym systemem BIOS , system BIOS i program ładujący działają w trybie rzeczywistym , a następnie jądro 64-bitowego systemu operacyjnego sprawdza i przełącza procesor w tryb długi, a następnie uruchamia nowe wątki trybu jądra z 64-bitowym kodem.

Na komputerze z systemem UEFI oprogramowanie układowe UEFI (z wyjątkiem CSM i starszej pamięci Option ROM ), program ładujący UEFI i jądro systemu operacyjnego UEFI działają w trybie długim.

Rodzaje instrukcji

Ogólnie rzecz biorąc, cechy nowoczesnego zestawu instrukcji x86 to:

  • Kompaktowe kodowanie
    • Niezależna zmienna długość i wyrównanie (kodowana jako little endian , tak jak wszystkie dane w architekturze x86)
    • Głównie instrukcje jednoadresowe i dwuadresowe, to znaczy pierwszy operand jest również miejscem docelowym.
    • Obsługiwane są operandy pamięci jako źródło i cel (często używane do odczytu/zapisu elementów stosu adresowanych za pomocą małych bezpośrednich przesunięć).
    • Zarówno ogólne, jak i niejawne użycie rejestru ; chociaż wszystkie siedem (liczących ebp) rejestrów ogólnych w trybie 32-bitowym i wszystkie piętnaście (liczących rbp) rejestrów ogólnych w trybie 64-bitowym mogą być swobodnie używane jako akumulatory lub do adresowania, większość z nich jest również domyślnie używana przez niektórych (więcej lub mniej) instrukcje specjalne; dotknięte rejestry muszą zatem być tymczasowo zachowane (zwykle ułożone w stos), jeśli są aktywne podczas takich sekwencji instrukcji.
  • Tworzy flagi warunkowe niejawnie przez większość instrukcji ALU z liczbami całkowitymi .
  • Obsługuje różne tryby adresowania, w tym natychmiastowy, offsetowy i skalowany indeks, ale nie względem komputera, z wyjątkiem skoków (wprowadzonych jako ulepszenie w architekturze x86-64 ).
  • Zawiera zmiennoprzecinek do stosu rejestrów.
  • Zawiera specjalne wsparcie dla atomowych read-modify-write instrukcją ( xchg, cmpxchg/ cmpxchg8b, xaddoraz całkowitą instrukcjami, które łączą się z lockprefiksu)
  • Instrukcje SIMD (instrukcje, które wykonują równolegle jednocześnie pojedyncze instrukcje na wielu operandach zakodowanych w sąsiednich komórkach szerszych rejestrów).

Instrukcje dotyczące stosu

Architektura x86 obsługuje sprzętowo mechanizm stosu wykonawczego. Instrukcje takie jak push, pop, calli retsą stosowane z prawidłowo skonfigurować stos przekazać parametry, aby przydzielić miejsca na danych lokalnych, a także, aby zapisać i punkty przywracania połączenia zwrotnego. ret Rozmiar instrukcja jest bardzo przydatne dla realizacji kosmicznych efektywny (i szybko) nazywając konwencje gdzie wywoływany jest odpowiedzialny za odzyskiwanie przestrzeni stosu zajęte przez parametry.

Podczas konfigurowania ramki stosu do przechowywania lokalnych danych procedury rekurencyjnej istnieje kilka możliwości; enterInstrukcja wysokiego poziomu (wprowadzona w 80186) pobiera argument głębi procedury zagnieżdżenia, a także argument rozmiaru lokalnego i może być szybsza niż bardziej jawne manipulowanie rejestrami (takie jak push bp ; mov bp, sp ; ). To, czy jest szybsze, czy wolniejsze, zależy od konkretnej implementacji procesora x86 oraz konwencji wywoływania używanej przez kompilator, programista lub konkretny kod programu; większość kodu x86 ma działać na procesorach x86 od kilku producentów i na różnych technologicznych generacjach procesorów, co oznacza bardzo zróżnicowane mikroarchitektury i rozwiązania mikrokodowe , a także różne wybory projektowe na poziomie bramki i tranzystora . sub sp, size

Pełny zakres trybów adresowania (w tym natychmiastowy i base+offset ) nawet dla instrukcji takich jak pushi pop, sprawia, że ​​bezpośrednie użycie stosu dla danych całkowitych , zmiennoprzecinkowych i adresowych jest proste, a specyfikacje i mechanizmy ABI są stosunkowo proste w porównaniu do niektóre architektury RISC (wymagają bardziej wyraźnych szczegółów stosu wywołań).

Integer instrukcje ALU

Zespół x86 ma standardowe operacje matematyczne add, sub, mul, with idiv; te operatory logiczne and , or, xor, neg; arytmetyczne i logiczne przesunięcie bitów , sal/ sar, shl/ shr; obracać z przeniesieniem i bez przeniesienia, rcl/ rcr, rol/ ror, uzupełnienie instrukcji arytmetycznych BCD , aaa, aad, daai inne.

Instrukcje zmiennoprzecinkowe

Język asemblera x86 zawiera instrukcje dla jednostki zmiennoprzecinkowej opartej na stosie (FPU). FPU był opcjonalnym oddzielnym koprocesorem dla 8086 do 80386, był on opcją na chipie dla serii 80486 i jest standardową funkcją każdego procesora Intel x86 od 80486, począwszy od Pentium. Instrukcje FPU obejmują dodawanie, odejmowanie, negację, mnożenie, dzielenie, resztę, pierwiastki kwadratowe, obcinanie liczb całkowitych, obcinanie ułamka i skalowanie przez potęgę dwójki. Operacje obejmują również instrukcje konwersji, które mogą ładować lub przechowywać wartość z pamięci w dowolnym z następujących formatów: dziesiętny kodowany binarnie, 32-bitowa liczba całkowita, 64-bitowa liczba całkowita, 32-bitowa liczba zmiennoprzecinkowa, 64-bitowa liczba zmiennoprzecinkowa punktowy lub 80-bitowy zmiennoprzecinkowy (po załadowaniu wartość jest konwertowana na aktualnie używany tryb zmiennoprzecinkowy). x86 zawiera również szereg funkcji transcendentalnych, w tym sinus, cosinus, tangens, arctangens, potęgowanie o podstawie 2 i logarytmy o podstawie 2, 10 lub e .

Rejestr stosu do formatu rejestru stosu instrukcji to zwykle lub , gdzie jest równoważne , i jest jednym z 8 rejestrów stosu ( , , ..., ). Podobnie jak w przypadku liczb całkowitych, pierwszy operand jest zarówno pierwszym operandem źródłowym, jak i operandem docelowym. i powinny być wyróżnione jako pierwsza zamiana operandów źródłowych przed wykonaniem odejmowania lub dzielenia. Instrukcje dodawania, odejmowania, mnożenia, dzielenia, przechowywania i porównywania zawierają tryby instrukcji, które podnoszą szczyt stosu po zakończeniu ich działania. Na przykład wykonuje obliczenia , a następnie usuwa ze szczytu stosu, tworząc w ten sposób wynik na szczycie stosu w . fop st, st(n)fop st(n), ststst(0)st(n)st(0)st(1)st(7)fsubrfdivrfaddp st(1), stst(1) = st(1) + st(0)st(0)st(1)st(0)

Instrukcje SIMD

Nowoczesne procesory x86 zawierają instrukcje SIMD , które w dużej mierze wykonują tę samą operację równolegle na wielu wartościach zakodowanych w szerokim rejestrze SIMD. Różne technologie instrukcji obsługują różne operacje na różnych zbiorach rejestrów, ale traktowane jako kompletna całość (od MMX do SSE4.2 ) obejmują ogólne obliczenia na arytmetyce liczb całkowitych lub zmiennoprzecinkowych (dodawanie, odejmowanie, mnożenie, przesuwanie, minimalizacja, maksymalizacja, porównywanie, dzielenie lub pierwiastek kwadratowy). Na przykład paddw mm0, mm1wykonuje 4 równoległe 16-bitowe (wskazane przez w) liczby całkowite, które dodają (wskazywane przez padd) mm0wartości do mm1i przechowują wynik w mm0. Streaming SIMD Extensions lub SSE obejmuje również pływającą trybie punktu, w którym tylko pierwszy wartość rejestrów jest rzeczywiście zmodyfikowaną (rozszerzony w SSE2 ). Dodano kilka innych nietypowych instrukcji, w tym sumę różnic bezwzględnych (używaną do szacowania ruchu w kompresji wideo , na przykład w MPEG ) oraz 16-bitową instrukcję akumulacji mnożenia (przydatną w przypadku programowego mieszania alfa i filtrowania cyfrowego ) . SSE (od SSE3 ) i 3DNow! rozszerzenia obejmują instrukcje dodawania i odejmowania do traktowania sparowanych wartości zmiennoprzecinkowych, takich jak liczby zespolone.

Te zestawy instrukcji zawierają również liczne stałe instrukcje podsłów do tasowania, wstawiania i wyodrębniania wartości w obrębie rejestrów. Dodatkowo istnieją instrukcje przenoszenia danych między rejestrami całkowitymi a rejestrami XMM (używanymi w SSE)/FPU (używanymi w MMX).

Instrukcje dotyczące manipulacji danymi

Procesor x86 zawiera również złożone tryby adresowania do adresowania pamięci z natychmiastowym przesunięciem, rejestr, rejestr z przesunięciem, rejestr skalowany z przesunięciem lub bez oraz rejestr z opcjonalnym przesunięciem i inny skalowany rejestr. Na przykład można zakodować mov eax, [Table + ebx + esi*4]jako pojedynczą instrukcję, która ładuje 32 bity danych z adresu obliczonego jako (Table + ebx + esi * 4)przesunięcie z dsselektora i przechowuje je w eaxrejestrze. Ogólnie rzecz biorąc, procesory x86 mogą ładować i używać pamięci dopasowanej do rozmiaru dowolnego rejestru, na którym działają. (Instrukcje SIMD zawierają również instrukcje dotyczące połowy obciążenia).

Zestaw instrukcji x86 obejmuje obciążenie strun, przechowywania, przenoszenia, skanowania i porównać z instrukcjami ( lods, stos, movs, scasi cmps), które wykonać każdą operację do określonej wielkości ( bdla 8-bitowych bajtów, wdla 16-bitowego słowa, ddla 32-bitowych podwójne słowo) następnie inkrementuje/zmniejsza (w zależności od DF, flagi kierunku) niejawny rejestr adresu ( sifor lods, difor stosi scas, oraz oba for movsi cmps). Dla obciążenia, sklepu i operacji skanowania, niejawny cel / źródło / porównanie rejestr jest w al, axlub eaxzarejestruj (w zależności od rozmiaru). Użyte niejawne rejestry segmentowe to dsfor sii esfor di. cxLub ecxrejestru jest używana, jako Zmniejszanie licznik, a operacja kończy się, gdy licznik osiągnie zero, albo (w przypadku skanowania i porównania) w przypadku wykrycia nierówności.

Stos jest zaimplementowany z niejawnym dekrementującym (push) i inkrementującym (pop) wskaźnikiem stosu. W trybie 16-bitowym ten niejawny wskaźnik stosu jest adresowany jako SS:[SP], w trybie 32-bitowym jest to SS:[ESP], a w trybie 64-bitowym jest to [RSP]. Wskaźnik stosu w rzeczywistości wskazuje na ostatnią wartość, która była przechowywana, przy założeniu, że jego rozmiar będzie odpowiadał trybowi pracy procesora (tj. 16, 32 lub 64 bity), aby dopasować domyślną szerokość instrukcji push/ pop/ call/ ret. Są także instrukcje enteri leavektóre zastrzegają i usunąć dane z wierzchu stosu podczas konfigurowania ramki wskaźnik stosu w bp/ ebp/ rbp. Jednak bezpośrednie ustawianie lub dodawanie i odejmowanie do rejestru sp/ esp/ rspjest również obsługiwane, więc instrukcje enter/ leavesą generalnie niepotrzebne.

Ten kod na początku funkcji:

 push    ebp       ; save calling function's stack frame (ebp)
 mov     ebp, esp  ; make a new stack frame on top of our caller's stack
 sub     esp, 4    ; allocate 4 bytes of stack space for this function's local variables

...jest funkcjonalnie równoważny tylko:

 enter   4, 0

Inne instrukcje do manipulowania stosem obejmują pushf/ popfdo przechowywania i pobierania rejestru (E)FLAGS. W pusha/ popainstrukcje pozwolą zachować i odzyskać całą Państwowego Rejestru całkowitą do i ze stosu.

Zakłada się, że wartości dla obciążenia lub magazynu SIMD są upakowane w sąsiednich pozycjach rejestru SIMD i dopasują je w sekwencyjnej kolejności little-endian. Niektóre instrukcje ładowania i przechowywania SSE wymagają do poprawnego działania 16-bajtowego wyrównania. Zestawy instrukcji SIMD zawierają również instrukcje "prefetch", które wykonują ładowanie, ale nie są ukierunkowane na żaden rejestr, używany do ładowania pamięci podręcznej. Zestawy instrukcji SSE zawierają również instrukcje przechowywania nieczasowego, które będą wykonywać operacje przechowywania bezpośrednio do pamięci bez wykonywania alokacji pamięci podręcznej, jeśli miejsce docelowe nie jest już buforowane (w przeciwnym razie będzie zachowywać się jak zwykły sklep).

Większość ogólnych instrukcji całkowitych i zmiennoprzecinkowych (ale bez SIMD) może używać jednego parametru jako adresu złożonego jako drugiego parametru źródłowego. Instrukcje Integer mogą również akceptować jeden parametr pamięci jako operand docelowy.

Przebieg programu

Zespół x86 ma bezwarunkową operację skoku jmp, która może przyjąć adres natychmiastowy, rejestr lub adres pośredni jako parametr (zauważ, że większość procesorów RISC obsługuje tylko rejestr łącza lub krótkie natychmiastowe przesunięcie do skoku).

Obsługiwanych jest również kilka skoków warunkowych, w tym jz(skok na zero), jnz(skok na wartość niezerową), jg(skok na większą niż, podpisany), jl(skok na mniej niż, podpisany), ja(skok na powyżej/większy niż, bez znaku) , jb(Przełącz na poniżej / mniej niż unsigned). Te operacje warunkowe bazują na stanie określonych bitów w rejestrze (E)FLAGS . Wiele operacji arytmetycznych i logicznych ustawia, czyści lub uzupełnia te flagi w zależności od ich wyniku. Porównanie cmp(porównanie) i testinstrukcje ustawiają flagi tak, jakby wykonywały odpowiednio odejmowanie lub bitową operację AND bez zmiany wartości argumentów. Istnieją również instrukcje, takie jak clc(wyczyść flagę przeniesienia) i cmc(uzupełnij flagę przeniesienia), które działają bezpośrednio na flagach. Porównania zmiennoprzecinkowe są wykonywane za pomocą instrukcji fcomlub ficominstrukcji, które ostatecznie muszą zostać przekonwertowane na flagi całkowite.

Każda operacja skoku ma trzy różne formy, w zależności od rozmiaru operandu. Krótki skok korzysta z 8-bitową argument, który jest względne przesunięcie od bieżącej instrukcji. Blisko skok jest podobny do krótkiego skoku ale wykorzystuje 16-bitową operand (w trybie rzeczywistym lub chronione) lub 32-bitowa operand (tylko w 32-bitowym trybie chronionym). Daleki skok jest taki, który wykorzystuje pełną bazę segmentu: offset jako wartość bezwzględną adres. Istnieją również formy pośrednie i indeksowane każdego z nich.

Oprócz prostych operacji skokowych istnieją instrukcje call(wywołaj podprogram) i ret(powrót z podprogramu). Przed przekazaniem sterowania do podprogramu, callodkłada adres przesunięcia segmentu instrukcji następującej po callznaku na stos; retzdejmuje tę wartość ze stosu i przeskakuje do niej, skutecznie zwracając przepływ sterowania do tej części programu. W przypadku a far callpodstawa segmentu jest przesuwana zgodnie z przesunięciem; far retwyskakuje odsunięcie, a następnie podstawę segmentu do zwrócenia.

Istnieją również dwie podobne instrukcje, int( przerwanie ), które zapisuje bieżącą wartość rejestru (E)FLAGS na stosie, a następnie wykonuje a far call, z tą różnicą, że zamiast adresu używa wektora przerwań , indeks do tablicy obsługi przerwań adresy. Zazwyczaj program obsługi przerwań zapisuje wszystkie inne rejestry procesora, których używa, chyba że są one używane do zwrócenia wyniku operacji do programu wywołującego (w oprogramowaniu zwanym przerwaniami). Dopasowany return z instrukcji przerwania to iret, który przywraca flagi po powrocie. Miękkie przerwania opisanego powyżej typu są używane przez niektóre systemy operacyjne do wywołań systemowych i mogą być również używane do debugowania twardych procedur obsługi przerwań. Twarde przerwania są wyzwalane przez zewnętrzne zdarzenia sprzętowe i muszą zachowywać wszystkie wartości rejestrów, ponieważ stan aktualnie wykonywanego programu jest nieznany. W trybie chronionym przerwania mogą być skonfigurowane przez system operacyjny, aby wyzwolić przełącznik zadań, który automatycznie zapisze wszystkie rejestry aktywnego zadania.

Przykłady

"Witaj świecie!" program dla DOS w asemblerze w stylu MASM

Używanie przerwania 21h do wyjścia – inne przykłady używają printf z libc do drukowania na standardowe wyjście .

.model small
.stack 100h

.data
msg	db	'Hello world!$'

.code
start:
	mov	ah, 09h   ; Display the message
	lea	dx, msg
	int	21h
	mov	ax, 4C00h  ; Terminate the executable
	int	21h

end start

"Witaj świecie!" program dla Windows w montażu w stylu MASM

; requires /coff switch on 6.15 and earlier versions
.386
.model small,c
.stack 1000h

.data
msg     db "Hello world!",0

.code
includelib libcmt.lib
includelib libvcruntime.lib
includelib libucrt.lib
includelib legacy_stdio_definitions.lib

extrn printf:near
extrn exit:near

public main
main proc
        push    offset msg
        call    printf
        push    0
        call    exit
main endp

end

"Witaj świecie!" program dla Windows w montażu w stylu NASM

; Image base = 0x00400000
%define RVA(x) (x-0x00400000)
section .text
push dword hello
call dword [printf]
push byte +0
call dword [exit]
ret

section .data
hello db "Hello world!"

section .idata
dd RVA(msvcrt_LookupTable)
dd -1
dd 0
dd RVA(msvcrt_string)
dd RVA(msvcrt_imports)
times 5 dd 0 ; ends the descriptor table

msvcrt_string dd "msvcrt.dll", 0
msvcrt_LookupTable:
dd RVA(msvcrt_printf)
dd RVA(msvcrt_exit)
dd 0

msvcrt_imports:
printf dd RVA(msvcrt_printf)
exit dd RVA(msvcrt_exit)
dd 0

msvcrt_printf:
dw 1
dw "printf", 0
msvcrt_exit:
dw 2
dw "exit", 0
dd 0

"Witaj świecie!" program dla systemu Linux w asemblerze w stylu NASM

;
; This program runs in 32-bit protected mode.
;  build: nasm -f elf -F stabs name.asm
;  link:  ld -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc.)
; Also change "-f elf " for "-f elf64" in build command.
;
section .data                           ; section for initialized data
str:     db 'Hello world!', 0Ah         ; message string with new-line char at the end (10 decimal)
str_len: equ $ - str                    ; calcs length of string (bytes) by subtracting the str's start address
                                            ; from this address ($ symbol)

section .text                           ; this is the code section
global _start                           ; _start is the entry point and needs global scope to be 'seen' by the
                                            ; linker --equivalent to main() in C/C++
_start:                                 ; definition of _start procedure begins here
	mov	eax, 4                   ; specify the sys_write function code (from OS vector table)
	mov	ebx, 1                   ; specify file descriptor stdout --in gnu/linux, everything's treated as a file,
                                             ; even hardware devices
	mov	ecx, str                 ; move start _address_ of string message to ecx register
	mov	edx, str_len             ; move length of message (in bytes)
	int	80h                      ; interrupt kernel to perform the system call we just set up -
                                             ; in gnu/linux services are requested through the kernel
	mov	eax, 1                   ; specify sys_exit function code (from OS vector table)
	mov	ebx, 0                   ; specify return code for OS (zero tells OS everything went fine)
	int	80h                      ; interrupt kernel to perform system call (to exit)

"Witaj świecie!" program dla systemu Linux w asemblerze w stylu NASM przy użyciu standardowej biblioteki C

;
;  This program runs in 32-bit protected mode.
;  gcc links the standard-C library by default

;  build: nasm -f elf -F stabs name.asm
;  link:  gcc -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc..)
; Also change "-f elf " for "-f elf64" in build command.
;
        global  main                                ;main must be defined as it being compiled against the C-Standard Library
        extern  printf                               ;declares use of external symbol as printf is declared in a different object-module.
                                                           ;Linker resolves this symbol later.

segment .data                                       ;section for initialized data
	string db 'Hello world!', 0Ah, 0h           ;message string with new-line char (10 decimal) and the NULL terminator
                                                    ;string now refers to the starting address at which 'Hello, World' is stored.

segment .text
main:
        push    string                              ;push the address of first character of string onto stack. This will be argument to printf
        call    printf                              ;calls printf
        add     esp, 4                              ;advances stack-pointer by 4 flushing out the pushed string argument
        ret                                         ;return

"Witaj świecie!" program dla 64-bitowego trybu Linux w asemblerze w stylu NASM

;  build: nasm -f elf64 -F dwarf hello.asm
;  link:  ld -o hello hello.o

DEFAULT REL			; use RIP-relative addressing modes by default, so [foo] = [rel foo]

SECTION .rodata			; read-only data can go in the .rodata section on GNU/Linux, like .rdata on Windows
Hello:		db "Hello world!",10        ; 10 = `\n`.
len_Hello:	equ $-Hello                 ; get NASM to calculate the length as an assemble-time constant
;;  write() takes a length so a 0-terminated C-style string isn't needed. It would be for puts

SECTION .text

global _start
_start:
	mov eax, 1				; __NR_write syscall number from Linux asm/unistd_64.h (x86_64)
	mov edi, 1				; int fd = STDOUT_FILENO
	lea rsi, [rel Hello]			; x86-64 uses RIP-relative LEA to put static addresses into regs
	mov rdx, len_Hello		; size_t count = len_Hello
	syscall					; write(1, Hello, len_Hello);  call into the kernel to actually do the system call
     ;; return value in RAX.  RCX and R11 are also overwritten by syscall

	mov eax, 60				; __NR_exit call number (x86_64)
	xor edi, edi				; status = 0 (exit normally)
	syscall					; _exit(0)

Uruchomienie go pod straceweryfikuje, czy w procesie nie są wykonywane żadne dodatkowe wywołania systemowe. Wersja printf wykonałaby znacznie więcej wywołań systemowych inicjujących libc i wykonujących dynamiczne łączenie . Ale jest to statyczny plik wykonywalny, ponieważ linkowaliśmy używając ld bez -pie lub jakichkolwiek bibliotek współdzielonych; jedyne instrukcje, które działają w przestrzeni użytkownika, to te, które podajesz.

$ strace ./hello > /dev/null                    # without a redirect, your program's stdout is mixed strace's logging on stderr.  Which is normally fine
execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0
write(1, "Hello world!\n", 13)          = 13
exit(0)                                 = ?
+++ exited with 0 +++

Korzystanie z rejestru flag

Flagi są często używane do porównań w architekturze x86. Kiedy dokonuje się porównania między dwoma danymi, CPU ustawia odpowiednią flagę lub flagi. Następnie można użyć instrukcji skoku warunkowego do sprawdzenia flag i rozgałęzienia kodu, który powinien zostać uruchomiony, np.:

	cmp	eax, ebx
	jne	do_something
	; ...
do_something:
	; do something here

Flagi są również używane w architekturze x86 do włączania i wyłączania niektórych funkcji lub trybów wykonywania. Na przykład, aby wyłączyć wszystkie maskowalne przerwania, możesz użyć instrukcji:

	cli

Dostęp do rejestru flag można również uzyskać bezpośrednio. Młodsze 8 bitów rejestru flag może być załadowanych ahza pomocą lahfinstrukcji. Cały rejestr flag można również przenieść na stos i ze stosu za pomocą instrukcji pushf, popf, int(w tym into) i iret.

Korzystanie z rejestru wskaźnika instrukcji

Wskaźnik instrukcji jest wywoływany ipw trybie 16-bitowym, eipw trybie 32-bitowym iw trybie rip64-bitowym. Rejestr wskaźnika instrukcji wskazuje na adres pamięci, który procesor będzie następnie próbował wykonać; nie można uzyskać do niego bezpośredniego dostępu w trybie 16-bitowym lub 32-bitowym, ale można zapisać sekwencję podobną do poniższej, aby umieścić adres next_linew eax:

	call	next_line
next_line:
	pop	eax

Ta sekwencja instrukcji generuje kod niezależny od pozycji, ponieważ callprzyjmuje natychmiastowy operand zależny od wskaźnika instrukcji opisujący przesunięcie w bajtach instrukcji docelowej od następnej instrukcji (w tym przypadku 0).

Zapisywanie do wskaźnika instrukcji jest proste — jmpinstrukcja ustawia wskaźnik instrukcji na adres docelowy, więc na przykład sekwencja taka jak poniższa wstawi zawartość eaxdo eip:

	jmp	eax

W trybie 64-bitowym instrukcje mogą odwoływać się do danych względem wskaźnika instrukcji, dzięki czemu nie ma potrzeby kopiowania wartości wskaźnika instrukcji do innego rejestru.

Zobacz też

Bibliografia

Dalsza lektura

Instrukcje

Książki