Przed uruchomieniem programu w języku C należy przetłumaczyć go na język poleceń procesora. Proces ten nazywa się kompilacją, a program który temu służy - kompilatorem . Znając podstawy języka poleceń procesora( czyli asemblera), można fragmenty programu zaprogramować od razu na tym poziomie. Ta umiejętność przydaje się, gdy chcemy wygenerować kod szybszy, niż zrobiłby to kompilator.
Poznamy podstawy łączenia kodu w języku C z kodem asemblera przy
użyciu tzw. wstawek asemblerowych. Zobaczymy jak z poziomu asemblera operować na zmiennych, tablicach i wskaźnikach, wykonywać skoki, kodować pętle i wywołania procedur. Użyjemy kompilatora GCC w środowisku Linux uruchomionym na maszynie kompatybilnej z 80x86.
Pierwszy program
Zacznijmy od zakodowania w asemblerze instrukcji przypisania zmiennej i
typu int
wartości 7
:
1
2
3
4
5
6
7
8
9
10
11
| //typ int ma 4 bajty
int i;
int main(void)
{
asm(" mov eax, 7; \
mov i, eax; \
");
printf("%i\n", i); //i==7
return 0;
} |
Instrukcja asm
wygląda jak zwykłe wywołanie funkcji przyjmującej argument typu char*. Tak jednak nie jest. Kompilator GCC natrafiwszy na instrukcję asm("rozkaz_asm")
umieści rozkaz_asm w generowanym przez siebie kodzie asemblerowym. Argument musi być stałą napisową!
Znak łamania wiersza "\"
zastosowaliśmy w celu poprawy czytelności.
Instrukcję z wierszy 5-6 można równie dobrze zapisać tak:
1
| asm(" mov eax,7; mov i,eax; "); |
.
Wewnątrz procesora wbudowano kilkaset bajtów bardzo szybkiej pamięci służącej do przechowywania
argumentów instrukcji asemblerowych, wyników pośrednich, adresów pamięci. Są to tak zwane rejestry procesora.
W naszym przykładzie użyliśmy 32-bitowego rejestru eax
( nazywanego niekiedy akumulatorem). Instrukcja
przypisała temu rejestrowi wartość
7
. Następnie wartość rejestru
eax
została przepisana do miejsca w pamięci skojarzonego ze zmienną
i
za pomocą instrukcji
Ogólna postać instrukcji przypisania:
Rozkaz skopiuje wartość operandu ( czyli inaczej argumentu)
źródlo
do operandu
cel
. Operand
źródło
może być (jak w pierwszej instrukcji) stałą, rejestrem (jak w drugiej) lub adresem w pamięci. Operand
cel
to rejestr (jak w pierwszej) lub adres w pamięci (jak w drugiej). Obydwa operandy nie mogą być jednocześnie adresami. Niemożliwym jest zatem przesłanie wartości pomiędzy dwoma miejscami w pamięci bez pośrednictwa rejestrów. Taka sama reguła obowiązuje dla pozostałych instrukcji asemblerowych.
Dwie kolejne instrukcje pozwalają na wykonywanie prostych operacji arytmetycznych.
- Dodaje wartość operandu
źródło
do wartości operandu cel
i wynik umieszcza w operandzie cel
.
- Odejmuje wartość operandu
źródło
do wartości operandu cel
i wynik umieszcza w operandzie cel
.
W powyższych instrukcjach operand źródło może być stałą, rejestrem lub adresem w pamięci. Operand cel to rejestr lub adres.
|
Ćwiczenie 1. Niech i,j,k będą zmiennymi typu int . Napisz wstawkę asemblerową wykonującą operację k = i + j . Użyj rejestrów eax i ebx .
Wpisz tylko instrukcje asemblerowe oddzielone średnikiem i znakiem "\" , jak w przykładzie.
|
|
Rozmiar ma znaczenie
Dlaczego w naszym przykładowym programie nie użyliśmy instrukcji mov i,7
? Otóż na poziomie asemblera nie mamy dostępu do informacji o typach zmiennych. Nie wiadomo zatem, czy pod adresem oznaczonym przez i
zarezerwowano bajt, dwa, cztery czy jeszcze więcej pamięci i na ilu bajtach trzeba zapisać tam liczbę 7
. Natomiast z rozkazu mov i,eax
kompilator wnioskuje, że będzie
ich tyle, ile wynosi rozmiar rejestru eax
, czyli 4 bajty. Możemy też poinstruować kompilator co do żądanego rozmiaru zapisywanych wartości używając specyfikatorów:
byte ptr, word ptr, dword ptr
oznaczających odpowiednio bajt, dwa bajty (słowo - word) i cztery bajty (podwójne słowo - double word). Zatem w naszym przykładzie wystarczy użyć instrukcji: mov dword ptr i,7
:
1
2
3
4
5
6
7
| int i;
int main(void)
{
asm("mov dword ptr i,7");
printf("%i\n", i); //i==7
return 0;
} |
Więcej o rejestrach
Pierwsze procesory firmy Intel miały 8-bitowe rejestry. W następnych generacjach ten rozmiar podwajał się osiągając ostatnio rozmiar 64-bitów. Jednocześnie wymóg zachowania kompatybilności wstecznej (możliwość uruchamiania kodu ze starszych generacji procesorów na nowszych) wymusił np. to, że w każdym procesorze 32-bitowym musi znajdować się 32, 16 i 8 bitowy akumulator.
Problem ten rozwiązano tak: Dwa mniej znaczące (tzw. młodsze) bajty eax
tworzą akumulator 16 bitowy - ax
. Rejestr ax
natomiast
składa się z dwóch bajtów bardziej znaczącego (tzw. starszego) ah
i mniej znaczącego 8-bitowego akumulatora al
.
Podobnie jest z innymi rejestrami arytmetycznymi: ebx,ecx,edx
.Instrukcje działają zwykle na argumentach o tej samej długości. Zatem np. mov ax,eax
oraz
mov eax,bx
są niepoprawne a add al,ch
- tak.
Przykład: poniższy program jest równowazny instrukcji
k = (i + j) % 256
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| //typ short int ma 2 bajty
short int i,j,k;
int main(void)
{
i = 67; j = 257;
asm(" mov ax, i; \
add ax, j; \
mov ah, 0; \
mov k, ax; \
");
printf("%i\n", k); //k==68
return 0;
} |