Dzisiaj zajmiemy się asemblerem w zakresie wymaganym na laboratoriach z „Architektura Systemów Komputerowych”.

Zanim zaczniemy, musimy poznać kilka podstawowych pojęć. Musimy dowiedzieć się między innymi, czym są rejestry. Powinniśmy także znać kilka podstawowych komend. Ale nie przejmuj się, nie ma ich wiele 🙂

Przećwicz dokładnie to, co poniżej zostało opisane. Krążą wieści, że Architektura Systemów Komputerowych to przedmiot, który jest najlepszym przesiewem 🙂

Podstawowe pojęcia

Będziemy operowali na rejestrach. Czym jest rejestr? Jest to najszybsza, ale i najmniejsza pamięć jaką dysponuje procesor. W procesorach x86 występuje stosunkowo mało rejestrów. Oto one:

  • EAX – akumulator – używany do przechowywania wyników wielu operacji
  • EBX – rejestr bazowy – służy do adresowania
  • ECX – rejestr licznikowy – służy jako licznik w pętli
  • EDX – rejestr danych – umożliwia przekaz/odbiór danych z portów we/wy
  • ESP – wskaźnik wierzchołka stosu
  • EBP – rejestr bazowy – służy do adresowania
  • ESI – rejestr źródłowy – trzyma źródło łańcucha danych
  • EDI – rejestr przeznaczenia – przetrzymuje informacje o miejscu docelowym łańcucha danych

Wszystkie powyższe rejestry są 32 bitowe. Są to rejestry ogólnego przeznaczenia. Oznacza to, że nie musimy ich wykorzystywać koniecznie do tego, do czego z reguły są używane. Wszystko zależy tylko od ciebie.

Warto wspomnieć także o rejestrze EIP. W przeciwieństwie do powyższych, nie jest to rejestr ogólnego przeznaczenia. W EIP zawsze przechowywany jest adres aktualnie wykonywanej instrukcji kodu.

Podstawowy zestaw narzędzi

Do naszych analiz użyjemy programu OllyDbg. (Do ściągnięcia wraz z odpowiednimi plikami – (będzie dostępne później). W paczce razem z OllyDbg znajduje się program test.exe, który będziemy „modyfikowali”. Na jego przykładzie będziemy obserwowali pracę procesora podczas wykonywania różnych instrukcji.

Okno programu Ollydbg
Okno programu ollydbg

Po otworzeniu programu OllyDbg ukaże nam się widok taki jak na powyższym zrzucie ekranu. Abyśmy mogli otworzyć interesujący nas plik, musimy kliknąć File->Open. Następnie wybieramy test.exe

UWAGA! Jeżeli na górnej belce pisze ci: module ntdll, to kliknij View->Executable Modules. W nowo utworzonym oknie wybierz moduł, na którym będziemy operowali (test)

OllyDbg z objaśnieniem

Ten widok może cię przerazić … O co tu w ogóle chodzi? Już wyjaśniam J

Większą część okna programu zajmują instrukcję, które zawiera nasz program. To, o czym mówię, kryje się pod cyferką 1. Pod cyferką 2 natomiast znajdują się omawiane wcześniej rejestry procesora – a konkretnie – ich zawartość. Ta część okna przykuje naszą główną uwagę.

Pod trójeczką kryje się zawartość pamięci RAM, która została przydzielona na poczet naszego programu.

Zanim zaczniemy cokolwiek robić, poznajmy kilka instrukcji asemblerowych, które przydadzą się w przyszłości.

Podstawowe instrukcje asemblera

mov cel, zrodlo – przenosi zawartość zrodlo do cel
jmp adres – wykonuje bezwarunkowy skok do miejsca określonego jako operand tego rozkazu. Rozkaz JMP zmienia zawartość PC (Program Counter) czyli rejestru EIP.
sub cel,zrodlo– wykonuje odejmowanie źródła od celu i wynik zapisuje w cel.
sbb cel,zrodlo– wykonuje odejmowanie z przeniesieniem. (Cel=źródło-cel)
rdtsc– wynikiem jest 64 bitowa liczba, która zawiera ilość cykli procesora od uruchomienia komputera. Części liczby są zapisane w rejestrach EDX (starsza część)  i EAX (młodsza część).

 Pierwszy program

Skoro znamy podstawowe instrukcje asemblera, to rozpocznijmy analizę najprostszego możliwego programu:

mov eax, 100
mov eax, 300
mov ebx, 400
mov ecx, 200
jmp 00401000

Wprowadź powyższy program do OllyDbg. Aby wprowadzić instrukcję, wystarczy kliknąć na danej linijce dwukrotnie. Pojawi się okienko assemble at, w które wprowadzisz instrukcję z przykładu.

Assemble at window

Okno programu OllyDbg po wprowadzeniu instrukcji powinno wyglądać następująco:

OllyDbg z programem

Obserwuj zawartość rejestrów i naciskaj F7. Na podstawie własnej wiedzy oceń, co robi ten program.

Dobra. Skoro dalej czytasz, to powiem: robi bardzo wiele J Najpierw zapisuje w rejestrze EAX wartość 100. Potem zapisuje do tego samego rejestru liczbę 300. Następnie do ebx’a kopiuje 400 a do ecx’a 200. Na końcu wykonywany jest skok do pierwszej linijki programu i cała historia zaczyna się od początku 🙂

Clue – mierzenie wydajności

Zajmiemy się teraz głównym celem naszych rozważań – mierzeniem wydajności 🙂

rdtsc
mov ds:[00403000],eax
mov ds:[00403004],edx
mov eax,200
mov eax,200
mov eax,200
mov eax,200
rdtsc
sub eax,ds:[00403000]
sbb edx,ds:[00403004]
jmp 00401000

Co robi powyższy program? Otóż, ma on nam pokazać, jak wiele cykli procesora zajmuje zapisanie pewnej wartości w rejestrze procesora.

Zajmijmy się najpierw dokładną analizą pierwszej instrukcji: rdtsc. Wiesz już mniej więcej, co ona robi. Zapisuje do rejestrów EAX i EDX liczbę cykli procesora, które procesor przeżył od chwili uruchomienia. Lecz, jak wielkie są to liczby? Przyjmijmy, że nasze CPU taktowane jest częstotliwością 3GHz. Oznacza to, że wartość ta przyrasta co chwilę o 3 000 000 000. Wydaje się, że jest to dużo … Obliczmy zatem  kiedy nasza 64 bitowa zmienna się przepełni.

Para rejestrów EAX, EDX pomieści maksymalnie 2^64. No ale procesor ma częstotliwość 3*10^9 Hz, więc następuje niezgodność potęg. Na potrzeby naszych obliczeń przyjmiemy pewne zaokrąglenie.

Obliczenia

Dobra. Wiemy, że miejsca nam nie zabraknie. Analizujmy więc kod dalej.

Co robimy w następnych dwóch linijkach? Pojawia się tajemnicze ds., cóż to takiego? ds oznacza data segment, czyli w skrócie pamięć RAM. To, co znajduje się w nawiasach kwadratowych to adres w pamięci, pod który zostanie przeniesiona kolejno zawartość rejestrów EAX i EDX.

Ale dlaczego zapisujemy to w pamięci RAM? Bo tego wymaga nasz algorytm postępowania. Najpierw sprawdzamy, ile cykli procesor wykonał do tej pory. Zapisujemy tę informację. Potem wykonujemy operację, której wydajność chcemy zmierzyć, a następnie ponownie pobieramy ilość cykli procesora wykonanych od startu PC-ta. Uzyskana różnica będzie poszukiwaną przez nas wartością.

Może cię zastanowić, dlaczego instrukcja mov eax, 200 powtarza się aż 4 razy … Odpowiedź jest prosta – dla zwiększenia dokładności. Zauważ, że po wykonaniu instrukcji RDTSC znajduje się omawiane wcześniej przenoszenie zawartości rejestrów do pamięci RAM. Operacja ta, jak każda, swoje cykle procesora kosztuje. I co najgorsze, te cykle wliczą nam się do końcowego wyniku. Tak więc, czym więcej czasu pochłonie nasza docelowa operacja, tym większą dokładność uzyskamy.

Jeśli dalej nie rozumiesz, wyobraź sobie, że startujesz w biegu na 200 metrów. Jesteś sam sobie sędzią. Przed startem musisz zapisać aktualny czas (godzinę, minutę, sekundę). Potem przebiegasz 100 metrów w tę i z powrotem. Ponownie trochę czasu zajmie ci najpierw spojrzenie na zegarek, a potem zapisanie odpowiedniej godziny. Okaże się, że wynik twojego pomiaru był nader niedokładny. Ta niedokładność stanie się mniejsza, gdybyś wydłużył dystans biegu.

Załóżmy, że bieg zajął ci 20 sekund, a zapisanie wyniku 2x5s. Wtedy łączny wynik to 30s, a błąd pomiarowy (10s) stanowi aż 1/3 całego wyniku. Ten sam błąd pomiarowy nie wpłynie tak bardzo na wynik wtedy, kiedy będziesz biegł 3600 sekund. Na zapisanie odpowiednich czasów dalej będziesz potrzebował tyle samo czasu (2x5s). Lecz tym razem pomyłka będzie stanowiła tylko 1/360 całego wyniku.

Na koniec wyjaśnijmy tajemnicę dwóch odejmowań. Dlaczego najpierw używamy instrukcji sub, a następnie instrukcji sbb? Dla uproszczenia, przyjmijmy że komputer liczy w systemie dziesiętnym.

Przypomnij sobie, jak wykonuje się odejmowanie pisemne. Ile to jest 13-7? Odpowiedź wydaje się prosta – 6. I, co najważniejsze, jest poprawna! Brawo.

Ale, co musiałeś zrobić, aby wykonać te obliczenie? Pożyczyłeś pewnie jedynkę z wcześniejszej pozycji. Dokładnie tak samo robi komputer.

Jaki będzie wynik działania 6-9? Poprawna odpowiedź brzmi: 7! Dlaczego? Bo pożyczyliśmy „niewidzialną” jedynkę z poprzedniego miejsca.

Sub wykonuje odejmowanie bez względu na wszystko. Nie patrzy na to, czy wcześniej było coś pożyczone czy nie. Jednakże, czasem może się okazać, że odejmujemy mniejszą liczbę od większej. Wtedy we fladze CF zostanie zanotowana informacja o tym fakcie.

I właśnie z tej flagi korzysta Sbb. Jeśli flaga CF jest równa 1, to „pożyczka” wystąpiła wcześniej i należy ją uznać przy wynikach działań. Spójrz na poniższy przykład:

Obliczenia Architektura Systemów Komputerowych

Obydwa wyniki są poprawne! W drugim wypadku wystąpiła niewidzialna, dodatkowa „pożyczka” która zapisana była we fladze CF.

No dobra, ale do czego nam to właściwie potrzebne. Nie moglibyśmy użyć po prostu sub? Nie, ponieważ nasza liczba jest rozbita na dwie części. I odejmujemy dwie części oddzielnie. Tak samo, jak odejmując 24 od 16 musimy pamiętać w odpowiednich momentach o „pożyczce”, tak samo i tutaj musimy pamiętać, że przy odejmowaniu młodszych części liczb może wystąpić konieczność pożyczki od starszej połowy tej liczby. I taką pożyczkę musimy uwzględnić w obliczeniach.

No dobrze, a dlaczego zatem nie użyjemy dwa razy sbb? Jest prosty powód. Flaga CF może być wcześniej zapalona (niekoniecznie przez nasz program) co mogłoby zafałszować wyniki obliczeń. Instrukcja sub wygasi flagę lub zapali ją, kiedy będzie potrzebna. Dzięki temu sbb będzie operowała na właściwym zestawie danych.

Dobra. Skoro rozumiesz już działanie powyższego programu to przepisz go do OllyDbg. Tym razem nasz tok postępowania będzie nieco inny. Po przepisaniu wszystkich instrukcji zaznacz ostatnią linijkę (tę ze skokiem – jmp). Następnie naciśnij klawisz F2. Spowoduje to ustawienie pułapki (breakpointa). Pułapka będzie zatrzymywała program zawsze, gdy dojdzie do oznaczonego przez nas momentu. Następnie naciśnij klawisz F8. Jaka liczba pojawiła się w rejestrze EAX?

Podsumowując

Znasz już kilka podstawowych pojęć asemblera, wiesz z czym się to je. Potrafisz także zmierzyć wydajność wykonywania poszczególnych instrukcji. Dla przećwiczenia spróbuj zmierzyć, jak dużo cykli procesora zajmuje przeniesienie danych z rejestru do pamięci RAM.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *