Wstawki asemblerowe w języku C, część 1

02.02.2010 - Piotr Witkowski
TrudnośćTrudnośćTrudność

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

1
mov eax,7
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
1
mov i,eax
Ogólna postać instrukcji przypisania:
1
mov cel,źródło
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.

  • 1
    
    add cel,źródło
    Dodaje wartość operandu źródło do wartości operandu cel i wynik umieszcza w operandzie cel.
  • 1
    
    sub cel,źródło
    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.

Snippet icon

Ć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;
}
Snippet icon

Ćwiczenie 2. Dane są zmienne i oraz k typu short int. Napisz wstawkę asemblerową przypisującą zmiennej k sumę starszego i młodszego bajtu zmiennej i

5
Twoja ocena: Brak Ocena: 5 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com