poniedziałek, 25 maja 2009

ASM: pisanie własnego MBR cz.2

Jak wspomniałem w poprzedniej części w tej postaram się wyjaśnić dlaczego rozpoczęcie prac nad MBR było trudne. A dokładniej czemu piekielne kompilatory ASM, które zawsze sprawdzały się dobrze, teraz postanowiły odmówić posłuszeństwa.

Największy problem to dyrektywa org. Bootloader to mały program (model tiny), a dokładnie aplikacja com (przynajmniej dla windowsowców). Istotne jest gdzie do pamięci ów kod jest ładowany przez wszechmocny BIOS. Kod z MBR ładowany jest pod adres: 0000:7C00h, a więc należy zadbać, aby po załadowaniu kodu CS:IP wskazywał dokładnie jego początek. Zadanie do trudnych nie należy, jednak TASM i MASM odmówiły posłuszeństwa napotykając dyrektywę org 7c00h. I to pomimo faktu dodania wszystkich innych potrzebnych tym kompilatorom głupot. No ni jak nie szło się z nimi dogadać.

Drugi problem polegał na architekturze. Tego to akurat za specjalnie nie rozumiem i wytłumaczenie może co najwyżej sprowadzać się do bo tak. Gdy natknąłem się na ten problem, to oczywiście rozwiązania szukałem po forach i innych takich zakamarkach. W zasadzie wyszło na to, że prawdopodobnie rozwiązanie nieco innego problemu rozwiązało i mój. Otóż linker z TASM'a zwracał mi jakiś dziwaczny błąd dotyczący segmentu stosu (którego nie deklarowałem), mówiący ogólnie, że nie może "opiekować się" 16b segmentami. Dyrektywa use 16/use 32 oczywiście nic nie zmieniła. Rozwiązaniem problemu było zostawić TASM32 i TLINK32 w spokoju, na rzecz starych dobrych TASM i TLINK. Zasmuciło mnie to, bo jednak chciałem skorzystać z nieco dłuższych rejestrów, ale zostałem zmuszony do użycia 16b.

Początek na początek

Każdy dobry projekt należy zaplanować. Przy tym kodzie trzeba przynajmniej główny przebieg zaplanować. Osobiście często sprowadzam to do bazgrolenia w zeszycie, albo na przypadkowych kartkach, tak więc nie zamieszczę schematu blokowego, którym się posłużyłem. Przy tak trywialnym zadaniu wystarczy jednak posłużyć się listą.
  1. sprawdź czy program jest zainstalowany
  2. zainstaluj program
  3. wyświetl zawartość pamięci
  4. powrót do 3.
Proste. Teraz można jeszcze rozwinąć poszczególne punkty. W tym arcie pozwolę sobie jednak zająć się wyłącznie punktem 3. W zasadzie stworzenie odpowiedniego kodu dla Niego zajęło mi nie mało czasu, nawet gdy już uporałem się ze środowiskiem.

Twój prywatny Matrix

Rozwińmy od razu punkt 3 do odpowiedniego algorytmu:
  1. załaduj segment pamięci ekranu do ES
  2. wyzeruj AX,BX,DS
  3. wyzeruj CX
  4. wczytaj do AH bajt spod adresu [DS:BX]
  5. do AL wstaw bajt atrybutu
  6. zapisz pod adres [ES:CX] zawartość akumulatora
  7. zwiększ BX
  8. sprawdź czy BX = 0, jeśli NIE przejdź do 10
  9. zwiększ DS o 1
  10. zwiększ CX o 2
  11. sprawdź czy CX>0FA0h jeśli TAK wróć do 3 , jeśli NIE wróc do 4
Powyższy kod to zejście o jeden poziom abstrakcji w całym naszym kodzie. Można uznać, że nawet o 2 ponieważ pokusiłem się o podanie nazw konkretnych rejestrów i konkretnych wartości. Przyznam szczerze, ta pierwotna postać algorytmu choć jest dalej realizowana przez kod, wydaje się w tej chwili aż nazbyt abstrakcyjna. Ale to chyba tylko dlatego, że uzyskałem końcowe rozwiązanie.

W pierwszym pseudokodzie cały ten algorytm został określony jako wypisanie pamięci. Na wszelki wypadek wyjaśniam więc, że polega on na odczytywaniu bajtów z pamięci i wypisywaniu ich kolejno na ekran w trybie znakowym. Całość sprowadza się do jednej pętli z dwoma licznikami: jednym przesuwanym po pamięci głównej i drugim przesuwanym po pamięci ekranu. Tutaj taka mała dygresja. nie każdy zdaje sobie sprawę, że w pewnym momencie wyświetlimy zawartość tego do czego dokładnie piszemy. Dziwne zapętlenie, ale świat dalej istnieje.

Pierwszy kod - trywializm

Na razie jeszcze nie do końca w ramach tego projektu warto podać jakikolwiek kod który może posłużyć do pisania MBR. Dokładnie to jest to baza wyjściowa dla tego kodu.

org 7C00h
start:
times 510 - ($ - start) db 0 ; dopełnienie do 510 bajtów
dw 0aa55h ; znacznik

Proste i trywialne. Całość kompiluje się do 512B aczkolwiek na koniec będzie zawierało prawie same 0. Jest to jednak baza do dalszej pracy. Jak widać zawiera wspomnianą wcześniej dyrektywę org. Znacznie istotniejsza jest jednak końcówka. Bootsector ma dokładnie 512B długości. Aby plik miał dokładnie tyle po kompilacji dodajemy przedostatnią linię, która uzupełnia program o bajty 00h. Dopełnienie jak widać jest do 510B, a to dlatego, że BootSector musi się kończyć odpowiednio. W ostatniej linii zostaje dodany 2Bajtowy znacznik końca. Razem mamy już 512B.

Pisanie po ekranie

Pierwszy problem na który się natykamy wykonując dokładnie obmyślony algorytm to pisanie na ekran. Przyjmijmy, że rozmiar ekranu wyznacza rozmiar "strony" w pamięci. W końcu przecież w ten sposób pamięć jest wypisywana na ekran. Domyślnym jest aktualnie tryb 80x25 i tego się trzymamy. Każdy znak na ekranie opisany jest przez 2 Bajty. Oznacza to, że mamy do dyspozycji 4000B na jedną "stronę". Stąd z resztą wartość 0FA0h. Problem polega na tym, że gdy dojdziemy do końca ekranu, wracamy na jego początek. Widzimy efekt nasuwania się na siebie stron. Po zapisaniu jednej zaczyna ją przesłaniać nowa.

Drugi problem to stosunkowo duża prędkość wyświetlania. Nie bardzo można zobaczyć co w ogóle się tam pojawia. Ekran szybko migocze i można co najwyżej dostać ataku epilepsji na to patrząc.

Trzeci problem to bajt atrybutu i sama zawartość akumulatora. Trzeba zawczasu ustalić jakie ma być wyświetlanie, aby te dane przygotować.

Problemami zajmiemy się w odwrotnej kolejności. Wszystko dlatego, że jest to znacznie łatwiejsza kolejność. Poza tym chodzi też o czas jaki rozwiązanie kolejnych problemów pochłonęło.

Bajt atrybutu

Miał być Matrix(TM), to będzie Matrix. Aby to się udało potrzebny nam jaskrawy zielony kolor na litery i czarne tło. Czarne tło oczywiście jest domyślne. Kolorowy tryb tekstowy, który w komputerach PC po starcie jest domyślny używa następującej struktury bajtu atrybutu:
  • 7 - Blink - migotanie
  • 6-4 - RGB Background - kolor tła
  • 3 - Intensity - intensywność
  • 2-0 - RGB Foreground - kolor liter
Wartość bajtu atrybutu dla nas:
00001010b==0Ah
Pisząc do pamięci musimy wiedzieć w jaki sposób dane są w niej przechowywane. Architektura PC przewiduję metodą Little Indian. Oznacza to, że młodszy Bajt w słowie przechowywany jest jako pierwszy, a starszy jako drugi. Jeśli o tym zapomnimy, to spotka nas niemiła niespodzianka. Wprowadza to pierwszą zmianę do algorytmu. Pozornie tylko kosmetyczną, ale pozwalającą na odpowiednie wyświetlanie. Bajty w akumulatorze należy zamienić. Bajt z pamięci ładujemy do AL, a atrybuty do AH.

Poczekajmy chwilę

Zajmijmy się problemem bardzo szybkiego migotania aplikacji. Nie trudno domyśleć się, że wyświetlanie realizowane jest tak szybko, jak szybko procesor jest w stanie przekazać dane między obszarami pamięci i kontroler ekranu wysłać je do monitora. To stanowczo za szybko dla ludzkiego oka. Z pomocą przychodzi przerwanie 15h BIOS. Dokładny jego opis można przeczytać na przykład w opisie do Emu8086. Interesuje nas Funkcja 86h, która pozwala na wstrzymanie aplikacji na CX:DX mikrosekund.

Wyliczmy odpowiedni czas wstrzymania. Ludzkie oko rejestruje około 24klatek na sekundę. w takim razie jedna klatka jest odrysowywana co ~41667 mikrosekund. Odrysujmy więc w jednej klatce cały wiersz, aby animacja była dość szybka i dość płynna. W ciągu sekundy wyświetlanie przesunie się o cały ekran. czas wstrzymania to 521us==209h. Wpisujemy kolejno do CX:=0000,DX:=0209h.

Należy pamiętać aby przed wywołaniem przerwania zapamiętać stan procesora i przywrócić go po jego zakończeniu. Dane potrzebne dla przerwania są tymczasowe i nie muszą zostać przez nas przechowane. Aby nie wybierać konkretnych rejestrów do zachowania lepiej posłużyć się rozkazami PUSHA i POPA.
Płynne pisanie
Na razie kod powoduje zasłanianie poprzednio wypisanej strony kolejną. Trzeba to jakoś zmienić. Moje rozwiązanie sprowadza się do przesuwania pamięci ekranu zamiast jej nadpisywania w całości. Pozwolę sobie opisać końcowe rozwiązanie zamiast całego procesu dochodzenia do niego. Był on z resztą dość długi i powodował sporo błędów po drodze.
  1. przesuń "obraz" o 80 znaków w prawo
  2. ustaw CX na 160
  3. pobierz znak z pamięci do AL
  4. wstaw bajt atrybutu do AH
  5. podstaw pod BX:=160-CX
  6. zapisz akumulator: [ES:BX]:=AX
  7. zmniejsz CX o 2
  8. jeśli CX!=0 wróć do 3
Przed wykonaniem tego algorytmu także trzeba zachować stan rejestrów. Najlepiej znowu użyć PUSHA i POPA, aby nie pomylić się w kolejności operacji, a także nie zapisać za mało.

Łatwo zauważyć że teraz od razu wypisywana jest cała linia z pamięci podczas jednego przebiegu głównej pętli programu. Wróćmy więc na chwilę do czasu wstrzymania aplikacji. Sleep na 521us to teraz za mało. Aplikację trzeba wstrzymać na około 80000us. Ja osobiście ustawiłem na 83334, czyli 14586h. Zapisujemy CX:=0001h, DX=4586h i wywołujemy przerwanie 15h.

Jakoś pusto tutaj

Pamięć komputera po jego uruchomieniu nie jest wypełniona zerami. Po pierwsze załadowany został już do niej BIOS. Po drugie nasz BootSector. Po trzecie stan większości komórek jest nieokreślony. Na ogół są tam losowe wartości, mogą tam nawet być śmieci z pozostałej sesji (nawet 15 minut po wyłączeniu stan pamięci potrafi się utrzymać). Dane te można więc odczytać i wyrzucić na ekran. Niestety w moim przypadku napotkałem całą masę pustych komórek. Pustych czyli wyraźnie zawierających same 00h. Zapewne właśnie dlatego, że kod testowany był pod maszyną wirtualną, a nie rzeczywistą. Uznałem jednak, że w prawdziwej sytacji może stać się podobnie, więc dodałem do aplikacji generator liczb pseudolosowych.
  1. DL:=AL
  2. DX+=10111101B
  3. DX+=BX (BX zawiera aktualny offset w pamieci)
  4. wykonaj obrót cykliczny na DX o 4 w prawo (ROR DX,04h).
  5. wynik odczytaj z DL
Jak widać seed dla generatora stanowi suma BX+AL. Jest ona na tyle zmienna i zależna od stanu aktualnego, że trudno przewidzieć jaki będzie wynik generatora. Doświadczalnie powiem, że okres ergodyczności ciągu kolejnych liczb losowych z generatora jest dostatecznie duży. Matematycznie nie zostało to jednak zbadane.

Przed wykonaniem kodu generatora zapamiętujemy stan maszyny. Tak jak już poprzednie razy. Generator oczywiście trzeba zapuścić dla każdej komórki o wartości 00h. Jak uzyskamy 00,h albo 32h, to się nie martwimy. Kod Matrix'a też miał sporo pustych przestrzeni.

Gotowy produkt

Kod jednego punktu głównego algorytmu udało mi się sprowadzić do 66 linii razem z komentarzami. Co ważne sam kod waży zaledwie 95B! Założę się, że można go skompresować jeszcze bardziej i uprościć, aby wykonał się szybciej.
    ;nasm -o bootsect.dos -f bin boot_vir.asm
    
    org 7c00h
    
    start:
    cz_inst:
    instal:

    print_mem:            ;wlasciwe dzialanie \"wirusa\"
      XOR AX,AX
      MOV DS,AX
      MOV BX,0B800h       ;ustawiamy adres pamieci ekranu w BX

      MOV ES,BX
      XOR BX,BX
    start_print:
      PUSHA
      MOV CX,0FA0h
    move_ekran:           ;przesuniecie ekranu o 80 znakow od razu
      MOV BX,CX

      MOV AX,[ES:BX-50h]
      MOV [ES:BX],AX
      DEC CX
      LOOP move_ekran
    po_move_ekran:
      POPA
      PUSH CX             ;zachowujemy CX (sam nie wiem czy potrzebnie)

      MOV CX,0A0h
    start_load:
      MOV byte AL,[DS:BX] ;czytamy bajt z pamieci
      CMP AL,00h          ;sprawdzamy czy komorka zawiera 0, jesli tak wygenerujemy jakis znak aby bylo ladnie

      JNZ short po_random
    random:               ;stosunkowo prosty generator liczb losowych - nie byl matematycznie badany
      ADD DX,10111101B    ;powinien byc tez dosc szybki

      ADD DX,BX           ;dzieki dodawaniu adresu pamieci do niego, jego okres aperiodycznosci wydaje sie byc duzy
      ROR DX,04h

      MOV AL,DL
    po_random:
      MOV byte AH,0Ah
      PUSH BX
      MOV BX,0A0h
      SUB BX,CX
      MOV word [ES:BX],AX ;piszemy na ekran

      POP BX
      INC BX              ;przesuwamy sie po pamieci
      JNZ short dalej
      PUSH DS
      POP DX
      ADD DX,1000h        ;zmieniamy segment, ale tak, aby nie nachodzil na poprzedni

      PUSH DX
      POP DS
    dalej:
      DEC CX
      LOOP start_load
      POP CX
    wait_some:
      PUSHA               ;zachowujemy rejestry
      MOV CX,0001h        ;licznik jest w CX:DX

      MOV DX,4586h        ;ustawiamy licznik na 83334(14586h) mikrosekund
      XOR AX,AX
      MOV CX,AX
      MOV AH,86h
      INT 15h             ;poczekajmy chwile aby animacja byla +/- plynna

      POPA                ;przywracamy stan rejestrow
      JMP short start_print
    
    times 510 - ($ - start) db 0    ; dope³nienie do 510 bajtów

    dw 0aa55h        ; znacznik

Starałem się opisać wszystko co można było w samym kodzie. Znajomość ASM jest wymagana. Poniżej odpalony kod na maszynie w VirtualBox.

Okno VirtualBox z uruchomionym "wirusem".
Na zakończenie tej części małe wyjaśnienie. W tekście zawarte jest dość mało gotowych przykładów kodów ASM głównie dlatego, że już mi się gotowego pliku na kawałki kroić nie chciało. Lenistwo jest straszne. Opisałem wszystko na tyle na ile mogłem, tak aby nawet początkujący był w stanie te fragmenty kodu napisać samodzielnie. Mam też nadzieję, że cały kod w jednym miejscu był w stanie rozjaśnić komuś w głowie dostatecznie.

W następnej części postaram się opisać dwa pozostałe punkty głównego algorytmu. Ich napisanie małpią metodą zajmuje piekielnie dużo czasu. Niestety każdy test może oznaczać uszkodzenie maszyny wirtualnej i konieczność stawiania na niej na nowo OS.

Do przejrzenia (Źródła):

Brak komentarzy:

Prześlij komentarz