Asembler x86/Zmienne/NASM

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.