Wstęp do programowania niskopoziomowego pod Windows
Wykonanie zadań wymaga zapoznania się z rejestrami procesora oraz funkcjami obsługiwanymi po wywołaniu przerwań 0x10, 0x16 i 0x21.
Pierwsze programy
Listing 1 Asemblerowe "Hello world!" - szablon pliku .COM
; wersja NASM: program zapisany w pliku tekstowym a.asm
; kompilacja: nasm -fbin -o a.com a.asm
org 0x100
section .data ; sekcja danych zainicjalizowanych
info: db "Hello world!",13, 10, '$' ; ciąg znaków zakończony znakiem dolara - wymagane przez funkcję ; o numerze 9, wyświetlającą ciąg, znaki 10 i 13 dają nową linię
section .bss ; sekcja danych niezainicjalizowanych
bufor: resb 5 ; bufor 5 bajtów
section .text ; sekcja kodu
mov ah, 9
mov dx, info
int 0x21 ; wywołaj przerwanie 0x21 - po załadowaniu 9 do AH wyświetlona będzie
; zawartość rejestru DX
mov ah,0
int 0x16 ; wywołaj przerwanie 0x16 - po załadowaniu 0 do AH program czeka
; na naciśnięcie klawisza
mov ax, 0x4C00 ; wyjście z programu z kodem błędu 0: w AH wpisane 4C, w AL wpisane 00,
; czyli kod wyjścia
int 0x21
Program napisany w szablonie .COM jest tłumaczony przez NASMa na wykonywalny plik binarny. Ograniczeniem na zastosowanie szablonu COM jest rozmiar wynikowego pliku binarnego (64K). Jeżeli program nie zmieści się w segmencie pamięci o rozmiarze 64KB musi być zbudowany jako plik .EXE.
NASM generuje format bin jako kod 16-bitowy. Z tego względu nie może on być uruchomiony na Windowsach 64 bitowych.
Listing 2 Asemblerowe "Hello world!" - szablon pliku .EXE
; To build:
; kompilacja nasm -fobj objexe.asm
; linkowanie val objexe.obj,objexe.exe;
segment code
..start: ; punkt wejściowy programu (dla linkera VAL)
mov ax,data ; inicjalizacja rejestrów - to jest standard
mov ds,ax
mov ax,stack
mov ss,ax
mov sp,stacktop
;kod użytkownika
mov dx,hello
mov ah,9
int 0x21
;koniec kodu użytkownika
mov ax,0x4c00 ; wyjście z programu
int 0x21
segment data ; segment danych
hello: db 'Hello world!', 13, 10, '$'
segment stack stack ; segment stosu
resb 64
stacktop:
Plik w szablonie .EXE jest kompilowany do pliku .OBJ, który należy następnie poddać linkowaniu. W przykładzie przedstawionym na Listingu 2 wykorzystano darmowy linker VAL.EXE. Informacje o innych linkerach można znaleźć tutaj.
Podstawowe operacje
MOV
mov eax, 3 ; zapis 3 do EAX (3 jest natychmiastowym operandem)
mov bx, ax ; zapis wartości z AX do BX
ADD
Instrukcję ADD używamy do dodawania liczb całkowitych.
add eax, 4 ; eax = eax + 4
add al, ah ; al = al + ah
SUB
Instrukcję SUB używamy do odejmowania liczb całkowitych.
sub bx, 10 ; bx = bx - 10
sub ebx, edi ; ebx = ebx - edi
INC i DEC
inc ecx ; ecx++
dec dl ; dl--
MUL i IMUL
Instrukcja MUL jest używana do mnożenia liczb całkowitych bez znaku, a instrukcja IMUL do mnożenia liczb całkowitych ze znakiem. Składnia instrukcji MUL i IMUL jest następująca:
mul source
imul dest, source, source1
Akcja wykonywana przez instrukcje mnożenia zależy od rozmiaru danych, co przedstawiono w Tabeli 1.
Tabela 1 Zestawy parametrów i wyniki instrukcji MUL (pierwsze 3 wiersze) i IMUL
mul cl ; AX = AL*CL
mul bx ; DX:AX = AX*BX
mul esi ; EDX:EAX = EAX*ESI
mul rdi ; RDX:RAX = RAX*RDI
imul eax ; EDX:EAX = EAX*EAX
imul ebx,ecx,2 ; EBX = ECX*2
imul ebx,ecx ; EBX = EBX*ECX
imul si,5 ; SI = SI*5
DIV i IDIV
Instrukcje DIV i IDIV wykonują dzielenia na liczbach całkowitych bez znaku (DIV) i ze znakiem (IDIV), wyliczając jednocześnie resztę z dzielenia. Składnia obu operacji jest identyczna:
div source
idiv source
Jeśli operand source jest 8-bitowy, to AX jest dzielony przez operand. Wynik dzielenia całkowitego jest umieszczany w AL a reszta z dzielenia w AH. Jeśli operand jest 16-bitowy, to dzielna znajduje się w DX:AX. Wynik dzielenia całkowitego jest umieszczany w AX a reszta w DX. Jeśli operand jest 32-bitowy, to dzielna jest pobierana z EDX:EAX, wynik trafia do EAX a reszta do EDX. Wynik nie mieszczący się w rejestrze powoduje błąd wykonania.
Przed wykonaniem dzielenia IDIV rejestry DX (EDX) należy zainicjalizować instrukcją cdq. Instrukcja ta rozszerza AX do DX:AX wykorzystując bit znaku z AX.
div cl ; AL = (AX div CL), AH = (AX mod CL)
div bx ; AX = (DX:AX div BX),
; DX = (DX:AX mod BX)
div edi ; EAX = (EDX:EAX div EDI),
; EDX = (EDX:EAX mod EDI)
div rsi ; RAX = (RDX:RAX div RSI),
; RDX = (RDX:RAX mod RSI)
Deklarowanie zmiennych
Informacje o deklarowaniu danych i tematach pokrewnych są dostępne po kliknięciu na poniższe linki:
Pamięć w programie asemblerowym może być zarezerwowana na dwa sposoby. W pierwszym przypadku rezerwujemy miejsce na dane, a w drugim: rezerwujemy miejsce i inicjalizujemy dane. Pierwsza metoda wykorzystuje dyrektywę RESX, gdzie X zastępujemy literą określającą rozmiar obiektu (byte B, word W, double word D, quad word Q, ten bytes T). Druga metoda wykorzystuje dyrektywę DX, gdzie X ma to samo znaczenie, jak w przypadku RESX. Zarezerwowane miejsca w pamięci oznacza się etykietami, które pozwalają w prosty sposób uzyskać dostęp do adresu komórki pamięci jak i do danych tam wpisanych.
L1 db 0 ; byte labeled L1 with initial value 0
L2 dw 1000 ; word labeled L2 with initial value 1000
L3 db 110101b ; byte initialized to binary 110101 (53 in decimal)
L4 db 12h ; byte initialized to hex 12 (18 in decimal)
L5 db 17o ; byte initialized to octal 17 (15 in decimal)
L6 dd 1A92h ; double word initialized to hex 1A92
L7 resb 1 ; 1 uninitialized byte
L8 db "A" ; byte initialized to ASCII code for A (65)
L9 db 0, 1, 2, 3 ; defines 4 bytes
L10 db "w", "o", "r", ’d’, 0 ; defines a C string = "word"
L11 db ’word’, 0 ; same as L10
L12 times 100 db 0 ; equivalent to 100 (db 0)’s
L13 resw 100 ; reserves room for 100 words
Etykiety używa się na dwa sposoby. Etykieta oznacza adres zmiennej, a więc pełni tą samą rolę co wskaźnik w C. Jeśli etykieta jest umieszczona w nawiasach kwadratowych, oznacza wartość wpisaną pod adresem skojarzonym z etykietą (* w C w połączeniu ze wskaźnikiem).
mov al, [L1] ; copy byte at L1 into AL
mov eax, L1 ; EAX = address of byte at L1
mov [L1], ah ; copy AH into byte at L1
mov eax, [L6] ; copy double word at L6 into EAX
add eax, [L6] ; EAX = EAX + double word at L6
add [L6], eax ; double word at L6 += EAX
mov al, [L6] ; copy first byte of double word at L6 into AL
Asembler nie sprawdza, czy etykiety są używane poprawnie, co może być źródłem wielu błędów. Przy próbie wpisywania danych do pamięci należy pamiętać o konwersji typów np.:
mov [L6], 1 ; próba zapisu 1 do L6 - niepoprawna, zwróci błąd operation size not specified
mov dword [L6], 1 ; próba zapisu 1 do L6 - poprawna
Wejście/wyjście
Wejście i wyjście z programu asemblerowego są, łagodnie rzecz ujmując, uciążliwe. Aby uprościć obsługę operacji wejścia/wyjścia, w programach asemblerowych można wywoływać funkcje C. Asemblerowy interfejs do funkcji printf i scanf jest zaimplementowany w bibliotece asm_io.
Tabela 2 Funkcje biblioteki asm_io
Po załadowaniu do rejestrów odpowiednich danych, procedury z Tabeli 2 woła się instrukcją call np.:
call print_int
Zastosowanie biblioteki asm_io wymaga pobrania paczki plików, z których modyfikowany będzie tylko plik asemblerowy, przedstawiony na Listingu 3.
Listing 3
%include "asm_io.inc" ;odpowiednik dyrektywy #include z C
segment .data
;
; dane zainicjalizowane
;
segment .bss
;
; dane niezainicjalizowane
;
segment .text
global _asm_main
_asm_main:
enter 0,0 ; setup
pusha
; ----
; Właściwy kod wstawiamy tu.
; Nie należy modyfikować kodu przed i po tym komentarzu
dump_regs 1 ; wypisuje zawartosc rejestrow (do debugowania)
; ---
popa
mov eax, 0 ; powrót do C
leave
ret
Na paczkę składają się pliki asm_io.asm, Listing3.asm, driver.c, cdecl.h i asm_io.inc. Zbudowanie programu .EXE wymaga wykonania kilku operacji:
nasm -felf -o asm_io.obj asm_io.asm
nasm -felf -o Listing3.obj Listing3.asm
gcc -o driver.obj -c driver.c
gcc -o Listing3.exe driver.obj asm_io.obj Listing3.obj
Zadanie 1
Proszę skompilować i uruchomić programy przedstawione na Listingach 1-3. Proszę zmodyfikować Listing 3, tak aby program wyrzucał na ekran tekst "Hello world!". Proszę zmodyfikować programy tak, aby pytały użytkownika o imię,a następnie wyświetlały formułkę powitalną, zawierającą imię (należy użyć funkcji 0ah przerwania 0x21).
Zadanie 2
Korzystając z funkcji biblioteki asm_io proszę napisać program czytający dwie liczby całkowite, sumujący je i wyrzucający wynik na ekran.
Zadanie 3
Proszę napisać program wczytujący liczbę z klawiatury, wyliczający jej kwadrat, sześcian, resztę z dzielenia przez zadaną liczbę...
Można używać asm_io.