ďťż

Blog literacki, portal erotyczny - seks i humor nie z tej ziemi


LEKCJA 16 - ASEMBLER TASM i BASM.
________________________________________________________________
W trakcie tej lekcji:
* dowiesz się , jak łączyć C++ z assemblerem
* poznasz wewnętrzne formaty danych
________________________________________________________________

WEWNĘTRZNY FORMAT DANYCH I WSPÓŁPRACA Z ASSEMBLEREM.

W zależności od wybranej wersji kompilatora C++ zasady
współpracy z asemblerem mogą się trochę różnić. Generalnie,
kompilatory współpracują z tzw. asemblerami in-line (np. BASM),
lub asemblerami zewnętrznymi (stand alone assembler np. MASM,
TASM). Wstawki w programie napisane w assemblerze powinny zostać

poprzedzone słowem asm (BORLAND/Turbo C++), bądź _asm (Microsoft

C++). Przy kompilacji należy zatem stosownie do wybranego
kompilatora przestrzegać specyficznych zasad współpracy. Np. dla

BORLAND/Turbo C++ można stosować do kompilacji BCC.EXE/TCC.EXE
przy zachowaniu warunku, że TASM.EXE jest dostępny na dysku w
bieżącym katalogu.

Typowymi sposobami wykorzystania assemblera z poziomu C++ są:

* umieszczenie ciągu instrukcji assemblera bezpośrednio w
źródłowym tekście programu napisanym w języku C/C++,
* dołączeniu do programu zewnętrznych modułów (np. funkcji)
napisanych w assemblerze.

W C++ w tekście źródłowym programu blok napisany w asemblerze
powinien zostać poprzedzony słowem kluczowym asm (lub _asm):

# pragma inline

void main()
{
asm mov dl, 81
asm mov ah, 2
asm int 33
}

Program będzie drukował na ekranie literę "Q" (ASCII 81).

JAK POSŁUGIWAĆ SIĘ DANYMI W ASEMBLERZE.

Napiszemy w asemblerze program drukujący na ekranie napis "tekst

- test". Rozpczynamy od zadeklarowania łańcucha znaków:

void main()
{
char *NAPIS = "tekst - test$"; /* $ - ozn. koniec */

Umieściliśmy w pamięci łańcuch, będący w istocie tablicą
składającą się z elementów typu char. Wskaźnik do łańcucha może
zostać zastąpiony nazwą-identyfikatorem tablicy. Zwróć uwagę, że

po łańcuchu znakowym dodaliśmy znak '$'. Dzięki temu możemy
skorzystać z DOS'owskiej funkcji nr 9 (string-printing DOS
service 9). Możemy utworzyć kod w asemblerze:

asm mov dx, NAPIS
asm mov ah, 9
asm int 33

Cały program będzie wyglądał tak:

[P054.CPP]

# pragma inline
void main()
{
char *NAPIS = "\n tekst - test $";

asm {
MOV DX, NAPIS
MOV AH, 9
INT 33
}
}

Zmienna NAPIS jest pointerem i wskazuje adres w pamięci, od
którego rozpoczyna się łańcuch znaków. Możemy przesłać zmienną
NAPIS bezpośrednio do rejestru i przekazać wprost przerywaniu
Int 33. Program assemblerowski (tu: TASM) mógłby wyglądać np.
tak:

[P055.ASM]

.MODEL SMALL ;To zwylke robi TCC
.STACK 100H ;TCC dodaje standardowo 4K
.DATA
NAPIS DB 'tekst - test','$'
.CODE
START:
MOV AX, @DATA
MOV DS, AX ;Ustawienie segmentu danych
ASM:
MOV DX, OFFSET NAPIS
MOV AH, 9
INT 21H ;Drukowanie
KONIEC:
MOV AH, 4CH
INT 21H ;Zakończenie programu
END START

Inne typy danych możemy stosować podobnie. Wygodną taktyką jest
deklarowanie danych w tej części programu, która została
napisana w C++, aby inne fragmenty programu mogły się do tych
danych odwoływać. Możemy we wstawce asemblerowskiej odwoływać
się do tych danych w taki sposób, jakgdyby zostały zadeklarowane

przy użyciu dyrektyw DB, bądź DW.

WEWNĘTRZNE FORMATY DANYCH W C++.

LICZBY CAŁKOWITE typów char, short int i long int.

Liczba całkowita typu short int stanowi 16-bitowe słowo i może
zostać zastosowana np. w taki sposób:

[P056.CPP]

#pragma inline
void main()
{
char *napis = "\nRazem warzyw: $";
int marchewki = 2, pietruszki = 5;
asm {
MOV DX, napis
MOV AH, 9
INT 33
MOV DX, marchewki
ADD DX, pietruszki
ADD DX, '0'
MOV AH, 2
INT 33
}
}

Zdefiniowaliśmy dwie liczby całkowite i łańcuch znaków - napis.
Ponieważ obie zmienne (łańcuch znków jest stałą) mają długość
jednego słowa maszynowego, to efekt jest taki sam, jakgdyby
zmienne zostały zadeklarowane przy pomocy dyrektywy asemblera DW

(define word). Możemy pobrać wartość zmiennej marchewki do
rejestru instrukcją

MOV DX, marchewki ;marchewki -> DX

W rejestrze DX dokonujemy dodawania obu zmiennych i wyprowadzamy

na ekran sumę, posługując się funkcją 2 przerywania DOS 33
(21H).

W wyniku działania tego programu otrzymamy na ekranie napis:

Razem warzyw: 7

Jeczsze jeden szczegół techniczny. Ponieważ stosowana funkcja
DOS pracuje w trybie znakowym i wydrukuje nam znak o kodzie
ASCII przechowywanym w rejestrze, potrzebna jest manipulacja:

ADD DX, '0' ;Dodaj kod ASCII "zera" do rejestru

Możesz sam sprawdzić, że po przekroczeniu wartości 9 przez sumę
wszystko się trochę skomplikuje (kod ASCII zera - 48). Z równym
skutkiem możnaby zastosować rozkaz

ADD DX, 48

Jeśli prawidłowo dobierzemy format danych, fragment programu
napisany w asemblerze może korzystać z danych dokładnie tak
samo, jak każdy inny fragment programu napisany w C/C++. Możemy
zastosować dane o jednobajtowej długości (jeśli drugi, pusty
bajt nie jest nam potrzebny). Zwróć uwagę, że posługujemy się w
tym przypadku tylko "połówką" rejestru DL (L - Low - młodszy).

[P057.CPP]

#pragma inline
void main()
{
const char *napis = "\nRazem warzyw: $";
char marchewki = 2, pietruszki = 5;
asm {
MOV DX, napis
MOV AH, 9
INT 33
MOV DL, marchewki
ADD DL, pietruszki
ADD DL, '0'
MOV AH, 2
INT 33
}
}

W tej wersji zadeklarowaliśmy zmienne marchewki i pietruszki
jako zmienne typu char, co jest równoznaczne zadeklarowaniu ich
przy pomocy dyrektywy DB.

Zajmijmy się teraz maszynową reprezentacją liczb typu unsigned
long int (długie całkowite bez znaku). Ze względu na specyfikę
zapisu danych do pamięci przez mikroprocesory rodziny Intel
80x86 długie liczby całkowite (podwójne słowo - double word) np.

12345678(hex) są przechowywane w pamięci w odwróconym szyku.
Zamieniony miejscami zostaje starszy bajt z młodszym jak również

starsze słowo z młodszym słowem. Liczba 12345678(hex) zostanie
zapisana w pamięci komputera IBM PC jako 78 56 34 12.

Gdy inicjujemy w programie zmienną

long int x = 2;

zostaje ona umieszczona w pamięci tak: 02 00 00 00 (hex).
Młodsze słowo (02 00) jest umieszczone jako pierwsze. To właśnie

słowo zawiera interesującą nas informację, możemy wczytać to
słowo do rejestru rozkazem

MOV DX, X

Jeśli będzie nam potrzebna druga połówka zmiennej - starsze
słowo (umieszczone w pamięci jako następne), możemy zastosować
pointer (czyli podać adres następnego słowa pamięci).

[P058.CPP]

# pragma inline
void main()
{
unsigned long marchewki = 2, pietruszki = 5;
const char *napis = "\nRazem warzyw: $";
asm
{
MOV DX, napis
MOV AH, 9
INT 33
MOV DX, marchewki
ADD DX, pietruszki
ADD DX, '0'
MOV AH, 2
INT 33
}
}

W przypadku liczb całkowitych ujemnych C++ stosuje zapis w
kodzie komplementarnym. Aby móc manipulować takimi danymi każdy
szanujący się komputer powinien mieć możliwość stosowania liczb
ujemnych.

Najstarszy bit w słowie, bądź bajcie (pierwszy z lewej) może
spełniać rolę bitu znakowego. O tym, czy liczba jest ze znakiem,

czy też bez decyduje wyłącznie to, czy zwracamy uwagę na ten
bit. W liczbach bez znaku, obojętnie, czy o długości słowa, czy
bajtu, ten bit również jest (i był tam zawsze!), ale
traktowaliśmy go, jako najstarszy bit nie przydając mu poza tym
żadnego szczególnego znaczenia. Aby liczba stała się liczbą ze
znakiem - to my musimy zacząć ją traktować jako liczbę ze
znakiem, czyli zacząć zwracać uwagę na ten pierwszy bit.
Pierwszy, najstarszy bit liczby ustawiony do stanu 1 będzie
oznaczać, że liczba jest ujemna - jeśli zechcemy ją potraktować
jako liczbę ze znakiem.

Filozofia postępowania z liczbami ujemnymi opiera się na
banalnym fakcie:

(-1) + 1 = 0

Twój PC "rozumuje" tak: -1 to taka liczba, która po dodaniu 1
stanie się 0. Czy można jednakże wyobrazić sobie np.
jednobajtową liczbę dwójkową, która po dodaniu 1 da nam w
rezultacie 0 ? Wydawałoby się, że w dowolnym przypadku wynik
powinien być conajmniej równy 1.

A jednak. Jeśli ograniczymy swoje rozważania do ośmiu bitów
jednego bajtu, może wystąpić taka, absurdalna tylko z pozoru
sytuacja. Jeśli np. dodamy 255 + 1 (dwójkowo 255 = 11111111):

1111 1111 hex FF dec 255
+ 1 + 1 + 1
___________ _____ _____
1 0000 0000 100 256


otrzymamy 1 0000 0000 (hex 100). Dla Twojego PC oznacza to, że w

ośmiobitowym rejestrze pozostanie 0000 0000 , czyli po prostu 0.

Nastąpi natomiast przeniesienie (carry) do dziewiątego (nie
zawsze istniejącego sprzętowo bitu).

Wystąpienie przeniesienia powoduje ustawienie flagi CARRY w
rejestrze FLAGS. Jeśli zignorujemy flagę i będziemy brać pod
uwagę tylko te osiem bitów w rejestrze, okaże się, że
otrzymaliśmy wynik 0000 0000. Krótko mówiąc FF = (-1), ponieważ
FF + 1 = 0.

Aby odwrócić wszystkie bity bajtu, bądź słowa możemy w
asemblerze zastosować instrukcję NOT. Jeśli zawartość rejestru
AX wynosiła np. 0000 1111 0101 0101 (hex 0F55), to instrukcja
NOT AX zmieni ją na 1111 0000 1010 1010 (hex F0AA). Dokładnie
tak samo działa operator bitowy ~_AX w C/C++. W zestawie
rozkazów mikroprocesorów rodziny Intel 80x86 jest także
instrukcja NEG, powodująca zamianę znaku liczby (dokonując
konwersji liczby na kod komplementarny). Instrukcja NEG robi to
samo, co NOT, ale po odwróceniu bitów dodaje jeszcze jedynkę.
Jeśli rejestr BX zawierał 0000 0000 0000 0001 (hex 0001), to po
operacji NEG AX zawartość rejestru wyniesie 1111 1111 1111 1111
(hex FFFF).

Zastosujmy praktycznie uzupełnienia dwójkowe przy współdziałaniu

asemblera z C++:

[P059.CPP]

#pragma inline
void main()
{
const char *napis = "\nRazem warzyw: $";
int marchewki = -2, pietruszki = 5;
asm {
MOV DX, napis
MOV AH, 9
INT 33
MOV DX, marchewki
NEG DX
ADD DX, pietruszki
ADD DX, '0'
MOV AH, 2
INT 33
}
}

Dzięki zamianie (-2) na 2 przy pomocy instrukcji NEG DX
otrzymamy wynik, jak poprzednio równy 7.

Przypomnijmy prezentację działania operatorów bitowych C++.
Wykorzystaj program przykładowy do przeglądu bitowej
reprezentacji liczb typu int (ze znakiem i bez).

[P060.CPP]

/* Program prezentuje format liczb i operatory bitowe */

# include "iostream.h"
# pragma inline

void demo(int liczba) //Definicja funkcji
{
int n = 15;
for (; n >= 0; n--)
if ((liczba >> n) & 1)
cout << "1";
else
cout << "0";
}

char odp;
char *p = "\nLiczby rozdziel spacja $";

int main()
{
int x, y;

cout ˙<< "\nPodaj dwie liczby calkowite od -32768 do +32767\n";

asm {
mov dx, p
mov ah, 9
int 33
}
cout << "\nPo podaniu drugiej liczby nacisnij [Enter]";
cout << "\nLiczby ujemne sa w kodzie dopelniajacym";
cout << "\nSkrajny lewy bit oznacza znak 0-Plus, 1-Minus";

for(;;)
{
cout << "\n";
cin >> x >> y;
cout << "\nX: "; demo(x);
cout << "\t\tY: "; demo(y);
cout << "\n~X: "; demo(~x);
cout << "\t\t~Y: "; demo(~y);
cout << "\nX & Y: "; demo(x & y);
cout << "\nX | Y: "; demo(x | y);
cout << "\nX ^ Y: "; demo(x ^ y);
cout << "\n Y: "; demo(y);
cout << "\nY >> 1: "; demo(y >> 1);
cout << "\nY << 2: "; demo(y << 2);

cout << "\n\n Jeszcze raz? T/N: ";
cin >> odp;
if (odp!='T'&& odp!='t') break;
}
}

Wstawka asemblerowa nie jest w programie niezbędna, ale w tym
miejscu wydaje się być "a propos". Przy pomocy programu
przykładowego możesz zobaczyć "na własne oczy" jak wygląda
reprezentacja bitowa liczb całkowitych i ich kody
komplementarne.

Praca bezpośrednio ze zmiennymi jest jednym ze sposobów
komunikowania się z programem napisanym w C++. Mogą jednak
wystąpić sytuacje bardziej skomplikowane, kiedy to nie będziemy
znać nazwy zmiennej, przekazywanej do funkcji. Jeśli napiszemy w

asemblerze funkcję w celu zastąpienia jakiejś funkcji
bibliotecznej C++ , program wywołując funkcję przekaże jej
parametry i będzie oczekiwał, iż funkcja pobierze sobie te
parametry ze stosu. Rozważmy się to zagadnienie dokładniej.
Typową sytuacją jest pisanie w asemblerze tylko kilku funkcji
(zwykle takich, które powinny działać szczególnie szybko). Aby
to zrobić, musimy nauczyć się odczytywać parametry, które
program przekazuje do funkcji w momencie jej wywołania.
Zaczynamy od trywialnej funkcji, która nie pobiera w momencie
wywołania żadnych parametrów. W programie może to wyglądać np.
tak:

[P061.CPP]

//*TEKST to znany funkcji zewnętrzny wskaźnik

#pragma inline

char *TEKST = "\ntekst - test$";

void drukuj(void); //Prototyp funkcji

void main()
{
drukuj(); //Wywołanie funkcji drukuj()
}

void drukuj(void) //Definicja funkcji
{
asm MOV DX, TEKST
asm MOV AH, 9
asm INT 33
}

Funkcja może oczywiście nie tylko zgłosić się napisem, ale także

zrobić dla nas coś pożytecznego. W kolejnym programie
przykładowym czyścimy bufor klawiatury (flush), co czasami się
przydaje, szczególnie na starcie programów.

[P062.CPP]

# pragma inline

char *TEKST = "\nBufor klawiatury PUSTY. $";

void czysc_bufor(); //Też prototyp funkcji

void main()
{
czysc_bufor(); //Czyszczenie bufora klawiatury
}

void czysc_bufor(void) //Definicja funkcji
{
START:
asm MOV AH, 11
asm INT 33
asm OR AL, AL
asm JZ KOMUNIKAT
asm MOV AH, 7
asm INT 33
asm JMP START
KOMUNIKAT:
asm MOV DX, TEKST
asm MOV AH, 9
asm INT 33
}

Póki nie wystąpi problem przekazania parametrów, napisanie dla
C++ funkcji w asemblerze jest banalnie proste. Zwróć uwagę, że
zmienne wskazywane w programach przez pointer *TEKST zostały
zadeklarowane poza funkcją main() - jako zmienne globalne.
Dzięki temu nasze funkcje drukuj() i czysc_bufor() mają dostęp
do tych zmiennych.

Spróbujemy przekazać funkcji parametr. Nazwiemy naszą funkcję
wyswietl() i będziemy ją wywoływać przekazując jej jako argument

znak ASCII przeznaczony do wydrukowania na ekranie:
wyswietl('A'); . Pojawia się zatem problem - gdzie program
"pozostawia" argumenty przeznaczone dla funkcji przed jej
wywołaniem? W Tabeli poniżej przedstawiono w skrócie "konwencję
wywoływania funkcji" (ang. Function Calling Convention) języka
C++.

Konwencje wywołania funkcji.
________________________________________________________________

Język Argumenty na stos Postać Typ wart. zwrac.
________________________________________________________________

BASIC Kolejno offset adresu Return n
C++ Odwrotnie wartości Return
Pascal Kolejno wartości Return n
________________________________________________________________

Return n oznacza liczbę bajtów zajmowanych łącznie przez
wszystkie odłożone na stos parametry.

W C++ parametry są odkładane na stos w odwróconej kolejności.
Jeśli chcemy, by parametry zostały odłożone na stos kolejno,
powinniśmy zadeklarować funkcję jako "funkcję z Pascalowskimi
manierami" - np.:

pascal void nazwa_funkcji(void);

Dodatkowo, w C++ argumenty są przekazywane poprzez swoją
wartość, a nie przez wskazanie adresu parametru, jak ma to
miejsce np. w BASICU. Istnieje tu kilka wyjątków przy
przekazywaniu do funkcji struktur i tablic - bardziej
szczegółowo zajmiemy się tym w dalszej części książki.

Rozbudujemy nasz przykładowy program w taki sposób, by do
funkcji były przekazywane dwa parametry - litery 'A' i 'B'
przeznaczone do wydrukowania na ekranie przez funkcję:

# pragma inline
void wyswietl(char, char); //Prototyp funkcji

void main()
{
wyswietl('A', 'B'); //Wywolanie funkcji
}

void wyswietl(char x, char y) //Definicja (implementacja)
{
....

Parametry zostaną odłożone na stos:

PUSH 'B'
PUSH 'A'

Każdy parametr (mimo typu char) zajmie na stosie pełne słowo.
C++ nie potrafi niestety układać na stosie bajt po bajcie.
Funkcja wyswietl() musi uzyskać dostęp do przekazanych jej
argumentówów. Odwołamy się do zmiennych C++ w taki sposób, jak
robiłaby to każda inna funkcja w C++:

[P063.CPP]

# pragma inline
void wyswietl(char, char); //Prototyp funkcji
void main()
{
_AH = 2; //BEEEEE !
wyswietl('A', 'B'); //Wywolanie funkcji
}

void wyswietl(char x, char y) //Definicja (implementacja)
{
_DH = 0; // To C++ nie TASM, to samo, co asm MOV DH, 0
_DL = x; // asm MOV DL, x
asm INT 33
_DH = 0; // asm MOV DH, 0
_DL = y; // asm MOV DL, y
asm INT 33
}

Aby pokazać jak dalece BORLAND C++ jest elastyczny wymieszaliśmy

tu w jednaj funkcji instrukcje C++ (wykorzystując pseudozmienne)

i instrukcje assemblera. Może tylko przesadziliśmy trochę
ustawiając rejestr AH - numer funkcji DOS dla przerywania int 33

przed wywołaniem funkcji wyswietl() w programie głównym. To
brzydka praktyka (ozn. //BEEEE), której autor nie zaleca.
Jak widzisz, przekazanie parametrów jest proste.
  • zanotowane.pl
  • doc.pisz.pl
  • pdf.pisz.pl
  • qualintaka.pev.pl
  •