ask2, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Wyklad


Kodowanie liczb całkowitych

0x01 graphic

gdzie m oznacza liczbę bitów rejestru lub komórki pamięci;

0x01 graphic

Przykłady

Liczba dziesiętna

Reprezentacja binarna jako liczby bez znaku

253

8-bit.

1111 1101

16-bit.

0000 0000 1111 1101

32-bit.

0000 0000 0000 0000 0000 0000 1111 1101

45 708

16-bit.

1011 0010 1000 1100

32-bit.

0000 0000 0000 0000 1011 0010 1000 1100

2007360447

32-bit.

0111 0111 1010 0101 1110 0011 1011 1111

Zakresy liczb:liczby 8-bitowe: <0, 255>liczby 16-bitowe <0, 65535>liczby 32-bitowe <0, 4 294 967 295>

liczby 64-bitowe <0, 18 446 744 073 709 551 615>

(osiemnaście trylionów

czterysta czterdzieści sześć biliardów

siedemset czterdzieści cztery biliony

siedemdziesiąt trzy miliardy

siedemset dziewięć milionów

pięćset pięćdziesiąt jeden tysięcy

sześćset piętnaście)

1 trylion = 1018

0x01 graphic
gdzie m oznacza liczbę bitów rejestru lub komórki pamięci.0x01 graphic
Przykłady

Liczba dziesiętna

Reprezentacja binarna jako liczby ze znakiem

w kodzie U2

−3

8-bit.

1111 1101

16-bit.

1111 1111 1111 1101

32-bit.

1111 1111 1111 1111 1111 1111 1111 1101

-19828

16-bit.

1011 0010 1000 1100

32-bit.

1111 1111 1111 1111 1011 0010 1000 1100

2007360447

32-bit.

0111 0111 1010 0101 1110 0011 1011 1111

Zakresy liczb w systemie U2:liczby 8-bitowe: <128, +127>liczby 16-bitowe <32768, +32767>liczby 32-bitowe <2 147 483 648, +2 147 483 647>liczby 64-bitowe<9 223 372 036 854 775 808, +9 223 372 036 854 775 807>

Przechowywanie liczb w pamięci komputera

liczby występujące w programach często przekraczają 255 i muszą być zapisywane na dwóch, czterech lub na większej liczbie bajtów;

w produkowanych obecnie procesorach stosuje się dwa podstawowe schematy rozmieszczenia poszczególnych bajtów liczby w pamięci: mniejsze niżej (ang. little endian) i mniejsze wyżej (ang. big endian); sposoby te wyjaśnia poniższy rysunek;

0x01 graphic

do konwersji liczby zapisanej w formacie mniejsze niżej na format mniejsze wyżej (i odwrotnie) można zastosować instrukcję BSWAP, której argumentem jest rejestr 32-bitowy; sposób działania instrukcji wyjaśnia poniższy rysunek;

0x01 graphic

przykładowo, po wykonaniu instrukcji

mov ebx, 12345678H

bswap ebx

rejestr EBX zawierać będzie liczbę 78563412H.

Wprowadzenie do programowania

przypuśćmy, że w pamięci głównej (operacyjnej) komputera, począwszy od adresu fizycznego 72308H, znajduje się tablica zawierająca pięć liczb 16-bitowych całkowitych bez znaku; tablica ta stanowi część obszaru danych programu, który zaczyna się od adresu fizycznego 720A0H;

spróbujmy napisać fragment programu, który przeprowadzi sumowanie liczb zawartej w tej tablicy;

dla uproszczenia problemu przyjmiemy, że program wykonywany będzie w trybie 16-bitowym; przyjmiemy również, że w trakcie sumowania wszystkie wyniki pośrednie dadzą się przedstawić w postaci liczby binarnej co najwyżej 16-bitowej — innymi słowy w trakcie sumowania na pewno nie wystąpi przepełnienie (nadmiar);

w procesorach o architekturze segmentowej wszelkie operacje wykonywane na danych znajdujących się w pamięci głównej (operacyjnej) muszą być poprzedzone wpisaniem do rejestru segmentowego adresu wskazującego początek obszaru danych;

w omawianym zadaniu obszar danych programu zaczyna się od adresu fizycznego 720A0H; ponieważ w trakcie wszelkich operacji na danych znajdujących się pamięci operacyjnej (w trybie rzeczywistym) zawartość rejestru segmentowego jest mnożona razy 16, więc do rejestru segmentowego należy wpisać liczbę 720A0H / 16 = 720AH;

0x01 graphic

procesor przyjmuje, że obszar danych programu wskazuje rejestr segmentowy DS, do którego powinna zostać wpisana liczba 720AH, co symbolicznie można zapisać w niżej podany sposób:

DS 720AH

lista instrukcji procesora zawiera rozmaite instrukcje przesyłania danych, oznaczone skrótem (mnemonikiem) MOV, wśród których istnieją także instrukcje umożliwiające wpisanie podanej liczby do rejestru;

jednakże zmiany zawartości rejestrów segmentowych następują w programie stosunkowo rzadko, i z tego powodu konstruktorzy procesora ograniczyli zestaw operacji dotyczących rejestrów segmentowych: dostępne rozkazy umożliwiają wpisanie do rejestru segmentowego zawartości innego rejestru ogólnego przeznaczenia lub zawartości lokacji pamięci;

w tej sytuacji wpisanie liczby do rejestru DS wymaga pośrednictwa innego rejestru 16-bitowego, np. BX, co można zapisać symbolicznie

DS BX 720AH

realizacja podanej operacji wymaga więc użycia dwóch instrukcji przesyłania MOV — w zapisie asemblerowym wygląda to tak

mov bx, 720AH

mov ds, bx

łatwo zauważyć, że w zapisie rozkazu (instrukcji) przesyłania pierwszy argument określa cel, czyli "dokąd przesłać", drugi zaś określa źródło, czyli "skąd przesłać" lub "co przesłać":

0x01 graphic

w omawianym tu fragmencie programu mnemonik operacji przesłania zapisywany jest małymi literami (mov), podczas w opisach używa się zwykle wielkich liter (MOV) — obie formy są równoważne;

MOV CX, 7305

0x01 graphic

podane dwa rozkazy (instrukcje) muszą być oczywiście przekształcone na ciągi zerojedynkowe, zrozumiałe przez procesor

10111011 00001010 01110010 (mov bx, 720AH)

10001110 11011011 (mov ds, bx)

przekształcenie takie wykonywane jest przez asembler — zasady kodowania w asemblerze omawiane będą w dalszej części wykładu;

po załadowaniu rejestru DS położenia poszczególnych elementów tablicy wskazywane będą przez ich odległości od adresu bazowego wskazywanego przez DS — ilustruje to poniższy rysunek

operacje sumowania zapiszemy najpierw w postaci symbolicznej

AX ds:[0268H]

AX AX + ds:[026AH]

AX AX + ds:[026CH]

AX AX + ds:[026EH]

AX AX + ds:[0270H]

równoważny kod asemblerowy ma postać

mov ax, ds:[0268H]

add ax, ds:[026AH]

add ax, ds:[026CH]

add ax, ds:[026EH]

add ax, ds:[0270H]

do sumowania używany jest rozkaz ADD, który przeprowadza sumowanie dwóch wartości wskazanych przez pierwszy i drugi operand, a wynik wpisywany jest do pierwszego operandu; zatem rozkaz

add cel, źródło

wykonuje dodawanie

cel cel + źródło

operandy cel i źródło mogą wskazywać na rejestry lub lokacje pamięci, jednak tylko jeden operand może wskazywać lokację pamięci;

adres lokacji pamięci zapisany w postaci ds:[026EH] wskazuje, że adres fizyczny tej lokacji powinien zostać obliczony na podstawie zawartości rejestru DS, tj. DS 16 + 026EH;

omawiane operacje w postaci zrozumiałej dla procesora wyglądają tak:

10100001 01101000 00000010 (mov ax, ds:[0268H])

00000011 00000110 01101010 00000010 (add ax, ds:[026AH])

00000011 00000110 01101100 00000010 (add ax, ds:[026CH])

00000011 00000110 01101110 00000010 (add ax, ds:[026EH])

00000011 00000110 01110000 00000010 (add ax, ds:[0270H])

Przykład obliczania wyrażenia arytmetycznego

0x01 graphic

Rozkazy mnożenia MUL i IMUL

mnożna

mnożnik

iloczyn

mnożenie liczb 8-bitowych

AL

podany w polu operandu

AX

mnożenie liczb 16-bitowych

AX

DX:AX

mnożenie liczb 32-bitowych

EAX

EDX:EAX

Rozkazy dzielenia DIV i IDIV

dzielna

dzielnik

iloraz

reszta

dzielenie liczby 16-bitowej przez 8-bitową

AX

podany w polu operandu

AL

AH

dzielenie liczby 32-bitowej przez 16-bitową

DX:AX

AX

DX

dzielenie liczby 64-bitowej przez 32-bitową

EDX:EAX

EAX

EDX

0x01 graphic

ADD SI, DI ; dodawanie SI ← SI + DI

ADD SI, 7 ; dodawanie SI ← SI + 7

MOV AX, SI ; przesłanie AX ← SI

SUB DI, 1 ; odejmowanie DI ← DI - 1

MUL SI ; mnożenie DX:AX ← AX ∗ SI

Modyfikacje adresowe

podany w poprzedniej części sposób sumowania elementów tablicy jest bardzo niewygodny, zwłaszcza jeśli ilość sumowanych liczb jest duża;

powtarzające się obliczenia wygodnie jest realizować w postaci pętli, ale wymaga to korekcji adresu instrukcji dodawania — w każdym obiegu pętli adres lokacji pamięci wskazujący dodawaną liczbę powinien być zwiększany o 2;

przedstawione problemy rozwiązuje się poprzez stosowanie modyfikacji adresowych — adres lokacji pamięci, na której wykonywane jest działanie określony jest nie tylko poprzez pole adresowe instrukcji, ale zależy również od zawartości jednego lub dwóch wskazanych rejestrów;

w procesorze Pentium dostępne są rozbudowane mechanizmy modyfikacji adresowych:

modyfikatorami mogą być 16-bitowe rejestry ogólnego przeznaczenia: BX, SI, DI, BP, a także ich pary: (BX, SI), (BX, DI), (BP, SI), (BP, DI);

modyfikatorami mogą być dowolne 32-bitowe rejestry ogólnego przeznaczenia: EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP;

może również wystąpić drugi rejestr modyfikacji spośród ww. wymienionych, z wyłączeniem rejestru ESP;

drugi rejestr modyfikacji może być skojarzony z tzw. współczynnikiem skali, który podawany jest w postaci 1, 2, 4, 8 — podana liczba wskazuje przez ile zostanie pomnożona zawartość drugiego rejestru modyfikacji podczas obliczania adresu;

0x01 graphic

adres efektywny, nazywany również adresem wirtualnym, wskazuje położenie lokacji pamięci względem początku segmentu; na podstawie adresu efektywnego i zawartości rejestru segmentowego procesor oblicza adres fizyczny;

adres efektywny w trybie 32-bitowym obliczany jest modulo 232, tzn. bierze się pod uwagę 32 najmłodsze bity uzyskanej sumy; analogicznie adres efektywny w trybie 16-bitowym obliczany jest modulo 216;

przykładowo, adres efektywny poniższego rozkazu

sub eax, ds:[123H][edx][ecx4]

zostanie jako obliczony jako suma:

liczby 123H,

zawartości rejestru EDX,

zawartości rejestru ECX pomnożonej przez 4;

w literaturze zawartość pierwszego rejestru modyfikacji nazywana jest adresem bazowym, a drugiego adresem indeksowym;

obliczanie adresu efektywnego w trybie 32- i 16-bitowym wg reguły modulo 232 i 216 pozwala uzyskiwać adresy efektywne mniejsze niż zawartość rejestru modyfikacji — odpowiednio duże liczby umieszczone w polu adresowym działają tak liczby ujemne; ilustruje to poniższy przykład (tryb 16-bitowy):

instrukcja mov ....,[bx] + FFFCH

. . .

. . .

FC

FF

rejestr BX

03A9

adres efektywny powyższej instrukcji MOV wynosi 03A5H i jest mniejszy niż zawartość rejestru BX;

niekiedy pole adresowe instrukcji jest całkowicie pominięte, a wartość adresu określona jest wyłącznie poprzez wskazane rejestry modyfikacji; takie rozwiązanie jest:

niezbędne, jeśli adres lokacji pamięci zostaje obliczony dopiero w trakcie wykonywania programu (nie jest znany ani w trakcie kodowania programu przez programistę ani też podczas translacji) — dotyczy to często kodu generowanego przez kompilatory języków wysokiego poziomu;

szczególnie korzystne w przypadku wielokrotnego odwoływania się do tej samej lokacji pamięci — ponieważ pole adresowe nie występuje, więc instrukcja może być zapisana na mniejszej liczbie bajtów (zwykle 2 bajty);

modyfikacja adresowa z użyciem rejestrów BP lub EBP działa trochę inaczej; rejestry te zostały bowiem zaprojektowane do wspomagania operacji przekazywania parametrów do procedur za pośrednictwem stosu — z tego względu użycie ww. rejestrów jako modyfikatorów powoduje, że procesor będzie używał domyślnego rejestru segmentowego SS a nie DS, co oznacza że operacja zostanie wykonana na danych zawartych w segmencie stosu;

w programach asemblerowych, modyfikację adresową deklaruje się poprzez podanie w polu adresowym rozkazu nazwy rejestru w nawiasie kwadratowym;

na poziomie asemblera podane schematy obliczenia adresu efektywnego z wykorzystaniem modyfikacji adresowych koduje się poprzez podanie nazw rejestrów modyfikacji w nawiasach kwadratowych, np. [bx]; oznacza to, że adres wynikowy (efektywny) rozkazu stanowi sumę liczby umieszczonej w polu adresowym rozkazu i zawartości rejestru modyfikacji adresowej (tu: BX); tego rodzaju operandy instrukcji nazywane są operandami modyfikacji adresowych; przykłady:

mov dh, [ebx]

mov [ecx], esi

mov al, [bx]

sub [bx][di], ax

; zapisy w dwóch poniższych wierszach są równoważne

mov bx, [eax] [edx2]

mov bx, [eax + edx2]

w operandach modyfikacji adresowych mogą także występować nazwy zmiennych i tablic, jak również pewne wartości stałe, np.

mov ax, tablica [ebx+ecx4+14]

posługując się modyfikacją adresową, omawiany wcześniej fragment programu obliczający sumę liczb można zakodować w formie pętli rozkazowej; w kolejnych obiegach pętli adres rozkazu dodawania ADD powinien zwiększać się o 2 — można to łatwo zrealizować poprzez uzależnienie adresu rozkazu od zawartości rejestru modyfikacji adresowej BX;

ponieważ w kolejnych obiegach pętli rejestr BX będzie zawierał liczby 0, 2, 4, ..., więc kolejne adresy efektywne rozkazu ADD, które stanowią sumę pola adresowego (tu: 0268H) i zawartości rejestru BX, będą wynosiły:

0268H, 026AH, 026CH, 026EH, 0270H

tak więc w każdym obiegu pętli do zawartości rejestru AX dodawane będą kolejne elementy tablicy liczb;

mov bx, 720AH

mov ds, bx ; ładowanie rejestru DS

mov cx, 0 ; licznik obiegów pętli

mov ax, 0 ; początkowa wartość sumy

mov bx, 0 ; początkowa zawartość rejestru

; modyfikacji adresowej

ptl_suma:

add ax, ds:[0268H][bx] ; dodanie kolejnego

; elementu tablicy

add bx, 2 ; zwiększenie modyfikatora

loop ptl_suma ; sterowanie pętlą

instrukcja loop stanowi typowy sposób sterowania pętlą: powoduje ona odjęcie 1 od zawartości rejestru CX, i jeśli wynik odejmowania jest różny od zera, to sterowanie przenoszone do instrukcji poprzedzonej podaną etykietą, a przeciwym razie następuje przejście do następnego rozkazu; ponieważ początkowa zawartość rejestru CX wynosiła 5, więc rozkazy wchodzące w skład pętli zostaną powtórzone 5 razy;

rozpatrując rozkaz LOOP jako rozkaz sterujący (skokowy) można powiedzieć, że warunek testowany przez rozkaz jest spełniony, jeśli po odjęciu 1 zawartość rejestru CX jest różna od zera — wówczas następuje skok, który polega na dodaniu do rejestru IP liczby umieszczonej w polu adresu rozkazu LOOP i zwiększeniu IP o 2 (liczba bajtów rozkazu LOOP); jeśli warunek nie jest spełniony, to IP zostaje zwiększony o 2;

modyfikatorami mogą być też rejestry 32-bitowe:

mov bx, 720AH

mov ds, bx

mov cx, 5 ; licznik obiegów pętli

mov ax, 0 ; początkowa wartość sumy

mov ebx, 0268H ; adres początku tablicy

mov edx, 0 ; indeks kolejnego elementu

ptl_suma:

add ax, ds:[ebx+edx2] ; dodanie kolejnego

; elementu tablicy

inc dx

loop ptl_suma ; sterowanie pętlą

Adresowanie pośrednie

omawiane dotychczas rodzaje modyfikacji adresowych polegały na dodawaniu (sumowaniu) zawartości rejestrów modyfikacji z polem adresowym rozkazu; w innych typach procesorów występuje również modyfikacja pośrednia, polegająca na wskazaniu komórki pamięci, w której znajduje potrzebny adres — ilustruje to poniższy rysunek;

0x01 graphic

w procesorze Pentium adresowanie pośrednie stosowane jest w instrukcjach sterujących (skokowych).

Zasady kodowania w asemblerze

kodowanie programu w asemblerze powinno pozwalać na zapisywanie algorytmów za pomocą pojedynczych instrukcji procesora; także definiowanie danych powinno być realizowane na poziomie pojedynczych bajtów lub słów;

asembler posiada rozmaite środki ułatwiające taki sposób kodowania programu;

najbardziej charakterystycznym elementem asemblera jest możliwość zapisu instrukcji w formie skrótów literowych zwanych mnemonikami;

struktura segmentowa programu w asemblerze stanowi odbicie architektury segmentowej procesorów Pentium;

jeden lub kilka segmentów tworzy moduł, który stanowi jednostkę poddawaną samodzielnej asemblacji;

program składa się z jednego lub kilku modułów; niektóre moduły mogą być kodowane w innych językach programowania.

Formaty wierszy źródłowych

uzup:

mov

cx, 78

; licznik obiegów

nazwa wiersza (etykieta)

akcja, którą na­leży wykonać

operandy (okreś­lają obiekty, na których zostanie wykonana akcja)

komentarz

jeśli w zapisie instrukcji występują dwa operandy, to przyjmuje się, że wynik operacji zostanie wpisany do pierwszego operandu, np.

sub ecx, eax ; obliczenie ECX ECX EAX

var2

db

23

; prędkość

nazwa zmiennej

dyrektywa

operand

komentarz

num1

EQU

18H

; wartość max

nazwa stałej

dyrektywa

operand

komentarz

Etykiety i zmienne w programie asemblerowym

w pamięci głównej (operacyjnej) przechowywane są instrukcje (rozkazy) i dane programu; instrukcje (rozkazy) programu wykonują działania na danych zawartych w rejestrach i lokacjach pamięci; poszczególne instrukcje i dane zajmują jeden lub więcej bajtów;

w programie asemblerowym niektóre obszary pamięci opatrywane są nazwami:

jeśli nazwa odnosi się do obszaru zawierającego instrukcję (rozkaz) programu, to nazwa taka stanowi etykietę,

jeśli obszar zawiera zmienną (daną), to nazwa obszaru stanowi nazwę zmiennej;

nazwę w sensie asemblera tworzy ciąg liter, cyfr i znaków ? (znak zapytania), @ (symbol at), _ (znak podkreślenia), $ (znak dolara); nazwa nie może zaczynać się od cyfry;

etykietę, wraz ze znakiem : (dwukropka), umieszcza się przed instrukcją, np.

powtorz: mov dl, [ecx]

taka konstrukcja oznacza, że obszar kilku bajtów pamięci, w których przechowywany jest kod podanej instrukcji mov dl, [ecx] ma swoją unikatową nazwę, czyli etykietę; w trakcie asemblacji programu etykiecie przypisywana jest wartość równa odległości pierwszego bajtu obszaru (liczonej w bajtach) od początku segmentu kodu;

zmienne w programie deklaruje się za pomocą dyrektyw:

DB - definiowanie bajtu (8 bitów),

DW - definiowanie słowa (16 bitów),

DD - definiowanie słowa podwójnej długości (32 bity),

DF - definiowanie 6 bajtów (48 bitów);

DQ - definiowanie słowa poczwórnej długości (64 bity),

DT - definiowanie 10 bajtów (80 bitów).

po lewej stronie dyrektywy podaje się nazwę zmiennej, a po prawej wartość początkową; znak zapytania (?) oznacza, że wartość początkowa zmiennej jest nieokreślona; nazwę zmiennej można pominąć; przykłady:

kcal DB 169

linia7a DQ ?

DW 19331

elem_sygn DD 18B706H

alfa dw 4567H, 5678H, 6789H

nazwy zmiennych mogą występować w polu operandu instrukcji — operandy tego rodzaju nazywa się operandami relokowalnymi, ponieważ położenie zmiennych nie jest ustalone podczas kodowania programu, np.:

xor ebx, elem_sygn

add si, alfa [bx]+2

w asemblerze przyjęto, że nazwa zmiennej występująca w po­lu operandu rozkazu (podobnie jak nazwa rejestru) oznacza, że działanie ma być wykonane na zawartości tej zmiennej;

w trakcie asemblacji nazwie każdej zmiennej przypisywana jest wartość równa odległości (liczonej w bajtach) pierwszego bajtu tej zmiennej od początku segmentu danych; jeszcze raz podkreślamy: omawiana wartość nie jest wartością zmiennej, ale jest przypisywana nazwie zmiennej i określa położenie zmiennej w pamięci;

w polu operandu dyrektywy DB mogą występować także łańcuchy znaków, np.

DB 'Politechnika Gdańska', 0DH, 0AH

jeśli tekst nie mieści się w jednym wierszu (który może zawierać co najwyżej 128 zna­ków), to można kontynuować w następnym wierszach, za każdym razem poprzedzając go dyrektywą DB;

jeśli podane wartości są liczbami całkowitymi, to asembler two­rzy ich reprezentacje binarne przyjmując, że naj­mniej znaczący bit ma wagę 20 ; liczby dziesiętne poprzedzone znakiem + i liczby bez znaku traktowane są jednakowo: zamiana na kod binarny wykonywana jest przy założeniu, że wszystkie bity w bajcie, w słowie, itd. są bitami znaczącymi (nie występuje bit znaku); dla liczb ujemnych stosuje się kod U2, przy czym przyjmuje się, że bit znaku zajmuje skrajną lewą pozycji bajtu, słowa, itd.; w szczególności oznacza to, że wiersze

DB 128

DB +128

generują takie same kody binarne 10000000;

należy zwracać uwagę na dopuszczalne zakresy zmienności argumentów podanych dyrektyw:

Dyrektywa

liczby bez znaku

liczby ze znakiem

DB

<0, 255>

<128, 127>

DW

<0, 65535>

< 32768, 32767>

DD

<0, 4294967295>

<2147483648, 2147483647>

DF

<0, 2481>

<247 , 2471>

DQ

<0, 2641>

<263 , 2631>

DT

<0, 2801>

<279 , 2791>

w polu operandu omawianych dyrektyw mogą występować zarówno pojedyncze liczby jak i ciągi liczb rozdzielonych przecinkami; jeśli ciąg składa się z jednakowych elementów, to można zastosować operator powtarzania DUP, np. wiersz:

sumy DW 5432H, 5432H, 5432H, 5432H, 5432H

można zapisać w krótszej postaci równoważnej:

sumy DW 5 DUP (5432H)

operator DUP generuje wskazaną ilość elementów podanych w nawiasie — stanowi więc wygodny sposób tworzenia tablic; w nawiasie moż­na także umieścić znak zapytania (?) oznaczający element o nieokreślonej wartości począt­kowej, co pozwala na tworzenie tablic złożonych z elementów o nieokreślonej wartości początkowej, np.

tabl_wynikow dd 24 dup (?)

za pomocą operatora powtarzania DUP definiuje się zwykle obszar segmentu stosu, np.

stos_pom SEGMENT stack

DW 128 DUP (?)

stos_pom ENDS

powyższy fragment programu opisuje segment stosu zawierający 256 bajtów o nieokreślonej wartości początkowej;

liczby z kropką oddzielającą część ułamkową mogą występować jako operandy dyrektyw DD, DQ, DT - liczby takie są przekształcane na postać zmiennoprzecinkową binarną, i jeśli w progra­mie użyto dyrektywy '.8087', '.287' lub '.387', to format zmiennoprzecinkowy jest zgodny z forma­tem akceptowanym przez koprocesor arytmetyczny, np.:

c_Eulera DT 0.577215 ; stała Eulera

— — — — — — — — — — — — — — —

fld c_Eulera ; załadowanie stałej Eulera ; na stos koprocesora

Rozmiary operandów

instrukcje procesora Pentium wykonują działania na bajtach (8 bitów), słowach (16-bitów), podwójnych słowach (32-bity), obiektach 48-, 64- i 80-bitowych; jeśli instrukcja wymaga podania dwóch operandów, to prawie zawsze muszą być one jednakowej długości, np.:

sub ecx, edi ; obliczenie ECX ECX EDI

add eax, dx ; błąd !!! — dodawanie zawartości

; rej. 16-bitowego do 32-bitowego

wyjątkowo, jeśli jeden z operandów znajduje się w pamięci, to możliwa jest doraźna zmiana rozmiaru (typu) zmiennej za pomocą operatora PTR, np.

blok_sys dd ? ; zmienna 32-bitowa

— — — — — — — — —

mov cx, word PTR blok_sys

mov dx, word PTR blok_sys+2

w powyższym przykładzie do rejestru CX zostanie wpisana młodsza część 32-bitowej zmiennej blok_sys, a rejestru DX starsza część tej zmiennej; w przypadku pominięcia operatora PTR sygnalizowany byłby błąd asemblacji wynikający z niejednakowej długości obu operandów;

ogólnie: operator PTR używany jest w wyrażeniach adresowych do ścisłego określania atrybu­tów symbolu występującego w wyrażeniu adresowym; po prawej stronie operatora występu­je wyrażenie adresowe, a po lewej atrybut spośród następujących: byte (8 bitów), word (16 bitów), dword (32 bity), fword (48 bitów), qword (64 bity), tword (80 bitów);

operator PTR stosuje się także w przypadkach, gdy wyrażenie adresowe nie pozwala na jednoznaczne przetłumaczenie instrukcji, np. poniższa instrukcja stanowi tzw, odwołanie anonimowe (nie zawiera nazwy zmiennej)

mov [bx], 65

operandy instrukcji nie precyzują czy liczba 65 ma zostać zapisana w pamięci jako liczba 8-bitowa, 16-bitowa czy 32-bitowa — zamiast podanej wyżej instrukcji programista musi wyraźnie określić rozmiar liczby podając jedną z trzech poniższych instrukcji

mov byte PTR [bx], 65

mov word PTR [bx], 65

mov dword PTR [bx], 65

operator PTR stosuje się także przy określaniu adresów instrukcji sterujących (skoków);

reguła dotycząca jednakowej długości operandów nie obowiązuje w odniesieniu do rozkazów MOVSX i MOVZX; rozkazy powodują przepisanie zawartości 8- lub 16-bitowego rejestru (lub zawartości lokacji pamięci) do rejestru 16- lub 32-bitowego; w przypadku rozkazu MOVZX brakujące bity w rejestrze docelowym uzupełniane są zerami, zaś w przypadku MOVSX bity te są wypełniane przez wielokrotnie powielony bit znaku kopiowanego rejestru; przykłady

liczba_proc db 145 ; zmienna 8-bitowa

— — — — — — — — —

movzx edx, liczba_proc

movsx edx, bh

rozkaz MOVSX stosuje się do kopiowania liczb ze znakiem — po zwiększeniu ilości bitów liczby, jej wartość pozostaje niezmieniona.

Operatory OFFSET i SEG w asemblerze

w praktyce programowania posługujemy się zarówno zawartościami pewnych obszarów pamięci, jak też i ich adresami;

bezpośrednie podawanie adresów obszarów pamięci, w których znajdują się instrukcje i dane jest wprawdzie możliwe, ale nie jest zalecane w programowaniu w asemblerze; programy z ustalonymi adresami są mało podatne na zmiany; z tego względu wybór położenia zmiennej lub instrukcji pozostawiamy asemblerowi; ustalanie położenia zmiennych i instrukcji jest możliwe za pomocą operatorów SEG i OFFSET;

położenie zmiennej lub instrukcji w programie można jednoznacznie określić poprzez podanie adresu fizycznego — dla procesorów Pentium pracujących w trybie rzeczywistym adres fizyczny zawiera 20 bitów;

w praktyce programowania zamiast adresu fizycznego zmiennej lub instrukcji podaje się adres segmentu, w którym ta zmienna (lub instrukcja) została zdefiniowana oraz jej położenie wewnątrz segmentu;

adres segmentu podaje operator SEG w formie liczby 16-bitowej, stanowiącej 16 bardziej znaczących bitów adresu fizycznego segmentu — przyjęto, że brakujące 4 bity z prawej strony zawierają zawsze zera; zatem adres fizyczny segmentu można wyznaczyć przez pomnożenie przez 16 wartości zwracanej przez operator SEG; w praktyce programowania posługujemy się wyłącznie 16-bitowymi adresami segmentów mając na uwadze ww. założenia;

położenie zmiennej (lub instrukcji) wewnątrz segmentu określa się poprzez podanie odległości, liczonej we bajtach, zmiennej od początku segmentu — odległość ta nazywana jest przesunięciem lub offsetem i można ją wyznaczyć za pomocą operatora OFFSET; tak więc dla podanej zmiennej lub etykiety operator OFFSET wyznacza jej położe­nie, liczone w bajtach, względem początku segmentu, w którym została zdefiniowana (położenie to można odszukać w sprawozdaniu z kompilacji zawartym w pliku .LST);

0x01 graphic

zastosowanie operatorów SEG i OFFSET wyjaśnia poniższy przykład:

parametry SEGMENT

moc_czynna dw 800

parametry ENDS

— — — — — — — — — — — —

mov ax, SEG parametry

mov ds, ax

mov bx, OFFSET moc_czynna

add word PTR ds:[bx], 75 ; dodanie 75

add ds:moc_czynna, 75 ; dodanie 75

wskutek wykonania dwóch instrukcji oznaczonych zmienna moc_czynna zostanie zwiększona o 75 — taki sam skutek ma wykonanie instrukcji oznaczonej ;

operand zawierający operator OFFSET traktowany jest jako liczba znana już w trakcie kompilacji;

położenie pewnej lokacji pamięci względem początku segmentu można także wyzna­czyć za pomocą instrukcji (rozkazu) LEA — podana wyżej sekwencja może być zastąpiona przez sekwen­cję równoważną:

lea bx, moc_czynna

add word PTR ds:[bx], 75 ; dodanie 75

instrukcja LEA wyznacza adres efektywny rozkazu, czyli położenie lokacji pamięci (względem początku segmentu), na której zostanie wykonana operacja;

instrukcja LEA wyznacza adres efektywny w trakcie wykonywania programu, podczas gdy wartość operatora OFFSET obliczana jest w trakcie translacji programu;

rzeczywiste położenie programu (i wszystkich zawartych w nim segmentów) w pamięci ope­racyjnej znane będzie dopiero w chwili ładowania tego programu do pamięci, bezpośrednio przed rozpoczęciem jego wykonywania — z tego powodu, w trakcie translacji programu, wartość operatora SEG obliczana jest tak jak gdyby program miał zostać umieszczony w pamięci operacyjnej począwszy od komórki 0; ostateczna korekcja fragmentów programu, w których użyto operatora SEG wykonywana jest przez system operacyjny bezpośrednio przed rozpoczęciem wykonywania programu;

zawartości rejestrów segmentowych CS i SS są zazwyczaj ustawiane przez system operacyjny bezpośrednio przed rozpoczęciem wykonywania programu; rejestry DS, ES, FS i GS, jeśli są używane, to muszą być zainicjalizowane przez program (dyrektywa ASSUME nie ładuje rejestrów segmentowych); typowa sekwencja ładowania rejestru segmentowego wygląda następująco (wpisanie liczby wymaga pośrednictwa innego rejestru roboczego):

mov ax, SEG zm

mov ds,ax

argumentem operatora SEG może być nazwa segmentu, etykiety lub zmiennej;

asembler dopuszcza pominięcie słowa SEG, jeśli argumentem operatora jest nazwa segmentu - wówczas ope­rator SEG występuje w postaci niejawnej; pominięcie operatora SEG zazwyczaj utrudnia analizę programu.

Przedrostek chwilowej zmiany segmentu

w procesorze Pentium (i w poprzednikach) przyjęto, że instrukcje programu umieszczone są w segmencie kodu, którego początek wskazuje rejestr CS (w trybie chronionym segmenty wskazywane za pośrednictwem tablicy deskryptorów), dane umieszczone są w segmencie wskazywanym przez rejestr DS, a stos wskazuje rejestr SS;

powyższe ustalenia mogą być w pewnym zakresie zmienione doraźnie, w odniesieniu do pojedynczej instrukcji, za pomocą dodatkowego bajtu zwanego przedrostkiem chwilowej zmiany segmentu; obowiązują tu następujące reguły:

instrukcje pobierane są zawsze z segmentu kodu wskazywanego przez rejestr CS;

dane przesyłane są z/do segmentu danych wskazywanego przez rejestr DS; poprzedzenie instrukcji przedrostkiem chwilowej zmiany segmentu umożliwia przesyłanie danych z segmentów wskazywanych przez inne rejestry: CS, ES, FS, GS, SS;

operacje stosu (np. PUSH) wykonywane są zawsze w segmencie wskazywanym przez rejestr SS;

jeśli do obliczenia adresu efektywnego instrukcji używany jest rejestr (E)BP, to operacja wykonywana jest na segmencie stosu; wprowadzenie przedrostka chwilowej zmiany umożliwia użycie innego rejestru segmentowego;

Rejestr

CS

DS

ES

FS

GS

SS

Kod binarny

00101110

00111110

00100110

01100100

01100101

00110110

Kod szesnastk.

2EH

3EH

26H

64H

65H

36H

na poziomie asemblera przedrostek chwilowej zmiany segmentu podawany w postaci nazwy rejestru segmentowego wraz z dwukropkiem, np.

sub edx, cs:wynik

dyrektywa asemblera ASSUME jest ściśle związana z przedrostkiem chwilowej zmiany segmentu; dyrektywa ta uwalnia programistę od konieczności wpisywania przedrostka chwilowej zmiany segmentu w zapisie instrukcji odwołujących się do lokacji pamięci; na podstawie informacji podanych w tej dyrektywie asembler automatycznie generuje bajty przedrostków wszędzie tam gdzie jest konieczne;

dyrektywa ASSUME wiąże segmenty programu i rejestry segmentowe, tj. dla każdego segmentu wymienionego na liście tej dyrektywy poda­je nazwę rejestru segmentowego (CS, DS, ES, FS, GS, SS), którego zawartość wskazuje początek tego segmentu;

związki opisane za pomocą dyrektywy ASSUME mają charakter obietnicy: dyrektywa ASSUME podaje przewi­dywane użycie rejestrów segmentowych (sama dyrektywa nie wpływa jednak w żaden spo­sób na ich zawartość);

wpisanie odpowiednich wartości do rejestrów segmentowych, zgodnie z obietnicą podaną w dyrektywie ASSUME, realizuje się za pomocą konwencjonal­nych rozkazów; początkowe zawartości rejestrów CS i SS ustawiane są przez system operacyjny bezpośrednio przed uruchomieniem programu;

dyrektywa ASSUME ma postać

ASSUME CS:<nazwa_seg>, DS:<nazwa_seg>, SS:<nazwa_seg>

ASSUME ES:<nazwa_seg>, FS:<nazwa_seg>, GS:<nazwa_seg>

gdzie <nazwa_seg> oznacza nazwę segmentu (nazwa segmentu musi być zdefiniowana wcześniej za pomocą dyrektywy SEGMENT);

zamiast nazwy segmen­tu można podać słowo nothing, co oznacza anulowanie związku określonego przez po­przednią dyrektywę ASSUME; przykład:

ASSUME CS:rozkazy, DS:nothing, ES:tablica

ze względu na specyficzny sposób dostępu do danych umieszczonych na stosie, zazwyczaj w dyrektywie ASSUME pomija się segment stosu i rejestr SS;

dyrektywa

ASSUME nothing

anuluje wszystkie dotychczas określone związki;

segment programu może być jedno­cześnie związany z kilkoma rejestrami segmentowymi;

rejestr segmentowy może być związany z co najwyżej jednym segmentem programu (ten warunek spełniony jest automa­tycznie, ponieważ każda kolejna dyrektywa ASSUME dotycząca pewnego rejestru segmen­towego anuluje związek określony przez poprzednią dyrektywę);

zmiana zawartości rejestru segmentowego musi być poprzedzona odpowiednio zaktualizowaną dyrektywą ASSUME;

niektóre asemblery pozwalają na pominięcie dyrektywy ASSUME przyjmując wówczas ustalenia domyślne; w zasadzie jednak w programie powinna wystąpić dyrektywa ASSUME określająca połączenie przynajmniej dla rejestru CS i segmentu zawierającego rozkazy; połączenia innych segmentów i rejest­rów można określić w tej samej dyrektywie albo w innych, umieszczonych w dalszej części programu.

Przykład

wyn_pomiar SEGMENT use16

t_stop db 'Stop'

tabl db 120 dup (?)

wyn_pomiar ENDS

analiza SEGMENT use16

ASSUME CS:analiza, ES:wyn_pomiar

mov ax, SEG wyn_pomiar

mov es,ax

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

mov dh, tabl [bx]

mov dh, es : tabl [bx]

mov dh, wyn_pomiar : tabl [bx]

powyższe trzy instrukcje MOV, mimo różnic w zapisie wyrażenia adresowego, będą przetłu­maczone na identyczny kod maszynowy;

kod binarny instrukcji, generowany przez kompilator, zostanie poprzedzony dodatkowym bajtem - przedrostkiem chwilowej zmiany rejest­ru segmentowego ES;

w przypadku, gdyby podana wyżej dyrektywa ASSUME nie zawierałaby części dotyczącej rejestru ES, to poprawny byłby jedynie drugi rozkaz mov, podczas gdy dwa pozostałe nie zostałyby zaakceptowane przez kompilator — kompila­tor nie wiedziałby jaki rejestr segmentowy zostanie użyty do przesłania danych z obszaru tabl.

Dyrektywa INCLUDE

niekiedy znaczna cześć programu w asemblerze zawiera fragmenty zaczerpnięte z bibliotek źródłowych, uprzednio starannie przetestowane przez autorów i nie wymagające uwagi programisty; takie fragmenty warto umieścić w odrębnym pliku, który będzie automatycznie odczytywany w trakcie asemblacji, jeśli tylko w programie zostanie umieszczona dyrektywa INCLUDE;

dyrektywa INCLUDE wskazuje plik, którego treść powinna zostać poddana asemblacji, tak jak gdyby była umieszczona w pliku źródłowym;

przykład: dyrektywa

INCLUDE korekcja.inc

powoduje, że asembler przerwie tłumaczenie wierszy źródłowych zawartych w bieżącym pliku, przetłumaczy wszystkie wiersze zawarte w pliku korekcja.inc, po czym powróci do tłumaczenia dalszych wierszy pobieranych z pliku bieżącego;

nazwa pliku może być poprzedzona ścieżką dostępu.

Asemblacja programów

w procesie translacji programu zakodowanego w asemblerze wyróżnia się asemblację, konsolidację (linkowa­nie) i ładowanie; w językach wysokiego translacja przebiega podobnie, przy czym zamiast asemblacji stosowana jest kompilacja;

w istocie zarówno asemblacja jak i kompilacja mają na celu przekształcenie kodu źródłowego programu na ciąg instrukcji procesora, aczkolwiek uzyskany ciąg (zawarty w pliku z rozszerzeniem .OBJ) wymaga jeszcze dalszych przekształceń, które wykonywane są w trakcie konsolidacji i ładowania;

asemblacja realizowana jest dwuprzebiegowo: w każdym przebiegu czytany jest cały plik źródłowy (ściśle: moduł) od początku do końca;

w pierwszym przebiegu asembler stara się wyznaczyć ilości bajtów zajmowane przez poszczególne instrukcje i dane; jednocześnie asembler rejestruje w słowniku symboli wszystkie pojawiające się definicje symboli (zmiennych i etykiet);

w drugim przebiegu asembler tworzy kompletną wersję przetłumaczo­nego programu określając adresy wszystkich instrukcji w oparciu o informacje zawarte w słowniku symboli;

w procesie asemblacji programów istotną rolę odgrywa rejestr programowy (tj. definiowany przez asembler), zwany licznikiem lokacji;

licznik lokacji określa lokację w pamięci operacyjnej, do której zostanie przesłany aktual­nie tłumaczony rozkaz lub dana; po załadowaniu rozkazu lub danej, licznik lokacji zostaje zwiększony o ilość bajtów zajmowanych przez ten rozkaz lub daną;

w asemblerach dla procesorów Pentium licznik lokacji nie wskazuje adresu fizycznego ładowanej loka­cji, lecz jedynie jej położenie względem początku segmentu; w trakcie tłumaczenia pierwszego wiersza segmentu licznik lokacji zawiera 0; w wyrażeniach adresowych licznik lokacji reprezentowany jest przez symbol $;

jeśli asembler napotka wiersz zawierający definicję symbolu, to rejestruje go w słow­niku symboli, jednocześnie przypisując temu symbolowi wartość równą aktualnej zawartoś­ci licznika lokacji; ponadto zapisywane są także atrybuty symbolu, jak np. far, byte, itp.

Organizacja stosu

w praktyce programowania występują wielokrotnie sytuacje, w których konieczne jest tymczasowe przechowanie zawartości rejestru — zazwyczaj rejestrów ogólnego przeznaczenia jest zbyt mało by przechowywać w nich wszystkie wyniki pośrednie występujące w trakcie obliczeń;

w takich sytuacjach, za pomocą instrukcji MOV, można zapisać zawartości rejestrów we wskazanych lokacjach pamięci operacyjnej; jednak jest zwykle dość niepraktyczne, ponieważ przechowywanie potrzebne jest tylko przez krótki odcinek czasu, natomiast lokacja pamięci musi być rezerwowana na cały czas wykonywania programu;

istnieją jeszcze inne powody, dla których w procesorach wprowadzono mechanizmy organizujące przechowywanie danych w postaci stosu;

stos stanowi pewien obszar pamięci operacyjnej, w którym możliwy jest zapis danych podobny do układania książek na stosie — każdy kolejny zapis "przykrywa" poprzedni;

stos stanowi liniową strukturę danych, dostępną do zapisywania i odczytywania tylko z jednego końca; stos klasyfikowany jest jako struktura danych typu LIFO (ang. Last In, First Out);

ze względu na przechowywanie innych typów danych stos zazwyczaj rośnie w kierunku malejących adresów ("wbrew prawom grawitacji");

w procesorach Pentium początek obszaru pamięci zajmowanego przez stos wskazuje rejestr segmentowy SS; wskaźnik stosu ESP (lub SP w trybie 16-bitowym) określa położenie wierzchołka stosu, tj. położenie ostatnio zapisanej danej;

zdefiniowano dwie podstawowe operacje stosu:

zapis wykonywany za pomocą instrukcji PUSH, np.

push bx

push edi

odczyt wykonywany za pomocą instrukcji POP, np.

pop edi

pop bx

rejestr ESP (SP) wskazuje wierzchołek stosu, czyli obszar 4- lub 2-bajtowy, w którym przechowywana jest ostatnio zapisana dana; na stosie mogą być zapisywane wyłącznie wartości 16- i 32-bitowe; zatem instrukcja push  dh jest błędna!

instrukcja PUSH powoduje zmniejszenie zawartości rejestru ESP o 4 (lub w trybie 16-bitowym SP o 2), a następnie zapisanie wskazanego rejestru do lokacji pamięci wskazanej przez rejestr ESP (SP) licząc od początku segmentu stosu; wartość zapisywana na stosie może znajdować się w rejestrze, a także we wskazanej lokacji pamięci — w tym przypadku instrukcja PUSH wykonuje przesłanie w obrębie pamięci operacyjnej; argumentem instrukcji PUSH może być takze wartość liczbowa;

instrukcja POP powoduje odczytanie zawartości lokacji pamięci wskazanej przez rejestr ESP (SP) licząc od początku segmentu stosu i przesłanie tej zawartości do wskazanego rejestru (lub do lokacji pamieci); nastepnie zawartość rejestru ESP zostaje zwiększona o 4 (lub w trybie 16-bitowym SP o 2);

dodatkową zaletą przechowywania danych na stosie jest krótszy kod instrukcji — zapisanie we wskazanej lokacji pamięci za pomocą instrukcji MOV wymaga podania adresu lokacji pamięci i symbolu (kodu) zapisywanego rejestru; zapisywanie na stosie wymaga podania tylko symbolu zapisywanego rejestru (położenie zapisywanej informacji określa rejestr ESP);

0x01 graphic

Instrukcje sterujące (skoki) bezwarunkowe

instrukcje sterujące bezwarunkowe zmieniają porządek wykonywania instrukcji, przyjmując że testowany warunek jest zawsze spełniony;

(E)IP (E)IP + <liczba bajtów aktualnie wykonywanej instrukcji>

+ <zawartość pola adresowego instrukcji>

w procesorze Pentium używany jest mnemonik JMP dla różnych typów skoku bezwarunkowego; skoki typu dalekiego (FAR) są obecnie rzadko używane; przykład:

jmp dalej

stosowane są skoki bezpośrednie, jeśli wartość wpisywana (albo dodawana) do rejestru (E)IP podana jest w polu adresowym instrukcji, a także skoki pośrednie, jeśli wartość podana w polu adresowym wskazuje rejestr lub lokację pamięci, w której znajduje się nowa zawartość (E)IP i ewentualnie CS;

przykłady:

jmp bx

jmp dword PTR ds:wybor

Wywołanie i powrót z podprogramu

wywołanie podprogramu realizuje się za pomocą rozszerzonej instrukcji skoku — konieczne jest bowiem zapamiętanie adresu powrotu, zwanego śladem, tj. miejsca, do którego ma powrócić sterowanie po zakończeniu wykonywania podprogramu; w procesorze Pentium adres powrotu zapisuje się na stosie; spotyka się inne typy procesorów, w których ślad zapisywany jest w rejestrach;

0x01 graphic

w procesorze Pentium ww. czynności wykonuje instrukcja CALL — występuje ona również w wersji bezpośredniej i pośredniej, a także near i far;

przykładowo, jeśli przyjmiemy, że instrukcja CALL zajmuje trzy bajty począwszy od adresu (offsetu) 5A34H, to kolejna instrukcja po CALL znajduje się w pamięci począwszy od offsetu 5A37H — zatem ostatnia instrukcja podprogramu powinna wpisać do rejestru (E)IP liczbę 5A37H, która powinna znajdować się na wierzchołku stosu; tak więc instrukcja CALL powinna zapisać na stosie liczbę

5A37H = położenie instrukcji CALL + liczba bajtów instrukcji CALL

0x01 graphic

ślad zapisany na stosie wskazuje miejsce w programie, dokąd należy przekazać sterowanie po wykonaniu podprogramu; innymi słowy: w chwili zakończenia wykonywania podprogramu zawartość wierzchołka stosu powinna zostać przepisana do rejestru (E)IP i ewentualnie także do CS — czynności te realizuje instrukcja RET;

w asemblerze podprogram rozpoczyna dyrektywa PROC a kończy dyrektywa ENDP, np.

skasuj PROC

— — — — — —

— — — — — —

skasuj ENDP

instrukcja CALL typu pośredniego używana jest m.in. przez kompilatory jezyka C do implementacji wywoływania funkcji przez wskaźnik, np.

int (wsk) (int p, int q);

if (a < b) wsk = fun1; else wsk = fun2;

c = (wsk) (a, b);

Usługi systemu operacyjnego

w początkowym okresie rozwoju informatyki każdy program był całkowicie samodzielny i w trakcie wykonywania nie korzystał z pomocy innych programów; tego rozdzaju rozwiązania spotyka się współcześnie w mikrokomputerach (mikrokontrolerach) obsługujących nieskomplikowane urządzenia;

zauważono wówczas, że wiele programów ma takie same lub bardzo podobne fragmenty, związane głównie z obsługą urządzeń wejścia/wyjścia (wówczas dalekopisu) — te fragmenty kodu zostały wyodrębnione i została im nadana postać podprogramów; podprogramy te zostały udostępnione innym programom, które mogły je wywoływać w trakcie wykonywania;

w ten sposób zaczęły powstawać systemy operacyjne; później obudowano te podprogramy dodatkowymi modułami, których zadaniem było zarządzanie systemem komputerowym.

0x01 graphic

System BIOS

sprzęt komputerowy powinien posiadać zdolność wprowadzania i uruchamiania krótkich programów bezpośrednio po włączeniu (lub zresetowaniu) komputera; takie krótkie programy ładujące, zwane bootstrap'ami zawierają kod, który wprowadza do pamięci i uruchamia właściwy system operacyjny; we współczesnych komputerach programy ładujące przechowywane są w pamięci ROM (ang. read-only memory — pamięć tylko do odczytu);

w pamięci ROM obok ww. programów ładujących umieszcza się także programy inicjalizujące, testujące i konfigurujące zainstalowany sprzęt; wstępne testowanie sprzętu przed uruchomieniem systemu operacyjnego zapobiega przed dalszymi powikłaniami, które mogą doprowadzić do uszkodzenia oprogramowania i danych;

w pamięci ROM komputerów osobistych przechowywany jest także pakiet podprogramów wykonujących podstawowe czynności sterowania i obsługi urządzeń (zwykle na poziomie portów);

można więc powiedzieć, że w pamięci ROM znajduje się mini-system operacyjny — w komputerach rodziny IBM PC system ten nosi nazwę BIOS — Basic Input Output System;

m.in. system BIOS zawiera zestaw programów testujących POST (ang. Power On Self Test), uruchamianych po włączeniu komputera;

funkcje usługowe udostępniane przez BIOS dla innych programów tworzą BIOS API (ang. application program interface — interfejs programowania aplikacji); BIOS API oferuje użytkownikowi elementarny zestaw operacji, niezależny od konstrukcji konkretnego komputera (zatem oprogramowanie BIOSu może "wyrównywać" różnice w konstrukcji sprzętu);

aktualnie używane wersje BIOSu posiadają także mechanizmy pozwalające na dynamiczną konfigurację systemu, w zależności od wyposażenia komputera;

system BIOS został tak skonstruowany, że niektóre jego fragmenty mogą być umieszczone są na kartach rozszerzeniowych komputera (np. na karcie sterownika graficznego);

BIOS zapisany jest w pamięci ROM (obecnie zwykle w EEPROM) i zajmuje obszar pamięci począwszy od adresu F000H:0000H; obszar zmiennych systemowych BIOSu umieszczony jest w pamięci RAM począwszy od adresu 0040H : 0000H;

do niedawna zmiana zawartości pamięci ROM wymagała profesjonalnych działań — obecnie stosowane są pamięci EEPROM, które mogą być aktualizowane przez użytkownika komputera;

współczesne systemy operacyjne w komputerach osobistych dość niechętnie korzystają z usług oferowanych przez BIOS API — BIOS pracuje w trybie 16-bitowym i nie jest przystosowany do zaawansowanych metod obsługi (np. wirtualizacji urządzeń); BIOS API był natomiast szeroko wykorzystywany przez system DOS.

Wywoływanie podprogramów systemowych

podprogramy systemowe realizują wiele różnych operacji, w tym podstawowe operacje związane z obsługą urządzeń wejścia/wyjścia w komputerze; w wielu systemach istnieje obowiązkowe pośrednictwo podprogramów systemowych w zakresie obsługi urządzeń;

bezpośrednie wywoływanie podprogramów systemowych wymaga znajomości adresów poszczególnych podprogramów, a te mogą się zmieniać w kolejnych wersjach systemu lub też mogą zależeć od konfiguracji systemu;

w wielu systemach zastosowano wywoływanie podprogramów systemowych w sposób pośredni, za pośrednictwem tablicy adresowej, zawierającej adresy podprogramów systemowych; w takim przypadku programista nie podaje adresu podprogramu, ale indeks w tablicy adresowej;

w kolejnych wersjach systemu adresy zawarte w tablicy ulegają zmianom, ale odwołania do konkretnych podprogramów realizowane są poprzez te same indeksy w tablicy adresowej;

w procesorach Pentium koncepcja ta została zrealizowana za pomocą tablicy adresowej zwanej tablicą wektorów przerwań (w trybie rzeczywistym) lub tablicy deskryptorów przerwań (w trybie chronionym);

struktura tablicy w trybie chronionym jest bardziej złożona; dalsze rozważania dotyczą trybu rzeczywistego, w którym omawiana tablica nosi nazwę tablicy wektorów przerwań;

ze względu na brak mechanizmów ochrony w trybie rzeczywistym tablica wektorów pozwala na wykonywanie interesujących eksperymentów w zakresie sprzętu i oprogramowania; w trybie chronionym podobne eksperymenty są trudne lub niemożliwe do realizacji;

tablica wektorów przerwań zawiera 256 adresów 4-bajtowych i umieszczona jest w pamięci operacyjnej począwszy od adresu fizycznego 0; adresy podprogramów w tej tablicy zapisywane są w układzie segment:offset; pole offset zajmuje dwa bajty o niższych adresach;

0x01 graphic

wywoływanie podprogramów za pośrednictwem tablicy wektorów przerwań można by wykonywać za pomocą instrukcji CALL typu pośredniego; jednak dla wygody programowania zdefiniowano instrukcję INT specjalnie dla tego typu wywołań;

w trybie rzeczywistym instrukcja INT może być zastąpiona przez instrukcję CALL typu pośredniego i instrukcję PUSHF; jednak w trybie chronionym podprogramy systemowe muszą być wykonywane na wyższym poziomie uprzywilejowania i instrukcja INT nie może być zastąpiona przez inne instrukcje;

instrukcję INT należy klasyfikować jako instrukcję skoku do podprogramu typu pośredniego, podobną do instrukcji CALL; jednocześnie instrukcja INT wykazuje zewnętrzne podobieństwo do przerwań sprzętowych i tego powodu została nazwana INTerrupt, czyli przerwanie; podobieństwo to może utrudniać zrozumienie istoty mechanizmu przerwań (omawianego w dalszej części wykładu);

instrukcja INT pozostawia ślad na stosie zawierający rejestry (E)IP, CS i rejestr znaczników (E)FLAGS;

ze względu na inną strukturę śladu, zdefiniowano oddzielną instrukcję powrotu z podprogramu IRET — instrukcja ta jest używana w przypadku gdy podprogram został wywołany za pomocą instrukcji INT albo wskutek wystąpienia przerwania sprzętowego lub wyjątku.

Wywoływanie usług (podprogramów) systemowych DOSu i BIOSu

liczba różnych podprogramów systemowych jest zwykle większa od 256, wobec czego zachodzi konieczność tworzenia zestawów podprogramów, nazywanych pakietami; adresu pakietu podany jest w tablicy wektorów przerwań, zaś poszczególne podprogramy identyfikowane są przez ustalone numery; wybór podprogramu następuje poprzez wpisanie odpowiedniego numeru do rejestru, zwykle do AH lub AX;

adres pakietu podprogramów wchodzących w skład systemu DOS/Windows umieszczony jest w wektorze 33 (21H); numer podprogramu podawany jest w rejestrze AH, stąd typowa sekwencja wywołania podprogramu systemowego ma postać, np.

— — — — — —

mov ah, 2

int 21H

znaczna część podprogramów wymaga podania parametrów wejściowych, które najczęściej umieszcza się w ustalonych rejestrach; również wyniki przekazywane są przez rejestry; wykaz wszystkich podprogramów sytemowych wraz z wymaganymi parametrami zawiera dokumentacja systemu operacyjnego; informacje te można znaleźć na znacznej liczbie stron internetowych, a także w wielu książkach — w języku polskim najbardziej obszerny opis zawiera książka Bułhaka i Goczyńskiego, DOS od środka (wraz z suplementem);

funkcje usługowe systemu DOS/Windows, do których parametry przekazywane są przez rejestry robocze, pozostawiają te rejestry niezmienione, chyba że służą one do wyprowadzania wyników;

0x01 graphic

inne wektory przerwań zawierają adresy rozmaitych pakietów podprogramów, m.in. wektor 33H zawiera adres pakietu podprogramów obsługi myszki; kilkanaście wektorów zawiera adresy różnych podprogramów udostępnianych przez system BIOS;

0x01 graphic

korzystając z usług systemu operacyjnego nie zawsze zdajemy sobie sprawę czy wywołujemy instrukcję procesora czy podprogram systemu operacyjnego; usługi systemu operacyjnego wraz z instrukcjami procesora tworzą pewne środowisko, które częściowo jest realizowane sprzętowo, a częściowo przez usługi systemu operacyjnego — powstaje w ten sposób pewna maszyna wirtualna, w której działa programista.

Przekazywanie parametrów do podprogramów

stosowane są dwa podstawowe sposoby przekazywania parametrów:

przekazywanie przez wartość (ang. call by value) - do podprogramu przekazywana jest bezpośrednio wartość parametru, którym może być liczba, tablica liczb, łańcuch znaków, itp;

przekazywanie przez adres (ang. call by location) - do podprogramu przekazywany jest adres lokacji pamięci, w której znajduje się przekazywana wartość;

przekazywanie adresu zmiennej umożliwia bezpośredni dostęp do niej wewnątrz podprogramu, m.in. pozwala to na wpisanie wyniku uzyskanego w trakcie wykonywania podprogramu bezpośrednio do tej zmiennej; taki sposób pozwala także na dostęp do tablicy bez konieczności przekazywania wartości wszystkich jej elementów;

drogi przekazywania parametrów:

przez rejestry,

przez stos (typowa, powszechnie stosowana metoda),

przez ślad (tj. przez obszary pamięci znajdujące się bezpośrednio za instrukcją CALL lub INT),

przez bufory (tj. przez zarezerwowane obszary pamięci o ustalonych adresach);

przekazywanie parametrów przez rejestry jest charakterystyczne dla funkcji systemowych BIOSu i DOSu.

Technika przekazywania parametrów przez stos

Przykład: podany niżej podprogram oblicza wartość wyrażenia

(długość + 5) szerokość + wysokość

przyjmujemy następujące założenia:

podprorgram obliczający wartość wyrażenia będzie wykonywany w trybie 32-bitowym; w tym trybie cały kod programu umieszczony jest zazwyczaj w tym samym segmencie, co oznacza, że do jego wywoływania powinien zostać użyty rozkaz CALL typu NEAR;

podane parametry, w postaci liczb 32-bitowych bez znaku, wpisywane są na stos przed wywołaniem procedury;

obliczona wartość wyrażenia (zakładamy, że również 32-bitowa) powinna zostać załadowana do rejestru EAX;

obowiązek zdjęcia parametrów ze stosu po wykonaniu obliczeń należy do podprogramu.

jeśli przykładowe wartości parametrów będą wynosiły:

długość = 17

szerokość = 5

wysokość = 7

to wywołanie podprogramu oblicz może mieć postać:

push 17 ; ładowanie parametru 'długość'

push 5 ; ładowanie parametru 'szerokość'

push 7 ; ładowanie parametru 'wysokość'

call oblicz

w wyniku wykonania podanych instrukcji zawartość stosu będzie następująca:

większe

adresy

długość

szerokość

wysokość

mniejsze

EIP

adresy

należy pamiętać, że stos "rośnie" w kierunku malejących adresów — zatem parametr długość znajdować się będzie w lokacji o adresie wyższym niż parametr wysokość.

Odczytywanie parametrów przez instrukcje wewnątrz procedury

odczytywanie parametrów za pomocą instrukcji POP byłoby kłopotliwe: wymagałoby uprzedniego odczytania wartości EIP, a a po wykonaniu obliczeń należało by ponownie załadować tę wartość na stos; odczytane parametry można by umieścić w rejestrach ogólnego przeznaczenia — rejestry te jednak używane są do wykonywania obliczeń i przechowywania wyników pośrednich, wskutek czego nadają się do przechowywania jedynie kilku parametrów;

w celu zorganizowania wygodnego dostępu do parametrów umieszczonych na stosie przyjęto, że obszar zajmowany przez parametry będzie traktowany jako zwykły obszar danych; w istocie stos jest bowiem umieszczony w pamięci RAM i nic nie stoi na przeszkodzie, by w pewnych sytuacjach traktować jego zawartość jako zwykły segment danych;

dostęp do danych zapisanych w segmencie danych wymaga znajomości ich adresów, to samo dotyczy stosu; jednak położenie danych zapisanych na stosie ustalane jest dopiero w trakcie wykonywania programu, i może się zmieniać przy kolejnych wywołaniach podprogramu;

konieczne jest więc opracowanie schematu wyznaczania adresów parametrów umieszczonych na stosie, dostosowanego do aktualnej sytuacji na stosie;

na podanym wcześniej rysunku widać, że parametry podprogramu odległe są o ustaloną liczbę bajtów od wierzchołka stosu (tu: o 4 bajty, na których zapisany jest ślad); ponieważ położenie wierzchołka stosu wskazuje rejestr ESP, więc na podstawie zawartości rejestru ESP można wyznaczyć adresy parametrów;

ponieważ jednak zawartość rejestru ESP może się zmieniać w trakcie wykonywania podprogramu, konieczne jest użycie innego rejestru, którego zawartość, ustalona przez cały czas wykonywania podprogramu, będzie wskazywała obszar parametrów na stosie — rolę tę pełni, specjalnie do tego celu zaprojektowany rejestr EBP;

modyfikacja adresowa wykonywana za pomocą rejestru EBP implikuje użycie rejestru SS jako domyślnego rejestru segmentowego — zatem odczyt pewnej lokacji pamięci, której adres podany jest w rejestrze EBP oznacza w istocie odczyt lokacji pamięci zapisanej na stosie;

jeśli zawartość rejestru EBP jest równa zawartości ESP, to adres określony przez EBP wskazuje wierzchołek stosu, a EBP zwiększony o 4, 8, 12,... wskazuje kolejne, uprzednio zapisane, podwójne słowa na stosie (należy pamiętać, że stos "rośnie" w kierunku malejących adresów, zatem lokacje bardziej odległe od wierzchołka stosu będą miały wyższe adresy).

oblicz PROC

push ebp ; przechowanie EBP na stosie

mov ebp,esp ; po wykonaniu tego rozkazu EBP

; będzie wskazywał wierzchoł. stosu

mov eax, [ebp] + 16 ; ładowanie parametru 'długość'

add eax,5 ; EAX EAX + 5

mul dword PTR [ebp] + 12 ; mnożenie przez 'szerokość'

add eax,[ebp] + 8 ; dodanie parametru 'wysokość'

; wynik obliczeń znajduje się w rejestrze EAX

pop ebp ; odtworzenie EBP

ret 12 ; powrót do programu wywołującego

; i usunięcie parametrów ze stosu

oblicz ENDP

sytuacja na stosie po wykonaniu instrukcji push ebp

[ebp] + 16

długość

[ebp] + 12

szerokość

[ebp] + 8

wysokość

[ebp] + 4

EIP

[ebp] + 0

EBP

Uwagi:

użycie operatora PTR w jednoargumentowej instrukcji

mul dword PTR [ebp]+12

jest niezbędne, ponieważ wyrażenie [ebp]+12 wskazuje położenie pewnej lokacji nie precyzując jednak czy chodzi to o bajt czy o słowo;

32-bitowy mnożnik implikuje 64-bitowy wynik mnożenia, umieszczany w rejestrach EDX:EAX; ponieważ przyjęliśmy, że wynik obliczeń będzie liczbą 32-bitową, więc starszą część wyniku mnożenia (w rejestrze EDX) można pominąć.

Prolog i epilog podprogramu

w tak skonstruowanym podprogramie początkowe i końcowe instrukcje mają jednakową postać, niezależnie od czynności wykonywanych przez podprogram — stanowią one prolog i epilog podprogram; zazwyczaj mają one postać:

Prolog Epilog

push ebp mov esp, ebp

mov ebp, esp pop ebp

sub esp, rozmiar ret liczba_bajtów

Instrukcje sub esp, rozmiar i mov esp, ebp potrzebne są tylko wówczas, gdy używane są zmienne dynamiczne (co będzie omówione dalej).

Łączenie języków programowania (programowanie mieszane)

stosowana obecnie technika kompilacji i linkowania pozwala na tworzenie programów kodowanych w kilku językach programowania; najczęściej jednak mamy do czynienia z łączeniem kodu napisanego w języku wysokiego poziomu z kodem w asemblerze;

w praktyce programowania zazwyczaj dostępne są dwie metody dołączania kodu asemblerowego:

1. w formie wstawek asemblerowych umieszczonych bezpośrednio wewnątrz kodu w języku wysokiego poziomu;

2. w formie odrębnych modułów asemblerowych, które są odrębnie kompilowane (asemblowane) i łączone z pozostałym kodem w trakcie linkowania;

poniżej podano przykładowy fragment programu w języku C, w którym występują wstawki asemblerowe

int liczba, wynik;

— — — — — — — — —

liczba = 49 ;

asm {

mov ax, liczba

mov bx, 0

mov dx, 1

}

ptl:

asm {

sub ax, dx

jb zak

inc bx

add dx, 2

jmp ptl

}

zak:

asm mov wynik, bx

wynik += 7;

w instrukcjach asemblerowych można odwoływać się zarówno do zmiennych lokalnych jak i globalnych (zewnętrznych);

stosowanie wstawek asemblerowych nie wymaga oddzielnej kompilacji (asemblacji) — używany jest asembler wbudowany w kompilator języka wysokiego poziomu;

jednak stosowanie wstawek asemblerowych w bardziej skomplikowanych przypadkach nie pozwala na pełne wykorzystanie możliwości właściwych dla kodowania w asemblerze;

stosowanie oddzielnych modułów asemblerowych umożliwia wyraźne wyodrębnienie części asemblerowej, która może być tworzona, np. przez osobę znającą doskonale zagadnienia sprzętowe; metoda ta pozwala na pełne wykorzystanie możliwości kodowania w asemblerze (np. włączenie mechanizmów makroprzetwarzania) i ułatwia diagnostykę programu; w dalszej cześci wykładu zajmować się będziemy wyłącznie tą metodą;

podprogram w asemblerze, przystosowany do wywoływania z poziomu języka wysokiego poziomu, musi być skonstruowany wg tych samych zasad co funkcje (procedury) tego języka; w szczególności należy stosować tę samą technikę przekazywania parametrów jak też odpowiednie nazwy segmentów (i ich atrybuty);

ustalony sposób przekazywania parametrów do/z funkcji (procedury) w danym języku programowania nosi nazwę standardu wywoływania (ang. calling convention); m.in. standard ten określa czy parametry przekazywane są przez rejestry czy przez stos, a jeśli przez stos to w jakiej kolejności są ładowane, czy dopuszcza się zmienną liczbę argumentów, itd.

Typowe standardy wywoływania funkcji

kompilatory języków programowania stosują kilka typowych sposobów przekazywania parametrów do funkcji, znanych jako:

standard Pascal,

standard C

standard StdCall;

czasami stosuje się też standard FastCall, w którym parametry przekazywane są przez rejestry procesora;

główne różnice między standardami dotyczą kolejności ładowania parametrów na stos i obowiązku zdejmowania parametrów, który należy najczęściej do wywołanego podprogramu (funkcji), jedynie w standardzie C zajmuje się tym program wywołujący; w standardzie Pascal parametry wywoływanej funkcji zapisywane są na stos kolejności od lewej do prawej, natomiast w standardzie C i StdCall od prawej do lewej; istnieją też inne różnice, które opisane są dalej;

Standard

Kolejność ładowania parametrów na stos

Obowiązek zdjęcia parametrów ze stosu

Pascal

od lewej do prawej

wywołany podprogram

C

od prawej do lewej

program wywołujący

StdCall

od prawej do lewej

wywołany podprogram

standard wywoływania funkcji i procedur typu Pascal jest szeroko stosowany w różnych systemach programowania (m.in. w systemie Windows); standard StdCall, stanowiący połączenie standardów C i Pascal, stosowany jest w 32-bitowych wersjach Windows do wywoływania funkcji wchodzących w skład interfejsu Win32 API;

warto zwrócić uwagę, że w wielu przypadkach parametry wywołania funkcji mają postać wyrażeń (tak jak w podanym wyżej przykładzie) — w takim przypadku najpierw obliczana jest wartość parametru, która następnie zapisywana jest na stosie;

podane standardy stosowane są w różnych środowiskach programistycznych, m.in. w systemach Borland C/C++ i Microsoft C++ można deklarować funkcje, w których stosowane są ww. standardy;

zasadniczo kompilatory języka C stosują standard C, jednakże umieszczenie w deklaracji funkcji słowa pascal powoduje, że w odniesieniu do tej funkcji stosowana będzie konwencja pascalowa; tak samo w programach kodowanych w Pascalu umieszczenie słowa cdecl w deklaracji powoduje, że do funkcji lub procedury stosowana będzie konwencja języka C; analogicznie słowo register powoduje, że parametry przekazywane będą przez rejestry; szczegółowe zalecenia podane są w dokumentacji kompilatora;

używane są też słowa _stdcall (albo stdcall) i cdecl wprowadzające dla deklarowanych funkcji, odpowiednio, standard StdCall i C;

standard register stanowi najbardziej efektywny sposób przekazywania parametrów, ale jego zastosowanie jego ograniczone ze względu na niewielką liczbę rejestrów procesora (używa się maksymalnie 3 rejestry); w niektórych kompilatorach przyjęto standard register jako domyślny;

Uwagi szczegółowe dotyczące sposobu wywoływania funkcji stosowanego przez kompilatory języka C

kompilatory języka C tworzą kod programu przy zastosowaniu techniki przekazywania parametrów "standard C" — parametry przekazywane są przez stos w kolejności odwrotnej, w stosunku do tej, w jakiej zostały podane w kodzie źródłowym;

jeśli używany jest stos 32-bitowy, to parametry krótsze niż 32 bitowe rozszerza się do 32 bitów; analogicznie rozszerza się parametry, gdy używany jest stos 16-bitowy (np. parametry bajtowe ładowane są w postaci słowa 16-bitowego);

po wykonaniu funkcji, obowiązek zdjęcia parametrów ze stosu należy do programu wywołującego;

parametry przekazywane są przez wartość, jedynie tablice przekazywane są przez adres;

wyniki funkcji przekazywane są przez rejestry:

wynik 1-bajtowy przez AL

wynik 2-bajtowy przez AX

wynik 4-bajtowy przez DX:AX (w trybie 16-bitowym) albo przez EAX (w trybie 32-bitowym);

w ten sposób przekazywane są również adresy 16- i 32-bitowe;

jeśli wynikiem funkcji jest liczba zmiennoprzecinkowa typu float lub double, to wynik ten dostępny jest na stosie rejestrów koprocesora (lub programu emulującego koprocesor);

działania wewnątrz funkcji (podprogramu) kodowanej w asemblerze mogą być wykonywane na różnych rejestrach, jednak bezpośrednio przed zakończeniem wykonywania funkcji zawartości niektórych rejestrów muszą być odtworzone wg pierwotnej zawartości; dotyczy to rejestrów ogólnego przeznaczenia: ESI (SI), EDI (DI), EBP (BP), EBX (w trybie 16-bitowym rejestr BX może zostać zmieniony); w przypadku podprogramu wykonywanego w trybie V86 mogą być również zmieniane rejestry segmentowe, ale przed zakończeniem jego wykonywania muszą być odtworzone wartości początkowe;

kompilatory języka C poprzedzają nazwy o zasięgu globalnym znakiem podkreślenia _

Program przykładowy — łączenie programu w języku C z procedurą asemblerową z zastosowaniem konwencji wywoływania funkcji wg standardu C (tryb 32-bitowy).

program wyznacza największy wspólny podzielnik (NWP) dla dwóch liczb naturalnych x i y, stosując poniższy algorytm:

while y <> 0 do

begin

x := x mod y (mod oznacza resztę z dzielenia x/y)

x y (zamiana x, y)

end

moduł w języku C:

#include <stdio.h>

extern int OBL_NWP (unsigned int x, unsigned int y, unsigned int * wyn);

int main (void)

{

unsigned int a, b, w;

int diagn;

printf ("\nProszę podać dwie liczby naturalne: ");

scanf("%u %u", &a, &b);

diagn = OBL_NWP (a, b, &w);

if (!diagn) printf ("\nNWP(%u,%u) = %u\n", a, b, w);

else

{

printf ("\nNiewłaściwe parametry");

return 1;

}

return 0;

}

moduł w asemblerze

.386

public _OBL_NWP

; prototyp funkcji na poziomie języka C

; int OBL_NWP ( unsigned int x,

unsigned int y,

unsigned int * wyn);

zm1 EQU [ebp]-4

_TEXT SEGMENT dword public 'CODE' use32

assume cs:_TEXT

_OBL_NWP PROC near

push ebp

mov ebp,esp

; rezerwacja 4 bajtów na zmienną 'zm1'

sub esp, 4

push ebx

; sprawdzanie czy x > 0 i y > 0

cmp dword PTR [ebp]+8, 0

je blad

cmp dword PTR [ebp]+12, 0

je blad

; petla while

p1: cmp dword PTR [ebp]+12, 0

je zak ;skok, gdy koniec 'while'

mov eax, [ebp]+8

xor edx, edx

div dword PTR [ebp]+12

; iloraz w EAX, reszta w EDX

mov [ebp]+8, edx

; odeslanie reszty do x

; zamiana liczb x, y poprzez zmienna 'zm1'

mov eax, [ebp]+8 ; zm1 <-- x

mov zm1, eax

mov eax, [ebp]+12 ; x <-- y

mov [ebp]+8, eax

mov eax, zm1 ; y <-- zm1

mov [ebp]+12, eax

jmp p1

; odesłanie wyniku obliczenia

; do lokacji wskazanej przez podany adres

zak: mov ebx, [ebp]+16

mov eax, [ebp]+8

mov [ebx], eax

; AX = 0 oznacza poprawne wykonanie funkcji

mov eax, 0

zak1:

pop ebx

mov esp, ebp

pop ebp

ret

blad: mov eax, -1

jmp zak1

_OBL_NWP ENDP

_TEXT ENDS

END


Zmienne statyczne i dynamiczne

w typowym programie w języku wysokiego poziomu (np. w Pascalu) potrzebne są m.in. zmienne, które muszą być dostępne dla różnych procedur i funkcji przez cały czas wykonywania programu - takie zmienne, zwane statycznymi, definiowane są w segmencie danych programu; na poziomie kodu rozkazowego omawiane procedury i funkcje mają postać podprogramów wywoływanych za pomocą instrukcji CALL;

procedury i funkcje, kodowane w językach wysokiego poziomu, wykonują także działania na zmiennych lokalnych, które są potrzebne tylko w trakcie wykonywania procedury czy funkcji (podprogramu); często zmienne takie nie muszą być inicjalizowane;

zmienne lokalne powinny zajmować obszar pamięci przydzielany tylko na czas wykonywania podprogramu — wygodnie jest umieścić ten obszar na stosie — w tym celu wystarczy tylko odpowiednio zmniejszyć wskaźnik stosu ESP, np.:

push ebp ; przechowanie EBP

mov ebp, esp

sub esp, 8 ; rezerwacja 8 bajtów

zmniejszenie rejestru ESP o 8 można interpretować jako zapisanie na stos 8 bajtów o nieokreślonej wartości początkowej — powstaje w ten sposób zarezerwowany obszar 8-bajtowy, w którym będą zapisywane wartości zmiennych lokalnych;

przydzielony obszar pamięci zwalnia się w końcowej części procedury poprzez odpowiednie zwiększenie wskaźnika stosu ESP lub przepisanie ESP  EBP;

zmienne lokalne lokowane na stosie nazywane są zmiennymi dynamicznymi; w języku C zmienne takie nazywane są zmiennymi automatycznymi.

0x01 graphic

Technika dostępu do zmiennych dynamicznych

sytuacja na stosie po rezerwacji obszaru danych lokalnych

tryb zwyky

większe

adresy

parametry przekazywane do procedury

ślad instrukcji CALL

EBP

mniejsze

adresy

obszar danych lokalnych

parametry przekazywane do podprogramu, ślad, rejestr EBP oraz obszar danych lokalnych tworzą pewien blok danych, z którego korzysta podprogram; położenie tego bloku na stosie, w zależności od jego zajętości, może się zmieniać przy kolejnych wywołaniach podprogramu; jak pokazano wcześniej adres omawianego bloku wskazuje rejestr ESP, którego zawartość kopiowana jest do rejestru EBP;

zauważmy dalej, że wprowadzenie obszaru danych lokalnych w niczym nie zmieniło sposobu dostępu do parametrów za pomocą rejestru EBP; pojawia się jednak pytanie czy rejestr EBP może wskazywać także zmienne lokalne, których adresy są mniejsze niż zawartość EBP?

adres efektywny, będący sumą zawartości pola adresowego instrukcji i rejestru modyfikacji adresowej, w trybie 32-bitowym obliczany jest modulo 232 — przy takiej regule odpowiednio duże liczby umieszczone w polu adresowym działają tak liczby ujemne (zob. opis modyfikacji adresowych);

zatem ten sam rejestr EBP może służyć zarówno do pobierania parametrów przekazywanych do podprogramu, jak też pobierania i zapisywania zmiennych lokalnych znajdujących się na stosie, np. podane instrukcje wpisują liczby do zmiennych lokalnych na stosie:

; wpisanie liczby 25 do zmiennej 32-bitowej

mov dword PTR [ebp] 4, 25

; wpisanie liczby 31 do zmiennej 64-bitowej

mov dword PTR [ebp] 8, 0

mov dword PTR [ebp] 12, 31

Zastosowanie instrukcji ENTER i LEAVE

lokowanie zmiennych dynamicznych i przekazywanie parametrów za pomocą stosu jest standardową metodą używaną przez kompilatory różnych języków programowania; występujące tu operacje można zakodować w nieco krótszy sposób za pomocą instrukcji ENTER i LEAVE; szczególności ciągi instrukcji podane w obu kolumnach są równoważne:

push ebp

mov ebp, esp enter k, 0

sub esp, k

gdzie k oznacza liczbę bajtów rezerwowanych na stosie jako pamięć dynamiczna procedury; w literaturze działania instrukcji ENTER  k, 0 określa się skrótowo jako "tworzenie na stosie obszaru przeznaczonego dla zmiennych lokalnych procedury" (ang. stack frame);

w omawianym przypadku drugi operand instrukcji ENTER ma wartość 0 (tzw. zerowy poziom zagłębienia); instrukcja ENTER z niezerowymi wartościami drugiego parametru jest przeznaczona do wywoływania procedur zagnieżdżonych (np. w Pascalu), i ze względu na niewielkie zastosowanie, i znaczny stopień skomplikowania, nie będzie tu omawiana;

równoważne są także ciągi instrukcji:

mov esp, ebp leave

pop ebp

80



Wyszukiwarka

Podobne podstrony:
Pytania przykl ASK2, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Opracowania
ask4, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Wyklad
ask1, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Wyklad
ask3, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Wyklad
ASK-koło pierwsze pytania z mojej grupy, Edukacja, studia, Semestr IV, Architektura Systemów Kompute
opracowane pytania na ASK@, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Oprac
Projekt 3, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Projekt, Projekt 3
Teoria 2003, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Opracowania pytań
assembler 1, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Projekt, Projekt 1
Pytania przykl ASK1, Edukacja, studia, Semestr IV, Architektura Systemów Komputerowych, Opracowania
TECHNIKA MIKROPROCESOROWA (1), Edukacja, studia, Semestr IV, Technika Mikroprocesorowa
liniowkaWKLEPANE PYTANIA, Edukacja, studia, Semestr IV, Układy Elektroniczne
pytania na smoki, Edukacja, studia, Semestr IV, Technika Mikroprocesorowa
arch02, UŁ Sieci komputerowe i przetwarzanie danych, Semestr II, Architektura systemów komputerowych
Układy Elektroniczne zagadnienia, Edukacja, studia, Semestr IV, Układy Elektroniczne
arch05, UŁ Sieci komputerowe i przetwarzanie danych, Semestr II, Architektura systemów komputerowych
arch07, UŁ Sieci komputerowe i przetwarzanie danych, Semestr II, Architektura systemów komputerowych
Optoelektronika kolo 1, Edukacja, studia, Semestr IV, Optoelektronika, Pytania na koła, zestaw 8
JavaScript- podstawy, Edukacja, studia, Semestr IV, Języki Programowania Wysokiego Poziomu, Java skr

więcej podobnych podstron