ĆWICZENIE 1
Łatka to kod poprawiający błąd w łatanym programie lub dodający nowe funkcje do programu. W AMASM łatki dodają obsługę nowych procesorów rozszerzając listę rozkazów.
Plik NEW_VARS.BAT zawiera ustawienia zmiennych systemowych potrzebnych do pracy z programem pwb.exe. Zmienne te zawierają ścieżki potrzebne programowi pwb.exe
Aby ustawić liczbę linii na 50 w edytorze pwb należy:
- z menu [Options] wybrać [Editor Settings]
- ustawić [Switch Type] na [Numeric]
- następnie z listy [Switch List] wybrać [height] i ustawić wartość na pożądaną (dostępne wartości 25, 43 or 50).
Przebieg procesu uruchamiania programu:
1.program źródłowy(plik.asm) 2.asemblacja programu źródłowego, wygenerowanie pliku program.lst 3.wygenerowanie pliku program.obj 4.linkowanie(konsolidacja) plików .obj oraz plików bibliotecznych .lib 5.wygenerowanie pliku wykonywalnego program.exe (program.com) oraz pliku program.map 6.uruchamianie, testowanie, debugowanie programu.
Proces asemblacji -tłumaczenie programu źródłowego na program wynikowy w postaci zbioru typu .obj
Linker, program konsolidujący - pozwala na wygenerowanie pliku wykonywalnego typu .exe lu .com z jednego lub wielu plików typu .obj oraz plików bibliotecznych .lib
Debuger, program uruchomieniowy - umożliwia śledzenie rejestrów procesora, zawartości pamięci, wykonywanie programu krokowo, ustawianie punktów zatrzymań programu (breakpoint)
Listing asemblacji (plik program.lst) zawiera: 1.I kolumna to offset(adres liczony względem początku segmentu) 2. II kolumna to szesnastkowa wartość pamięci operacyjnej. W segmencie danych kolejne bajty to np kody ASCII napisów zaś w segmencie programu - szesnastkowe kody rozkazów procesora 3.Następne kolumny to program źródłowy identyczny z programem w pliku program.asm.
Druga część pliku .lst zawiera zestawienie segmentów programu oraz tablicę nazw symbolicznych. Nazwy zaczynające się od znaku @ są predefiniowane.
W przypadku błędu syntaktycznego w programie źródłowym asembler poinformuje o tym zarówno w czasie procesu asemblacji jak i wprowadzając odpowiedni komunikat do listingu asemblacji.
Standardowo linker nie generuje tzw. pliku z mapą (plik program.map)
Możliwości programu CodeView (debugger) 1.disasemblowanie programu binarnego znajdującego się w pamięci operacyjnej (odtworzenie symbolicznych kodów programu na podstawie binarnej zawartości pamięci operacyjnej) 2. wyświetlanie w różnych formatach i modyfikowanie zmiennych oraz zawartości pamięci operacyjnej 3. sterowanie wykonaniem programu (run, step, trace, animate) 4. możliwość ustawiania punktów kontrolnych breakpointów
TITLE prog
.MODEL small, c, os_dos
.DOSSEG
.STACK
.DATA
msg1 BYTE "To Dziala!!!", 13, 10, "$"
.CODE
.STARTUP
; Czyszczenie ekranu
mov cx, 25 ; 25 lini
while1: ; petla
mov ah, 2 ; wyswietlenie:
mov dl, 0Ah ; \r
int 21h
mov dl, 0Dh ; \n
int 21h
loop while1 ; skok na pocztek petli
; Przesuniecie kursora do 0,0
mov ah, 2
mov bh, 0 ; nr strony
mov dl, 0 ; nr kolumny
mov dh, 0 ; nr wiersza
int 10h
; Pobranie znaku
mov ah, 1 ; nr funkcji pobierajacej znak
int 21h ; pobranie znaku
; Wypisanie pobranego znaku
mov ah, 2 ; nr funkcji wyswietlajacej
mov dl, al ; znak pobrany a AL
int 21h ; wyswietlenie znaku
.EXIT 0
END
ĆWICZENIE 2
W debugerze codeview można podglądać wartości rejestrów 32-bitowych. Debuger informuje nas o zmiane wartości rejestru za pomocą podświetlenia rejestru którego wartość uległa zmianie.
Adres początku programu został wyliczony przez dodanie adresu segmentu przemnożonego przez 16 i offsetu.
Pułapkę w cv można zrobić klikając na margines linii, gdzie chcemy ustawić pułapkę lub wpisując w oknie Command polecenie bp. Można także ustawić pułapkę warunkową w zależności od wartości zmiennej.
Aby sprawdzic na jaki kod maszynowy została zamieniona jakaś instrukcja należy w debugerze Codeview wybrać: Options->Source Window->Show Machine Code i tam wybieramy: Display Mode->Assembly.
Najczęściej popełniane błędy: 1. nieprawidłowa kolejność argumentów instrukcji 2. korzystanie z rejestrów 8-bitowych a później 16 - lub 32 - bitowych bez wcześniejszego wyzerowania ich starszej części 3. Zmiana zawartości rejestrów przez wywoływany podprogram (brak instrukcji PUSH i POP lub ich nieprawidłowa kolejność) 4. bledy związane ze stosem: przepełnienie stosu (szczególnie przy algorytmach rekurencyjnych) lub brak czyszczenia stosu po wyjściu z podprogramu 5. błędy w wartości zmiennej sterującej pętlą 6. niewłaściwe określanie granic zmiennych i tablic (co często powoduje zmianę wartości innych - sąsiednich zmiennych)
print_bufor macro push ax mov ah, 9 mov dx, offset bufor int 21h pop ax endm dane segment bufor db 5 dup (0) db 0dh, 0ah,"$" dycha equ 10 dane ends ;-------------- ; segment stosu stosik segment stack dw 1024 dup (0) stosik ends ;-------------- ; segment programu ; program "główny" tylko po to, aby móc wywołać ; prawidłowo uruchamiany podprogram BD16 program segment 'code' .286 ; nie korzystamy z instrukcji 32-bitowych assume cs:program, ds:dane, ss:stosik start: mov ax, seg dane mov ds, ax mov bx, seg stosik mov ss, bx mov ax, 0ffffh ; taką liczbę chcemy przetworzyć i wyświetlić petla: mov si, offset bufor mov cx, size bufor call BD16 print_bufor ; dla kontroli wydrukujemy zawartość bufora dec ax jnz petla mov ax, 4C00h ; koniec programu int 21h ;-------- ; podprogram BD16 ;-------- ; funkcja: Przetwarzanie liczby binarnej 16-bitowej na postać ; dziesiętną w kodzie ASCII (1 bajt na cyfrę) ; parametry wejściowe: ; SI - adres dla cyfr dziesiętnych ; AX - liczba binarna |
; CX - liczba bajtów, zawiera po wykonaniu liczbę cyfr ; rejestry wykorzystywane: ; AX, CX, SI ;-------- BD16 proc near push ax ; rejestry na stos push bx push dx ; obliczenie adresu ostatniego bajtu cyfry add si, cx dec si ; adres ostatniego bajtu xor bx, bx ; pętla dla wszystkich cyfr BD16_1: xor dx, dx ; DX = 0 dla div push cx mov cx, dycha div cx pop cx ; wynik w AX, reszta (=cyfra) w DX or dl, 30h ; kod ASCII mov byte ptr [si], dl ;!! inc bx ; liczę ilość dec si ; następna cyfra ; porównuję długość pola z liczbą cyfr cmp bx, cx je BD16_3 ; pole jest pełne and ax, ax ; tylko zera? jnz BD16_1 ; dalej ; wypełnienie spacją push cx sub cx, bx BD16_2: mov byte ptr [si], ' ' dec si loop BD16_2 pop cx inc si BD16_3: pop dx pop bx pop ax ret BD16 endp program ends end start
|
ĆWICZENIE 3
DYREKTYWA EXTERNDEF ( EXTERNDEF[langtype]name:type[,[langtype]name:type]... ) Dyrektywa definiuje jedna lub więcej zewnętrznych nazw zmiennych, etykiet(adresów symbolicznych) lub innych symboli, których typ jest "type". Jeśli dyrektywa występuje w module, w którym dana nazwa symboliczna jest zdefiniowana to nazwa ta staje się globalną wewnętrzną. Jeśli dyrektywa występuje w module w którym dana nazwa symboliczna nie jest zdefiniowana ale jest używana to nazwa to staje się globalną zewnętrzną. Najczęściej dyrektywę umieszcza się w oddzielnym module dołączonym do pozostałych modułów programu dyrektywą .INCLUDE. NP.: externdef wyswietl_ten_bufor:far.
EXTERNDEF działa jak EXTERN jeśli w pliku w którym go użyto znajduje się dana nazwa symboliczna, natomiast jak PUBLIC jeśli nie. Dzięki temu można włączyć w kilku plikach nagłówek z deklaracją EXTERNDEF i nie rozdzielać tego na dyrektywy EXTERN i PUBLIC.
Dyrektywa EXTERN lub EXTRN ( EXTERN[langtype]name:type[,[langtype]name:type]... ) Dyrektywę EXTERN stosujemy w modułach, w których będziemy używali nazw symbolicznych zdefiniowanych w innych modułach, a więc nazw globalnych zewnętrznych. W definicji w polu parametrów musimy podać wszystkie nazwy, które nie zostały zdefiniowane w danym module. Niezbędne jest także określenie typu każdej z tych nazw, aby asembler wiedział, ile "miejsca" zarezerwować w generowanym zbiorze .obj dla późniejszego uzupełnienia odpowiednich wpisów do zbioru .exe. Jeżeli nazwa symboliczna jest etykietą, to typ może być NEAR lub FAR. Gdy mamy odczynienia z nazwa zmiennej, to typem może być każdy dopuszczalny rodzaj, a więc: BYTE, WORD, DWORD, QWORD, TBYTE lub OWORD.
W pliku *.map można znaleźć adresy początkowe, końcowe jak i długość wszystkich segmentów istniejących w programie. Znajdują się tam również adres startu programu oraz adresy powrotu z procedur.
Program zawiera: segmenty danych, segmenty stosu i segmenty kodu.
Dyrektywa ASSUME służy do deklarowania asemblerowi, jaka jest zawartość rejestrów segmentowych w czasie realizacji programu, natomiast ASSUME DS:NOTHING wyłącza automatyczne generowanie przedrostków przez asembler dla segmentu danych.
Dyrektywa BYTE - Rozmieszczenie segmentu na granicy bajtu, używana przy definicji segmentu.
Wielkość 1 segmentu = 64kB
Dyrektywa PRIVATE powoduje iż w czasie linkowania segment taki nie jest łączony z innymi segmentami.
Segmenty zdefiniowane jako COMMON, a mające tę samą nazwę, są ładowane do pamięci z takim samym adresem początkowym segmentu. Wielkość tego segmentu po połączeniu jest równa wielkości największego z łączonych segmentów.
Dyrektywa PUBLIC - wszystkie połączone segmenty mają wspólny adres początku segmentu, a offset zwiększa się. Dyrektywa PUBLIC ( PUBLIC[langtype] name[,[langtype]name]... ) Za pomocą dyrektywy PUBLIC informujemy program asemblera, ze nazwy wymienione jako argumenty tej dyrektywy są zdefiniowane w danym module i dopuszczamy ich użycie w innych modułach. Dyrektywa spowoduje, że w wygenerowanym przez asembler zbiorze .obj umieszczone zostaną pewne informacje, które w czasie procesu konsolidacji wykorzystane będą przez program linker.exe do uzupełnienia brakujących definicji w pozostałych łączonych modułach. Parametr langtype jest istotny w przypadku łączenia modułów przygotowywanych w różnych językach.
Jeżeli zadeklarujemy w polu langtype język C, to asembler konsekwentnie dopisze przed nazwami symbolicznymi podkreślenie zgodnie ze standardem w tym języku.
Parametry segmentów: private - segment wykorzystywany wyłącznie w 1 module || readonly - zawartość segmentu nie będzie zmieniana w czasie wykonywania || common - segment wykorzystywany w kilku modułach || byte - zapewni, że nie będzie żadnego odstępu w pamięci operacyjnej między dwoma częściami danego segmentu || para - druga część danego segmentu rozpoczyna się od najbliższego adresu podzielnego przez 16 || code - segmenty z tym parametrem pomimo innych nazw będą zgrupowane w jeden segment
W trybie adresacji rzeczywistej, rejestry segmentowe zawierają adresy początków segmentu podzielone przez 16, czyli aby uzyskać pełny adres początki segmentu w przestrzeni 1-megabajtowej musimy do tych wartości dopisać 0h np. 25C30h
Przekształcić program tak, aby długość bufora była definiowana dowolną stałą a nie liczbą 80 (uwaga! rejestry MMX mają długość 64b, a długość bufora nie musi być podzielna przez 8). IF MMX .586 .MMX mov ebx, buf/8 ;ilosc wykonan petli operujacej na 8 bajtach ;(rej. mm0) mov ecx, buf mod 8 ;ilosc wykonan petli operujacej na jednymbajcie test ecx,ecx ;ustawienie flagi ZF jz podzielne ;mozna wykonac tylko na mm0 lub wcale mov edx, buf-1 ;ustawinie poczatku 'reszty' sub edx, ecx ;jw. reszta: mov al, bufor[edx+ecx] ;skopiowanie znaku do al inc al ;zwiekszenie kodu znaku o '1' mov bufor[edx+ecx], al ;wstawienie zmodyfiko kodu do bufora loop reszta ;skok do poczatku petli, dekrementacja ecx podzielne: test ebx,ebx ;ustawienie flagi ZF jz gotowe ;jesli nie ma potrzeby operacji na mm0 mov ecx, ebx ;ilosc wykonan petli (ebx=buf/9) petla: movq mm0, qword ptr bufor[ecx*8-8] paddusb mm0, jedynki movq qword ptr bufor[ecx*8-8], mm0 loop petla gotowe: .8086 ENDIF |
Stworzyć ODDZIELNY segment kodu z procedurą realizującą proste zadanie (np. obsługa naciśnięcia klawisza).
oddzielny_segment segment public byte assume cs:oddzielny_segment, ss:stosik, ds:oddzielny_segment wlasna_procedura proc far push ds mov ax, seg oddzielny_segment mov ds, ax mov ah, 9 mov dx, offset info int 21h mov ah, 0 int 16h ;al mov klawisz, al mov ah, 9 mov dx, offset wyswietl int 21h pop ds ret wlasna_procedura endp info db "Nacisnij jakis klawisz." ,10,13,"$" wyswietl db "Wcisnales " klawisz db ? db "$" oddzielny_segment ends
|
ĆWICZENIE 4
To co znajduje się poniżej dyrektywy .NOLIST nie zostanie dodane do listingu.
.NOCREF - Wyklucza z listingu symbole zdefiniowane jako parametry dyrektywy (jeśli są zdefiniowane) lub wszystkie.
.LISTALL - Wszystko to co znajduje się poniżej tej dyrektywy zostanie dodane do listingu.
include c:\masm32\include\debug.inc ;makra do debugowania: PrintText PrintDec
includelib c:\masm32\lib\debug.lib ;biblioteka
Za pomocą debuggera OllyDbg dowiedzieliśmy się iż np.: program rozpoczyna się od adresu: 00401000.
Plik symboli programu nie jest niezbędny do debuggowania. Informacje zawarte w nim mogą być umieszczone bezpośrednio w pliku wykonywalnym. WinDbg File Symbol File Path OllyDbg Debug -> Select path for symbols
W debuggerze WinDbg pułapkę ustawiamy naciskając klawisz F9 w miejscu, w którym ma być ona ustawiona, natomiast w OllyDbg służy do tego klawisz F2. W obu programach warunki pułapki można bardziej precyzować za pomocą odpowiednich opcji zawartych pod tymi przyciskami.
INT 3 - Jest to przerwanie obsługiwane przez system operacyjny, które w praktyce przechwytuje debugger i zatrzymuje program w miejscu jego wystąpienia.
Wywołanie funkcji MessageBox za pomocą push i i invoke:
PUSH push MB_OK push offset Tytul_okna push offset Tekst_w_oknie push 0 call MessageBox |
INVOKE invoke MessageBox,0,offset Tekst_w_oknie,offset Tytul_okna,MB_OK
|
Parametry MessageBox i ExitProcess:
MessageBox
HWND hWnd Wskazuje na właściciela okna MessageBox; gdy wartość jest równa NULL nie ma okna,
które było by związane z nowostworzonym.
LPCTSTR lpText Wskazuje na łańcuch tekstowy zakończony zerem, który zostanie wyświetlony jako treść okna.
LPCTSTR lpCaption Wskazuje na łańcuch tekstowy zakończony zerem, który jest tytułem okna.
UINT uType Definiuje styl okna, przyjmuje wartość:
MB_ABORTRETRYIGNORE - okno zawiera przyciski: Abort, Retry, Ignore.
MB_OK - okno zawiera przycisk Ok.
MB_OKCANCEL - okno zawiera przyciski: Ok., Cancel.
MB_RETRYCANCEL - okno zawiera przyciski: Retry, Cancel
MB_YESNO - okno zawiera przyciski: Yes, No
MB_YESNOCANCEL - okno zawiera przyciski: Yes, No, Cancel
ExitProcess
UINT uExitCode Określa kod wyjścia.
Nazwy symboliczne zaczynają sie od znaku @ to nazwy predefiniowane.
Dyrektywy .NOLIST oraz .NOCREF powodują nieumieszczenie w listingu asemblacji zawartości dołączanych plików bibliotecznych oraz z deklaracjami i definicjami. Brak tych dyrektyw spowoduje że plik .lst będzie miał wielkość kilku megabajtów i będzie całkowicie nieczytelny, bowiem zawierać będzie wszystkie informacje z plików dołączanych.
Wszystkie adresy są liniowe i zaczynają sie od wartości wirtualnej 00000000h
Program standardowo ładowany będzie od adresu 00400000h zaś etykieta startowa będzie pod adresem 00401000h.
ĆWICZENIE 5
System Windows dzieli dostęp do procesora pomiędzy programy, natomiast program pisany pod DOS zajmuje 100% czasu procesora. W systemie Windows każdy program ma swoją własną przestrzeń adresową o wielkości 4GB, natomiast w systemie DOS programy mogą widzieć się nawzajem. Dzięki temu dane różnych programów nie będę nadpisywane Pod Win32 występuje model pamięci Flat i nie ma tam segmentów tak jak pod DOSem. Nie trzeba ustawiać rejestrów segmentowych, można używać jakiegokolwiek adresu w przestrzeni pamięci.
Komunikat WM_DESTROY jest to ostatni komunikat wywoływany po zamknięciu aplikacji, a jego wywołanie następuje po komunikacie WM_CLOSE.
Powrót z funkcji GetMessage następuje po odebraniu komunikatu WM_QUIT.
Do podstawowych różnic pomiedzy programem w prostarcie, a winasmie zaliczamy: szablon wygenerowany w prostarcie posiada procedurę RegisterWinClass odpowiedzialną za wypełnienie struktury WNDCLASSEX; || w szablonie wygenerowanym przez prostart znajduje się dodatkowa funkcja InitCommonControls, która służy za wstawianie odwołań do biblioteki comctl32.dll; || inna lista rozkazów; || przykładowy program znajduje się w jednym pliku, natomiast prostart dzieli kod na kilka plików; || projekt prostartu ma dołączone dodatkowo pliki dbmacros.asm i errormac.asm odpowiedzialne za obsługę bibliotek dberror.dll i dbshow.dll.
Do najważniejszych różnic pomiedzy programem w prostarcie, a RadAsmie zaliczamy: prostart umieszcza makra w osobnym pliku; || w prostarcie domyślnym zestawem instrukcji jest .486, a w radasmie .586; || inny sposób tworzenia okna i ustawienia jego stylu; || radasm nie tworzy żadnych makroinstrukcji; || prostart rozdziela kod na kilka plików natomiast radasm generuje wszystko w jednym; || inna obsługa komunikatów i sposób wywoływania funkcji API.
Przykładowy program, wyświetlanie czasu w okienku.
W sekcji danych:
szTitle db " ",0 ; string z aktualnym czasem
szFormat db "%2d:%02d:%02d",0 ; format czasu
Przed główną pętlą programu:
invoke SetTimer, hWnd, 128, 1000, NULL ; timer zgłaszający się co sekundę
Zmienna w procedurze MyWndProc:
LOCAL time: SYSTEMTIME
Dopisany fragment procedury MyWndProc odpowiadający za aktualizację czasu:
.elseif uMsg == WM_TIMER
.if wParam == 128
invoke GetLocalTime,ADDR time
xor eax,eax
push eax
mov ax, time.wSecond
push eax
mov ax,time.wMinute
push eax
mov ax, time.wHour
push eax
push offset szFormat
push offset szTitle
call wsprintf
invoke SetWindowText, hWnd, ADDR szTitle
return 0
.endif