Esencja wiedzy o Asemblerze x64 w błękitnej szklanej fiolce
Spis treści
- Podstawowe pojęcia
- Bity, bajty i słowa
- Kolejność bajtów
- Systemy liczbowe: binarny i heksadecymalny
- Liczby ze znakiem i bez znaku
- Dopełnienie zerami oraz rozszerzenie z zachowaniem znaku
- Przepełnienie całkowitoliczbowe i saturacja
- Skróty związane z poszczególnymi wersjami architektury x86/x64
- Budowanie programu, czyli asemblacja i konsolidacja
- Inżynieria wsteczna i analiza plików binarnych, czyli dezasemblacja
- Organizacja pamięci
- Rejestry ogólnego przeznaczenia
- Rejestr indeksowy źródła (ang. source index)
- Rejestr indeksowy docelowy (ang. destination index)
- Rejestr wskaźnika bazowego (ang. base pointer)
- Rejestr wskaźnika stosu (ang. stack pointer)
- Rejestr wskaźnika instrukcji
- Rejestry segmentowe
- Rejestr flag/znaczników
- Rejestry jednostki zmiennoprzecinkowej x87
- Rejestry rozszerzenia MultiMedia eXtensions (MMX)
- Rejestry rozszerzeń SSE/AVX/AVX-512
- Witaj, świecie Asemblera x64 dla Windows!
- Kod źródłowy programu Witaj, świecie!
-
Podstawy składni Microsoft Macro Assembler (MASM x64)
- Dane lokalne (LOCAL)
- Stałe (EQU)
- Dane o rozmiarze bajta (BYTE, SBYTE oraz DB)
- Dane o rozmiarze słowa (WORD, SWORD oraz DW)
- Dane o rozmiarze podwójnego słowa (DWORD, SDWORD oraz DD)
- Dane o rozmiarze poczwórnego słowa (QWORD, SQWORD oraz DQ)
- Dane o rozmiarze 48 bitów (FWORD oraz DF)
- Dane o rozmiarze 80 bitów (TBYTE oraz DT)
- Dane o rozmiarze ośmiokrotnego słowa (OWORD)
- Dane typu REAL4, REAL8 oraz REAL10 (pol. liczby rzeczywiste)
- Dane typu MMWORD (64 bity)
- Dane typu XMMWORD (128 bitów)
- Dane typu YMMWORD (256 bitów)
- Dane typu ZMMWORD (512 bitów)
- Aliasy (nowe nazwy) dla istniejących typów (TYPEDEF)
- Etykiety (ang. labels)
- Tablice (ang. arrays)
- Ciągi tekstowe (ang. text strings)
- Struktury (ang. structures)
- Unie (ang. unions)
- Zagnieżdżone struktury i unie
- Procedury (ang. procedures)
- Makra (ang. macros)
- Podstawowe rozkazy procesora x86/x64
- Format rozkazów procesorów x64
- Funkcje Windows API
- Optymalizacja kodu pod względem rozmiaru
- Podstawy rozszerzenia MultiMedia eXtensions (MMX)
- Podstawy rozszerzeń SSE (Streaming SIMD Extensions)
- Podstawy rozszerzeń Advanced Vector eXtensions (AVX, AVX-512)
Podstawowe pojęcia
- asembler (ang. assembler) — program narzędziowy używany w procesie budowania plików wykonywalnych (etap asemblacji).
- Asembler (ang. Assembly Language) — język programowania niskiego poziomu abstrakcji.
- konsolidator (ang. linker) — program narzędziowy używany w procesie budowania plików wykonywalnych (etap konsolidacji).
- dezasembler (ang. disassembler) — program narzędziowy pozwalający odzyskać kod źródłowy w języku Asembler z pliku wykonywalnego.
- dekompilator (ang. decompiler) — program narzędziowy pozwalający odzyskać kod źródłowy z pliku wykonywalnego w języku wyższego poziomu abstrakcji niż Asembler.
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.
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ą.
Tablice prawdy (ang. truth tables) nazywane też matrycami logicznymi są przejrzystym sposobem na prezentację działania poszczególnych funkcji logicznych.
A | B |
A and B
|
0
|
0
|
0
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
1
|
1
|
A | B |
A or B
|
0
|
0
|
0
|
0
|
1
|
1
|
1
|
0
|
1
|
1
|
1
|
1
|
A | B |
A xor B
|
0
|
0
|
0
|
0
|
1
|
1
|
1
|
0
|
1
|
1
|
1
|
0
|
A |
not A
|
0
|
1
|
1
|
0
|
A | B |
A nand B
|
0
|
0
|
1
|
0
|
1
|
1
|
1
|
0
|
1
|
1
|
1
|
0
|
A | B |
A nor B
|
0
|
0
|
1
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
1
|
0
|
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:
- Tradycyjne (powiązane z IEEE)
- Prostokątne (powiązane z IEC)
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):
- bit
- półbajt (ang. nibble) — 4 bity
- bajt (ang. byte) — 8 bitów
- słowo (ang. word) — 16 bitów
- podwójne słowo (ang. doubleword) — 32 bity
- poczwórne słowo (ang. quadword) — 64 bity
- ośmiokrotne słowo (ang. octword) — 128 bitów
- dwa ośmiokrotne słowa (ang. double octword) — 256 bitów
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.
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#.
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:
- bajt (ang. byte) — 8 bitów
- słowo (ang. word) — 16 bitów
- podwójne słowo (ang. doubleword) — 32 bity
- poczwórne słowo (ang. quadword) — 64 bity
- ośmiokrotne słowo (ang. octword) — 128 bitów
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.
Liczby całkowite ze znakiem są reprezentowane w formacie uzupełnień do dwóch (ang. two's complement format).
- Niech przykładową liczbą będzie
-7
. - Wartość bezwzględna z
-7
to liczba7
. Można to zapisać|-7| = 7
- Liczba
7
dziesiętnie to0111b
(dwójkowo, binarnie). -
Teraz dopełnienie zerami do docelowego rozmiaru.
Jeśli liczba ujemna ma być zapisana jako bajt, to
0000 0111b
. Natomiast jeśli liczba ujemna ma być zapisana jako słowo, to0000 0000 0000 0111b
. A jeśli podwójne słowo, to0000 0000 0000 0000 0000 0000 0000 0111b
. Analogicznie dla typów danych o innych rozmiarach. -
Dalej zaprzeczenie logiczne (negacja), czyli
NOT
0000 0111b
=1111 1000b
(jedynki stają się zerami, a zera jedynkami). -
Kolejny krok to dodanie
1
(jeden) do wartości z poprzedniego etapu.1111 1000b + 1 =
1111 1001b
-
Wynikiem jest
1111 1001b
, czyli-7
(dziesiętnie) to1111 1001b
(binarnie).
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.
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.
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:
0 - 3 = 0
(saturacja)0 + 3 = 3
(saturacja niepotrzebna)128 + 128 = 255
(saturacja)200 + 100 = 255
(saturacja)127 + 128 = 255
(saturacja niepotrzebna)
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).
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.
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.
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:
- Przechowywanie operandów i rezultatów operacji arytmetycznych i logicznych.
-
Przechowywanie operandów i rezultatów wraz z rejestrem danych (
EDX
), gdy wynik nie mieści się w pojedynczym rejestrze. -
Odczytywanie informacji o procesorze rozkazem
CPUID
. - Przechowywanie wartości zwracanej przez funkcje Windows API.
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:
- Do przechowywania adresu bazowego w starszym kodzie.
-
Odczytywanie informacji o procesorze rozkazem
CPUID
.
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:
- W rozkazach przesunięć i obrotów bitowych jako indeks (o ile bitów przesunąć czy obrócić wartość).
- W pętlach i powtarzalnych operacjach jako licznik iteracji.
-
Odczytywanie informacji o procesorze rozkazem
CPUID
.
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:
- Do przechowywania operandów i rezultatów w operacjach mnożenia i dzielenia.
-
Przechowywanie operandów i rezultatów wraz z rejestrem akumulatora (
EAX
), gdy wynik nie mieści się w pojedynczym rejestrze. - Do przechowywania numeru portu w operacjach wejścia-wyjścia.
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 pomocnicze.
-
Rejestry
R10
iR11
są ulotne (ang. volatile) i ich wartość może być utracona przy wywołaniach systemowych (SYSCALL
). Dlatego może być konieczność zachowania ich aktualnej wartości przed wywołaniem. RejestrR11
przechowuje wartość flag (RFLAGS
) w rozkazachSYSCALL
iSYSRET
.
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:
- Przechowywanie adresu operandu źródłowego dla rozkazów operujących na ciągach znaków.
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:
- Przechowywanie adresu operandu docelowego dla rozkazów operujących na ciągach znaków.
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:
- Przechowywanie adresu bazowego wskaźnika ramki stosu (ang. stack frame).
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:
- Przechowywanie adresu wskaźnika wierzchołka stosu (adresu ostatniej wartości odłożonej na stosie).
- W połączeniu z odpowiednim przesunięciem (ang. offset) pozwala na uzyskanie dostępu do określonych wartości na stosie programu.
Rejestr wskaźnika instrukcji
Domyślne zastosowanie:
- Przechowuje adres następnego rozkazu do wykonania. Na przykład. Przez odłożenie tego adresu na stosie przed wywołaniem procedury możliwy jest później powrót do tego adresu. Inny przykład. W przypadku kodu niezależnego od miejsca w pamięci (np. wstrzykiwanego w proces) możliwe jest adresowanie względem wskaźnika instrukcji (ang. RIP relative addressing) co zapobiega popsuciu adresów do danych, gdy kod zmieni swoje miejsce w pamięci operacyjnej.
Rejestry segmentowe
Domyślne zastosowanie:
-
W płaskim modelu pamięci rejestry
CS
,DS
,ES
iSS
są traktowane jakby adresem bazowym segmentów było zero. -
W przypadku systemu operacyjnego Windows rejestry
FS
iGS
w trybie użytkownika (ang. user mode, ring 3) wskazują na wewnętrzne struktury systemowe takie jak TEB czy PEB.
Rejestr flag/znaczników
Bit 0 (CF)
: Flaga przeniesienia (ang. carry flag) — Flaga jest ustawiana na wartość jeden, jeśli ostatnie dodawanie spowodowało przeniesienie poza najstarszy bit w wyniku lub ostatnie odejmowanie spowodowało pożyczenie spoza tego bitu. W przeciwnym wypadku flaga jest zerowana. Instrukcje logiczne (AND
,OR
,XOR
) powodują wyzerowanie tej flagi. Flagę można ustawić rozkazemSTC
, a wyzerować za pomocąCLC
.Bit 2 (PF)
: Flaga parzystości (ang. parity flag) — Flaga jest ustawiana na wartość jeden, jeśli (dla niektórych operacji) najmłodszy bajt zawiera parzystą liczbę bitów o wartości jeden. W przeciwnym wypadku flaga jest zerowana.Bit 4 (AF)
: Flaga przeniesienia pomocnicznego (ang. auxiliary carry flag) — Flaga jest ustawiana na wartość jeden, jeśli operacja arytmetyczna spowoduje przeniesienie poza trzeci bit wyniku lub pożyczenie spoza tego bitu. W przeciwnym wypadku flaga jest zerowana.Bit 6 (ZF)
: Flaga zerowa (ang. zero flag) — Flaga jest ustawiana na wartość jeden, jeśli ostatnia operacja arytmetyczna zwróciła w wyniku zero. W przeciwnym wypadku flaga jest zerowana. Rozkazy porównań (CMP
,TEST
etc.) mają wpływ na tę flagę.Bit 7 (SF)
: Flaga znaku (ang. sign flag) — Flaga jest ustawiana na wartość jeden, jeśli ostatnia operacja arytmetyczna zwróciła wartość ujemną. W przeciwnym wypadku (dla wartości dodatniej) flaga jest zerowana. Innymi słowy: Flaga jest ustawiana zgodnie z najstarszym bitem wyniku (bitem znaku).Bit 10 (DF)
: Flaga kierunku (ang. direction flag) — Flaga decyduje w jakim kierunku mają być przetwarzane ciągi znaków. Dotyczy to rozkazów operacji na napisach (MOVSx
,SCASx
,LODSx
,CMPSx
,OUTSx
,INSx
). Flagę można ustawić rozkazemSTD
, a wyzerować rozkazemCLD
. Jeśli flaga jest ustawiona, to wskaźnik na fragment napisu jest zmniejszany. W przeciwnym wypadku jest zwiększany.Bit 11 (OF)
: Flaga przepełnienia (ang. overflow flag) — Flaga jest ustawiana na jeden, jeśli w ostatniej operacji arytmetycznej najstarszy bit wyniku (bit znaku) jest różny od bitu znaku operandów. W przeciwnym wypadku flaga jest zerowana. Innymi słowy: Ustawienie flagi oznacza, że wynik operacji nie mieści się w typie danych (rozmiarze) operandu docelowego. Dla rozkazu dzieleniaDIV
flaga ta jest niezdefiniowana. Instrukcje logiczne powodują wyzerowanie tej flagi.
Flagi systemowe
Bit 8 (TF)
: Flaga pułapki (ang. trap flag) — Programy ustawiają tę flagę na jeden, aby aktywować tryb wykonywania rozkazów krok po kroku na potrzeby debugowania. Wyzerowanie tego bitu wyłącza ten tryb. Gdy tryb wykonywania krok po kroku jest aktywny, to natychmiast po wykonaniu pojedynczego rozkazu generowany jest wyjątek (ang. debug exception), który obsługują odpowiednie procedury używane przez narzędzia typu debugger.Bit 9 (IF)
: Flaga przerwania (ang. interrupt flag) — Programy ustawiają tę flagę na jeden, aby aktywować odpowiedzi na przerwania. W przypadku wyzerowania tej flagi przerwania są wstrzymywane.Bit 13:12 (IOPL)
: Flaga uprzywilejowania I/O (ang. I/O privilege level) — Dwubitowe pole IOPL określa poziom uprzywilejowania wymagany do wykonywania rozkazów uzyskujących dostęp do adresów w przestrzeni wejścia/wyjścia. Dla programów, które wykonują tego rodzaju rozkazy, aktualny poziom uprzywilejowania (CPL) musi być większy bądź równy od wartości pola IOPL. Aktualny poziom uprzywilejowania dla programu jest przechowywany w dwóch najmłodszych bitach rejestru segmentowego CS. Tryb użytkownika to poziom 3 (ang. ring 3), natomiast tryb jądra to poziom 0 (ang. ring 0). Istnieją też poziomy 1 i 2.Bit 14 (NT)
: Flaga zagnieżdżenia zadań (ang. nested task) — RozkazIRET
odczytuje poleNT
, aby określić czy aktualne zadanie jest zagnieżdżone w innym. Jeśli flagaNT
jest ustawiona na jeden, to aktualne zadanie jest zagnieżdżone. W przeciwnym wypadku aktualne zadanie jest „na samej górze” (niezagnieżdżone). Procesor ustawia tę flagę, gdy następuje przełączenie wynikające z rozkazuCALL
, przerwania czy wyjątku.Bit 16 (RF)
: Flaga wznowienia wykonania (ang. resume flag) — Jeśli flaga jest ustawiona na jeden, to tymczasowo wstrzymuje reportowanie pułapek w trybie debugowania, aby zapobiec wielokrotnym rzucaniem wyjątków (ang. debug exception). Pozwala to na wznowienie wykonania rozkazu, który został wstrzymany z powodu wyjątku debugowania. Flaga jest zerowana po każdej pomyślnie wykonanej instrukcji pozaJMP
,CALL
,INTn
orazIRET
(może ustawiać flagęRF
).Bit 17 (VM)
: Flaga wirtualnego trybu 8086 (ang. virtual-8086 mode) — Programy ustawiają tę flagę na jeden, aby aktywować tryb wirtualny procesora 8086. W przypadku wyzerowania tej flagi następuje wyjście z tego trybu.Bit 18 (AC)
: Flaga sprawdzenia wyrównania (ang. alignment check) — Programy ustawiają tę flagę na jeden (wraz z bitemAM
w rejestrzeCR0
), aby wykonywać automatyczne sprawdzanie wyrównania. W prostych słowach: Dostęp do danych niewyrównanych (np. słowo maszynowe pod adresem nieparzystym) powoduje wyjątek typu alignment-check.Bit 19 (VIF)
: Wirtualna flaga przerwania (ang. virtual interrupt flag) — Wirtualna flaga przerwania, która pozwala programom w trybie wirtualnym 8086 korzystać z flagi przerwania bez powodowania wyjątków, które mogłyby wystąpić przy modyfikacji flagiIF
z bitu 9 rejestru RFLAGS.Bit 20 (VIP)
: Flaga oczekującego przerwania (ang. virtual interrupt pending) — Flaga informuje czy przerwanie oczekuje wykonania (wartość jeden). W przypadku, gdy flaga jest wyzerowana nie ma oczekujących przerwań.Bit 21 (ID)
: Flaga identyfikacji możliwości procesora (ang. ID flag) — Jeśli program może modyfikować tę flagę, to oznacza, że wspierana jest instrukcjaCPUID
, która pozwala odczytać wiele informacji o możliwościach procesora.
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 rozszerzenia MultiMedia eXtensions (MMX)
MMX to rozszerzenie zestawu instrukcji o rozkazy pozwalające operować na rejestrach 64-bitowych od MM0
do MM7
.
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.
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!
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
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.
Uruchomienie kreatora aplikacji dla Windows Desktop jest jednym ze sposobów utworzenia projektu Visual C++.
Od wpisanej nazwy projektu będzie między innymi zależała nazwa pliku wykonywalnego (*.exe).
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).
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.
W oknie dialogowym należy podać nazwę pliku źródłowego (np. Main) z rozszerzeniem .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.
Zaznaczenie elementu masm pozwoli na korzystanie z narzędzia Microsoft Macro Assembler.
W celu uniknięcia błędów kompilacji należy zweryfikować właściwości 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.
W pliku Main.asm należy umieścić przykładowy kod źródłowy.
Niniejszy komunikat informuje o błędach budowania — plik .exe nie został utworzony.
Entry Point (pol. punkt wejścia) w pliku źródłowym Main.asm jest różny od punktu wejścia w ustawieniach projektu.
Właściwości projektu można dostosować poprzez wybranie Project > ... Properties.
W zaawansowanych ustawieniach konsolidatora należy wpisać odpowiednią nazwę procedury dla punktu wejścia programu.
Jeśli proces budowania zakończył się bez błędów, to zostanie utworzony i uruchomiony plik wykonywalny *.exe.
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
Main proc
local var1:dword, var2:qword
mov rax, 0DEADBEEFC0FFEE00h
mov var1, eax ;var1 = C0FFEE00h
mov var2, rax ;var2 = 0DEADBEEFC0FFEE00h
ret
Main endp
end
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:
-
Odwołanie do wartości o rozmiarze poczwórnego słowa (
qword ptr ...
, ang. qword pointer). -
Wartość znajduje się w segmencie stosu
qword ptr ss:[...]
. Uwaga: w płaskim modelu pamięci (ang. flat) selektor segmentu stosuSS
ma wartość zero, gdyż pamięć to ciągła przestrzeń niepodzielona na segmenty. -
Adres wartości to aktualny wskaźnik wierzchołka stosu (
RSP
), zachowany przez instrukcje prologu procedury w rejestrzeRBP
, zmniejszony o wartość0Ch
.
;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
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
.
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
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:
- Kopiowanie wartości natychmiastowej (ang. immediate) do danej nazwanej (pamięci):
mov madByte, 0FFh ;madByte = 255
- Kopiowanie danej w pamięci do rejestru za pomocą rozkazu
MOV
:mov al, madByte ;AL = madByte
- Wczytanie adresu danej w pamięci do rejestru:
mov rdx, offset madByte ;lea rdx, myByte
- Wpisanie wartości pod określony adres w pamięci:
mov byte ptr [rdx], 0CCh ;wpisanie wartości 0CCh do bajtu w pamięci pod adresem znajdującym się w RDX
- Wczytanie rozmiaru danej w pamięci (w bajtach) do rejestru:
mov r8, sizeof kilobyte ;R8 = 1024
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
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).
;(...)
.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).
;(...)
.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).
;(...)
.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.
;(...)
.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.
;(...)
.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).
;(...)
.data
octalWord oword 0
;(...)
Dane typu REAL4, REAL8 oraz REAL10 (pol. liczby rzeczywiste)
Typy danych do pracy z jednostką zmiennoprzecinkową x87 (FPU).
.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.
.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).
.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.
.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.
.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ą.
;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
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
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).
.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
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).
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
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.
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
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
).
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
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.
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
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
.
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
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:
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
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
.
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
Podstawowe rozkazy procesora x86/x64
Przegląd najważniejszych rozkazów rdzenia x86/x64.
Instrukcje transferu danych
Przykładowe operacje:
mov rax, 0C0FFEEh ;wczytanie do rejestru ogólnego przeznaczenia wartości natychmiastowej
mov rcx, rax ;kopiowanie wartości rejestru RAX do rejestru RCX
mov var1, rcx ;kopiowanie wartości rejestru RCX do danej o nazwie var1 (zdefiniowanej w sekcji .data jako poczwórne słowo)
mov rdx, offset buffer ;wczytanie do rejestru RDX adresu danej o nazwie buffer (zdefiniowanej w sekcji .data)
xchg rax, rcx ;zamiana wartościami rejestrów RAX i RCX
mov rax, "Go blue!" ;wczytanie ciągu znaków do rejestru RAX (da się, gdyż napis ma 8 bajtów i rozmiary operandów się zgadzają)
;(...)
Instrukcje arytmetyczne
Przykład dodawania:
-
mov rax, 1 ;wczytanie do rejestru RAX wartości natychmiastowej jeden
add rax, 7 ;dodanie do wartości w rejestrze RAX wartości siedem, czyli RAX równa się osiem
Przykład odejmowania:
-
mov rax, 8 ;wczytanie do rejestru RAX wartości natychmiastowej osiem
mov rcx, 1 ;wczytanie do rejestru RCX wartości natychmiastowej jeden
sub rax, rcx ;odjęcie wartości rejestru RCX od wartości rejestru RAX — rezultat w rejestrze RAX (siedem)
Przykład mnożenia ze znakiem:
-
mov rax, 4 ;wczytanie do rejestru RAX wartości natychmiastowej cztery
mov r8, -4;wczytanie do rejestru R8 wartości natychmiastowej minus cztery
imul r8 ;mnożenie ze znakiem wartości z rejestru RAX przez wartość z R8 — rezultat w rejestrze RAX (minus szesnaście)
Przykład mnożenia bez znaku:
-
mov rax, 2 ;wczytanie do rejestru RAX wartości natychmiastowej dwa
mov rcx, 4 ;wczytanie do rejestru RCX wartości natychmiastowej cztery
mul rcx ;mnożenie bez znaku wartości z rejestru RAX przez wartość z RCX — rezultat w rejestrze RAX (osiem)
Przykład dzielenia ze znakiem:
-
mov rax, -5 ;wczytanie do rejestru RAX wartości natychmiastowej minus pięć (RAX == -5 == 0xfffffffffffffffb)
cqo ;rozszerzenie z zachowaniem znaku wartości rejestru RAX na parę rejestrów RDX:RAX
idiv mem64 ;dzielenie ze znakiem wartości w parze rejestrów RDX:RAX przez daną mem64 — RAX zawiera iloraz, RDX zawiera resztę z dzielenia
Przykład dzielenia bez znaku:
-
mov rax, 5 ;wczytanie do rejestru RAX wartości natychmiastowej pięć
cqo ;rozszerzenie z zachowaniem znaku wartości rejestru RAX na parę rejestrów RDX:RAX
mov rcx, 2 ;wczytanie do rejestru RCX wartości natychmiastowej dwa
div rcx ;dzielenie bez znaku wartości w parze rejestrów RDX:RAX przez wartość z rejestru RCX — RAX zawiera iloraz, RDX zawiera resztę z dzielenia
Przykład inkrementacji:
-
mov r8, 1 ;wczytanie do rejestru R8 wartości natychmiastowej jeden
inc r8 ;zwiększenie wartości w rejestrze R8 o jeden (R8 jest równy dwa po tym rozkazie)
inc r8 ;zwiększenie wartości w rejestrze R8 o jeden (R8 jest równy trzy po tym rozkazie)
inc r8 ;zwiększenie wartości w rejestrze R8 o jeden (R8 jest równy cztery po tym rozkazie)
inc r8 ;zwiększenie wartości w rejestrze R8 o jeden (R8 jest równy pięć po tym rozkazie)
inc r8 ;zwiększenie wartości w rejestrze R8 o jeden (R8 jest równy sześć po tym rozkazie)
inc r8 ;zwiększenie wartości w rejestrze R8 o jeden (R8 jest równy siedem po tym rozkazie)
Przykład dekrementacji:
-
mov r8, 3 ;wczytanie do rejestru R8 wartości natychmiastowej trzy
dec r8 ;zmniejszenie wartości w rejestrze R8 o jeden (R8 jest równy dwa po tym rozkazie)
dec r8 ;zmniejszenie wartości w rejestrze R8 o jeden (R8 jest równy jeden po tym rozkazie)
dec r8 ;zmniejszenie wartości w rejestrze R8 o jeden (R8 jest równy zero po tym rozkazie)
dec r8 ;zmniejszenie wartości w rejestrze R8 o jeden (R8 jest równy minus jeden (0xFFFFFFFFFFFFFFFF) po tym rozkazie)
Przykład negacji z uzupełnieniem do dwóch:
-
mov r9, 7 ;wczytanie do rejestru R9 wartości natychmiastowej siedem
neg r9 ;negacja z uzupełnieniem do dwóch wartości w rejestrze R9, czyli siedem staje się ujemne (0xfffffffffffffff9)
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):
-
movzx r8, mem16 ;wczytanie danej mem16 do rejestru R8 z dopełnieniem zerami
movzx r9, mem16_2 ;wczytanie danej mem16_2 do rejestru R9 z dopełnieniem zerami
and r8, r9 ;koniunkcja logiczna wartości rejestrów R8 i R9 — rezultat w R8
Przykładowa alternatywa (OR):
-
movzx r8, mem16 ;wczytanie danej mem16 do rejestru R8 z dopełnieniem zerami
movzx r9, mem16_2 ;wczytanie danej mem16_2 do rejestru R9 z dopełnieniem zerami
or r8, r9 ;alternatywa logiczna wartości rejestrów R8 i R9 — rezultat w R8
Przykładowa alternatywa wykluczająca (XOR):
-
movzx r8, mem16 ;wczytanie danej mem16 do rejestru R8 z dopełnieniem zerami
movzx r9, mem16_2 ;wczytanie danej mem16_2 do rejestru R9 z dopełnieniem zerami
xor r8, r9 ;alternatywa wykluczająca logiczna wartości rejestrów R8 i R9 — rezultat w R8
Przykładowe zaprzeczenie (NOT):
-
movzx r8, mem16 ;wczytanie danej mem16 do rejestru R8 z dopełnieniem zerami
not r8 ;zaprzeczenie logiczne wartości rejestru R8
Przykładowa dysjunkcja (ANDN):
-
movzx r8, mem16 ;wczytanie danej mem16 do rejestru R8 z dopełnieniem zerami
movzx r9, mem16_2 ;wczytanie danej mem16_2 do rejestru R9 z dopełnieniem zerami
andn r8, r8, r9 ;dysjunkcja logiczna wartości operandów drugiego (R8) i trzeciego (R9) — rezultat w operandzie pierwszym (R8)
Instrukcje przesunięć i obrotów
Przykładowe przesunięcie logiczne w lewo:
-
mov r8, 1 ;wczytanie do rejestru R8 wartości natychmiastowej jeden
shl r8, 5 ;przesunięcie logiczne wartości w rejestrze R8 w lewo o pięć bitów
Przykładowe przesunięcie arytmetyczne w lewo:
-
mov r8, 1 ;wczytanie do rejestru R8 wartości natychmiastowej jeden
sal r8, 5 ;przesunięcie arytmetyczne wartości w rejestrze R8 w lewo o pięć bitów
Przykładowe przesunięcie logiczne w prawo:
-
mov r8, 010000000b ;wczytanie do rejestru R8 wartości natychmiastowej sto dwadzieścia osiem (010000000b)
shr r8, 2 ;przesunięcie logiczne wartości w rejestrze R8 w prawo o dwa bity
Przykładowe przesunięcie arytmetyczne w prawo (zachowuje bit znaku):
-
mov r8, 010000000b ;wczytanie do rejestru R8 wartości natychmiastowej sto dwadzieścia osiem (010000000b)
sar r8, 2 ;przesunięcie arytmetyczne wartości w rejestrze R8 w prawo o dwa bity
;rozkaz SAR zachowuje bit znaku!
Przykładowa rotacja bitów w lewo:
-
mov r8, 000000000000000Fh ;wczytanie do rejestru R8 wartości natychmiastowej piętnaście (0Fh)
rol r8, 10h ;rotacja logiczna wartości w rejestrze R8 w lewo o szesnaście bitów (10h)
;R8 = 00000000000F0000h
Przykładowa rotacja bitów przez flagę przeniesienia CF w lewo:
-
mov r8, 0F000000000000000h ;wczytanie do rejestru R8 wartości natychmiastowej 0F000000000000000h
rcl r8, 11h ;rotacja logiczna (przez flagę przeniesienia CF) wartości w rejestrze R8 w lewo o siedemnaście bitów (11h)
;R8 = 000000000000F000h
Przykładowa rotacja bitów w prawo:
-
mov r8, 000000000000000Fh ;wczytanie do rejestru R8 wartości natychmiastowej piętnaście (0Fh)
ror r8, 10h ;rotacja logiczna wartości w rejestrze R8 w prawo o szesnaście bitów (10h)
;R8 = 000F000000000000h
Przykładowa rotacja bitów przez flagę przeniesienia CF w prawo:
-
mov r8, 000000000000000Fh ;wczytanie do rejestru R8 wartości natychmiastowej 0F000000000000000h
rcr r8, 11h ;rotacja logiczna (przez flagę przeniesienia CF) wartości w rejestrze R8 w prawo o siedemnaście bitów (11h)
;R8 = 000F000000000000h
Przykładowe przesunięcie logiczne pary rejestrów w lewo:
-
xor r8, r8 ;wyzerowanie rejestru R8
xor rax, rax ;wyzerowanie rejestru RAX
not rax ;rejestr RAX jest teraz równy 0FFFFFFFFFFFFFFFFh
shld r8, rax, 3 ;przesunięcie logiczne w lewo pary rejestrów R8:RAX o trzy bity
Przykładowe przesunięcie logiczne pary rejestrów w prawo:
-
xor r8, r8 ;wyzerowanie rejestru R8
xor rax, rax ;wyzerowanie rejestru RAX
not rax ;rejestr RAX jest teraz równy 0FFFFFFFFFFFFFFFFh
shrd r8, rax, 3 ;przesunięcie logiczne w prawo pary rejestrów R8:RAX o trzy bity
Instrukcje kontroli przepływu wykonania
Przykładowy program z pętlą oraz instrukcjami skoku:
.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
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).
.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
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).
Przykładowe operacje:
.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
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.
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
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).
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:
- prefiks REX (
0x4D
), który oznacza m.in., że używane są rejestry 64-bitowe. -
kod operacyjny
0x33
, który określa, że rozkaz toXOR
, którego operandy to rejestry lub pamięć. - bajt ModR/M, który zawiera m.in. informacje o adresowaniu.
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.
Funkcje Windows API
W przypadku funkcji, które przyjmują cztery lub mniej parametrów, to wartości całkowite przekazywane są przez rejestry:
RCX
(pierwszy argument)RDX
(drugi argument)R8
(trzeci argument)R9
(czwarty argument)
Wartości zmiennoprzecinkowe (ang. floating point) są przekazywane przez rejestry:
XMM0
/YMM0
/ZMM0
(pierwszy argument)XMM1
/YMM1
/ZMM1
(drugi argument)XMM2
/YMM2
/ZMM2
(trzeci argument)XMM3
/YMM3
/ZMM3
(czwarty argument)
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:
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
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.
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ą.
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ą.
.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
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.
.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
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
.
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
.
.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
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
.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
Podstawowe operacje arytmetyczne i logiczne za pomocą instrukcji SSE
.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
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
.
.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
Wykaz literatury
- [1] https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html [dostęp: 2024-02-08]
- [2] https://www.amd.com/en/search/documentation/hub.html [dostęp: 2024-02-08]
- [3] https://learn.microsoft.com/en-us/cpp/assembler/masm/microsoft-macro-assembler-reference [dostęp: 2024-02-08]