Video of the week

This is a must-watch video about one of us trying to reach the stars :-)

Well done #HRejterzy

Badanie pamięci I - Turbo Assembler

Przedstawię poniżej krótki asemblerowy program, który pobiera od użytkownika string, wyświetla go, pobiera następnie znak i wyświetla go. O co chodzi? - postaram się zadać możliwe dużo pytań odnośnie programu tak, aby lepiej ogarnąć zasady działania programów. Absolutnie nie jest to jakiś wstęp do kursu czy instrukcja obsługi - tego w I-necie nie brakuje. Dobrze jest choć trochę rozumieć o co chodzi tam pod spodem, poniżej interfejsu programu - nie wiem jak masz Ty, ale sam nie biorę się za coś, jeśli wcześniej nie rozszczepię tego jakimś skalpelem. Jeśli coś robisz w Pascalu czy innym języku HLL to musisz spróbować Asemblera. Inaczej to jak jazda rowerem tylko po własnym podwórku.

  1. .MODEL SMALL
  2.  
  3. ; .STACK 100H
  4.  
  5. .DATA
  6. newline db 13,10,'$'
  7. napis0 db 13,10,'Welcome, I/O 2011. Podaj string: ',13,10,'$'
  8. napisal db 13,10,'Napisales: $'
  9. pyt0 db 'Podaj pojedynczy znak: $'
  10. wynik0 db 10,13,'Podales znak: $'
  11. napis1 db 64 dup (64,0) ; [64 - rozmiar bufora][0]000..
  12.  
  13. .CODE
  14.  
  15. Start:
  16.  
  17. mov ax,@DATA
  18. mov ds,ax
  19.  
  20. ; mov ax,@STACK
  21. ; mov ss,ax
  22.  
  23. mov ah,09h ; output string (Welcome,)
  24. lea dx,napis0
  25. int 21h
  26.  
  27. mov ah,0ah ; BUFFERED INPUT
  28. mov dx, OFFSET napis1
  29. int 21h
  30.  
  31. mov bx, OFFSET napis1 ; zapamietaj adres bufora
  32. inc bx
  33.  
  34. mov ah,09h ; output string (Napisales:)
  35. lea dx,napisal
  36. int 21h
  37.  
  38. petla:
  39. inc bx ; przejdz na kolejny element tablicy
  40.  
  41. xor ax,ax
  42. mov al, byte ptr [bx] ; skopiuj bajt do nizszego slowa rejestru ax
  43.  
  44. cmp al,0dh ; sprawdz czy kolejny bajt jest 0d,00,0a,00
  45. je dalej
  46.  
  47.  
  48. mov ah,02h ; output string
  49. mov dl,al ; w dl znajduje sie ASCII kod znaku do wyswietlenia
  50. int 21h ;
  51.  
  52. jmp petla
  53.  
  54. dalej:
  55.  
  56. mov ah,09h ; output string (13,10)
  57. lea dx,newline
  58. int 21h
  59.  
  60. mov ah,09h ; output string (Podaj pojedynczy znak:)
  61. lea dx,pyt0
  62. int 21h
  63.  
  64. mov ah,01h ; input character 1h
  65. int 21h
  66.  
  67. mov ah,09h
  68. mov dx, OFFSET wynik0
  69. int 21h
  70.  
  71. mov ah,02h ; output character 2h
  72. mov dl, al
  73. int 21h
  74.  
  75. mov ah,4ch ; terminate program
  76. int 21h
  77.  
  78. END Start

1. Co przyda się do badania? Asembler nie może kojarzyć się jedynie z epoką zamierzchłą i dosowymi okienkami ale na tym etapie badań dobrze byłoby wyemulować środowisko 16 bitowe przy pomocy aplikacji DOSBox - tutaj. Aplikacja pozwala uniknąć problemów przy uruchamianiu aplikacji 16 bitowych w systemach 32 bitowych ( bugi związane z ntvdm.exe, malware podczepiony pod tą wirtualną maszynę po pliki konfiguracyjne autoexec.nt, config.nt, and so on.. ). Dla zainteresowanych polecam rozwiązania niektórych z tych problemów: Microsoft - tutaj oraz tutaj - wersja bardziej przystępna z forum searchengines.pl gdzie piszą na ogół ogarnięci ludzie. Zainstaluj DOSBox'a, uruchom i podmontuj partycję, na której masz katalog TASM'a ( Turbo Assemblera poszukaj np. na chomiku ):

  1. Z:>mount h h:\
  2. Z:>H:
  3. H:>cd katalog_tasm
  4. H:>dir

2. Co to jest .MODEL SMALL ? Dyrektywa .MODEL dotyczy wyboru pewnej konwencji ułożenia danych programu i kodu programu w pamięci operacyjnej. W tym momencie kodu źródłowego masz opcję wyboru czy np. chcesz przechowywać swoje zmienne (a,b,c,..) bardzo niedaleko instrukcji programu (mov ah,09h,..), czy też może chcesz wyraźnie oddzielić te elementy i podkreślić ich niezależność. Czyli idea dyrektywy jest już mniej więcej znana, ale jak to ma się do rzeczywistości? Na poniższym rysunku przedstawiam 3 (nie wszystkie) popularne modele pamięci przy okazji konstruowania programów asemblerowych:

modele pamieci

Na przykład model Tiny składa się z dwóch prostokątów - większy to cała przestrzeń adresowa - pamięć operacyjna. Mniejszy z napisami CS,DS,SS,ES oznacza 64KB segment - fragment pamięci operacyjnej. Ten fragment dzielą wspólnie 4 główne segmenty, które tworzą program: segment kodu (CS), segment danych (DS), segment stosu (SS) i dodatkowy segment (ES). W kolejnych modelach widać już stopniowy podział i niezależność kolejnych segmentów. Zatem model pamięci jaki został wybrany w naszym programie to model środkowy - chcemy wyraźnie oddzielić kod od danych programu.

3. Dobrze, wiem już czym jest segment ale dlaczego segment i dlaczego tylko 64KB?

Segmentacja to po prostu jeden ze sposobów organizacji pamięci w kontekście procesu. Możesz spotkać się również z hasłem stronicowania, które oznaczać będzie podział pamięci na tak zwane strony o wielkości najczęściej 4KB. Ze stronami wiązać się bedą dodatkowo hasła: katalogu i tablic stron - w procesorach 32 bitowych, aby zlokalizować coś w pamięci trzeba się będzie przyjrzeć czemuś takiemu:

stronicowanie

Obrazek pochodzi z Wikipedii - żeby zlokalizować coś w pamięci trzeba najpierw odnieść się do katalogu stron, którego adres przechowuje specjalny rejestr CR3. Katalog wskazuje odpowiadnią tablicę a na końcu stronę i przesunięcie tego czegoś na stronie. Z segmentacją jest trochę prościej bo adres w pamięci to po prostu SEGMENT:OFFSET czyli adres początku segmentu i przesunięcie danej w segmencie. Każdy segment posiada swój deskryptor - identyfikator, który zawiera min adres bazowy (początkowy) tego segmentu. Na Twoje jeszcze nie zadane pytanie - ale co mam u siebie w systemie? Segmentację czy stronicowanie? - odpowiem, że masz MIX: segmenty podzielone na strony.

No ale dlaczego 64KB? Procesory 8086 / 8088, które były mocno testowane przez programistów posiadały 20 styków adresowych, które pozwalały zaadresować 1MB pamięci operacyjnej: 2^20 komórek. Jednak najlepiej operowały na 16 bitach i tylko tyle pamięci zdołały ogarnąć na raz. Dlatego właśnie segment to 64KB komórek pamięci a nie 1MB. Ale za to, na pocieszenie trzeba napisać, że 1MB pozwalał na skupienie aż 16 segmentów po 64KB każdy.

4. Okey, a czy może dałoby się pokazać ten jeden chociaż segment na obrazku?


Można - nawet trzeba. Potrzebny będzie windowsowski debuger z głównego katalogu (Windows\System32\debug.exe). Najlepiej przekopiuj go sobie do katalogu TASM lub TASM\BIN (lepiej). Wpisałem - choć niepotrzebnie dodatkową instrukcję: mov ax,cs - bez wnikania chwilowo: umieszczę sobie adres początku segmentu kodu (cs) w jednym z rejestrów procedora (ax). Okey uruchamiam debuger - sprawdzam sobie na start zawartość rejestrów (-r) i po wpisaniu znaku 'p' przechodzę przez kolejne etapy programu. Już przy sprawdzeniu zawartości rejestrów widać (pierwszy czerwony klex), że adres bazowy segmentu kodu to 0x077F. Kolejna instrukcja (drugi klex) tylko ładuje tę samą wartość do rejestru ax (trzeci klex). Okey mamy już adres bazowy więc teraz podejrzyjmy co znajduje się pod tym adresem 077F(segment):0000(offset):

code segment

Cały segment kodu - od pierwszego do ostatniego bajtu zaklexowałem. Skąd wiem, że 8C C8 .. to początek segmentu? Oprócz tego, że wskazuje na to sam adres offsetowy ( 0000 ) to właśnie "8C C8" to nic innego jak nasza pierwsza instrukcja z kodu źródłowego powyżej. Skąd wiem, że CD 21 to ostatnia faza segmentu? Popatrz na zawartość tego segmentu (między adresami a ostatnią kolumną) - czy aby CD 21 to nie instrukcja INT 21? Po załadowaniu do rejestru ah wartości 4ch (3ci bajt od końca segmentu) wywoływane jest tylko ostatnie przerwanie i na tym koniec segmentu kodu.

5. A co z segmentem danych? Z obrazka wynika, że adres segmentu danych to DS=076F a to przecież wyżej niż segment kodu. Na obrazku widać też, że dane znajdują się za kodem - co tutaj się nie zgadza?

Zgadza się wszystko. Trzeba wziąć poprawkę na to, że debuger nie doszedł jeszcze do instrukcji: mov ax,@DATA oraz mov ds, ax - dopiero po tych instrukcjach w DS będzie właściwy adres segmentu danych - popatrz:

segment danych

Na obrazie widać, jak wartość DS zmienia się z początkowej 076F na 0784 i dopiero teraz wszystko się zgadza. Dokonujemy jeszcze podglądu tego obszaru (-d 0784:0000) i widać tam nasze napisy (dane).

6. Patrzę na ten asemblerowy kod ale w ogóle nie przypomina on kodu programu w języku wysokiego poziomu. Jakieś dziwne instrukcje, ani jednej znajomej instrukcji readln albo writeln; żadnego integera - o co w takim programie może chodzić?

Zasada powyższego programu nie jest bardzo trudna, w skrócie: znajduje w dokumentacji numer jakiejś funkcji i umieszczam ją w rejestrze ah (constans). Następnie czytam trochę o tej funkcji i zwracam uwagę jakie parametry przyjmuje - jeśli potrzebny jest jakiś parametr w postaci napisu to sprawdzam w którym innym rejestrze musi znaleźć się adres napisu i go tam umieszczam. Ostatnim etapem - po ustawieniu parametrów - jest wywołanie dosowskiego przerwania INT 21H. Przerwanie to wywołuje funkcje - koniec.

W skrócie to byłoby tyle: funkcja - parametry - int 21h. Cały program opiera się właściwie na wielokrotnym zastosowaniu takiego łańcucha działań. Czym są funkcje? Zajrzyj - tutaj - opis 39 funkcji DOS; są tutaj m.in. informacje o tym czego funkcja oczekuje w sensie parametru (Entry) oraz gdzie zwróci wynik (Return) plus inne uwagi. Funkcje dos to nie jedyne funkcje dostępne dla programisty. Ostatecznie i tak dostęp do sprzętu zapewnia BIOS, który też ma swoje funkcje, z których można bezpośrednio korzystać. Jeśli poprzedni opis funkcji nie wiele Ci mówi to polecam tę stronę (bardziej pro moim zdaniem) - www - znajdziesz tutaj opisy zarówno funkcji jak i samych przerwań ( choć w sumie poza INT 21H (33DEC) nie powinno Ciebie teraz więcej interesować ).

7. Okey mam tę liste funkcji, ale chciałbym spróbować użyć jakiejś samodzielnie - możesz dokładniej omówić jak sam to robisz np. z funkcją 0ah?

Na początek muszę dostarczyć sobie pewnej wiedzy na temat funkcji więc zaglądam np. tutaj. Z opisu wynika, że adres bufora na string ma znajdować się w adresie: ds:dx czyli inaczej ds+dx ( np. 100:10 == 110 ). Czyli najpierw tradycyjnie trzeba będzie umieścić w rejestrze ah numer funkcji - a - albo dokładniej 0a albo jeszcze dokładniej 0ah. Potem trzeba będzie umieścić adres początku obszaru w pamięci do którego zapisane zostaną kolejne znaki stringu: mov dx, OFFSET napis1 i prawie koniec. Prawie bo tradycyjnie aby wywołać funkcję trzeba zainicjować przerwanie numer 21.


Ale to jeszcze nie koniec - z opisu można wyczytać również coś takiego:

0ah

Z tej tabelki wynika, że pierwszy bajt obszaru na string ma być informacją o tym, ile maxymalnie string ma mieć znaków (bajtów). Drugi bajt będzie zawierać informację o ilości faktycznie wpisanych znaków ze standardowego wejścia. Natomiast od trzeciego bajtu zaczyna się nasz string. Teraz rozumiesz czemu zmienna napis1 została zadeklarowana jako tablica bajtów zapełnionych  wartością 64 plus 63ema zerami? :

 

  1. napis1 db 64 dup (64,0) ; [64 - rozmiar bufora][0]000..

 

W pierwszym bajcie tego obszaru ustaliłem maxymalną długość (64 dziesiętnie) a dalej idą już same zera. Okey teraz jest już wszystko jasne i wiadomo, że funkcja zadziała.

8. Czym różni się zapis mov ax, bx od zapisu mov ax, [bx]?

BX czy rejestr CX, DX czy AX (rys) to rejestr 16 bitowy, który przechowuje coś co składa się z 16 bitów lub 8 lub 4 lub nawet jednego bitu ( 00000000(ah) - 00000001(al) == 1 dziesiętnie ):

ax

To coś co przechowuje rejestr może być tylko liczbą ale liczba może być potraktowana również jako adres ( adres w końcu również jest liczbą ). Okey ale do czego zmierzam: zatem instrukcja mov ax, bx to nic innego jak przeniesienie liczby ( lub liczby będącej adresem ) do rejestru ax. Ale co z [bx]?

0xcd

Taki zapis to informacja, że rejestr bx zawiera adres komórki z której chce skopiować coś do rejestru AX. 65h to inaczej 101 dziesiętnie, czyli chce sobie skopiować liczbę 0xcd, która znajduje się w sto-pierwszej komórce pamięci. Zauważ w kodzie dwie instrukcje inc - dwukrotnie "zwiększam" rejestr bx a właściwie to zwiększam to co on zawiera - po co? Jak sądzisz - podpowiedź jest taka, że ma to zwiazek z próbą wyświetlenia napisu, który wklepałeś z klawiatury przy wywołaniu funkcji 0ah.

Odpowiedź: przypomnij sobie, co zawiera bufor na ten string: [ile_bajtow][ile_wpisano_faktycznie][znak0_stringu][znak1_stringu][..][znak63_stringu]. Aby wyświetlić string przy użyciu funkcji 09h trzeba podać adres pierwszego bajtu tego stringu do rejestru dx. Dwukrotne wykonanie operacji inc (zwiększenie wartości o 1) powoduje ominięcie dwóch pierwszych komórek i umieszczenie w bx adresu pierwszego wklepanego z klawiatury znaku - ma to sens? Musi mieć.

9. Widzę jakieś instrukcje skoku ale brakuje charakterystycznych dla HLL instrukcji warunkowych - czy są i jeśli tak to gdzie one się podziały w tym kodzie?

Możesz i owszem spotkać kiedyś w programie asemblerowym coś na wzór if np .IF , .ELSEIF, .ELSE, .ENDIF - będą to jednak dyrektywy, które program tłumaczący (asembler) zamieni na szereg innych instrukcji dodatkowo powiązanych z instrukcjami skoku. W powyższym kodzie jednak instrukcje warunkowe opierają się na trochę innej konstrukcji a mianowicie:

  1. cmp al, 0dh
  2. je dalej

Zasada jest taka - jeśli zmieni się stan jednej z interesujących Cie flag procesora - wykonaj lub nie jakieś instrukcje. Znaczniki stanowią kolejny rejestr procesora o którym więcej możesz poczytać w innych źródłach. Jedna z najbardziej popularnych flag - ZF - ZERO FLAG przyjmuje dwa stany: 0 lub 1 w zależności od tego czy wynik ostatniego działania procesora przyniósł wynik 0. Np. jeśli w rejestrze BX znajduje się wartość 10 dec - i będziesz chciał dekrementować tę wartość - dec bx to po dziesiątym razie stan flagi ZF zmieni się na 1. Instrukcja CMP zapali flagę ZF jeśli al będzie zawierać wartość 0dh - wtedy program przeskoczy do kolejnej instrukcji za etykietą "dalej". JE tłumaczy się jako JUMP IF EQUAL - jest bardzo dużo a nawet więcej instrukcji skoków o których możesz więcej poczytać w I-necie ( JA, JAE, JB, JBE, .., JZ, JNZ, .., JNL ).