Jak pisać programy w języku asembler?

Część 1 - Podstawy, czyli czym to się je

Wyobraźcie sobie, jakby to było móc programować maszynę bezpośrednio, czyli rozmawiać z procesorem bez pośrednictwa struktur wysokiego poziomu, na przykład takich jak spotykamy w języku C. Bezpośrednie operowanie na procesorze umożliwia przecież pełną kontrolę jego działań! Bez zbędnych instrukcji i innych śmieci spowalniających nasze programy.

Czy już czujecie chęć pisania najkrótszych i najszybszych programów na świecie?
Programów, których czasem w ogóle NIE MOŻNA napisać w innych językach? Brzmi wspaniale, prawda?

Tylko pomyślcie o tym, co powiedzieliby znajomi, gdybyście się im pochwalili. Widzicie już te ich zdumione miny?

Miła perspektywa, prawda? No, ale dość już gadania. Zabierajmy się do rzeczy!


Zacznijmy od krótkiego wprowadzenia:

Niedziesiętne systemy liczenia

  1. Dwójkowy (binarny)

    Najprostszy dla komputera, gdzie coś jest albo włączone, albo wyłączone. System ten operuje na liczbach zwanych bitami (bit = binary digit = cyfra dwójkowa). Bit przyjmuje jedną z dwóch wartości: 0 lub 1.
    Na bajt składa się 8 bitów. Jednym bajtem można przedstawić więc 2^8=256 możliwości.

    Przeliczenie liczby zapisanej w systemie dwójkowym na dziesiętny jest proste. Podobnie jak w systemie dziesiętnym, każdą cyfrę mnożymy przez odpowiednią potęgę podstawy (podstawa wynosi 2 w systemie dwójkowym, 10 w systemie dziesiętnym).

    Oto przykład (niech daszek ^ oznacza potęgowanie):
    1010 1001 dwójkowo =
    1*(2^7) + 0*(2^6) + 1*(2^5) + 0*(2^4) + 1*(2^3) + 0*(2^2) + 0*(2^1) + 1*(2^0) =
    128 + 32 + 8 + 1 =
    169 dziesiętnie (lub dec, od decimal).

    Działanie odwrotne też nie jest trudne: naszą liczbę dzielimy ciągle (do chwili uzyskania ilorazu równego 0) przez 2, po czym zapisujemy reszty z dzielenia wspak:
    (przeskocz konwersję liczby dziesiętnej na dwójkową)
    	169	|
    	 84	| 1
    	 42	| 0
    	 21	| 0
    	 10	| 1
    	  5	| 0
    	  2	| 1
    	  1	| 0
    	  0	| 1
    Wspak dostajemy: 1010 1001, czyli wyjściową liczbę.

  2. Szesnastkowy (heksadecymalny, w skrócie hex)

    Jako że system dwójkowy ma mniej cyfr niż dziesiętny, do przedstawienia względnie małych liczb trzeba użyć dużo zer i jedynek. Jako że bajt ma 8 bitów, podzielono go na dwie równe, czterobitowe części. Teraz bajt można już reprezentować dwoma znakami, a nie ośmioma. Na każdy taki znak składa się 2^4=16 możliwości. Stąd wzięła się nazwa szesnastkowy.
    Powstał jednak problem: cyfr jest tylko 10, a trzeba mieć 16. Co zrobić?
    Postanowiono liczbom 10-15 przyporządkować odpowiednio znaki A-F.
    Na przykład
    Liczba 255 dziesiętnie = 1111 1111 binarnie = FF szesnastkowo (1111 bin = 15 dec = F hex)
    Liczba 150 dziesiętnie = 1001 0110 binarnie = 96 szesnastkowo.

    Należy zauważyć ścisły związek między systemem dwójkowym i szesnastkowym: 1 cyfra szesnastkowa to 4 bity, co umożliwia błyskawiczne przeliczanie między obydwoma systemami: wystarczy tłumaczyć po 4 bity (1 cyfrę hex) na raz i zrobione.

    Przeliczenie liczby zapisanej w systemie szesnastkowym na dziesiętny jest równie proste, jak tłumaczenie z dwójkowego na dziesiętny. Każdą cyfrę mnożymy przez odpowiednią potęgę podstawy (podstawa wynosi 16 w systemie szesnastkowym).

    Oto przykład:
    10A szesnastkowo =
    1*16^2 + 0*16^1 + A*16^0 =
    256 + 0 + 10 =
    266 dziesiętnie.

    Działanie odwrotne też nie jest trudne: naszą liczbę dzielimy ciągle (do chwili uzyskania ilorazu=0) przez 16, po czym zapisujemy reszty z dzielenia wspak:
    (przeskocz konwersję liczby dziesiętnej na szesnastkową)
    	 266	|
    	 16	| 10
    	  1	| 0
    	  0	| 1
    Wspak dostajemy kolejno: 1, 0 i 10, czyli 10A, czyli wyjściową liczbę.

    Podczas pisania programów, liczby w systemie szesnastkowym oznacza się przez dodanie na końcu litery h (lub z przodu 0x), a liczby w systemie dwójkowym - przez dodanie litery b.
    Tak więc, 101 oznacza dziesiętną liczbę o wartości 101, 101b oznacza liczbę 101 w systemie dwójkowym (czyli 5 w systemie dziesiętnym), a 101h lub 0x101 oznacza liczbę 101 w systemie szesnastkowym (czyli 257 dziesiętnie).


Język asembler i rejestry procesora

Co to w ogóle jest asembler?

Asembler jest to język programowania, należący do języków niskiego poziomu. Znaczy to tyle, że jednej komendzie asemblera odpowiada dokładnie jeden rozkaz procesora. Asembler operuje na rejestrach procesora.

A co to jest rejestr procesora?

Rejestr procesora to zespół układów elektronicznych, mogący przechowywać informacje (taka własna pamięć wewnętrzna procesora).

Zaraz podam Wam podstawowe rejestry, na których będziemy operować. Wiem, że ich liczba może przerazić, ale od razu mówię, abyście NIE uczyli się tego wszystkiego na pamięć! Najlepiej zrobicie, czytając poniższą listę tylko 2 razy, a potem wracali do niej, gdy jakikolwiek rejestr pojawi się w programach, które będę później prezentował w ramach tego kursu.
Oto lista interesujących nas rejestrów:

  1. ogólnego użytku:


  2. rejestry segmentowe (wszystkie 16-bitowe):


  3. rejestr stanu procesora: FLAGI (16-bitowe), E-FLAGI (32-bitowe) lub R-FLAGI (64-bitowe).
    Służą one przede wszystkim do badania wyniku ostatniej operacji (na przykład czy nie wystąpiło przepełnienie, czy wynik jest zerem, itp.). Najważniejsze flagi to CF (carry flag - flaga przeniesienia), OF (overflow flag - flaga przepełnienia), SF (sign flag - flaga znaku), ZF (zero flag - flaga zera), IF (interrupt flag - flaga przerwań), PF (parity flag - flaga parzystości), DF (direction flag - flaga kierunku).

Użycie litery R przed symbolem rejestru, na przykład RCX, oznacza rejestr 64-bitowy, dostępny tylko na procesorach 64-bitowych.
Użycie litery E przed symbolem rejestru, na przykład EAX, oznacza rejestr 32-bitowy, dostępny tylko na procesorach rodziny 80386 lub lepszych. Nie dotyczy to rejestru ES.

Napisy
RAX = EAX+starsze 32 bity; EAX=AX + starsze 16 bitów; AX=AH+AL
oznaczają takie zależności między tymi rejestrami:
(przeskocz rozwinięcie rejestru RAX)
				RAX (64 bity)
				    |               EAX (32b)
   00000000000000000000000000000000 | 0000000000000000 | 00000000 | 00000000
     		  32b	     	    |	     16b       |        AX(16b)
                       		    | 		       |   AH(8b) |   AL(8b)
Napisy
RSI = ESI + starsze 32 bity; ESI = SI + starsze 16 bitów; SI = SIL+starsze 8 bitów
oznaczają:
(przeskocz rozwinięcie rejestru RSI)
				RSI (64 bity)
				    |               ESI (32b)
   00000000000000000000000000000000 | 0000000000000000 | 00000000 | 00000000
       		  32b	            |	     16b       |        SI(16b)
                       		    |		       |    8b    |  SIL(8b)

Tak, w DOSie można używać rejestrów 32-bitowych (o ile posiada się 80386 lub nowszy). Można nawet 64-bitowych, jeśli tylko posiada się właściwy procesor.

Jedna ważna uwaga - między nazwami rejestrów może pojawić się dwukropek w dwóch różnych znaczeniach:

Na razie nie musicie się przejmować tymi dwukropkami. Mówię to tylko dlatego, żebyście nie byli zaskoczeni, gdyż w przyszłości się pojawią.

Programista może odnosić się bezpośrednio do wszystkich wymienionych rejestrów, z wyjątkiem *IP oraz flag procesora (z wyjątkami).

Jak widać po ich rozmiarach, do rejestrów 8-bitowych można wpisać liczbę z przedziału 0-255 (lub od -128 do 127, gdy najwyższy, siódmy bit służy nam jako bit oznaczający znak liczby), w 16-bitowych zmieszczą się liczby 0-65535 (od -32768 do 32767), a w 32-bitowych - liczby od 0 do 4.294.967.295 (od -2.147.483.648 do 2.147.483.647)

Dobrym, choć trudnym w odbiorze źródłem informacji są: Intel Architecture Software Developer's Manual (IASDM) dostępny ZA DARMO ze stron Intela oraz DARMOWE podręczniki AMD64 Architecture Programmer's Manual firmy AMD




Pisanie i kompilowanie (asemblowanie) swoich programów

Jak pisać programy w asemblerze?

Należy zaopatrzyć się w:

Wtedy wystarczy napisać w edytorze tekstu plik zawierający komendy procesora (o tym później), zapisać go z rozszerzeniem .ASM, po czym użyć kompilatora, aby przetworzyć program na kod rozumiany przez procesor.

Jakiego kompilatora użyć?

Istnieje wiele kompilatorów języka asembler. Do najpopularniejszych należą Turbo asembler firmy Borland, Microsoft Macro asembler (MASM), Netwide asembler Project (NASM), A86/A386, NBASM, FASM, HLA.

Można je ściągnąć z internetu:
(przeskocz adresy stron kompilatorów)

Po skompilowaniu pliku z kodem źródłowym należy użyć programu łączącego, dostępnego zwykle z odpowiednim kompilatorem (na przykład tlink z tasm, link z masm).

Mamy więc już wszystko, co potrzeba. Zaczynamy pisać. Będę tutaj używał składni Turbo asemblera zgodnego z MASMem oraz FASMa i NASMa.


(przeskocz program w wersji TASM)
	; wersja TASM
	.model tiny
	.code
	org 100h

	start:
		mov	ah, 9
		mov	dx, offset info
		int	21h

		mov	ah, 0
		int	16h

		mov	ax, 4C00h
		int	21h

	info	db	"Czesc.$"

	end start

Teraz wersja NASM:


(przeskocz program w wersji NASM)
	; wersja NASM

	; nie ma ".model" ani ".code"
	; tu można wstawić:
	; section .text
	; aby dać znać NASMowi, że to będzie w sekcji kodu.
	; Nie jest to jednak wymagane, bo to jest sekcja domyślna.

	org 100h

	start:				; nawet tego NASM nie wymaga
		mov	ah, 9
		mov	dx, info	; nie ma słowa "offset"
		int	21h

		mov	ah, 0
		int	16h

		mov	ax, 4C00h
		int	21h

	info	db	"Czesc.$"

	; nie ma "end start"

Teraz wersja FASM


(przeskocz program w wersji FASM)
	; wersja FASM

	format binary

	; nie ma ".model" ani ".code"
	org 100h

	start:				; nawet tego FASM nie wymaga
		mov	ah, 9
		mov	dx, info	; nie ma słowa "offset"
		int	21h

		mov	ah, 0
		int	16h

		mov	ax, 4C00h
		int	21h

	info	db	"Czesc.$"

	; nie ma "end start"

Bez paniki! Teraz omówimy dokładnie, co każda linia robi.

Programik kompilujemy poleceniem:

	tasm naszplik.asm
	tlink naszplik.obj /t

(opcja /t - utworzy plik typu .com).
Jeśli otrzymujecie komunikaty o braku pamięci, możecie wypróbować następujący sposób:

	tasmx naszplik.asm
	tlink naszplik.obj /t /3

Dla NASMa kompilacja wygląda tak:

	nasm -o naszplik.com -f bin naszplik.asm

(-o = nazwa pliku wyjściowego
-f = format pliku. Bin = binarny = na przykład COM lub SYS).

A dla FASMa:

	fasm naszplik.asm naszplik.com

Kompilacja, nawet programu w asemblerze (zwana czasem asemblacją), ma kilka etapów:

Jeśli do programu nie dołączamy innych już skompilowanych plików ani bibliotek, to niektóre kompilatory nie wymagają osobnego linkera i mogą same sobie poradzić z wygenerowaniem programu wyjściowego. Widać to na przykład w wywołaniach NASMa i FASMa powyżej.

Teraz uruchamiamy naszplik.com i cieszymy się swoim dziełem.

Miłego eksperymentowania.

Na świecie jest 10 rodzajów ludzi:
ci, którzy rozumieją liczby binarne i ci, którzy nie.


Kolejna część kursu (Alt+4)
Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)


Ćwiczenia

  1. Poeksperymentujcie sobie, wstawiając z różne znaki do napisu. Na przykład, znaki o kodach ASCII 10 (Line Feed), 13 (Carriage Return), 7 (Bell). Pamiętajcie tylko, że znak dolara $ musi być ostatni, dlatego róbcie coś w stylu:
    		info db "Czesc.", 00, 01, 02, 07, 10, 13, 10, 13, "$"