Asembler x86/Zmienne/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 szesnastkowydb 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 + 8ECXmov 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.