Podstawy teoretyczne
- Procesor wykonując program działa sekwencyjnie wg następującego schematu:
- Pobierz rozkaz wskazywany przez IP (Instruction Pointer)
- Zdekoduj rozkaz
- Wykonaj rozkaz i zaktualizuj flagi stanu
- Zwiększ IP, wróć do punktu 1
- Przykład, kod w asemblerze:
start:
mov eax, 10
mov ecx, 2
sub edx, edx
idiv ecxstart:
0: b8 0a 00 00 00 mov eax,10
5: b9 02 00 00 00 mov ecx,2
a: 29 d2 sub edx,edx
c: f7 f9 idiv ecx - Prześledźmy to po kolei:
IP = 0
, wskazuje na instrukcjęmov eax, 10
- Procesor odczytuje bajt
b8
- Procesor dekoduje, że jest to instrukcja
mov eax, LICZBA
, pobiera jeszcze 4 bajty stanowiące liczbę - W tym momencie
IP = 5
, wskazuje na bajtb9
czyli instrukcjęmov ecx, LICZBA
, itd.
- Pisząc w asemblerze mamy możliwość odwzorowania 1-do-1 tego co zaprogramujemy i tego co zostanie wykonane. Natomiast kompilator C (i innych języków) zamienia program na określone sekwencje instrukcji asemblera - czasem lepiej, czasem gorzej niż człowiek.
- Wyżej wspomniane zostały
eax
iecx
, co to jest? Procesor ma wbudowany w siebie szereg rejestrów. Każdy rejestr to jednostka pamiętająca pewną liczbę bitów. Można powiedzieć w uproszczeniu, żeeax
iecx
przypominają zmienne z języka C. - Dostępne rejestry:
32-bit | EAX | ECX | EDX | EBX | EDI | ESI | ||||||||||||||||||
16-bit | AX | CX | DX | BX | DI | SI | ||||||||||||||||||
8-bit | AH | AL | CH | CL | DH | DL | BH | BL |
- Przykład: (AX = 1234, AH = 4, AL = 210)
AX | b15 | b14 | b13 | b12 | b11 | b10 | b09 | b08 | b07 | b06 | b05 | b04 | b03 | b02 | b01 | b00 |
AH | b15 | b14 | b13 | b12 | b11 | b10 | b09 | b08 | ||||||||
AL | b07 | b06 | b05 | b04 | b03 | b02 | b01 | b00 | ||||||||
123410 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 |
- Podstawowe instrukcje:
mov CEL, ŹRÓDŁO
- przekopiuj wartość ze ŹRÓDŁA do CEL u np.mov eax, ecx
skopiuje zawartośćecx
doeax
add CEL, WARTOŚĆ
- dodaj do CELu WARTOŚĆ np.add eax, 1
doda 1 doeax
sub CEL, WARTOŚĆ
- jw. ale odejmowaniediv REJESTR
- dzieli zawartośćeax
przez REJESTR. Wynik zapisany będzie weax
, reszta z dzielenia wedx
ale UWAGA!, rejestredx
musi być wcześniej wyzerowany!mul REJESTR
- mnożyeax
przez REJESTR
- Procesor posiada jeszcze specjalny rejestr stanu, do którego zazwyczaj nie sięga się bezpośrednio. Jak napisano powyżej, rejestr stanu zmienia się w ogólności po każdym wykonaniu instrukcji i odzwierciedla on status systemu.
- Aby wykonać kod warunkowo (tzn. odpowiednik if() czy pętli z języka C), należy ze sobą połączyć dwie instrukcje:
cmp CEL, WARTOŚĆ
- porównanie CELu z WARTOŚCIĄ np.cmp eax, ecx
porówna rejestreax
zecx
; po takim porównaniu ustawione zostaną flagij?? ETYKIETA
- jest to skok warunkowy do ETYKIETY; znaki zapytania to odpowiednie litery, których należy użyć do określonej funkcjonalności:je
- jump-if-equal, skocz jeśli porównane wartości są równejne
- jump-if-not-equal, skocz jeśli nie są równejl
orazjle
- jump-if-less oraz jump-if-less-or-equal, skocz jeśli CEL jest mniejszy od WARTOŚCI (jle
= jeśli mniejszy lub równy)jg
orazjge
- jump-if-greater oraz jump-if-greater-or-equal, skocz jeśli CEL jest większy od WARTOŚCI (jle
= jeśli większy lub równy)
Instrukcja warunkowa if()
C:
...
}
Asembler:
cmp eax, 4
jle koniec_if
...
koniec_if:
Warto zauważyć, że w C programista pisze if() myśląc przy jakim warunku wykonać kod, natomiast w asemblerze programista myśli przy jakim warunku przeskoczyć kod :)
Pętla while()
C:
...
n = n + 1;
}
Asembler:
cmp eax, 5
jge koniec_while
...
add eax, 1
jmp poczatek_while
koniec_while:
Instrukcja jmp
to bezwarunkowy skok - zawsze skacze do podanej etykiety, niezależnie od rejestru stanów.
Szablon do własnej pracy
Używany kompilator na zajęciach: http://flatassembler.net/download.php
Przykładowy kod: http://www.cs.put.poznan.pl/tzok/example.asm
entry start
include 'win32ax.inc'
section '.import' import data readable
library msvcrt, 'msvcrt.dll'
import msvcrt,\
printf, 'printf',\
scanf, 'scanf',\
system, 'system'
section '.data' data readable writeable
format_in db '%d', 0
format_out db '%d', 13, 10, 0
n dd ?
section '.code' code readable writeable executable
start:
cinvoke scanf, format_in, n
mov eax, [n]
add eax, 10
sub edx, edx
mov ecx, 5
idiv ecx
cinvoke printf, format_out, eax
cinvoke system, 'pause'
ret
Komentarz:
- Do wywołania funkcji z języka C używamy składni
cinvoke NAZWA, ARGUMENT1, ARGUMENT2, ...
- Napisy to zmienne typu
db
, koniecznie zakończone zerem. Znak nowej linii to13, 10
(zobacz przykład powyżej, zmiennaformat_out
) - Liczby całkowite to zmienne typu
dd
- Zapis
mov eax, [n]
oznacza "zawartość zmiennej n wpisz do rejestru eax"
Debugowanie
Używany debuger na zajęciach: http://www.ollydbg.de/odbg110.zip
Debugowanie to proces kontrolowanego uruchomienia aplikacji. Oznacza to tyle, że można wykonywać program instrukcja po instrukcji obserwując dokładnie jak zmieniają się wartości w rejestrach i flagi stanu.
Po włączeniu OllyDbg wczytujemy nasz skompilowany program przez opcję File->Open

Legenda:
- zielony - adresy załadowanego do pamięci programu
- fioletowy - instrukcje w czytelnej dla maszyny postaci
- żółty - instrukcje w czytelnej dla człowieka postaci :)
- czerwony - wartości rejestrów (pionowa kolumna poniżej to wszystkie flagi tworzące razem rejestr stanu)
Aby przejść jedną instrukcję do przodu, naciskamy F8. Po każdym kroku zmienione rejestry oraz flagi zapalają się na czerwono.
Ćwiczenia na laboratorium
- Proszę wykorzystać poniższy, minimalny kod do tworzenia rozwiązań zadań na ćwiczeniach:
format pe console
entry start
include 'win32ax.inc'
section '.code' code readable writeable executable
start:
mov bl, 1
mov cl, 2
mov dl, 3
; kontynuuj tutaj!
ret
Tematy zaawansowane
- Jak widać na powyższych przykładach, kod to po prostu bajty, pewne liczby... Można je zmieniać w samym programie, jest to idea SMC (self-modifying code) używana np. przez wirusy i z tego też powodu często zabroniona przez systemy operacyjne. Przykład:
start:
mov eax, 10
sub eax, 1
mov dword [start + 1], eax
jnz start - Dlaczego to działa?
- Do
eax
trafia wartość 10 - Następnie w
eax
znajduje się wartość o 1 mniejsza - Zapisujemy to pod adres
[start + 1]
, czyli co to oznacza? Spójrz na sam początek tej strony gdzie znajduje się przykład jak wygląda instrukcjamov eax, LICZBA
; są to bajtyb8 ?? ?? ?? ??
gdzie znaki zapytania oznaczają liczbę. A zatem zapis pod adres[start + 1]
nadpisuje te cztery bajty! - Instrukcja
sub
ustawia flagęZF
jeśli po jej wykonaniu wynikiem będzie zero. Natomiast instrukcjamov
nie zmienia żadnych flag. Dlatego sekwencja:sub
,mov
,jnz start
to prosta pętla, która będzie wracać do punktu startu dopóki weax
nie znajdzie się zero.
- Do
- Procesory Intel/AMD mają bardzo bogaty zbiór instrukcji. Niektóre są bardzo specyficzne, niektóre są nawet nieudokumentowane ;). Tę samą rzecz można często wykonać na kilka sposobów. Przykład, jak dodać 1 od rejestru
eax
:; dodaj 1
83 c0 01 add eax,0x1
; inkrementuj
40 inc eax
; odejmij -1
83 e8 ff sub eax,0xffffffff
; wpisz 1 do rejestru, odejmij od niego
29 d2 sub edx,edx
42 inc edx
29 d0 sub eax,edx - Idąc dalej tym tropem, okazuje się, że nawet taka sama instrukcja może mieć kilka wersji maszynowych (różne bajty kodują to samo), przykład:
6a 00 push 0x0
68 00 00 00 00 push 0x06a
oznacza umieść na stosie wartość o wielkości 1-bajta, natomiast68
to umieść na stosie wartość o wielkości 4-bajtów - Z innych ciekawych instrukcji:
jecxz ETYKIETA
- skocz do etykiety jeśliecx
jest równe 0 (nie potrzeba wykonywać insturkcjicmp
)cmov?? CEL, WARTOŚĆ
- działa jak instrukcjamov
, ale działa warunkowo; pod znaki zapytania należy wstawić końcówki jak w insturkcjach skoku np.cmovz
albocmovle
- Jak w języku C zamienić dwie wartości miejscami? Trzeba się posłużyć zmienną pomocniczą:
int m = 2, n = 5;
int tmp = m;
m = n;
n = tmp;xchg eax, ecx