Asembler x86/Zmienne/NASM

Z Wikibooks, biblioteki wolnych podręczników.
Przejdź do nawigacji Przejdź do wyszukiwania

Zmienne

GNU As
FASM
NASM

Segmenty[edytuj]

Tworzenie segmentów[edytuj]

Czym są segmenty zostało wyjaśnione w rozdziale Architektura. Dzielą one kod programu na części ładowane do pamięci. Zależnie od formatu pliku wyjściowego jaki chcemy otrzymać liczba i nazwy segmentów są z góry określone lub też są zupełnie dowolne. Formaty z określonymi nazwami segmentów to mn. formaty Uniksowe i format bin. Są to:

  • .code - segment z kodem
  • .data - segment z danymi
  • .bss - segment z danymi niezainicjowanymi

Aby zdefiniować dowolny segment należy w tym celu użyć dyrektywy SECTION (równoważne z SEGMENT). Oto schemat użycia:

section nazwa opcje

Przy czym w pole nazwa wpisujemy dowolną nazwę, składającą się z małych i dużych liter, liczb oraz znaków. W polu opcje możemy wpisywać po spacjach dowolną kombinację poniższych parametrów:

  • align=n - Wyrównuje początek segmentu do n bajtów. Jako n możemy podać 1, 2, 4, 16, 256, 4096. Wyrównanie segmentu oznacza, że zostanie pominięta pewna liczba bajtów tak aby początek segmentu zaczynał się od adresu podzielnego przez n.
  • sposób łączenia
    • PUBLIC - po napotkaniu innego segmentu o tej samej nazwie, oba segmenty zostaną połączone w jeden większy. Segment zbiorczy ma rozmiar równy sumie wszystkich podsegmentów.
    • STACK - to samo co powyżej, tyle że dodatkowo segment staje się stosem programu.
    • PRIVATE - segment nigdy nie zostanie połączony z innym segmentem; segmenty o identycznej nazwie są zezwolone jedynie w oddzielnych plikach.
    • COMMON - tak jak w przypadku PUBLIC, segmenty o tej samej nazwie są łączone, z tą różnicą, że wszystkie segmenty o tej samej nazwie mają ten sam adres tzn. nie są ustawiane jeden za drugim lecz są na siebie nałożone. Rozmiar segmentu zbiorczego jest równy wielkości największego z podsegmentów.
  • CLASS=nazwa - przydziela segment do jakiejś klasy; segmenty z identyczną klasą są układane obok siebie.
  • OVERLAY=tekst - przekazuje parametr do linkera.
  • USE16 lub USE32 - nakazuje przejść asemblerowi w tym segmencie w tryb 16- lub 32-bitowy.
  • FLAT - należy przekazywać ten parametr segmentom, gdy programuje się z użyciem płaskiego modelu pamięci. Wszystkie segmenty z tym parametrem są łączone w specjalną grupę FLAT (informację czym są grupy znajdziesz w podrozdziale poniżej).
  • ABSOLUTE=n - wymusza adres n dla segmentu. Obecnie brak informacji o linkerach obsługujących ten parametr; parametr dostępny tylko w przypadku formatu obj.

Jeśli nie zdefiniujemy żadnych parametrów dla dyrektywy SECTION domyślnie przyjmuje parametry PUBLIC ALIGN=1 USE16.

Grupy segmentów[edytuj]

Segmenty można łączyć w grupy za pomocą dyrektywy GROUP, której używa się wg poniższego schematu:

group nazwa segment1 segment2 segment3...

W polu nazwa podajemy nazwę dla naszej grupy, zaś po niej podajemy dowolną liczbę segmentów, które będą stanowić grupę. Od tej pory gdy zechcemy odwołać się do dowolnej zmiennej w którymkolwiek z segmentów naszej grupy, możemy dostać się do niej poprzez odwołanie do grupy a nie konkretnego segmentu. Dzięki temu możemy dla przykładu odwoływać się do 3 segmentów jednocześnie przy użyciu jednego rejestru segmentowego, który przechowuje adres grupy, nie zaś konkretnego segmentu. Oczywiście po połączeniu w grupę, segmentów nadal można używać indywidualnie.

Zmienne[edytuj]

Tworzenie zmiennych[edytuj]

Aby utworzyć nową zmienną należy użyć schematu:

nazwa typ wartość
ID nazwa ang. nazwa polska rozmiar
b byte bajt 1 bajt
w word słowo 2 bajty
d double word podwójne słowo 4 bajty
f threefold word potrójne słowo/sześć bajtów 6 bajtów
q quad word poczwórne słowo 8 bajtów
t ten bytes dziesięć bajtów 10 bajtów

W polu nazwa wpisujemy nazwę dla naszej zmiennej (pole to jest opcjonalne); w polu typ wpisujemy typ zmiennej (patrz: tabela po prawej), zaś w polu wartość wpisujemy startową wartość dla naszej zmiennej. W celu ułatwienia adresowania zmienne należy deklarować wewnątrz któregoś obszaru definiującego segment. Oto przykład deklaracji zmiennej w połączeniu z naszą wiedzą z poprzedniego podrozdziału o segmentach:

section dane
   liczba db 10
          db 18
          db 3

Pamiętając o tym, że w czystym asemblerowym kodzie bez specjalnych dyrektyw to co jest niżej w kodzie jest dalej w pamięci oraz pliku wykonywalnym, możemy się odwoływać do 2 pozostałych zmiennych bez nazw dodając do adresu liczby ich przesunięcie względem niej (więcej w podrozdziale Adresowanie). Powyższy zapis jest dodatkowo równoznaczny z poniższym:

 section dane
    liczba db 10, 18, 3

Grupę zmiennych do których odwołujemy się przy użyciu tej samej nazwy nazywamy tablicą i w niektórych przypadkach łańcuchem. Tablica jest również łańcuchem, gdy poszczególne jej elementy to znaki ASCII. Aby utworzyć ciąg takich znaków możemy napisać, przy użyciu nabytej właśnie wiedzy:

 section dane
    lancuch db "n","a","p","i","s",0

Umieszczenie czegokolwiek między cudzysłowami jak już się zapewne domyślasz nakazuje zakodować to jako znak ASCII. To co widzisz powyżej z pewnością wydaje się bardzo nieczytelne. Całe szczęście między cudzysłowami możemy wstawiać cały tekst, zamiast pojedyncze znaki:

 section dane
    lancuch db "napis",0

Oba zapisy są równoznaczne. Jak z pewnością zauważyłeś na końcu naszego łańcucha stoi 0. Gdy znamy z góry długość naszego łańcucha jest on zbędny, lecz gdy korzystamy z zewnętrznych funkcji, nie znają one tej długości. Problem rozwiązano właśnie poprzez kończenie każdego łańcucha zerem. Każda funkcja przetwarzając nasz łańcuch (np. funkcja API wyświetlająca nasz łańcuch) kończy zabawę z naszym łańcuchem, gdy napotka zero. Niektóre funkcje jako symbol końca łańcucha traktują inne wartości. Być może pamiętasz, że w naszym przykładowym programie w rozdziale Pierwszy program był to znak dolara $.<br\ > Gdy nie potrzebujemy nadawać naszej zmiennej początkowej wartości możemy użyć poniższego schematu:

nazwa resX n

W tym przypadku nazwa jest również opcjonalna. W polu resX podmieniamy literę X na literę identyfikującą typ naszej zmiennej, zaś w pole n wstawiamy ilość kopii. Pamiętając aby to umieścić w segmencie .bss. Przykład:

segment .bss
   bufor resb 256

Poprzez tą komendę zostanie utworzonych 256 niezainicjowanych zmiennych (przez co będą mieć nieco losowe wartości).

Systemy liczbowe[edytuj]

Do tej pory używaliśmy jedynie stałych zapisywanych w systemie dziesiętnym. Aby zaznaczyć, że dana liczba jest zapisana w innym systemie liczbowym musimy dodać odpowiedni przyrostek lub przedrostek:

  • system szesnastkowy - przedrostek 0x lub przyrostek h albo H. W przypadku zastosowania przyrostka należy dodać 0 na początku naszej liczby, jeśli pierwszy jej znak to litera, a nie cyfra.
  • system dziesiętny - bez przedrostków/przyrostków.
  • system ósemkowy - przyrostek o, O, q lub Q.
  • system binarny - przyrostek b, B, y lub Y.

Przykłady:

db 0xFF         ; zapis szesnastkowy
db A7h          ; źle, pierwszy znak to litera, brakuje zera
db 0A7h         ; to poprawny zapis szesnastkowy
db 18           ; zapis dziesiętny
db 73o          ; zapis ósemkowy
db 11000111b    ; zapis binarny

Adresowanie[edytuj]

Aby odnieść się do konkretnego adresu w pamięci możemy użyć tak jak to robiliśmy do tej pory nazwy symbolicznej lub też konkretnej liczby albo adresu zawartego w dowolnym rejestrze. Aby oświadczyć, że dana wartość ma być traktowana jako offset (przesunięcie względem początku segmentu) musimy umieścić ją między nawiasami kwadratowymi. Oto przykłady, które powinny zobrazować zagadnienie:

mov eax ds:[4]    ; do eax kopiowana jest wartość spod offsetu 4 w rejestrze DS
mov ecx zmienna   ; do ecx kopiowana jest wartość spod adresu wskazywanego przez zdefiniowaną nazwę symboliczną
mov edx [eax]     ; do edx kopiowana jest wartość spod offsetu przechowywanego w EAX w segmencie DS

W przypadku użycia konkretnych liczb definiujemy o który segment nam chodzi, gdy używamy nazwy symbolicznej segment zależny jest od miejsca definicji naszej zmiennej, zaś skąd wiemy z którego segmentu będzie czytać procesor w przypadku korzystania z rejestrów, tak jak w ostatnim przypadku? We wszystkich przypadkach procesor odnosi offset względem segmentu DS chyba że rejestrem adresującym jest EBP lub ESP, gdyż w ich przypadku procesor odnosi offset względem segmentu SS. Oto przykłady:

mov eax [edx]    ; do eax kopiowana jest wartość spod adresu DS + EDX
mov ecx [ebp]    ; do ecx kopiowana jest wartość spod adresu SS + EBP
mov [edx] ebp    ; pod adres DS + EDX kopiowana jest wartość ebp

Między nawiasami kwadratowymi można stosować przeniesienia oraz skalowanie używając operatory + oraz *. Operator przeniesienia (+) może być stosowany tylko z liczbami 8-, 16- i 32-bitowymi, zaś operator skalowania (*) może być użyty w wyrażeniu tylko jeden raz oraz współczynnikiem skalowania może być tylko 2, 4 lub 8. Oto przykłady

mov eax [edx+10]      ; do eax kopiowana jest wartość spod adresu DS + EDX + 10
mov [ebp*2] eax       ; pod adres SS + 2EBP kopiowana jest wartość rejestru eax
mov ebx [eax+ecx+4]     ; do ebx kopiowana jest wartość spod adresu DS + EAX + ECX + 4
mov ebx [eax+ecx*8]   ; do ebx kopiowana jest wartość spod adresu DS + EAX + 8ECX
mov ebx [eax*2+ecx*8] ; źle, istnieje ograniczenie do jednego skalowania między nawiasami!
mov ebx [eax*3]       ; źle, nie wolno skalować przez 3!

A co jeśli do obliczenia adresu zastosujemy jednocześnie rejestr EBP (który odnosi się względem segmentu DS) oraz np. rejestr EAX (który odnosi się do segmentu SS)? W tym przypadku jeden z nich traktowany jest jako główny i to jego przyporządkowanie do segmentu jest brane pod uwagę. Który z nich ma być główny? Istnieją dwie zasady:

  • jeśli między nawiasami występuje skalowanie to rejestrem głównym jest skalowany rejestr
  • w każdym innym przypadku rejestrem głównym jest pierwszy rejestr w wyrażeniu

Struktury[edytuj]

Struktury w NASM są jedynie wytworem wyobraźni asemblera i programisty. Na dłuższą metę nie są niczym fizycznym! Struktura pomaga w organizowaniu danych i jest tak jakby pojemnikiem przechowującym różne pomniejsze dane. Oto schemat definicji struktury:

struc NAZWA_STRUKTURY
 dane
endstruc

A oto przykład:

struc czlowiek
  imie: resb 64
  nazwisko: resb 64
  wiek: resw 1
endstruc

Dzięki temu zabiegowi mamy zdefiniowaną strukturę czlowiek opisywaną przez zmienne imie, nazwisko oraz wiek. Jest to tylko definicja, aby utworzyć w pamięci kopię należy postąpić wg schematu:

NAZWA_KOPII:
   istruc NAZWA_STRUKTURY
       definicje_zmiennych
   iend

Przykład:

Jacek:
   istruc czlowiek
       at imie, db "Jacek"
       at nazwisko, db "Jackowski"
       at wiek, dw 8

Aby w kodzie naszego programu zmienić wartość dowolnej zmiennej wewnątrz naszej struktury postępujemy jak w poniższym przykładzie:

mov [Jacek+imie] "Zenek"

Jak łatwo można się domyślić nazwa symboliczna Jacek określa początek struktury, zaś poszczególne nazwy symboliczne definiujące naszą strukturę określają offset, względem niego.

Pola bitowe[edytuj]

Obecnie asembler NASM nie obsługuje pól bitowych jako takich. Same w sobie nie wnoszą nic do funkcjonalności asemblera, gdyż można je zastąpić definiując różne nazwy symboliczne.