ethical.blue Magazine

// Cybersecurity clarified.

Esencja wiedzy o Asemblerze x64 w błękitnej szklanej fiolce

...
Assembly Language // // Dawid Farbaniec
Ostatnia modyfikacja:

Spis treści

Podstawowe pojęcia

Bity, bajty i słowa

Powszechne systemy komputerowe działają na wartościach binarnych (dwójkowych). Oznacza to, że informacje mogą być reprezentowane w postaci ciągów bitów. Bit jest to najmniejsza jednostka informacji pamięci komputerowej. Może on przyjmować jeden z dwóch stanów: wysoki lub niski. Spotyka się też inne określenia np. jeden lub zero, ustawiony lub nieustawiony, prawda lub fałsz etc. Osiągnięcie tylko dwóch stanów jest możliwe dzięki zastosowaniu sygnału cyfrowego zamiast analogowego. Sygnał analogowy, nawet jeśli jest w określonych granicach napięcia, to posiada ciągły przedział wartości. Za pomocą kwantyzacji, czyli zmniejszenia precyzji możliwe jest uzyskanie wartości napięcia w postaci liczb całkowitych. Dzięki temu stan wysoki to np. 5 V (wolt), a stan niski to 0 V (wolt). Należy oczywiście wziąć poprawkę, że te wartości napięcia nie są idealne, a ich odchylenie od stałej wartości jest nazywane szumem (ang. noise), czyli 4.95 V to nadal stan wysoki, a 0.05 V to stan niski.

...
Przykładowy sygnał analogowy oraz cyfrowy

Wartości binarne (dwójkowe) jako, że przyjmują tylko jeden z dwóch możliwych stanów: zero lub jeden, nazywane są też wartościami logicznymi. W celu przetwarzania tego rodzaju danych stosuje się mechanizmy nazywane bramkami logicznymi (ang. logic gate). Elementy tego typu mogą realizować różne funkcje logiczne np. alternatywa czy koniunkcja. Koniunkcja (iloczyn logiczny) zwraca w wyniku prawdę, wtedy i tylko wtedy, gdy wszystkie przyjmowane argumenty są prawdą. Natomiast alternatywa (suma logiczna) zwraca w wyniku prawdę, gdy chociaż jeden argument jest prawdą.

...
Przykładowe układy realizujące funkcje OR (alternatywa) i AND (koniunkcja)

Tablice prawdy (ang. truth tables) nazywane też matrycami logicznymi są przejrzystym sposobem na prezentację działania poszczególnych funkcji logicznych.

Koniunkcja (iloczyn logiczny, AND)
A B A and B
0 0 0
0 1 0
1 0 0
1 1 1
Alternatywa (suma logiczna, OR)
A B A or B
0 0 0
0 1 1
1 0 1
1 1 1
Alternatywa wykluczająca (alternatywa rozłączna, suma modulo 2, XOR)
A B A xor B
0 0 0
0 1 1
1 0 1
1 1 0
Zaprzeczenie (negacja, NOT)
A not A
0 1
1 0
Dysjunkcja (zanegowany iloczyn logiczny, NAND)
A B A nand B
0 0 1
0 1 1
1 0 1
1 1 0
Zaprzeczona alternatywa (zanegowana suma logiczna, NOR)
A B A nor B
0 0 1
0 1 0
1 0 0
1 1 0
Zaprzeczona alternatywa wykluczająca (logicznie równe, XNOR)
A B A xnor B
0 0 1
0 1 0
1 0 0
1 1 1

Poszczególne bramki logiczne mają nadane symbole, które pozwalają je łączyć w schematy układów.

Istnieją dwa główne rodzaje symboli:

...
Symbole bramek logicznych — tradycyjne oraz prostokątne
Tablice prawdy funkcji logicznych dla trzech argumentów
A B C and or xor nand nor xnor
0 0 0 0 0 0 1 1 1
0 0 1 0 1 1 1 0 0
0 1 0 0 1 1 1 0 0
0 1 1 0 1 0 1 0 1
1 0 0 0 1 1 1 0 0
1 0 1 0 1 0 1 0 1
1 1 0 0 1 0 1 0 1
1 1 1 1 1 1 0 0 0

Bity połączone w oktet (8 bitów) tworzą bajt. Z kolei dwa bajty (16 bitów) komponują się w słowo maszynowe (ang. word). Jednak niezawsze słowo maszynowe jest rozmiaru 16 bitów. Na platformie sprzętowej x86-64 (x64) słowo ma rozmiar 16 bitów, ale np. w urządzeniach opartych o Arm64 (AArch64) słowo ma rozmiar 32 bity.

Dane bez określonego typu (x86/x64):

...
Dane bez określonego typu (x86/x64)

Kolejność bajtów

Bajt jest najmniejszą możliwą do zaadresowania jednostką pamięci. Dane w pamięci operacyjnej, wartości w rejestrach ogólnego przeznaczenia czy kod maszynowy (rozkazy procesora w formie binarnej) są przedstawiane w formie ciągu bajtów. Urządzenia oparte o architekturę x86/x64 korzystają z kolejności bajtów nazywanej Little Endian (LE). Oznacza to, że najmłodszy bajt (określany też najmniej znaczącym bajtem, ang. low byte) jest zapisywany jako pierwszy.

Istnieje także inna konwencja określająca kolejność bajtów — Big Endian (BE). W przypadku BE najstarszy bajt (określany też najbardziej znaczącym bajtem, ang. high byte) jest zapisywany jako pierwszy.

...
Kolejność bajtów w procesorach x86/x64 — LE (Little Endian)

Systemy liczbowe: binarny i heksadecymalny

System binarny oparty o dwie cyfry: 0 i 1, jest przyjazną formą reprezentowania bitów. Jednak większe wartości liczbowe zapisywane w tym systemie są dość długie np. 65535 dziesiętnie to 1111111111111111 binarnie (wszystkie bity ustawione). Natomiast system heksadecymalny oparty o szesnaście znaków: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F jest przyjazną formą reprezentowania bajtów np. 65535 dziesiętnie to 0FFFFh heksadecymalnie. Mamy tutaj dwa bajty o wartościach 0FFh (255 dziesiętnie bez znaku). Liczby binarne w asemblerze MASM x64 (ml64.exe) zapisuje się z przyrostkiem b, a wartości szesnastkowe z przyrostkiem h. Inną powszechnie stosowaną notacją zapisu liczb heksadecymalnych jest przyrostek 0x np. 0xFFFF. Ten sposób reprezentowania liczb szesnastkowych obowiązuje w C++ i C#.

...
Kalkulator systemu Windows — zamiana systemów liczbowych
Systemy liczbowe — przykładowe wartości
Dziesiętny bez znaku Binarny (dwójkowy) Szesnastkowy (heksadecymalny)
0 0b 0h
1 1b 1h
2 10b 2h
3 11b 3h
4 100b 4h
5 101b 5h
6 110b 6h
7 111b 7h
8 1000b 8h
9 1001b 9h
10 1010b 0Ah
11 1011b 0Bh
12 1100b 0Ch
13 1101b 0Dh
14 1110b 0Eh
15 1111b 0Fh
16 10000b 10h
17 10001b 11h
18 10010b 12h
... ... ...
99 1100011b 63h
100 1100100b 64h
101 1100101b 65h
... ... ...
254 11111110b 0FEh
255 11111111b 0FFh
256 100000000b 100h
257 100000001b 101h
... ... ...
512 1000000000b 200h
... ... ...
1024 10000000000b 400h
... ... ...
65535 1111111111111111b 0FFFFh
... ... ...

Liczby ze znakiem i bez znaku

Dostępne typy danych do przechowywania liczb całkowitych to:

Jeśli przechowywana wartość jest traktowana jako liczba całkowita ze znakiem, to najstarszy bit (najbardziej znaczący bit) nazywany bitem znaku określa czy wartość jest ujemna czy dodatnia. Gdy bit znaku ma wartość 1 (jeden), to liczba jest ujemna, a jeśli ma wartość 0 (zero), to liczba jest dodatnia.

...
Przykładowe typy danych — liczba całkowita ze znakiem (ang. signed integer)

Liczby całkowite ze znakiem są reprezentowane w formacie uzupełnień do dwóch (ang. two's complement format).

...
Przykładowa liczba całkowita 0FFh (szesnastkowo) traktowana jako bez znaku (255) albo ze znakiem (-1)

Dopełnienie zerami oraz rozszerzenie z zachowaniem znaku

W celu zamiany typu danych na typ o większym rozmiarze należy zastosować odpowiednio: dopełnienie zerami (dla liczby całkowitej bez znaku) albo rozszerzenie z zachowaniem znaku (dla liczby całkowitej ze znakiem). To właśnie dzięki praktykom, nazywanym po angielsku zero extension oraz sign extension, możliwa jest zamiana danych na typ o większym rozmiarze bez utraty poprawności wartości liczbowej.

...
Dopełnienie zerami oraz rozszerzenie z zachowaniem znaku — przykład

Przepełnienie całkowitoliczbowe i saturacja

Przepełnienie całkowitoliczbowe (ang. integer overflow) można porównać do przekręcenia się licznika w starym samochodzie. Najprościej będzie to przedstawić na przykładzie. Typ danych o rozmiarze jednego bajta przechowuje wartość, która powoduje ustawienie wszystkich bitów na 1 (jeden), czyli 1111 1111b. Dziesiętnie jest to równe 255 bez znaku lub -1 ze znakiem. Co jeśli zwiększy się tę wartość o 1? (będąc cały czas ograniczonym do rozmiaru 8 bitów — bajt). Nastąpi nazywane potocznie przekręcenie licznika, czyli otrzymana wartość to 0000 0000b. Podobnie jest z odejmowaniem. Jeśli od wartości 0000 0000b odejmie się, np. tym razem 2 (10b), to wynikiem będzie 1111 1110b, czyli 254 bez znaku lub -2 ze znakiem.

...
Porównanie przepełnienia całkowitoliczbowego do licznika w starym samochodzie
...
Porównanie przepełnienia całkowitoliczbowego do licznika w starym samochodzie (2)

Zabezpieczeniem przed błędami przepełnienia może być saturacja wartości. Działanie tego rodzaju polega na zawężeniu wyniku do określonego przedziału (minimum oraz maksimum). Saturacja wartości liczbowej oprócz ochrony przed błędami typu integer overflow powoduje również mniej zakłóceń w przetwarzanym sygnale.

Na przykład. Minimum jest ustalone jako 00h (0), a maksimum jako 0FFh (255). Wtedy przykładowe operacje wyglądają następująco:

Nie wszystkie rozkazy arytmetyczne przeprowadzają saturację wartości i z tego powodu mogą być podatne na błędy przepełnienia (ang. overflow) i niedomiaru (ang. underflow).

Skróty związane z poszczególnymi wersjami architektury x86/x64

Rodziny procesorów w architekturze 16-bitowej to: 8086, 8186, 8286. Natomiast w architekturze 32-bitowej to: 8386, 8486, 8586, 8686 etc. Architektura 64-bitowa określana jest jako x86-64. Inne określenia dla architektury 32-bitowej to: x86-32, x86 (32-bit), x32, IA-32. Natomiast dla architektury 64-bitowej to: x86-64, x86 (64-bit), x64, Intel 64, AMD64.

Budowanie programu, czyli asemblacja i konsolidacja

W języku Asembler zamianę kodu źródłowego z postaci tekstowej na plik wykonywalny można podzielić na dwa podstawowe etapy takie jak asemblacja i konsolidacja. Narzędzie asembler tworzy pliki binarne, ale to konsolidator (ang. linker) łączy te pliki z bibliotekami w celu stworzenia pliku wykonywalnego (*.exe) czy biblioteki dołączanej dynamicznie (.dll).

...
Uproszczony schemat procesu budowania programu z kodu źródłowego do pliku wykonywalnego (*.exe)

Inżynieria wsteczna i analiza plików binarnych, czyli dezasemblacja

Za pomocą narzędzi typu dezasembler czy dekompilator możliwe jest odzyskanie kodu źródłowego z plików binarnych. Dziedzina polegająca na analizie działania programów w postaci binarnej nazywana jest inżynierią wsteczną kodu (ang. Reverse Code Engineering). Podczas prób zrozumienia jak działa zamknięte oprogramowanie przydatnym narzędziem jest również debugger, który pozwala m.in. zatrzymać program na określonej instrukcji, podejrzeć czy zmienić wartości rejestrów czy zrzucić pamięć programu na dysk komputera w celach badawczych.

...
Uproszczony schemat procesu dezasemblacji programu

Organizacja pamięci

Pamięć fizyczna to moduły sprzętowe zainstalowane w urządzeniu, czyli np. dyski twarde, pamięci zewnętrzne czy kości pamięci operacyjnej. Natomiast pamięć wirtualna to ciągła przestrzeń adresowa od 0 do 264 - 1 (od 0000000000000000h do FFFFFFFFFFFFFFFFh). Wirtualne obszary pamięci dostępne dla programów są tłumaczone na mniejsze obszary pamięci fizycznej za pomocą różnych mechanizmów sprzętowych i programowych. Dawniej w systemie MS-DOS (programy 16-bitowe) pamięć była podzielona na segmenty takie jak np. segment kodu, danych czy stosu. Adresowanie odbywało się poprzez podanie selektora segmentu i wartości przesunięcia. W trybie 64-bitowym adresy są liniowe, a adresem bazowym selektorów segmentowych jest zero.

...
Model pamięci „płaski” (ang. flat memory model)

Rejestry ogólnego przeznaczenia

Rejestry to nieadresowalne elementy podobne do pamięci, ale mniejszych rozmiarów (najczęściej od 8 bitów do 512 bitów).

Rejestr akumulatora (ang. accumulator)

Rejestr ulotny (ang. volatile). Nie ma konieczności przywrócenia rejestrowi poprzedniej wartości przed wyjściem z funkcji.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr akumulatora (ang. accumulator)

Rejestr bazowy (ang. base)

Rejestr nieulotny (ang. nonvolatile) — jego wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tego rejestru.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr bazowy (ang. base)

Rejestr licznika (ang. counter)

Rejestr ulotny (ang. volatile). Nie ma konieczności przywrócenia rejestrowi poprzedniej wartości przed wyjściem z funkcji.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr licznika (ang. counter)

Rejestr danych (ang. data)

Rejestr ulotny (ang. volatile). Nie ma konieczności przywrócenia rejestrowi poprzedniej wartości przed wyjściem z funkcji.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr danych (ang. data)

Rejestry dodatkowe (ang. extra)

Rejestry R12, R13, R14 oraz R15 są nieulotne (ang. nonvolatile) — ich wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tych rejestrów.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestry dodatkowe (ang. extra)

Rejestr indeksowy źródła (ang. source index)

Rejestr nieulotny (ang. nonvolatile) — jego wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tego rejestru.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr indeksowy źródła (ang. source index)

Rejestr indeksowy docelowy (ang. destination index)

Rejestr nieulotny (ang. nonvolatile) — jego wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tego rejestru.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr indeksowy docelowy (ang. destination index)

Rejestr wskaźnika bazowego (ang. base pointer)

Rejestr nieulotny (ang. nonvolatile) — jego wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tego rejestru.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr wskaźnika bazowego (ang. base pointer)

Rejestr wskaźnika stosu (ang. stack pointer)

Rejestr nieulotny (ang. nonvolatile) — jego wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tego rejestru.

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr wskaźnika stosu (ang. stack pointer)

Rejestr wskaźnika instrukcji

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr wskaźnika instrukcji (ang. instruction pointer)

Rejestry segmentowe

Domyślne zastosowanie:

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestry segmentowe (ang. segment registers)

Rejestr flag/znaczników

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestr flag/znaczników (ang. flags register)

Flagi systemowe

Rejestry jednostki zmiennoprzecinkowej x87

Nazwa koprocesor pochodzi z dawnych czasów, kiedy to procesor posiadał osobną, dodatkową jednostkę wspomagającą obliczenia. Rozszerzenie nazywane x87 zawiera własny zestaw rejestrów i instrukcji do operacji na liczbach zmiennoprzecinkowych.

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestry jednostki zmiennoprzecinkowej x87 (ang. Floating-Point Unit)

Rejestry rozszerzenia MultiMedia eXtensions (MMX)

MMX to rozszerzenie zestawu instrukcji o rozkazy pozwalające operować na rejestrach 64-bitowych od MM0 do MM7.

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestry rozszerzenia MMX (MultiMedia eXtensions)

Rejestry rozszerzeń SSE/AVX/AVX-512

SSE to rozszerzenia zestawu instrukcji o rozkazy pozwalające operować na rejestrach 128-bitowych od XMM0 do XMM31. AVX to rozszerzenia zestawu instrukcji o rozkazy pozwalające operować na rejestrach 256-bitowych od YMM0 do YMM31. AVX-512 to rozszerzenie zestawu instrukcji o rozkazy pozwalające operować na rejestrach 512-bitowych od ZMM0 do ZMM31. Wprowadzane kolejne wersje rozszerzeń miały za podstawowy cel możliwość równoległego przetwarzania danych, czyli przeprowadzania operacji na wektorach o coraz to większych rozmiarach. Należy również dodać, że AVX zawiera rozkazy o trzech operandach, a AVX-512 pozwala stosować maski bitowe, które decydują o tym, które bity wyzerować, a które zostawić.

Rejestry od XMM6 do XMM15 są nieulotne (ang. nonvolatile) — ich wartość powinna zostać zachowana na początku funkcji i przywrócona przed wyjściem z funkcji, która używa tych rejestrów.

...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestry rozszerzeń SSE/AVX/AVX-512
...
Rejestry procesora w Asemblerze x86-64 (x64) — Rejestry używane jako maski bitowe w rozkazach rozszerzenia AVX-512

Witaj, świecie Asemblera x64 dla Windows!

W celu wyświetlenia prostego okna dialogowego w systemie Windows można skorzystać z funkcji API o nazwie MessageBox, która posiada dwie wersje takie jak MessageBoxA (ANSI) oraz MessageBoxW (WIDE). W prostych przykładach edukacyjnych można korzystać z wersji ANSI, natomiast w poważniejszych aplikacjach — m.in. w celu zapewnienia poprawnego wyświetlania znaków specjalnych z różnych języków świata — zalecane jest używać funkcji z przyrostkiem W.

Odwołania do zewnętrznych funkcji API można zdefiniować za pomocą dyrektywy extrn. Przykładowy program typu Witaj, świecie! korzysta tutaj z dwóch funkcji Windows API: MessageBoxA (okno dialogowe) oraz ExitProcess (zakończenie programu).

W sekcji .data znajdują się dane zainicjalizowane. Przykładowy program zawiera tutaj ciąg znaków (bajtów) ethical.blue Magazine zakończony wartością NULL (zero). Do zdefiniowanego ciągu znaków można odwoływać się po nadanej mu nazwie, tutaj jest to szText. Bajty i ciągi bajtów można definiować dyrektywą DB co oznacza z ang. define byte.

W sekcji .code umieszcza się kod programu w formie procedur. Przykładowy program zawiera jedną procedurę główną o nazwie Main, której początek i koniec jest oznaczony dyrektywami asemblera o nazwach proc i endp.

Niezwykle ważne jest, aby zgodnie z konwencją wywoływania procedur x64 zarezerwować na stosie programu tak zwane shadow space (miejsce na przekazywane parametry funkcji) oraz, aby stos programu był wyrównany do 16 bajtów. Jeśli parametry funkcji są cztery lub mniej, to przekazuje się je przez specjalne rejestry (RCX/RDX/R8/R9 dla wartości całkowitoliczbowych). Jeśli parametrów jest więcej niż cztery, to pozostałe przekazuje się przez umieszczenie ich na stosie. W przykładowym programie parametrów jest cztery, ale na stosie jest także wcześniej odłożony adres powrotu do Windows po zakończeniu programu. Na stosie jest zatem pięć wartości o rozmiarze 8 bajtów: (4 × 8) + 8 = 40. Stos wtedy nie jest wyrównany do 16 bajtów (liczba 40 nie dzieli się na 16 bez reszty). W celu wyrównania należy zaalokować dodatkowe 8 bajtów, czyli będzie: (4 × 8) + 8 + 8 = 32 + 8 + 8 = 48. Teraz 48 jest podzielne przez 16 bez reszty, czyli stos jest wyrównany. Miejsce na stosie można zaalokować np. rozkazem odejmującym określoną wartość od rejestru wskaźnika stosu, czyli sub rsp, 28h.

Funkcja MessageBoxA, która wyświetla proste okno dialogowe przyjmuje cztery parametry, a jej prototyp prezentuje się następująco:

int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

Pierwszy parametr (uchwyt okna nadrzędnego) jest przekazywany przez rejestr RCX — można podać wartość zero, wtedy okno nadrzędne nie będzie określone. Drugi parametr (treść okna komunikatu) jest przekazywany przez rejestr RDX. Za pomocą rozkazu LEA (ang. load effective address) do rejestru danych wczytywany jest adres ciągu bajtów o nazwie szText. Trzeci parametr (tytuł okna komunikatu) jest przekazywany przez rejestr R8. Tytułem okna jest ten sam ciąg znaków co we wcześniejszym parametrze. Czwarty i ostatni parametr (rodzaj komunikatu, ikony, przyciski) jest przekazywany przez rejestr R9. Wartość zero osiągnięta poprzez wykonanie alternatywy wykluczającej (XOR) rejestru R9 z wartością tego właśnie rejestru określa domyślny typ okna komunikatu z przyciskiem OK.

Program kończy działanie poprzez wywołanie funkcji ExitProcess, która posiada tylko jeden parametr — kod wyjścia. Przyjmuje się, że program zakończony z sukcesem zwraca wartość zero. Należy tutaj zauważyć, że wspomniane wcześniej miejsce shadow space nie musi być zwalniane, ponieważ funkcja ExitProcess zamyka program poprzez zakończenie procesu. Jeśli wyjście z funkcji głównej Main i powrót do systemu Windows byłby realizowany rozkazem RET (ang. return), to należy zwolnić zaalokowane wcześniej miejsce na stosie instrukcją odwrotną do odejmowania (sub rsp, 28h), czyli dodaniem wartości 28h do wskaźnika wierzchołka stosu RSP:

;(...)
    add rsp, 28h
    ret
Main endp
end

Kod źródłowy programu Witaj, świecie!

Code:
extrn MessageBoxA : proc
extrn ExitProcess : proc

.data
    szText db "ethical.blue Magazine", 0

.code
Main proc
    ;(4 * 8) + 8 + 8 = 32 + 8 + 8 = 48
    ;48 / 16 = 3 (O.K. - wyrównane do 16 bajtów)
    sub rsp, 28h

    xor r9, r9
    lea r8, szText
    lea rdx, szText
    xor rcx, rcx
    call MessageBoxA

    xor rcx, rcx
    call ExitProcess
Main endp
end
Download:

Tworzenie projektu dla języka Asembler w środowisku Microsoft Visual Studio

Utworzenie programu w języku Asembler x64 za pomocą środowiska Microsoft Visual Studio polega na modyfikacji projektu dla technologii Visual C++ — należy wykonać kilka prostych czynności, które pozwolą na korzystanie z asemblera MASM x64.

...
Microsoft Visual Studio — Utworzenie nowego projektu

Uruchomienie kreatora aplikacji dla Windows Desktop jest jednym ze sposobów utworzenia projektu Visual C++.

...
Microsoft Visual Studio — Wygodnym rozwiązaniem jest kreator aplikacji dla Windows Desktop (Visual C++)

Od wpisanej nazwy projektu będzie między innymi zależała nazwa pliku wykonywalnego (*.exe).

...
Microsoft Visual Studio — Wpisanie nazwy projektu oraz wybranie lokalizacji dla plików rozwiązania

W celu wyłączenia generowania kodu początkowego w Visual C++ należy wybrać Empty project (pol. pusty projekt). Wybranie Console Application utworzy domyślnie program z widoczną konsolą tekstową. Natomiast wybranie Desktop Application utworzy program dla Windows bez konsoli tekstowej. Możliwe jest też tworzenie bibliotek (ang. library).

...
Microsoft Visual Studio — Aplikacja typu Desktop nie będzie domyślnie tworzyć konsoli tekstowej

Plik z kodem źródłowym można dodać poprzez kliknięcie prawym przyciskiem myszy na Source Files w oknie Solution Explorer i wybranie Add > New Item.

...
Microsoft Visual Studio — W plikach źródłowych okna Eksplorator rozwiązań dodać nowy element

W oknie dialogowym należy podać nazwę pliku źródłowego (np. Main) z rozszerzeniem .asm.

...
Microsoft Visual Studio — Plik z kodem w języku Asembler powinien mieć rozszerzenie .asm

Dalej należy włączyć obsługę Microsoft Macro Assembler poprzez kliknięcie prawym przyciskiem myszy na gałęzi projektu w oknie Solution Explorer i wybranie Build Dependencies > Build Customizations.

...
Microsoft Visual Studio — Dostosowanie budowania projektu poprzez wybranie Build Customizations

Zaznaczenie elementu masm pozwoli na korzystanie z narzędzia Microsoft Macro Assembler.

...
Microsoft Visual Studio — Wybranie masm pozwoli korzystać z Asemblera x86/x64 (marmasm dotyczy Asemblera ARM)

W celu uniknięcia błędów kompilacji należy zweryfikować właściwości pliku źródłowego Main.asm.

...
Microsoft Visual Studio — Otworzenie okna z właściwościami pliku źródłowego Main.asm

Plik źródłowy Main.asm powinien mieć ustawiony odpowiedni typ oraz nie być wyłączony z procesu budowania.

...
Microsoft Visual Studio — Ustawienie rodzaju elementu (Item Type) na Microsoft Macro Assembler

W pliku Main.asm należy umieścić przykładowy kod źródłowy.

...
Microsoft Visual Studio — Uruchomienie przykładowego kodu w Asemblerze w trybie debugowania

Niniejszy komunikat informuje o błędach budowania — plik .exe nie został utworzony.

...
Microsoft Visual Studio — Mogą pojawić się błędy budowania, które należy rozwiązać

Entry Point (pol. punkt wejścia) w pliku źródłowym Main.asm jest różny od punktu wejścia w ustawieniach projektu.

...
Microsoft Visual Studio — Punkt wejścia w kodzie źródłowym to procedura o nazwie Main, natomiast konsolidator oczekiwał nazwy WinMainCRTStartup

Właściwości projektu można dostosować poprzez wybranie Project > ... Properties.

...
Microsoft Visual Studio — Otworzenie właściwości projektu w celu zmiany punktu wejścia programu (ang. entry point)

W zaawansowanych ustawieniach konsolidatora należy wpisać odpowiednią nazwę procedury dla punktu wejścia programu.

...
Microsoft Visual Studio — Zmiana nazwy procedury, która jest punktem wejścia (ang. entry point) na Main

Jeśli proces budowania zakończył się bez błędów, to zostanie utworzony i uruchomiony plik wykonywalny *.exe.

...
Microsoft Visual Studio — Przykładowy program został pomyślnie uruchomiony (wyświetlenie okna MessageBox)

Podstawy składni Microsoft Macro Assembler (MASM x64)

Podstawowe elementy składni Microsoft Macro Assembler (MASM x64, ML64.exe).

Dane lokalne (LOCAL)

Dane lokalne tworzone dyrektywą local są przechowywane na stosie i istnieją na czas wykonywania procedury w której są zaalokowane. Przykładowy kod zawiera podwójne słowo (ang. dword) o nazwie var1 oraz poczwórne słowo (ang. qword) o nazwie var2. Odwoływanie się do danych lokalnych następuje za pomocą nadanej im nazwy. Np. rozkaz mov var1, eax kopiuje wartość z rejestru EAX (rozmiar 32-bity) do danej o nazwie var1 (rozmiar 32-bity), a rozkaz mov var2, rax kopiuje wartość z rejestru RAX (64-bity) do danej o nazwie var2.

Code:
.code
Main proc
    local var1:dword, var2:qword

    mov rax, 0DEADBEEFC0FFEE00h
    mov var1, eax ;var1 = C0FFEE00h
    mov var2, rax ;var2 = 0DEADBEEFC0FFEE00h

    ret
Main endp
end
Download:

Słowo local jest dyrektywą i podczas budowania programu jest ono zamieniane na odpowiednie rozkazy. Instrukcje generowane na początku procedury określane są słowem prolog. Następuje tutaj odłożenie na stos nieulotnego rejestru RBP (push rbp), a wartość wskaźnika wierzchołka stosu RSP jest kopiowana do rejestru RBP (mov rbp, rsp). Natomiast rozkaz add rsp, 0FFFFFFFFFFFFFFF0h rezerwuje na stosie miejsce na dane lokalne. Zwracająca uwagę wygenerowana przez narzędzie instrukcja add rsp, 0FFFFFFFFFFFFFFF0h w celu zwiększenia przejrzystości kodu mogłaby być zastąpiona przez sub rsp, 0Fh (sub rsp, 16), czyli zamiast dodawać (ADD) do RSP wartość -16 — można po prostu odjąć (SUB) od RSP wartość 16.

Instrukcje usuwające dane lokalne ze stosu umieszczane na końcu procedury nazywane są słowem epilog. Za pomocą rozkazu mov rsp, rbp przywracana jest początkowa wartość wskaźnika stosu RSP (przed wykonaniem ADD wskaźnik stosu RSP został zachowany w rejestrze RBP przez instrukcję mov rbp, rsp w prologu). Rozkaz pop rbp w epilogu procedury zdejmuje ze stosu ostatnio odłożoną wartość i umieszcza ją w rejestrze RBP. Następuje tu po prostu przywrócenie wartości rejestrowi RBP, gdyż zgodnie z konwencją wywoływania procedur rejestr RBP jest nieulotny (ang. nonvolatile) i należy przywrócić jego początkową wartość przed wyjściem (RET, ang. return) z procedury, która go modyfikuje (używa).

Warty zanotowania jest także sposób odwoływania się do danych lokalnych. Nazwy var1 czy var2 obowiązujące w kodzie źródłowym są zamieniane na odwołania względem wierzchołka stosu. Zatem rozkaz mov qword ptr ss:[rbp-C], rax oznacza:

Code:
;disassembly
push rbp
mov rbp, rsp
add rsp, 0FFFFFFFFFFFFFFF0h

mov rax, 0DEADBEEFC0FFEE00h
mov dword ptr ss:[rbp-4], eax ;var1 = eax
mov qword ptr ss:[rbp-C], rax ;var2 = rax

mov rsp, rbp
pop rbp
ret
Download:

Stałe (EQU)

Sekcja .const może zawierać wartości stałe, czyli nazwy do których za pomocą dyrektywy EQU (ang. equal) przypisano wartość liczbową lub wyrażenie. Pozwala to uniknąć w kodzie źródłowym tzw. magic numbers, czyli wartości liczbowych, których znaczenie może być niejasne dla osoby czytającej. Na przykład. Wartość 10h jako czwarty parametr funkcji MessageBoxA może wymagać sięgnięcia do dokumentacji w celu sprawdzenia co ta wartość oznacza. Dlatego dobrym zwyczajem jest zastosowanie stałej MB_ICONERROR z nazwy której możliwe jest odczytanie, że wartość ta ustawia ikonę błędu dla okna dialogowego MessageBoxA.

Code:
extrn ExitProcess : proc
extrn MessageBoxA : proc

.const
    MAX equ 0FFFFFFFFFFFFFFFFh
    TRUE equ 1
    FALSE equ 0
    NULL equ 0
    X equ 'X' ;58h
    MINUS16 equ NOT 0Fh ;0FFFFFFFFFFFFFFF0h (-16)
    MsgTitle equ "Error", 0

    ;MessageBoxA
    MB_ICONERROR equ 00000000000000010h

.data
    szCaption db MsgTitle
    szText db "ethical.blue Magazine", NULL

.code
Main proc
    sub rsp, 28h

    mov r9, MB_ICONERROR
    lea r8, szCaption
    lea rdx, szText
    xor rcx, rcx
    call MessageBoxA

    xor rcx, rcx
    call ExitProcess
Main endp
end
Download:

Dane o rozmiarze bajta (BYTE, SBYTE oraz DB)

Dane z nadaną wartością początkową definiuje się w sekcji .data. Za pomocą dyrektywy byte lub w skrócie db (ang. define byte) możliwe jest stworzenie danej o rozmiarze bajta lub danych w postaci ciągu bajtów. Bajt ze znakiem można zdefiniować za pomocą dyrektywy sbyte (ang. signed byte). Nic nie stoi też na przeszkodzie, aby zdefiniować kilobajt np. kilobyte db 1024 dup(0).

Ciągi znaków (napisy) to również bajty (szInformation db "Information", 0). Dlatego możliwe jest np. wstawienie znaku nowej linii obowiązującego w systemie Windows, czyli dwóch bajtów o wartościach 13 i 10:

szText db "ethical.blue", 0Dh, 0Ah, "Magazine", 0

Przykładowe operacje:

Code:
extrn MessageBoxA : proc

.data
    madByte byte 1
    madByte2 db 2
    signedByte sbyte -1
    sevenBytes db 00h, 00h, 00h, 00h
        db 00h, 00h, 00h
    kilobyte db 1024 dup(0)
    szText db "ethical.blue", 0Dh, 0Ah, "Magazine", 0

.code
Main proc
    sub rsp, 28h

    xor r9, r9
    lea r8, szText
    lea rdx, szText
    xor rcx, rcx
    call MessageBoxA

    add rsp, 28h
    ret
Main endp
end
Download:

Dane o rozmiarze słowa (WORD, SWORD oraz DW)

Dane o rozmiarze słowa maszynowego (16 bitów) można definiować dyrektywami WORD lub w skrócie DW (ang. define word). Dla typów danych ze znakiem można użyć dyrektywy SWORD (ang. signed word).

Code:
;(...)
.data
    machineWord word 0
    machineWord2 dw 1
    signedWord sword -1
;(...)

Dane o rozmiarze podwójnego słowa (DWORD, SDWORD oraz DD)

Dane o rozmiarze podwójnego słowa maszynowego (32 bity) można definiować dyrektywami DWORD lub w skrócie DD (ang. define dword). Dla typów danych ze znakiem można użyć dyrektywy SDWORD (ang. signed dword).

Code:
;(...)
.data
    machineDword dword 0
    machineDword2 dd 1
    signedDword sdword -1
;(...)

Dane o rozmiarze poczwórnego słowa (QWORD, SQWORD oraz DQ)

Dane o rozmiarze poczwórnego słowa maszynowego (64 bity) można definiować dyrektywami QWORD lub w skrócie DQ (ang. define qword). Dla typów danych ze znakiem można użyć dyrektywy SQWORD (ang. signed qword).

Code:
;(...)
.data
    machineQword qword 0
    machineQword2 dq 1
    signedQword sqword -1
;(...)

Dane o rozmiarze 48 bitów (FWORD oraz DF)

Typ danych dla liczb zmiennoprzecinkowych i BCD.

Code:
;(...)
.data
    sampleFword fword 0.00
    sampleFword2 df 1.00
;(...)

Dane o rozmiarze 80 bitów (TBYTE oraz DT)

Typ danych dla liczb zmiennoprzecinkowych i BCD.

Code:
;(...)
.data
    tenBytes tbyte 0.25
    tenBytes2 dt 1.33
;(...)

Dane o rozmiarze ośmiokrotnego słowa (OWORD)

Typ danych o rozmiarze ośmiokrotnego słowa (128 bitów).

Code:
;(...)
.data
    octalWord oword 0
;(...)

Dane typu REAL4, REAL8 oraz REAL10 (pol. liczby rzeczywiste)

Typy danych do pracy z jednostką zmiennoprzecinkową x87 (FPU).

Code:
.data
    a real4 0.1 ;4 bajty (pojedyncza precyzja)
    b real8 0.2 ;8 bajtów (podwójna precyzja)
    c real10 0.3 ;10 bajtów (rozszerzona precyzja)

    num1 real8 0.25
    num2 real8 0.15
    num3 real8 0.0

.code
Main proc
    ;Przykład dodawania dwóch liczb rzeczywistych
    fld num1 ;ST0 = num1
    fadd num2 ;ST0 = ST0 + num2
    fstp num3 ;num3 = ST0

    ret
Main endp
end

Dane typu MMWORD (64 bity)

Typ danych do pracy z technologią MMX.

Code:
.data
    mem64 mmword 1.25

.code
Main proc
    mov rax, 7 ;RAX = 7
    movq mm0, rax ;MM0 = RAX
    movq mm1, mem64 ;MM1 = mem64

    ret
Main endp
end

Dane typu XMMWORD (128 bitów)

Typ danych do pracy z instrukcjami rozszerzeń Streaming SIMD Extensions (SSE).

Code:
.data
    mem128 dword 1.0, 2.0, 3.0, 4.0
    mem128b qword 1, 2

.code
Main proc
    ;Przykład transferu wektora liczb zmiennoprzecinkowych pomiędzy pamięcią a rejestrem
    mov rdx, offset mem128 ;RDX = adres wektora mem128
    movups xmm0, xmmword ptr [rdx] ;XMM0 = wartości wektora spod adresu w RDX

    ;Transfer wektora liczb zmiennoprzecinkowych pomiędzy rejestrami
    movaps xmm1, xmm0 ;XMM1 = XMM0

    ;Przykład transferu wektora liczb całkowitych pomiędzy pamięcią a rejestrem
    mov rdx, offset mem128b ;RDX = adres wektora mem128b
    movdqu xmm0, xmmword ptr [rdx] ;XMM0 = wartości spod adresu w RDX

    ;Transfer wektora liczb całkowitych pomiędzy rejestrami
    movdqa xmm1, xmm0 ;XMM1 = XMM0

    ret
Main endp
end

Dane typu YMMWORD (256 bitów)

Typ danych do pracy z instrukcjami rozszerzeń AVX.

Code:
.data
    mem256 dword 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
    mem256b dword 1, 2, 3, 4, 5, 6, 7, 8

.code
Main proc
    ;Przykładowy transfer wektora liczb zmiennoprzecinkowych pomiędzy pamięcią mem256, a rejestrem YMM0
    mov rdx, offset mem256
    vmovups ymm0, ymmword ptr [rdx]

    ;Przykładowy transfer wektora liczb zmiennoprzecinkowych pomiędzy rejestrami (YMM1 = YMM0)
    vmovaps ymm1, ymm0

    ;Przykładowy transfer wektora liczb całkowitych pomiędzy pamięcią mem256b, a rejestrem YMM0
    mov rdx, offset mem256b
    vmovdqu ymm0, ymmword ptr [rdx]

    ;Przykładowy transfer wektora liczb całkowitych pomiędzy rejestrami (YMM1 = YMM0)
    vmovdqa ymm1, ymm0

    ret
Main endp
end

Dane typu ZMMWORD (512 bitów)

Typ danych do pracy z instrukcjami rozszerzenia AVX-512.

Code:
.data
    mem512 dword 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0
    mem512b dword 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16

.code
Main proc
    ;Przykładowy transfer wektora liczb zmiennoprzecinkowych pomiędzy pamięcią mem512 a rejestrem ZMM0
    mov rdx, offset mem512
    vmovups zmm0, zmmword ptr [rdx]

    ;Przykładowy transfer wektora liczb zmiennoprzecinkowych pomiędzy rejestrami (ZMM1 = ZMM0)
    vmovaps zmm1, zmm0

    ;Przykładowy transfer wektora liczb całkowitych pomiędzy pamięcią mem512 a rejestrem ZMM0
    mov rdx, offset mem512b
    vmovdqu64 zmm0, zmmword ptr [rdx]

    ;Przykładowy transfer wektora liczb całkowitych pomiędzy rejestrami (ZMM1 = ZMM0)
    vmovdqa64 zmm1, zmm0

    ret
Main endp
end

Aliasy (nowe nazwy) dla istniejących typów (TYPEDEF)

Utworzenie nowych nazw (aliasów) dla wbudowanych typów danych może zwiększyć przejrzystość kodu źródłowego. Przykładem może być imitacja dwustanowego typu logicznego, który przyjmuje jedną z dwóch wartości: prawda lub fałsz. Tego rodzaju nowy typ danych można utworzyć dyrektywą typedef. Dla typu logicznego bazującego na podwójnym słowie (32-bity) zapis wygląda następująco: bool typedef dword. Wartościami dla typu logicznego mogą być stałe np. false equ 0 (fałsz, czyli zero) oraz true equ 1. Teraz dzięki tym zabiegom możliwe jest utworzenie danej nowego typu np. check bool false. Pamięć o nazwie check imituje teraz dwustanową daną logiczną.

Code:
;Przykładowe aliasy typów danych
bool typedef dword ;bool to dodatkowa nazwa dla dword
char typedef byte ;char to dodatkowa nazwa dla byte
int64 typedef sqword ;int64 to dodatkowa nazwa dla sqword
uint64 typedef qword ;uint64 to dodatkowa nazwa dla qword

.const
    false equ 0 ;stała o nazwie false równa zero
    true equ 1 ;stała o nazwie true równa jeden

.data
    check bool false ;dane typu bool (dword) o wartości false (zero)
    num1 int64 0DEADBEEFh ;dane typu int64 o wartości 0xDEADBEEF

.code
Main proc
    ;Przykład pobrania rozmiaru danego typu w bajtach
    mov r8, sizeof int64 ;R8 = 8
    mov r9, sizeof bool ;R9 = 4

    ret
Main endp
end

Etykiety (ang. labels)

Etykietami można oznaczać miejsca w kodzie źródłowym do których przekazywane jest wykonanie programu. Dzięki temu mechanizmowi możliwe jest wielokrotne wykonywanie fragmentu kodu (pętla) czy też pominięcie fragmentu kodu poprzez skok za ten kod. Etykiety anonimowe oznaczane są znakami @@: natomiast etykiety nazwane mogą mieć nadaną wymyśloną nazwę np. _exit, _next, _skip etc. W przykładowym programie instrukcja JNE @b (@b, czyli ang. backward) wykonuje warunkowy (jeśli nierówne, ang. jump if not equal) skok wstecz do najbliższej anonimowej etykiety (@@). Podobnie jest z przejściem do najbliższej anonimowej etykiety wprzód skokiem bezwarunkowym JMP @f (ang. jump forward). W przypadku etykiety nazwanej zamiast @b (ang. backward) czy @f (ang. forward) należy podać nazwę tej etykiety jako operand rozkazu zmieniającego przepływ wykonania.

Code:
.code
Main proc
    mov rax, 3 ;rejestr RAX przyjmuje wartość trzy
@@:
    dec rax ;zmniejsz wartość rejestru RAX o jeden
    test rax, rax ;sprawdź czy rejestr RAX równy zero
    jne @b ;jeśli nie, to skocz wstecz do anonimowej etykiety @@

    mov rax, 9 ;rejestr RAX przyjmuje wartość dziewięć
    jmp @f ;bezwarunkowy skok wprzód do anonimowej etykiety @@
    mov rax, 5 ;to się nie wykona (jest przeskoczone)
@@:
    sub rax, rax ;wyzerowanie rejestru RAX
    jmp _exit ;bezwarunkowy skok do etykiety nazwanej _exit

_exit:
    ret ;powrót do systemu Windows
Main endp
end
Download:

Tablice (ang. arrays)

Ciąg elementów takiego samego typu można nazwać tablicą. Jeśli tablica przechowuje wartości liczbowe dla rozkazów typu SIMD, to używana jest też nazwa wektor. Napisy (ciągi znaków) to również tablice — są to bajty odpowiadające poszczególnym znakom według zastosowanego kodowania (np. ASCII).

Code:
.data
    var1 byte 1, 2, 3, 4, 5, 6, 7 ;przykładowa tablica bajtów
    var2 word 1, 2, 3, 4, 5, 6, 7 ;przykładowa tablica słów maszynowych
    var3 dword 1, 2, 3, 4, 5, 6, 7 ;przykładowa tablica podwójnych słów maszynowych
    var4 qword 1, 2, 3, 4, 5, 6, 7 ;przykładowa tablica poczwórnych słów maszynowych

    bigArray1 dword 64 dup(0FFFFFFFFh) ;przykładowa tablica 64 elementy (podwójne słowa) z początkową wartością 0FFFFFFFFh każdego elementu
    bigArray2 byte 512 dup('?') ;przykładowa tablica 512 bajtów z początkową wartością 03Fh (kod ASCII znaku zapytania) każdego elementu

.code
Main proc
    ;Przykładowe uzyskania dostępu do określonego elementu tablicy

    xor rax, rax ;wyzerowanie rejestru RAX
    lea rdx, var1 ;wczytanie do rejestru RDX adresu tablicy var1
    mov rcx, 4 ;indeks elementu tablicy do którego uzyskuje się dostęp (od zera!)
    mov al, byte ptr [rdx + rcx * sizeof byte] ;kopiowanie piątego elementu tablicy bajtów do rejestru AL

    xor rax, rax ;wyzerowanie rejestru RAX
    lea rdx, var2 ;wczytanie do rejestru RDX adresu tablicy var2
    mov rcx, 3 ;indeks elementu tablicy do którego uzyskuje się dostęp (od zera!)
    mov ax, word ptr [rdx + rcx * sizeof word] ;kopiowanie czwartego elementu tablicy słów do rejestru AX

    xor rax, rax ;wyzerowanie rejestru RAX
    lea rdx, var3 ;wczytanie do rejestru RDX adresu tablicy var3
    mov rcx, 2 ;indeks elementu tablicy do którego uzyskuje się dostęp (od zera!)
    mov eax, dword ptr [rdx + rcx * sizeof dword] ;kopiowanie trzeciego elementu tablicy podwójnych słów do rejestru EAX

    xor rax, rax ;wyzerowanie rejestru RAX
    lea rdx, var4 ;wczytanie do rejestru RDX adresu tablicy var4
    mov rcx, 1 ;indeks elementu tablicy do którego uzyskuje się dostęp (od zera!)
    mov rax, qword ptr [rdx + rcx * sizeof qword] ;kopiowanie drugiego elementu tablicy poczwórnych słów do rejestru RAX

    ret
Main endp
end
Download:
Przykładowa iteracja po tablicy bajtów (pętla)

Przykładowy kod w którym umieszczono zaciemniony (ang. obfuscated), ale niegroźny ładunek (ang. payload) w postaci ciągu bajtów kodu maszynowego nazwanego payload, to tylko mała prezentacja możliwości kontroli nad programem w języku Asembler, a czego nie pozwoli zrobić większość języków wysokiego poziomu abstrakcji.

Ciąg bajtów nazwany payload jest umieszczony nietypowo w sekcji kodu (.code), a nie danych (.data). Bajty te są potraktowane alternatywą wykluczającą (XOR) z kluczem o wartości trzynaście (0Dh). Domyślnie nie ma możliwości zapisu do sekcji kodu, dlatego można wyłączyć to zabezpieczenie funkcją VirtualProtect, która zmienia atrybuty pamięci na PAGE_EXECUTE_READWRITE (pol. wykonywanie, czytanie i pisanie). Teraz powinna stać się możliwa modyfikacja własnego kodu przez program. Po wykonaniu pętli, która odciemnia bajty kodu maszynowego rozkazem xor byte ptr [rdx + rcx * sizeof byte], 0Dh następuje wczytanie adresu ładunku (ang. payload) do rejestru RAX (lea rax, payload) i wywołanie tak, jakby to była procedura (call rax).

;payload
90h | nop ;9Dh xor 0Dh = 90h
90h | nop ;(...)
90h | nop
90h | nop
90h | nop
90h | nop
90h | nop
C3h | ret ;CEh xor 0Dh = C3h

Ładunek (ang. payload) zawiera zawiera siedem pustych instrukcji NOP (ang. No Operation) oraz rozkaz powrotu z wywołania RET (ang. return).

Code:
extrn VirtualProtect : proc
extrn ExitProcess : proc

.const
    PAGE_EXECUTE_READWRITE equ 040h
    NULL equ 0

.data
    oldProtect dd 0

.code
Main proc
    sub rsp, 28h

    mov r9, offset oldProtect ;zachowanie poprzednich atrybutów pamięci
    mov r8, PAGE_EXECUTE_READWRITE ;flaga ustawiająca blokowi kodu atrybuty pozwalające na wykonanie, czytanie i pisanie
    mov rdx, sizeof payload ;rozmiar bloku bajtów do zmiany atrybutów
    lea rcx, payload ;adres do bloku bajtów
    call VirtualProtect ;zmiana atrybutów pamięci

    sub rcx, rcx ;wyzerowanie rejestru licznika RCX
    lea rdx, payload ;wczytanie do rejestru RDX adresu pamięci o nazwie payload
@@:
    xor byte ptr [rdx + rcx * sizeof byte], 0Dh ;alternatywa wykluczająca poszczególnych bajtów ładunku (ang. payload) z kluczem 0Dh
    inc rcx ;zwiększenie o jeden wartości rejestru RCX
    cmp rcx, sizeof payload ;sprawdzenie czy wartość licznika RCX osiągnęła wartość rozmiaru ładunku (ang. payload)
    jne @b ;jeśli nie, to skok do etykiety anonimowej wstecz (pętla)

    lea rax, payload ;wczytanie do rejestru RAX adresu do bajtów ładunku (ang. payload)
    call rax ;odłożenie na stos adresu powrotnego i skok do bajtów ładunku (ang. payload)
    xor rcx, rcx ;kod wyjścia = zero
    call ExitProcess ;zakończenie programu

    ;ciąg bajtów zawierający kod maszynowy jest zdefiniowany w sekcji kodu
    payload db 09Dh, 09Dh, 09Dh, 09Dh, 09Dh, 09Dh, 09Dh, 0CEh
Main endp
end
Download:

Ciągi tekstowe (ang. text strings)

W celu poprawnego wyświetlania i przetwarzania napisów zalecane jest używanie funkcji WinAPI z przyrostkiem W (np. MessageBoxW zamiast MessageBoxA). Jednak nie zawsze jest to możliwe. W celu uniknięcia błędów kodowania może być wymagane przeprowadzenie konwersji napisu z ANSI do Unicode. Przykładowy program prezentuje wywołanie funkcji MultiByteToWideChar w celu uzyskania napisu w kodowaniu Unicode, który zostanie później wyświetlony przez funkcję MessageBoxW. Ciąg tekstowy zawiera cyrylicę Мова Aсемблера (Mova Asemblera), aby zapewnić możliwość łatwego zweryfikowania czy tekst zostanie poprawnie wyświetlony.

Code:
extrn MessageBoxW : proc
extrn MultiByteToWideChar : proc
extrn ExitProcess : proc

.const
    CP_UTF8 equ 65001

.data
    multibyteText db "Мова Aсемблера (Mova Asemblera)", 0
    wideText db 512 dup(0)

.code
Main proc
    sub rsp, 38h

    mov qword ptr [rsp+28h], sizeof wideText
    lea rdx, wideText
    mov qword ptr [rsp+20h], rdx
    mov r9, sizeof multibyteText
    lea r8, multibyteText
    xor rdx, rdx
    mov rcx, CP_UTF8
    call MultiByteToWideChar

    xor r9, r9
    lea r8, wideText
    lea rdx, wideText
    xor rcx, rcx
    call MessageBoxW

    xor rcx, rcx
    call ExitProcess
Main endp
end
Download:

Struktury (ang. structures)

Dyrektywy asemblera struct i ends pozwalają na tworzenie nowych typów danych w postaci struktur. Tego rodzaju kompozyt może zawierać dane różnego typu. Przykładowy program zawiera strukturę przedmiotu (ang. item) na którą składa się tytuł (napis titleEN o maksymalnej długości 128 bajtów) oraz cena (liczba rzeczywista price typu real4). Dane typu strukturalnego można definiować w sekcji .data podając nazwę (sampleItem), typ (ItemStructure) oraz wartości początkowe w nawiasach ostrych (<"This is sample item.", 9.99>). Dostęp do określonego pola struktury można uzyskać za pomocą kropki (np. sampleItem.titleEN czy sampleItem.price).

Code:
extrn MessageBoxA : proc
extrn ExitProcess : proc

ItemStructure struct
    titleEN byte 128 dup(0)
    price real4 0.0
ItemStructure ends

.data
    sampleItem ItemStructure <"This is sample item.", 9.99>

.code
Main proc
    sub rsp, 28h
    xor r9, r9
    lea r8, sampleItem.titleEN
    lea rdx, sampleItem.titleEN
    xor rcx, rcx
    call MessageBoxA

    xor rcx, rcx
    call ExitProcess
Main endp
end
Download:

Unie (ang. unions)

Unia różni się od struktury między innymi tym, że pola w niej zawarte są na siebie nałożone. Przykładowy program zawiera unię myUnion z trzema polami: var1, var2 i var3. Nałożenie tych pól na siebie oznacza, że po zapisaniu do pola var2 przykładowej wartości 01234h, a następnie odczytaniu wartości innego pola (var3) w rezultacie otrzymaną wartością będzie 01234h. Z tego też wynika, że rozmiar unii jest równy rozmiarowi jej największego pola.

Code:
myUnion union
    var1 dword 0
    var2 qword 0
    var3 qword 0
myUnion ends

.data
    union1 myUnion <7>

.code
Main proc
    mov r8d, union1.var1
    mov r9, union1.var2
    mov r10, union1.var3

    ;sizeof myUnion == sizeof qword == 8 bajtów
    mov r8, sizeof myUnion

    ret
Main endp
end
Download:

Zagnieżdżone struktury i unie

Struktury i unie umieszczone jedne w drugiej pozwalają na tworzenie nawet bardzo złożonych typów kompozytowych. Przykładowy program zawiera strukturę reprezentującą broń (ang. weapon), która zawiera w sobie pola nameEN (pol. nazwa), price (pol. cena) oraz weight (pol. waga). Struktura Weapon zawiera też unię dotyczącą zadawanych obrażeń (meleeDamage i rangedDamage) oraz zagnieżdżoną strukturę określającą czy broń jest zatruta (poisoned) czy poświęcona (ang. holy).

Wartości początkowe nadaje się w nawiasach ostrych (steelAxe Weapon <"Steel Axe", 449.99, <8>, 300, <0,1>>). Dostęp do poszczególnych pól możliwy jest za pomocą kropki, a rozmiar struktury da się obliczyć za pomocą sizeof.

Code:
Weapon struct
    nameEN byte 128 dup(0)
    price real4 0.0
    union
        meleeDamage qword 0
        rangedDamage qword 0
    ends
    weight qword 0
    struct
        poisoned byte 0
        holy byte 0
    ends
Weapon ends

.data
    steelAxe Weapon <"Steel Axe", 449.99, <8>, 300, <0,1>>

.code
Main proc

    ;150 bajtów == 128 (nameEN) + 4 (price) + 8 (union) + + 8 (weight) + 1 (poisoned) + 1 (holy)
    mov r8, sizeof steelAxe

    ret
Main endp
end
Download:

Procedury (ang. procedures)

Tworzenie procedur ma głównie na celu podzielenie kodu źródłowego na mniejsze fragmenty, które mogą być później wielokrotnie wywoływane, zamiast wklejania ich w miejsca gdzie są potrzebne. Przykładowy program prezentuje wydzieloną procedurę SampleProcedure, która wyświetla okno dialogowe MessageBoxA.

Procedura SampleProcedure nie zawiera danych lokalnych, ale zgodnie z konwencją przygotowuje ramkę stosu rozkazami:

push rbp
mov rbp, rsp
sub rsp, 20h

A przed wyjściem z procedury (RET) zwalnia miejsce na stosie rozkazami:

add rsp, 20h
leave

Kod źródłowy przykładowego programu:

Code:
extrn MessageBoxA : proc
extrn ExitProcess : proc

.data
    szCaption db "ethical.blue", 0
    szText db "Calling procedure example.", 0

.code
SampleProcedure proc
    push rbp
    mov rbp, rsp
    sub rsp, 20h

    xor r9, r9
    lea r8, szCaption
    lea rdx, szText
    xor rcx, rcx
    call MessageBoxA

    add rsp, 20h
    leave
    ret
SampleProcedure endp

Main proc
    sub rsp, 28h
    call SampleProcedure

    xor rcx, rcx
    call ExitProcess
Main endp
end
Download:

Makra (ang. macros)

Makra pozwalają ukryć fragmenty kodu pod warstwą abstrakcji. Mechanizm działania makroinstrukcji jest inny od procedur. Nie używa się tutaj rozkazów wywoływania (CALL) czy rezerwowania miejsca na stosie.

Makro można sobie wyobrazić jako szablon kodu, który zostanie wklejony w miejscu użycia makra. Na przykład:

bye macro
    xor rcx, rcx
    call ExitProcess
endm

pozwala na użycie w kodzie makroinstrukcji bye, która podczas budowania programu zostanie zamieniona na fragment kodu:

xor rcx, rcx
call ExitProcess

Przykładowy program zawiera też inne makro o nazwie msgbox, które przyjmuje dwa parametry text i caption decydujące o treści i tytule okna dialogowego MessageBoxA.

Code:
extrn MessageBoxA : proc
extrn ExitProcess : proc

msgbox macro text, caption
    xor r9, r9
    lea r8, caption
    lea rdx, text
    xor rcx, rcx
    call MessageBoxA
endm

bye macro
    xor rcx, rcx
    call ExitProcess
endm

.data
    szCaption db "ethical.blue", 0
    szText db "Welcome to Windows x64 Assembly Language world!", 0

.code
Main proc
    sub rsp, 28h
    msgbox szText, szCaption

    bye
Main endp
end
Download:

Podstawowe rozkazy procesora x86/x64

Przegląd najważniejszych rozkazów rdzenia x86/x64.

Instrukcje transferu danych

Przykładowe operacje:

Instrukcje arytmetyczne

Przykład dodawania:

Przykład odejmowania:

Przykład mnożenia ze znakiem:

Przykład mnożenia bez znaku:

Przykład dzielenia ze znakiem:

Przykład dzielenia bez znaku:

Przykład inkrementacji:

Przykład dekrementacji:

Przykład negacji z uzupełnieniem do dwóch:

Instrukcje logiczne

W celu zwiększenia przejrzystości działania poszczególnych operacji logicznych można zadeklarować dwie przykładowe dane o nazwach mem16 i mem16_2, którym przypisano wartości w reprezentacji binarnej (przyrostek b).

.data
mem16 word 1010110001110111b
mem16_2 word 0111101110110110b
;(...)

Przykładowa koniunkcja (AND):

Przykładowa alternatywa (OR):

Przykładowa alternatywa wykluczająca (XOR):

Przykładowe zaprzeczenie (NOT):

Przykładowa dysjunkcja (ANDN):

Instrukcje przesunięć i obrotów

Przykładowe przesunięcie logiczne w lewo:

Przykładowe przesunięcie arytmetyczne w lewo:

...
Przesunięcie arytmetyczne/logiczne w lewo (SAL/SHL) — Asembler x64

Przykładowe przesunięcie logiczne w prawo:

...
Przesunięcie logiczne w prawo (SHR) — Asembler x64

Przykładowe przesunięcie arytmetyczne w prawo (zachowuje bit znaku):

...
Przesunięcie arytmetyczne w prawo (SAR) — Asembler x64

Przykładowa rotacja bitów w lewo:

...
Rotacja bitów w lewo (ROL) — Asembler x64

Przykładowa rotacja bitów przez flagę przeniesienia CF w lewo:

...
Rotacja bitów przez flagę przeniesienia CF w lewo (RCL) — Asembler x64

Przykładowa rotacja bitów w prawo:

...
Rotacja bitów w prawo (ROR) — Asembler x64

Przykładowa rotacja bitów przez flagę przeniesienia CF w prawo:

...
Rotacja bitów przez flagę przeniesienia CF w prawo (RCR) — Asembler x64

Przykładowe przesunięcie logiczne pary rejestrów w lewo:

...
Przesunięcie logiczne pary rejestrów w lewo (SHLD) — Asembler x64

Przykładowe przesunięcie logiczne pary rejestrów w prawo:

...
Przesunięcie logiczne pary rejestrów w prawo (SHRD) — Asembler x64

Instrukcje kontroli przepływu wykonania

Przykładowy program z pętlą oraz instrukcjami skoku:

Code:
.code
Main proc
    xor rax, rax ;wyzerowanie rejestru RAX
    mov rcx, 13 ;ustawienie rejestru licznika RCX na wartość trzynaście
@@:
    ;rozkazy umieszczone tutaj wykonają się trzynaście razy
    inc rax ;zwiększenie wartości rejestru RAX o jeden (przykładowa instrukcja)
loop @b ;zmniejszenie rejestru RCX o jeden i dopóki ma wartość większą od zera następuje skok wstecz do etykiety @@

    mov rax, 1 ;wczytanie do rejestru RAX wartości natychmiastowej jeden
    mov rcx, 2 ;wczytanie do rejestru RCX wartości natychmiastowej dwa
    add rax, rcx ;dodanie wartości z rejestru RCX do wartości w rejestrze RAX (rezultat w RAX)
    cmp rax, 3 ;porównanie czy wartość rejestru RAX to trzy
    je _seven ;jeśli tak, to skok do etykiety _seven
    jmp _eight ;jeśli nie, to skok do etykiety _eight

_seven:
    mov rax, 7 ;wczytanie do rejestru RAX wartości natychmiastowej siedem
    ret ;powrót do systemu Windows

_eight:
    mov rax, 8 ;wczytanie do rejestru RAX wartości natychmiastowej osiem
    ret ;powrót do systemu Windows
Main endp
end
Download:

Rozgałęzień i skoków można uniknąć stosując inne rozkazy warunkowe. Na przykład instrukcja CMOVZ Operand I, Operand II powoduje kopiowanie operandu drugiego do operandu pierwszego, ale tylko wtedy, gdy rezultatem ostatniej operacji arymetycznej lub logicznej jest wartość zero (jeśli flaga ZF jest ustawiona na jeden).

Code:
.data
    mem64 dq 0C0FFEEh ;przykładowe dane (poczwórne słowo)
    mem64_2 dq 07EAh ;przykładowe dane (poczwórne słowo)

.code
Main proc

    mov rax, 3 ;wczytanie do rejestru RAX wartości natychmiastowej trzy
    sub rax, 3 ;rezultat operacji arymetycznej/logicznej to wartość zero
    cmovz rcx, mem64 ;dlatego kopiowanie danej mem64 do rejestru RCX wykona się

    mov rax, 3 ;wczytanie do rejestru RAX wartości natychmiastowej trzy
    sub rax, 1 ;rezultat operacji arymetycznej/logicznej to wartość różna od zera
    cmovz rcx, mem64_2 ;dlatego kopiowanie danej mem64_2 do rejestru RCX w tym przypadku nie wykona się

    ret
Main endp
end
Download:

Instrukcje do operacji na stosie

W obszarze pamięci programu nazywanej stosem znajduje się miejsce m.in. na dane lokalne (dyrektywa LOCAL), adresy powrotne odłożone przez rozkaz CALL (pobierane przez rozkaz RET, aby określić do jakiego miejsca/instrukcji wrócić) czy też miejsce nazywane shadow space na parametry procedury (miejsce należy zapewnić zgodnie z konwencją).

W wielu tekstach stos jest nazywany kolejką LIFO (pol. ostatni na wejściu, to pierwszy na wyjściu). Można to rozumieć, że ostatnia wartość odłożona przez rozkaz PUSH, zostanie zdjęta jako pierwsza przez rozkaz POP i umieszczona w operandzie docelowym.

Dalej idąc za tą logiką można zauważyć, że odkładając dwie przykładowe dane na stos:

push mem64
push mem64_2

W celu odczytania mem64 należy najpierw zdjąć ze stosu daną mem64_2, ponieważ została odłożona jako ostatnia, czyli jest pierwsza na wyjściu.

pop r8 ;r8 = mem64_2
pop r9 ;r9 = mem64

W ten sposób zachowuje się struktura danych o nazwie kolejka LIFO określana też stosem. Jednak w Asemblerze możliwe jest odczytanie wartości ze stosu, która mówiąc obrazowo „jest przyłożona innymi wartościami”. Można to wykonać np. rozkazem mov rax, qword ptr [rsp+8h], który odczytuje przedostatnią wartość (poczwórne słowo, 8 bajtów) odliczając od wierzchołka stosu (RSP) i zachowuje tę wartość w rejestrze RAX. Bardzo ważny jest też fakt, aby nie polegać na odczytywanych wartościach względem wierzchołka stosu z ujemnym przesunięciem (np. [rsp-8]), ponieważ wartości te mogą zostać zmodyfikowane (odczytana wartość może być inna, niż wartość zapisana wcześniej w tym miejscu).

...
Stos programu — schemat fragmentu pamięci

Przykładowe operacje:

Code:
.data
    mem64 qword 0C0FFEEh

.code
Main proc
    enter 0, 0 ;przygotowanie ramki stosu dla procedury bez parametrów (zachowanie rejestru RSP w rejestrze RBP)

    ;Przykładowe operacje na stosie programu

    push mem64 ;odłożenie na stos danej mem64 (poczwórne słowo)

    mov rax, mem64 ;kopiowanie pamięci mem64 do rejestru RAX
    push rax ;odłożenie wartości rejestru RAX na stosie

    push 07EAh ;odłożenie wartości natychmiastowej na stosie

    pop r8 ;pobranie ostatniej wartości ze stosu i umieszczenie w rejestrze R8
    pop r9 ;pobranie ostatniej wartości ze stosu i umieszczenie w rejestrze R9
    pop rcx ;pobranie ostatniej wartości ze stosu i umieszczenie w rejestrze RCX

    sub rsp, 8 ;zmniejszenie wartości rejestru wskaźnika stosu (RSP) o wartość osiem
    mov qword ptr [rsp], 0DEADBEEFh ;odłożenie wartości natychmiastowej na wierzchołek stosu

    pop rax ;pobranie ostatniej wartości ze stosu i umieszczenie w rejestrze RAX

    leave ;usunięcie ramki stosu i przywrócenie wartości rejestrowi RSP
    ret
Main endp
end
Download:

Instrukcja do generowania liczb pseudolosowych

Rozkaz RDRAND (ang. read random) pozwala odczytać wygenerowaną sprzętowo wartość pseudolosową i umieścić ją w operandzie docelowym. Ziarno (ang. seed) generatora jest ustalane przez układ elektroniczny.

Code:
extrn ExitProcess : proc

.code
Main proc
    rdrand rcx ;wygenerowanie wartości pseudolosowej w rejestrze RCX
    jnc _error ;jeśli wystąpił błąd, to przejdź do procedury obsługi błędów
    rdrand rcx ;wygenerowanie kolejnej wartości pseudolosowej w rejestrze RCX
    jnc _error
    rdrand rcx ;wygenerowanie kolejnej wartości pseudolosowej w rejestrze RCX
    jnc _error
    rdrand rcx ;wygenerowanie kolejnej wartości pseudolosowej w rejestrze RCX
    jnc _error
    rdrand rcx ;wygenerowanie kolejnej wartości pseudolosowej w rejestrze RCX
    jnc _error

_error:
    ;tutaj powinna być obsługa błędów
    sub rsp, 28h
    xor rcx, rcx
    call ExitProcess
Main endp
end
Download:

Format rozkazów procesorów x64

Instrukcje procesora w formie tekstowej umieszczane w kodzie źródłowym nazywane są mnemonikami (ang. mnemonic) np. ADD (dodawanie), SUB (odejmowanie) czy XOR (alternatywa wykluczająca).

...
Przykładowe rozkazy procesora x64 w postaci tekstowej (kod źródłowy)

Natomiast kody operacyjne (and. opcode), to rozkazy zakodowane w formie binarnej np. 0x90 (NOP) czy 0xC3 (RET). Jednak instrukcja w formie binarnej przeważnie zawiera nie tylko kod operacyjny, ale także prefiksy, bajt ModR/M, bajt SIB etc.

Na przykład rozkaz xor r8, r9 zawiera:

Format kodowania rozkazów procesora x64 (włączając rozszerzenia podstawowego zestawu instrukcji) to bardzo rozległy temat. Wartości do jakich są kodowane podstawowe instrukcje z czasem się zapamiętuje i rozpoznaje np. w zrzucie pamięci (ang. dump). Jednak zagłębienie się w szczegóły techniczne formatu takie jak np. co oznaczają poszczególne bity i jak jedne wartości są zależne od drugich może być wymagane w przypadku tworzenia własnego modułu typu dezasembler.

...
Przykładowe rozkazy procesora x64 w postaci binarnej (kod maszynowy)

Funkcje Windows API

W przypadku funkcji, które przyjmują cztery lub mniej parametrów, to wartości całkowite przekazywane są przez rejestry:

Wartości zmiennoprzecinkowe (ang. floating point) są przekazywane przez rejestry:

Jeśli parametrów jest więcej niż cztery, to reszta parametrów jest przekazywana przez stos.

Przykład dla funkcji CreateFileA prezentuje się następująco:

Code:
mov qword ptr [rsp+30h], 0 ;siódmy parametr
mov qword ptr [rsp+28h], FILE_ATTRIBUTE_NORMAL ;szósty parametr
mov qword ptr [rsp+20h], CREATE_ALWAYS ;piąty parametr
xor r9, r9 ;czwarty parametr
xor r8, r8 ;trzeci parametr
mov rdx, GENERIC_WRITE ;drugi parametr
mov rcx, offset szDestFileName ;pierwszy parametr
call CreateFileA
Download:

Wartości zwracanej z funkcji WinAPI należy oczekiwać w rejestrze akumulatora RAX. Wywołania funkcji API mogą zniszczyć wartości rejestrów R10 i R11, ponieważ rejestry te są używane przy wywołaniach systemowych SYSCALL. Rejestry R12, R13, R14, R15, RSI, RDI, RBX, RBP, RSP, XMM6:XMM15 są nieulotne (ang. nonvolatile) i należy zachować ich wartość na początku funkcji, a potem ją przywrócić przed powrotem z funkcji, która modyfikuje te rejestry.

Optymalizacja kodu pod względem rozmiaru

Odpowiedni wybór rozkazów do wykonania zaplanowanych działań może pozwolić uzyskać mniejszy rozmiar pliku wykonywalnego. Ma to znaczenie w różnych sytuacjach np. przy pobieraniu ładunku (ang. payload) z sieci czy wyszukiwaniu w pliku wykonywalnym miejsca na umieszczenie implantu.

...
Przykładowe optymalizacje kodu względem rozmiaru, czyli jak uzyskać mniej bajtów kodu maszynowego

Podstawy rozszerzenia MultiMedia eXtensions (MMX)

Rozszerzenie typu SIMD (pol. jedna instrukcja - wiele danych). Zawiera rozkazy do operacji na wektorach wartości całkowitych jak i zmiennoprzecinkowych. Rezultat może być wartością ze znakiem, bez znaku lub z saturacją.

...
Operacje na wektorach (ang. packed) i skalarach (ang. scalar)

Transfer danych za pomocą instrukcji MMX

Przykładowy kod prezentuje jak za pomocą rozkazów MOVD (ang. move dword) oraz MOVQ (ang. move quadword) możliwe jest przesyłanie danych pomiędzy rejestrami ogólnego przeznaczenia i rejestrami MMX oraz pomiędzy rejestrami MMX i pamięcią.

Code:
.data
    mem64 mmword 1.0
    myDword dword 0C0FFEEh
    myQword qword 0ADD1C7EDh
.code
Main proc
    movd mm0, myDword ;kopiowanie danych z pamięci o rozmiarze podwójnego słowa do rejestru MM0
    movq mm1, myQword ;kopiowanie danych z pamięci o rozmiarze poczwórnego słowa do rejestru MM1

    mov eax, 7 ;kopiowanie wartości natychmiastowej do rejestru akumulatora EAX
    movd mm0, eax ;kopiowanie wartości z rejestru ogólnego przeznaczenia (EAX) do rejestru MM0

    mov rax, 8 ;kopiowanie wartości natychmiastowej do rejestru akumulatora RAX
    movq mm0, rax ;kopiowanie wartości z rejestru ogólnego przeznaczenia (RAX) do rejestru MM0

    mov rcx, offset mem64 ;wczytanie adresu danych w pamięci o rozmiarze 64-bit do rejestru RCX
    movq mm1, mmword ptr [rcx] ;wczytanie wartości spod adresu w RCX do rejestru MM1

    mov r8, sizeof mmword ;wczytanie do rejestru R8 rozmiaru typu mmword (w bajtach)

    ;(...)

    ret
Main endp
end
Download:

Operacje arytmetyczne na wektorach w technologii MMX

Przykładowy kod prezentuje jak za pomocą rozkazów PADDB, PADDW, PADDD możliwe jest dodawanie wektorów bajtów, słów oraz podwójnych słów.

Code:
.data
    mem64a qword 0FEFFFFFFFFFFFEFDh ;dane o rozmiarze poczwórnego słowa
    mem64b qword 00101010101010101h ;dane o rozmiarze poczwórnego słowa
.code
Main proc
    movq mm0, mem64a ;kopiowanie pamięci mem64a do rejestru MM0
    movq mm1, mem64b ;kopiowanie pamięci mem64b do rejestru MM1
    paddb mm0, mm1 ;dodanie wektora bajtów z MM1 do wektora bajtów w MM0

    mov rax, 0FFFEFFFDFFFEFFFFh ;kopiowanie wartości natychmiastowej do rejestru RAX
    movq mm0, rax ;kopiowanie wartości z rejestru RAX do rejestru MM0
    mov rax, 00001000100010001h ;kopiowanie wartości natychmiastowej do rejestru RAX
    movq mm1, rax ;kopiowanie wartości z rejestru RAX do rejestru MM1
    paddw mm0, mm1 ;dodanie wektora słów z MM1 do wektora słów w MM0

    mov rax, 0FFFFFFFAFFFFFFFFh ;kopiowanie wartości natychmiastowej do rejestru RAX
    movq mm0, rax ;kopiowanie wartości z rejestru RAX do rejestru MM0
    mov rax, 00000000100000001h ;kopiowanie wartości natychmiastowej do rejestru RAX
    movq mm1, rax ;kopiowanie wartości z rejestru RAX do rejestru MM1
    paddd mm0, mm1 ;dodanie wektora podwójnych słów z MM1 do wektora podwójnych słów w MM0

    ;PADDSB - dodawanie wektorów bajtów ze znakiem z saturacją
    ;PADDSW - dodawanie wektorów słów ze znakiem z saturacją
    ;PADDUSB - dodawanie wektorów bajtów bez znaku z saturacją
    ;PADDUSW - dodawanie wektorów słów bez znaku z saturacją

    movq mm0, mem64a ;kopiowanie pamięci mem64a do rejestru MM0
    movq mm1, mem64b ;kopiowanie pamięci mem64b do rejestru MM1
    psubb mm0, mm1 ;odjęcie wektora bajtów z MM1 od wektora bajtów w MM0

    ;(...)

    ret
Main endp
end
Download:

Przewaga technologii MMX nad zwykłymi instrukcjami polegała m.in. na możliwości dodania np. ośmiu bajtów jedną instrukcją, zamiast dodawania pojedynczo bajtów w stylu:
MOV AL, 0FEh ;kopiowanie wartości 0FEh do rejestru AL
ADD AL, 01h ;dodanie jeden do wartości w rejestrze AL, czyli AL teraz 0FFh

Przykładowa operacja dodawania wektorów bajtów: 0xFEFFFFFFFFFFFEFD + 0x0101010101010101 = 0xFF0000000000FFFE za pomocą rozkazu PADDB.

...
Dodawanie wektorów bajtów za pomocą MMX

Operacje logiczne na wektorach w technologii MMX

Przykładowy kod prezentuje operacje logiczne takie jak: koniunkcja (pand), dysjunkcja (pandn), alternatywa (por), alternatywa wykluczająca (pxor) oraz przesunięcie logiczne w lewo (psllw). Wartości w przykładowym kodzie zostały tak dobrane, aby w łatwy sposób zrozumieć działanie instrukcji logicznych. Jeśli przy koniunkcji: 1 and 1 = 1, to dla bajtów z wszystkimi bitami ustawionymi: 0FFh and 0FFh = 0FFh. Dalej, jeśli: 1 and 0 = 0, to 0FFh and 00h = 00h. Pozostałe operacje analogicznie.

Poniżej wymienionych operacji logicznych umieszczono przykładowy rozkaz PSLLW, który powoduje przesunięcie bitów w wektorze słów o określoną wartość indeksu (tutaj dwa). Wyraźnie to widać na przykładowej liczbie binarnej 00000001000000010000000100000001b. Po wykonaniu rozkazu psllw mm0, 2, bity w każdym słowie (00000001b) przykładowego wektora zostały przesunięte w lewo o dwa, czyli słowo o wartości 00000001b ma teraz wartość 00000100b.

Code:
.data
    mem64a qword 0FF00FF0000FF00FFh
    mem64b qword 000FF00FF00FFFF00h
.code
Main proc
    movq mm0, mem64a ;kopiowanie pamięci mem64a do rejestru MM0
    movq mm1, mem64b ;kopiowanie pamięci mem64b do rejestru MM1
    pand mm0, mm1 ;MM0 = MM0 and MM1

    movq mm0, mem64a ;kopiowanie pamięci mem64a do rejestru MM0
    movq mm1, mem64b ;kopiowanie pamięci mem64b do rejestru MM1
    pandn mm0, mm1 ;MM0 = MM0 andn MM1

    movq mm0, mem64a ;kopiowanie pamięci mem64b do rejestru MM1
    movq mm1, mem64b ;kopiowanie pamięci mem64b do rejestru MM1
    por mm0, mm1 ;MM0 = MM0 or MM1

    movq mm0, mem64a ;kopiowanie pamięci mem64b do rejestru MM1
    movq mm1, mem64b ;kopiowanie pamięci mem64b do rejestru MM1
    pxor mm0, mm1 ;MM0 = MM0 xor MM1

    mov rax, 00000001000000010000000100000001b ;kopiowanie wartości natychmiastowej do rejestru RAX
    movq mm0, rax ;kopiowanie wartości z rejestru RAX do rejestru MM0
    psllw mm0, 2 ;przesunięcie wektora słów w lewo o 2 bity
    ;MM0 = 0...00000100000001000000010000000100b

    ;(...)

    ret
Main endp
end
Download:

Podstawy rozszerzeń SSE (Streaming SIMD Extensions)

Instrukcje kolejnych wersji rozszerzeń SSE (SSE, SSE2, SSE3, SSSE3, SSE4 etc.) wprowadzały różne usprawnienia takie jak m.in. nowe rozkazy, dodatkowe rejestry czy rozszerzenie istniejących rejestrów.

Transfer danych za pomocą instrukcji SSE

Code:
.data
    mem128a dword 1.0, 2.0, 3.0, 4.0 ;wektor podwójnych słów
    mem128b qword 1.0, 2.0 ;wektor poczwórnych słów
    mem128c qword 1, 2 ;wektor poczwórnych słów
.code
Main proc
    ;MOVUPS (niewyrównane) vs. MOVAPS (wyrównanie wymagane)
    ;MOVUPD (niewyrównane) vs. MOVAPD (wyrównanie wymagane)
    ;MOVDQU (niewyrównane) vs. MOVDQA (wyrównanie wymagane)
    ;[i] Pamięć, która jest wyrównana do 16 bajtów posiada adres, który jest wielokrotnością dwójki.

    movups xmm0, mem128a ;kopiowanie wektora czterech niewyrównanych wartości zmiennoprzecinkowych pojedynczej precyzji z pamięci do rejestru XMM0
    movaps xmm1, xmm0 ;w przypadku kopiowania między rejestrami warto używać wersji z wyrównaniem

    movupd xmm0, mem128b ;kopiowanie wektora czterech niewyrównanych wartości zmiennoprzecinkowych podwójnej precyzji z pamięci do rejestru XMM0
    movapd xmm1, xmm0 ;w przypadku kopiowania między rejestrami warto używać wersji z wyrównaniem

    ;MOVDQU - kopiowanie dwóch poczwórnych słów bez wyrównania
    mov rdx, offset mem128c ;adres pamięci mem128c do rejestru RDX
    movdqu xmm0, xmmword ptr [rdx] ;wartość spod adresu w RDX do rejestru XMM0

    movdqa xmm1, xmm0 ;w przypadku kopiowania między rejestrami warto używać wersji z wyrównaniem

    ;(...)

    ret
Main endp
end
Download:

Podstawowe operacje arytmetyczne i logiczne za pomocą instrukcji SSE

Code:
.data
    mem128a dword 1.0, 2.0, 3.0, 4.0 ;wektor wartości zmiennoprzecinkowych
    mem128b dword 1.0, 2.0, 3.0, 4.0 ;wektor wartości zmiennoprzecinkowych
    mem128c dword 10.0, 5.0, 8.0, 2.0 ;wektor wartości zmiennoprzecinkowych
    mem128d dword 1.0, 2.0, 4.0, 3.0 ;wektor wartości zmiennoprzecinkowych
    mem128e dword 1.0, 0.0, 0.0, 1.0 ;wektor wartości zmiennoprzecinkowych
    mem128f dword 1.0, 0.0, 1.0, 0.0 ;wektor wartości zmiennoprzecinkowych
    mem128g dword 0, 0, 1, 1 ;wektor wartości całkowitych
    mem128h dword 0, 1, 0, 1 ;wektor wartości całkowitych
.code
Main proc
    ;MOVUPS - kopiowanie wektora czterech wartości zmiennoprzecinkowych pojedynczej precyzji pomiędzy rejestrami XMM i pamięcią
    movups xmm0, mem128a
    movups xmm1, mem128b
    addps xmm0, xmm1 ;dodawanie wektorów wartości zmiennoprzecinkowych z rejestru XMM1 do wartości w rejestrze XMM0
    ;xmm0 = xmm0 + xmm1

    ;MOVUPS - kopiowanie wektora czterech wartości zmiennoprzecinkowych pojedynczej precyzji pomiędzy rejestrami XMM i pamięcią
    movups xmm0, mem128c
    movups xmm1, mem128d
    subps xmm0, xmm1 ;odejmowanie wektorów wartości zmiennoprzecinkowych z rejestru XMM1 od wartości w rejestrze XMM0
    ;xmm0 = xmm0 - xmm1

    movups xmm0, mem128e
    movups xmm1, mem128f
    xorps xmm0, xmm1 ;alternatywa wykluczająca wektorów wartości zmiennoprzecinkowych pojedynczej precyzji
    ;xmm0 = xmm0 xor xmm1

    mov rdx, offset mem128g ;kopiowanie adresu pamięci mem128g do rejestru RDX
    movdqu xmm1, xmmword ptr [rdx] ;odczytanie wartości spod adresu w RDX i kopiowanie tej wartości do rejestru XMM1
    mov rdx, offset mem128h ;kopiowanie adresu pamięci mem128h do rejestru RDX
    movdqu xmm2, xmmword ptr [rdx] ;odczytanie wartości spod adresu w RDX i kopiowanie tej wartości do rejestru XMM2
    pxor xmm1, xmm2 ;alternatywa wykluczająca wektorów wartości w rejestrze XMM1 i wartości w XMM2 lub pamięci

    ;Inne rozkazy:
    ;ANDPS - koniunkcja wektorów wartości zmiennoprzecinkowych pojedynczej precyzji
    ;ANDNPS - dysjunkcja wektorów wartości zmiennoprzecinkowych pojedynczej precyzji
    ;ORPS - alternatywa wektorów wartości zmiennoprzecinkowych pojedynczej precyzji
    ;ANDPD - koniunkcja wektorów wartości zmiennoprzecinkowych podwójnej precyzji
    ;ANDNPD - dysjunkcja wektorów wartości zmiennoprzecinkowych podwójnej precyzji
    ;ORPD - alternatywa wektorów wartości zmiennoprzecinkowych podwójnej precyzji
    ;XORPD - alternatywa wykluczająca wektorów wartości zmiennoprzecinkowych podwójnej precyzji

    ;(...)

    ret
Main endp
end
Download:

Podstawy rozszerzeń Advanced Vector eXtensions (AVX, AVX-512)

Zestawy instrukcji nazwane AVX to kolejny krok w rozbudowywaniu technologii SSE. Zwiększoną wydajność prezentuje już nawet prosty XOR, który wykonany np. rozkazem VPXORQ z rozszerzenia AVX pozwala jedną instrukcją przetworzyć 64 bajty (512 bitów).

Dodatkowo za pomocą maski bitowej możliwe jest unikanie rozgałęzień, czyli używania dodatkowych instrukcji warunkowych zmniejszających wydajność w przetwarzaniu poszczególnych fragmentów wektora. Do rejestrów k0...k7 używanych jako maski można wczytać odpowiednią maskę bitową rozkazami KMOVB (maska 8-bitowa), KMOVW (maska 16-bitowa), KMOVD (maska 32-bitowa) czy KMOVQ (maska 64-bitowa). Rejestr zawierający maskę dopisuje się do rozkazu np. VADDPS zmm1 {k1}, zmm2, zmm3, a w przypadku maski zerującej VADDPS zmm1 {k1}{z}, zmm2, zmm3.

Code:
.data
    mem256a dword 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 8.0
    mem256b dword 3.0, 5.0, 9.0, 1.0, 5.0, 0.0, 2.0, 4.0
    mem512a dword 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 8.0
        dword 3.0, 7.0, 1.0, 2.0, 5.0, 8.0, 0.0, 4.0
    mem512b dword 7.0, 9.0, 4.0, 5.0, 8.0, 1.0, 2.0, 9.0
        dword 2.0, 1.0, 7.0, 4.0, 3.0, 5.0, 1.0, 11.0

    mem256c dword 1, 0, 1, 0, 1, 0, 1, 0
    mem256d dword 0, 1, 0, 1, 0, 1, 0, 1
    mem512c dword 1, 0, 1, 0, 1, 0, 1, 0
        dword 1, 0, 0, 1, 1, 0, 0, 1

    mem512d dword 0, 1, 0, 1, 0, 1, 0, 1
        dword 1, 0, 0, 1, 1, 0, 0, 1

.code
Main proc
    ;VMOVUPS to rozszerzona wersja rozkazu MOVUPS
    vmovups ymm1, mem256a ;kopiowanie wektora z pamięci do rejestru YMM1
    vmovups ymm2, mem256b ;kopiowanie wektora z pamięci do rejestru YMM2
    ;VADDPS to rozszerzona wersja rozkazu ADDPS
    vaddps ymm0, ymm1, ymm2 ;dodawanie wartości z rejestru YMM2 do YMM1 i zapisanie wyniku w YMM0

    vmovups ymm1, mem256a ;kopiowanie wektora z pamięci do rejestru YMM1
    vmovups ymm2, mem256b ;kopiowanie wektora z pamięci do rejestru YMM2
    ;VSUBPS to rozszerzona wersja rozkazu SUBPS
    vsubps ymm0, ymm1, ymm2 ;odejmowanie wartości z rejestru YMM2 od YMM1 i zapisanie wyniku w YMM0

    vmovups ymm1, mem256a ;kopiowanie wektora z pamięci do rejestru YMM1
    vmovups ymm2, mem256b ;kopiowanie wektora z pamięci do rejestru YMM2
    ;VXORPS to rozszerzona wersja rozkazu XORPS
    vxorps ymm0, ymm1, ymm2 ;alternatywa wykluczająca wartości wektorów z rejestru YMM2 i YMM1 oraz zapisanie wyniku w YMM0

    mov rdx, offset mem256c ;kopiowanie adresu wektora w pamięci do rejestru RDX
    vmovdqu ymm2, ymmword ptr [rdx] ;kopiowanie wartości spod adresu w pamięci do rejestru YMM2
    mov rdx, offset mem256d ;kopiowanie adresu wektora w pamięci do rejestru RDX
    vmovdqu ymm3, ymmword ptr [rdx] ;kopiowanie wartości spod adresu w pamięci do rejestru YMM3
    ;VPXOR to rozszerzona wersja rozkazu PXOR
    vpxor ymm1, ymm2, ymm3 ;alternatywa wykluczająca wartości wektorów z rejestru YMM3 i YMM2 oraz zapisanie wyniku w YMM1

    ;---------- AVX-512 ----------;

    vmovups zmm1, mem512a ;kopiowanie wektora z pamięci do rejestru ZMM1
    vmovups zmm2, mem512b ;kopiowanie wektora z pamięci do rejestru ZMM2
    vaddps zmm0, zmm1, zmm2 ;dodawanie wektorów wartości zmiennoprzecinkowych pojedynczej precyzji (ZMM0 = ZMM1 + ZMM2)

    vmovups zmm1, mem512a ;kopiowanie wektora z pamięci do rejestru ZMM1
    vmovups zmm2, mem256b ;kopiowanie wektora z pamięci do rejestru ZMM2
    vsubps zmm0, zmm1, zmm2 ;odejmowanie wartości wektorów zmiennoprzecinkowych pojedynczej precyzji (ZMM0 = ZMM2 - ZMM1)

    vmovups zmm1, mem512a ;kopiowanie wektora z pamięci do rejestru ZMM1
    vmovups zmm2, mem512b ;kopiowanie wektora z pamięci do rejestru ZMM2
    vxorps zmm0, zmm1, zmm2 ;alternatywa wykluczająca wektorów wartości zmiennoprzecinkowych pojedynczej precyzji, czyli ZMM0 = ZMM1 xor ZMM2

    mov rdx, offset mem512c ;kopiowanie adresu wektora w pamięci do rejestru RDX
    vmovdqu64 zmm2, zmmword ptr [rdx] ;kopiowanie wartości spod adresu w pamięci do rejestru ZMM2
    mov rdx, offset mem512d ;kopiowanie adresu wektora w pamięci do rejestru RDX
    vmovdqu64 zmm3, zmmword ptr [rdx] ;kopiowanie wartości spod adresu w pamięci do rejestru ZMM3
    vpxord zmm1, zmm2, zmm3 ;alternatywa wykluczająca wektorów podwójnych słów (wartości całkowite), czyli ZMM1 = ZMM2 xor ZMM3

    ;(...)

    ret
Main endp
end
Download:

Wykaz literatury

  1. [1] https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html [dostęp: 2024-02-08]
  2. [2] https://www.amd.com/en/search/documentation/hub.html [dostęp: 2024-02-08]
  3. [3] https://learn.microsoft.com/en-us/cpp/assembler/masm/microsoft-macro-assembler-reference [dostęp: 2024-02-08]