====== Laboratorium Języków Assemblerowych ====== ===== Ćwiczenie 1 ===== Laboratorium języków assemblerowych, ćwiczenie 1 ==== Przygotowanie: Środowisko sprzętowe i programistyczne ==== Do wykonania ćwiczenia niezbędny jest komputer PC w postaci maszyny fizycznej lub wirtualnej z systemem Windows 7, 8, 8.1 lub 10, z zainstalowanym i skonfigurowanym środowiskiem programistycznym Microsoft Visual Studio 2019 w wrsji co najmniej Community Edition ((https://visualstudio.microsoft.com/vs/)). Do pracy w laboratorium nada się również wersja VS Professional lub Enterprise. Proszę natomiast nie pomylić z Visual Studio Code, to zupełnie inny produkt, który nie nadaje się wprost do wykorzystania w naszym laboratorium do realizacji zadań, w szczególności kolejnych ćwiczeń. Jednym z najprostszych rozwiązań do przygotowania środowiska laboratoryjnego jest wykorzystanie gotowej maszyny wirtualnej, która jest dostępna na stronach Microsoft. Można ją wykorzystać w celach testowych, w okresie nie dłuższym niż 30-90dni (zależnie od daty pobrania i wersji). Dostępne są maszyny wirtualne dla różnych platform wirtualizacyjnych. W przypadku chęci skorzystania z gotowej maszyny, obraz można pobrać m.in. z poniższego linku: * Opcja 1: Windows 10 + Visual Studio 2019 Enterprise ((https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/)); * Opcja 2: Windows 10, a następnie własnoręczna instalacja Visual Studio 2019 Community((https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/)); Jako środowisko witualizacyjne można wykorzystać Oracle Virtual Box((https://www.virtualbox.org)), dostępne praktycznie na każdą platformę desktopową. W przypadku instalacji Linuxowych typu headless, można wykorzystać KVM((https://www.linux-kvm.org/page/Main_Page)). Maszyny wirtualne z pewnością będą wymagały zainstalowania aktualizacji Windows i aplikacji, w tym kilkukrotnych restartów, zatem instalację należy wykonać zawczasu i doprowadzić ją do stanu stabilnego PRZED zajęciami. Proszę rownież pamiętać o zainstalowaniu dodatków specyficznych dla platformy wirutalizacyjnej (np, Oracle Virtual Box Add-ons), co może pociągać za sobą konieczności w zmianie konfiguracji maszyny wirtualnej (np. konieczność dołożenia napędu wirtualnego w celu zainstalowania ww dodatków). Sugeruję rownież zwiększenie pamięci RAM do co najmniej 8GB (o ile to możliwe), liczby dostępnych wątków procesora (przynajmniej 2) oraz zmianę wirtualnej karty graficznej na zgodną z akceleracją sprzętową DirectX/Direct 3D (np. w Oracle Vitual Box jest to karta VBoxSVGA). Niezależnie od przyjętego modelu należy pamiętać, że zajęcia wymagają 64-bitowego systemu operacynego. === Instalacja Visual Studio (Community Edition 2019) === Instalator Visual Studio Communicy Edition 2019 pozwala na zainstalowanie wielu różnych komponentów. Minimalny zestaw komponentów został przedstawiony na poniższych rysunkach {{ref>VS2019CE1}}, {{ref>VS2019CE2}}, {{ref>VS2019CE3}}:
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_11_03_2021_18_57_52.png?600|}} Opcje instalacyjne część 1
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_11_03_2021_18_58_28.png?600|}} Opcje instalacyjne część 2
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_11_03_2021_18_59_35.png?600|}} Opcje instalacyjne część 3 - uwaga - frameworki .NET po prawej stronie trzeba zaznaczyć ręcznie!
=== Dodatek AsmDude do VS 2019 === Visual Studio natywnie nie posiada narzędzi wspomagających pracę dewelopera z kodem źródłowym w assemblerze. Jednak ze względu na istniejących mechanizm dodatków, można doinstalować zewnętrzne narzędzia. Nie jest to obowiązkowe, ale upraszcza pracę dewelopera, w szczególności powoduje, że kod w assemblerze zostanie pokolorowany i łatwiej będzie znaleźć ew. błędy, podczas debuggowania będą podpowiadane automatycznie wartości rejestrów, jak równnież będzie się pojawaiała (w ograniczonym zakresie) kontekstowa informacja o znaczeniu instrukcji, dyrektyw, zmiennych, etykiet, etc. Dodatek AsmDude można zainstalować za pomocą mechanizmów VS w automatyczny sposób. Opis dodatku znajduje się na stronie autora((https://marketplace.visualstudio.com/items?itemName=Henk-JanLebbink.AsmDude)). ==== Wymagania ==== Studenci muszą rozumieć sposób tworzenia i działania aplikacji typu "Command Line", podstawy języka assembler, C++ i C# ==== Wstęp ==== W ramach tego ćwiczenia stworzysz w czystym assemblerze dla x64 (procesory Intel/AMD, 64 bity) aplikację konsolową, która będzie prowadziła interakcję z systemem operacyjnym Windows 10 (7, 8, 8.1) wywołując jego funkcje poprzez interfejs Windows API. Jest to przykład technologiczny, ponieważ tworzenie w ten sposób skomplikowanych aplikacji i złożonych interfejsów użytkownika jest ... bezsensowne :-D, choć możliwe. Natomiast zdedycowanie przyda się przy robieniu prostych usług obliczeniowych, narzędzi, etc., które wymagają zwartości kodu i wykonywania zadań z maksymalną prędkością.\\ Zwróć uwagę, że podane przykłady nie ograniczają się do funkcji interfejsu użytkownika, a obejmują całe Windows API. **A ze względu na rozmiar aplikacji, całość niejednokronie mieści się w pamięci L1 procesora, zatem wykonuje się niespotykanie szybko.**\\ Ponieważ w Visual Studio nie ma gotowego modelu projektu na potrzeby aplikacji w czystym assemblerze, wykorzystamy model aplikacji konsolowej w języku C++ i przezbroimy go na potrzeby kompilacji zamiast kodu w języku C++, kodu w czystym assemblerze.\\ Takie podejście wymaga ręcznego przekonfigurowania kilku pramatetrów we właściwościach projekt, a w szczególności: * zmiany kompilatora na ''masm''; * zmiany docelowej platformy na x64; * podania nazwy głównej funkcji jako punktu wejścia ([Entry Point]) tak, aby sytem operacyjny wiedział, którą funkcję ma zawołać po uruchomieniu aplikacji - w wielu językach i środowiskach programowania jest to domyślnie funkcja ''main()'', natomaist w przypadku assemblera, nie ma takiej reguły i trzeba podać samemu, o którą funkcję chodzi; * uzupełnienia brakujących bibliotek: od wersji 2019 środowiska (w szczególności pod Windows 10) cześć standardowych funkcji dostępnych uprzednio domyślnie w bibliotekach linkowanych do każdego projektu, aktualnie zostało wyłączonych i trzeba je jawnie wskazać; uwaga - dokładny skład bibliotek jest zależny oczywiście od tego, co jest wykorzystywane (wywoływane) w kodzie źródłowym - w środowisku produkcyjnym należy zoptymalizować kod poprzez usunięcie zbędnych bibliotek; Uwaga! Ponieważ wykorzystujemy pliki ''asm,'' które natywnie nie są rozpoznawane przez Visual Studio jako pliki z kodem źródłowym, nie są one automatycznie zapisywane w momencie wywołania kompilacji, jak ma to miejsce w przypadku plików ''.cpp, .cs'' itp. Co prawda istnieje możliwość nauczenia VS 2019, aby plik był uznawany za źródłowy, niemniej jednak wykracza to poza niniejszą instrukcję. Proszę zatem pamiętać za każdym razem kliknąć zapis do pliku (Ctrl+s) przed kompilacją. ==== Powtórka z teorii: Funkcje i ich wywoływanie z poziomu assemblera ==== W przypadku pisania kodu w języku assembler, każdy bajt musi być przydzielony, zadeklarowany i użytkowany w odpowiedni sposób, szczególnie w kontekście wywoływania funkcji Windows API.\\ Ponieważ pracujemy na systemie 64.-bitowym, konieczne jest stosowane reguł wywołania funkcji zgodnie z kanonem. === Standardy wywoływania funkcji === Parametry wywołania funkcji przekazujemy poprzez rejestry, o ile ich liczba nie przekracza czterech. W przeciwnym wypadu przekazujemy je przez stos (pozostałe). Natomiast obowiązkowo należy zaalokować na stosie przestrzeń na argumenty nawet ... jeśli przekazujemy je przez rejestry :-)((https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160)). == Przekazanie argumentów całkowitych == W przypadku, gdy do funkcji przekazujemy argumenty numeryczne całkowite, w szczególności np. adresy i wskaźniki, należy użyć w kolejności następujących rejestrów: - argument 1 -> RCX, - argument 2 -> RDX, - argument 3 -> R8, - argument 4 -> R9, - każdy kolejny argument umieszczamy na stosie z zachowaniem szesnastobitowego wyrównania danych (z przyczyn wydajnościowych). Przykładowy kod w C++:\\ func1(int a, int b, int c, int d, int e, int f); // a w RCX, b w RDX, c w R8, d w R9, f, a następnie e, są odkładane na stosie == Przekazywanie argumentów zmiennoprzecinkowych == W przypadku przekazywania argumentów zmiennoprzecinkowych, kolejne wartości łądujemy do rejestrów XMM: - argument 1 -> XMM0, - argument 2 -> XMM1, - argument 3 -> XMM2, - argument 4 -> XMM3, - analogicznie jak dla stałoprzecinkowych. Przykładowy kod w C++:\\ func2(float a, double b, float c, double d, float e, float f); // a jest XMM0, b w XMM1, c w XMM2, d w XMM3, f a następnie e są odkładane na stosie == Przekazywanie argumentów mieszanych == W przypadku mieszania argumentów zmiennoprzecinkowych z dziesiętnymi, stosuje się system pozycyjny w zależności od typu, a zatem jeśli przekazujemy np, int1, float1, float2, int2, to wartości należy umieścić w rejestrach (w kolejności): - int1 -> RCX, - float1 -> XMM1, - float2 -> XMM2, - int2 -> R9, - piąty i następny argument poprzez stos. Przykładowy kod w C++:\\ func3(int a, float b, float c, int d, int e, float f); // a w RCX, b w XMM1, c w XMM2, d w R9, f, a następnie e, poprzez stos Rejestry po wywołaniu funkcji ulegają zniszczeniu (zmianie wartości), zatem nie należy oczekiwać, że po wywołaniu funkcji, rejestry RCX, RDX, R8, R9, XMM0-XMM3 będą miały wartości takie, jak przed wywołaniem funkcji! == Zwrot wartości z funkcji == Wartość całkowita (w tym wskaźnik) zwracana jest w rejestrze RAX.\\ Wartości zmiennoprzecinkowa zwracana jest w rejestrze XMM0. == Rejestry "bezpiecznie" i "niebezpieczne" == Niektóre z rejestrów musza pozostać niezmienione w momencie powrotu z funkcji, w stosunku do ich zawartości na początku, zaraz po wywołaniu funkcji (tzw. non-volatile). Inne rejestry można dowolnie zmieniać i nie będzie miało to wpływu na działanie systemu operacyjnego (volatile).\\ W przypadku konieczności skorzystania z tej pierwszej grupy, zawartość rejestru można odłożyć na stos lub zachować w pamięci i przywrócić przed wykonaniem instrukcji ''ret''. Rejestry, które można bezkarnie zmieniać wewnątrz kodu assemblerowej funkcji, bez konieczności przywracania oryginalnej wartości, to ([[https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#callercallee-saved-registers|x64 calling conventions]]): * RAX, RCX, RDX, R8, R9, R10, R11, oraz XMM0-XMM5, * dodatkowo w procesorach, które są wyposażone w rejestry YMM, górne połowy rejestrów YMM0-YMM15 również można zmieniać bez konieczności przywracania ich wartości, * a ponadto w procesorach wyposażonych w funkcje AVX512VL, wszystkie rejestry ZMM, YMM oraz XMM16-XMM31. Rejestry, których nie wolno zmieniać (albo raczej należy przywrócić ich oryginalne wartości przed opuszczeniem funkcji) to: * RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 oraz XMM6-XMM15. ==== Praktyka: Tworzenie projektu C++ i konsersja do ASM ==== Stwórz nowy projekt: aplikacja typu command-line (konsolowa) dla języka C++, natywnego (__nie C++ .NET__) rysunek {{ref>jalabstep1}}
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_01_48.png?600|}} Tworzenie projektu typu C++, kod natywny
=== Tworzenie pliku z kodem źródłowym === Usuń wygenerowany plik .cpp (z poziomu VS) a następnie dodaj nowy plik .asm (można to zrobić klikając w projekt (nie w solucję!) i wybierając dodanie nowego elementu rysunek {{ref>jalabstep21}}, a następnie wybrać plik typu tekstowego i zmienić jego nazwę oraz rozszerzenie rysunek {{ref>jalabstep22}}:
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_07_27.png?600|}} Dodanie nowego elementu
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_08_50.png?600|}} Dodanie nowego elementu - plik asm
=== Konwersja projektu z C++ na ASM === W celu konwersji należy poinformować VS, że pliki źródłowe będą kompilowane za pomocą MASM a nie kompilatora C/C++. W tym celu klikamy prawym przyciskiem na projekt i wybieramy opcję dostosowania budowania projektu rysunek {{ref>jalabstep31}}. W ustawieniach należy wybrać ''masm(.targets, .props)'' rysunek {{ref>jalabstep32}}.
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_17_17.png?600|}} Wybór menu dostosowanie budowania projektu
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_17_40.png?600|}} Wybór kompilatora MASM
Następnie należy zmienić rodzaj budowanej aplikacji z ''x86'' (aplikacja 32-bitowa) na ''x64'' (aplikacja 64-bitowa): menu Build->Configuration Manager rysunek {{ref>jalabstep4}}.
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_21_07.png?600|}} Konfiguracja środowiska - budowa wersji 64-bitowej
=== Utworzenie punktu startowego oraz uzupełnienie bibliotek === W tym kroku poinformujemy linker, jaki punkt startowy ma wpisać do pliku wykonywanego. W językach wysokiego poziomu jest to domyślnie funkcja ''main()''. W przypadku assemblera nie ma takiego założenia, a zatem kod źródłowy musi być zgodny z ustawioną tutaj wartością. Podana do linkera nazwa funkcji punktu wejścia do kodu musi zgadzać się z kodem źródłowym. W przypadku rozbieżności, program będzie działał niepoprawnie, nie uruchomi się lub rzuci wyjątkiem przy starcie! W podanym przykładzie, nazwa funkcji to ''mojaFunkcjaAsm''. Aby dostać się do okna konfiguracyjnego należy w drzewie projektu wskazać projekt (nie solucję!), a następnie prawym przyciskiem myszy kilknąć w projekt, wybrać pozycję ''Properties'', lub na skróty [Alt+Enter]. Pojawi się okienko jak na rysunku {{ref>jalabstep51}}. Następnie w sekcji ''Linker'' należy wybrać opcję ''Advanced'' i w polu ''Entry Point'' wpisać nazwę funkcji.
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_09_06_12.png?600|}} Konfiguracja punktu wejścia do programu
W tej sekcji należy również dołożyć brakujące biblioteki, z których będziemy korzystać. Uwaga - w zależności od wersji Windows i bieżącego poziomu aktualizacji, ten krok może nie być potrzebny, ale nie zaszkodzi. W szczególności w tych bibliotekach znajdują się finkcje typu ''sprintf''.\\ Uwaga - podane biblioteki należy dołożyć do tych, które standardowo już tam się znajdują. Usunięcie znajdujących się tam bibliotek (m.in. ''kernel32.lib'',''user32.lib'') spowoduje brak możliwości użycia standardowych okienek dialogowych oraz interakcji z Windows API. Aby dołożyć biblioteki należy wskazać w menu ''Linker'' pozyję ''Input'', a następnie wybrać ''Additional Dependencies'' i opcję '''' po rozwinięciu listy wyboru. Następnie w górnym polu wpisać nazwy plików bibliotecznych, które mają być dostępne dodatkowo podczas kompilacji (jedna pozycja w linii). \\ Biblioteki do uzupełnienia to: * ''legacy_stdio_definitions.lib'' * ''legacy_stdio_wide_specifiers.lib'' Szczegóły na rysunku {{ref>jalabstep52}}.
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_12_03_2021_08_50_25.png?600|}} Konfiguracja bibliotek dodatkowych
=== Oznaczenie pliku ASM do kompilacji === W niektórych przypadkach zdarza się, że plik ''.asm'' nie jest automatycznie rozpoznawany jako plik źródłowy z kodem w assemblerze. W związku z tym należy jawnie poinformować o tym środowiko. W tym celu należy kliknąć nazwę pliku w drzewie projektu prawym przyciskiem i wybrać opcję ''Properites'', a następnie w sekcji ''General'' wskazać ''Item Type'' i ustawić jego wartośc na ''Microsoft Macro Assembler''. Szczegóły na rysunku {{ref>jalabstep6}}.
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_27_03_2021_19_44_41.png?600|}} Oznaczenie pliku ASM jako pliku z kodem źródłowym w assemblerze
=== Instalacja dodatku kolorującego kod źródłowy w assemblerze === Na sieci jest dostępne rozszerzenie do Visual Studio o nazwie **AsmDude**, które (m.in.) automatycznie koloruje kod źródłowy w assemblerze, pomaga w pisaniu kodu (podaje nazwy etykiet, zmiennych, itp) oraz podczas debbugowania wyświetla okienka z wartościami rejestrów. Plugin ten może zostać zainstalowany poprzez managera rozszerzeń Visual Studio((https://marketplace.visualstudio.com/items?itemName=Henk-JanLebbink.AsmDude)). ==== Praktyka: Zadanie do wykonania ==== Zaimplementuj odczyt daty (użyj funkcji ''GetSystemTime''(sysinfoapi.h)((https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtime)) odczytującej strukturę ''_SYSTEMTIME''(minwinbase.h)((https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime)). Struktura składa się z 8 wartości szesnastobitowych. Uwaga - nie deklaruj struktury, jako takiej, tylko dla uproszczenia zadeklaruj docelową, statyczną tablicę 8 wartości typu Word (16bit) w segmencie danych. Skonwertuj rok, miesiąc i dzień do postaci tekstowej ASCII i wyświetl na ekranie za pomocą: * w linii poleceń (cmd) za pomocą funkcji ''WriteConsoleA''; * w okienku dialogowym, za pomocą funkcji ''MessageBoxA''; Zwróć uwagę na położenie poszczególnych słów w strukturze i odpowiadające im wartości. Możesz dodatkowo wyświetlić bieżącą godzinę, minuty i sekundy. === Fragmenty kodu do wykorzystania === Pamiętaj o deklaracji segmentu kodu i danych za pomocą operatorów ''.data'' i ''.code''. Na końcu kodu obowiązkowo umieść klauzulę ''END''. == Segment danych == Każdy łańcuch tekstowy musi kończyć się znacznikem o binarnej wartości 0. Nie używamy polskich znaków, ponieważ korzystamy z zestawu znaków ASCII (8-bit), a nie Unicode.\\ Przykładowy segment danych, który był używany w prezentacji wygląda tak:\\ ... .data STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 STD_ERROR_HANDLE = -12 handler dq 0 stdHandle dq 0 datetimeStruct dw 16 dup (0) hello_msg db "Hello world", 0 info_msg db "Info", 0 title_msg db "Window Title JA",0 text_msg db "A to jest bardzo dlugi tekst do wyswietlenia w konsoli, ale bez polskich znakow!",13,10 dummy dq 0 ... Aby móc skorzystać z zewnętrznych funkcji Windows API, trzeba poinformować assembler, jak one się nazywają, aby linker mógł potem dopasować nazwy z Twojego kodu do tych, które znajdują się w bibliotece.\\ W tym celu trzeba użyć dyrektywy ''EXTERN'' oraz ''PROC'':\\ ... includelib kernel32.lib EXTERN MessageBoxA: PROC EXTERN GetForegroundWindow: PROC EXTERN WriteConsoleA: PROC EXTERN SetConsoleTitleA: PROC EXTERN GetStdHandle: PROC EXTERN GetSystemTime: PROC ... Zwyczajowo umieszczamy również informację o bibliotece. == Segment kodu == Po uruchomieniu programu musimy zachować wartości pewnych rejestrów, w celu ich późniejszego przywrócenia. Na sam koniec należy również wywołać funkcję kończącą program i zwracającą tzw kod powrotu do systemu operacyjnego.\\ Minimalny zestaw instrukcji wygląda następująco: ... .code hello_world_asm PROC push rbp ; save frame pointer mov rbp, rsp ; fix stack pointer sub rsp, 8 * (4 + 2) .... ; tutaj kod Twojego programu mov rsp, rbp pop rbp ret hello_world_asm ENDP END Uwaga, w powyższym przykładzie, główną procedurą jest ''hello_world_asm'' i to ona została wskazana jako [Entry Point] (punkt wejścia) do programu, w konfiguracji opisanej powyżej. Dostosuj do Twojego nazewnictwa. Jak widać, całość sprowadza się do zarezerwowania i odłożenia na stosie przestrzeni na tzw. shadow registry, czyli na zachowanie wartości rejestrów i przywrócenie ich po zakończeniu wykonywania programu. W programie wykorzystujemy interakcję z elementami graficznymi takimi jak okienko główne (Foreground Window) oraz strumień danych (STD_OUT). Podczas wywoływania funkcji, należy jednoznacznie wskazać, względem którego obiektu graficznego chcemy wykonać interakcję lub stworzyć obiekt potomny (np. MessageBox).\\ W tym celu musimy pobrać dwa uchwyty (handlery): * uchwyt do bieżącego głównego okienka aplikacji na poziomie systemu operacyjnego (tzw. Foreground Window) * uchwyt do strumienia danych typu terminalowego, do którego można wpisywać informacje. Uwaga! Aby wyświetlić coś na konsoli, należy zawsze nacisnąć Enter, nawet ... wirtualnie. A zatem dowolny ciąg znaków przekierowany na STD_OUT pojawi się na konsoli o ile ... na końcu znajdzie się specjalny kod oznaczający znak ENTER. Są to dwie wartości numeryczne, w kolejności 13 i 10. Stąd właśnie w przykładie w segmencie danych, po treści znajduje się '',13,10''. Jeśli tego nie zrobisz, konieczne będzie przejście do okienka i ręczne naciśnięcie ENTER w celu wyświetlenia przekierowanego tam ciągu znaków z Twojego programu. Inaczej okienko pozostanie puste i tekst nie pojawi się! W celu odczytania uchwytów przydatne będą dwie funkcje z Windows API, które zwrócą nam wskaźniki (uchwyty) do odpowiednich obiektów. Wartości te należy zachować i można je wykorzystywać wielokrotnie. Odczyt uchwytu do okienka głównego, ten uchwyt należy zastosować przy wywoływaniu okna dialogowego typu Message Box (i każdego innego): ... call GetForegroundWindow mov handler, rax ... W powyższym przykładzie zwracana przez funkcję wartość (przez rejestr RAX) zapisywana jest w zmiennej ''handler'', typu DQ (64 bity, czyli po prostu adres w pamięci). Aby odczytać uchwyt do wnętrza terminala, należy skorzystać z innej funkcji: ... mov rcx, STD_OUTPUT_HANDLE call GetStdHandle mov stdHandle, rax ... Przy czym ta funkcja wymaga jednego argumentu, o który strumień danych nam chodzi. Są trzy możliwości: * STD_INPUT_HANDLE = -10 - standardowe wejście - można go użyć do odczytu danych z konsoli * STD_OUTPUT_HANDLE = -11 - standardowe wyjście - jego używamy aby coś napisać w terminalu; pamiętaj o kończącym ENTER, czyli ''13,10''!; * STD_ERROR_HANDLE = -12 - strumień dla błędów; Przykładowy kod wyswietlający na konsoli tekst znajdujący się w zmiennej (pod adresem) ''text_msg'' znajduje się poniżej. Pierwszy parametr to odczytany wyżej uchwyt do konsoli. W trzecim parametrze należy podać, ile znaków ma zostać wyświetlonych. Czwarty parametr to wskaźnik do miejsca, gdzie funkcja zwróci informacje, ile tak na prawdę wyświetliła znaków (nie do końca wiadomo, czemu to służy). ... ; WINAPI WriteConsoleA( ; RCX => _In_ HANDLE hConsoleOutput, ; RDX => _In_ const VOID *lpBuffer, ; R8 => _In_ DWORD nNumberOfCharsToWrite, ; R9 => _Out_opt_ LPDWORD lpNumberOfCharsWritten, mov rcx, stdHandle lea rdx, text_msg mov r8, sizeof text_msg mov r9, dummy call WriteConsoleA ... W celu wyświetlenia okienka dialogowego, należy użyć wskaźnika do okna głównego aplikacji. Jako parametry przekazywane są w kolejności: uchwyt, dwa wskaźniki, do tekstów (muszą kończyć się zerem, RDX to wskaźnik do treści, którą wyświetli dialog, a R8 wskaźnik do nagłówka okienka, który zotanie nadany okienku), które mają pojawić się w nagłówku oraz treści dialogu oraz jako ostatni, maska binarna określająca, jakie mają być zastosowane przyciski i ikony w okienku dialogowym((https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa)). ... ; WINUSERAPI int WINAPI MessageBoxA( ; RCX => _In_opt_ HWND hWnd, ; RDX => _In_opt_ LPCSTR lpText, ; R8 => _In_opt_ LPCSTR lpCaption, ; R9 => _In_ UINT uType); mov rcx, handler mov rdx, offset hello_msg mov r8, offset info_msg mov r9, 0 ; MB_OK and rsp, not 8 call MessageBoxA ... Uwaga - w powyższym przypadku wykonano jawnie wyrównanie stosu do wartości podzielnej przez 16 (''and rsp, not 8''). W większości przypadków nie jest to konieczne, ponieważ organizacja danych najczęściej jest 64-bitowa. Gdyby jednak okazało się, że gdzieś został odłożony na stosie pojedynczy bajt lub słowo, ulegnie to zmianie i program może rzucić wyjątkiem. W związku z tym w takim przypadku należy zadbać o wyrównanie wskaźnika stosu tak, aby był podzielny przez 16. ==== Debuggowanie i uruchamianie ==== Uwagi praktycznie odnośnie debbugowania i podglądania pamięci: * Pamiętaj, aby na poziomie wywołań funkcji Windows API nigdy nie wykonywać operacji wejścia do wewnątrz ich definicji (Step Into, F11), o ile wcześniej nie zainstalowałeś Windows SDK wraz z definicjami i kodem źródłowym. Zamiast tego należy używać przejściea do następnej instrukcji (Step Over, F10) * Okno rejestrów domyślnie pokazuje tylko podstawowy zestaw: więcej po kliknięciu prawym przyciskiem myszy i włączeniu innych zestawów rejestrów * Okno podglądu pamięci pozwala na zmianę formatowania danych z domyślnego, bajtowego, na wiele innych. Np w kontekście podglądu ww. struktury składającej się z danych 16-bitowych zdecydowanie warto zmienić podgląd na 2-bajtowy. Natomiast do podglądania ciągów znaków ASCII należy wrócić do jednobajtowego. Aby podejrzeć zawartość pamięci, najprościej jest odnaleźć adres zmiennej w rejestrze, po wykonaniu instrukcji ''LEA''(Load Effective Address). Wartość z okienka podglądu stanu rejestrów należy przekopiować do okienka podglądu stanu pamięci. Uwaga - powyższą operację należy wykonać pod debuggerem zaraz po wykonaniu instrukcji ''LEA'' ponieważ następujący po niej ''CALL'' powoduje zmiany zawartości rejestrów i adres zostanie zamazany. Na rysunku rysunek {{ref>debugmem1}} przedstawiono kolejność działań w celu odczytania (odnalezienia) miejsca w pamięci, gdzie znajduje się zmienna. Breakpoint (pułapkę) należy ustawić bezpośrednio na następnej instrukcji znajdującej sie po LEA, chyba że jest gwarancja, iż instrukcje pomiędzy LEA i breakpointem nie modyfikują danego rejestru. Odczytaną wartość należy przenieść z okienka rejestrów do okienka pamięci. Po wywołaniu funkcji, zawartość rejestru RCX zmienia się, natomiast dane zwracane są do pamięci, jak na rysunku rysunek {{ref>debugmem2}}. Zadanie dodatkowe: zamień widoczną na rysunku zawartość pamięci, a dowiesz się, kiedy powstał screenshot ;-).
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_11_03_2021_22_30_04.png?800|}} Wyciąganie adresu dla zmiennej (stan bezpośrednio po wykonaniu LEA)
{{:pl:edu:zmitac:n1z:ja:virtualbox_msedge_-_win10_11_03_2021_22_30_28.png?800|}} Obserwacja wyniku w pamięc, po wywołaniu funkcji
==== Metoda weryfikacji wyników ==== Teksty oraz okienka pojawiające się na ekranie zgadzają się z algorytmem. Zwróć szczególną uwagę na to, czy nie są generowane losowe ciągi znaków, co może sugerować, że zapomniałeś o kończącej łańcuch wartości 0. ==== Pomocne zasoby ==== * Visual Studio 2019: [[https://visualstudio.microsoft.com/vs/]] * Przydatne funkcje Windows API: [[https://docs.microsoft.com/en-us/windows/win32/api/_winmsg/]]