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

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

Operacje na wskaźnikach

Jaki będzie efekt wykonania poniższego kodu?

1
2
3
4
5
6
7
8
9
10
unsigned int i;
int main(void) 
{
 asm(" mov eax, offset i;    \
       mov ebx, 7       ;    \
       mov [eax], ebx;       \
     ");
 printf("%i\n", i); // i == ? 
 return 0;
}
Otóż instrukcja mov eax,offset i załaduje do rejestru eax adres zmiennej i. Natomiast mov [eax],ebx spowoduje załadowanie pod adres zapisany w eax wartości rejestru ebx. Na ekranie zostanie więc wypisana liczba 7. Możemy wyeliminować użycie rejestru ebx za cenę zastosowania specyfikatora rozmiaru:
1
2
3
4
5
6
7
8
9
unsigned int i;
int main(void) 
{
 asm(" mov eax, offset i;       \
       mov dword ptr [eax], 7;  \
     ");
 printf("%i\n", i); // i == 7 
 return 0;
}
W naszym systemie (i w Twoim pewnie też) adresy mają rozmiar 32 bitów. Pamiętaj więc, żeby do ich przechowywania używać 32-bitowych rejestrów(czyli np. eax,ebx,ecx,edx). Adres taki może wskazywać na obiekty o różnym rozmiarze. W naszym przypadku w eax jest adres czterobajtowej zmiennej. Stąd konieczność użycia specyfikatora by poinformować asembler, na jakiej ilości bajtów zapisać liczbę 7.

Bardziej skomplikowany przykład

Poniższy program sumuje elementy tablicy N-elementowej.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define N 50
const char k = N;
char tab[N] = {1,2,3,4,250};
//pozostałe elementy tablicy są domyslnie równe 0
unsigned short sum;
int main(void) 
{
 asm(" mov cl, 0;              \
       mov ax, 0;              \
       mov dh, 0;              \
       mov ebx, offset tab;    \
       start:                  \
       mov dl, byte ptr [ebx]; \
       add ax, dx;             \
       inc ebx;                \
       inc cl;                 \
       cmp k, cl;              \
       jg start;               \
       mov sum, ax;            \
     ");
 
 printf("%i\n", sum); // i == 260 
 return 0;
}
Założyliśmy tu, że tablica tab będzie dosyć niewielka. Jej rozmiar przechowywany jest w zmiennej k na jednym bajcie, czyli może być maksymalnie równy 255. Zatem wystarczy, że do przechowywania bieżącego indeksu w tej tablicy użyjemy jednobajtowego rejestru cl. Także jej elementy są pojedyńczymi bajtami. Maksymalna wartość sumy elementów takiej tablicy mieści się w dwóch bajtach. Sumę tą będziemy więc liczyli w 16-bitowym rejestrze, w przykładzie jest to ax. Do młodszego bajtu rejestru dx, czyli do dl ładujemy bajt spod adresu zapamiętanego w ebx rozkazem mov dl,byte ptr [ebx] . Następnie dodajemy do rejestru ax rejestr dx. Ponieważ tylko młodszy bajt dx zawiera wartość do zsumowania, to w wierszu 10 zerujemy starszy bajt tego rejestru. W języku C zmienna identyfikująca tablicę jest adresem jej pierwszego elementu. Do ebx ładujemy ten adres w wierszu 11. Ciało pętli stanowią wiersze 12-18. Po wykonaniu dodawania zwiększamy zarówno licznik cl jak i adres następnego elementu do przetworzenia w bx. To, że bx zwiększamy o 1 wynika z jednobajtowego rozmiaru elementu tablicy. Gdybyśmy mieli do czynienia z tablicą elementów typu np. dwubajtowego short int to należałoby zwiększyć ten indeks o 2. Kod ten można nieco uprościć eliminując inkrementację rejestru ebx:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define N 50
const char k = N;
char tab[N] = {1,2,3,4,250};
//pozostałe elementy tablicy są domyslnie równe 0
unsigned short sum;
int main(void) 
{
 asm(" mov ecx, 0;                   \
       mov ax, 0;                    \
       mov dh, 0;                    \
       mov ebx, offset tab;          \
       start:                        \
       mov dl, byte ptr [ebx + ecx]; \
       add ax, dx;                   \
       inc cl;                       \
       cmp k, cl;                    \
       jg start;                     \
       mov sum, ax;                  \
     ");
 
 printf("%i\n", sum); // i == 260 
 return 0;
}
Teraz obliczanie adresu bieżącego elementu i jego pobranie dokonuje się jednym rozkazem procesora: mov dl,byte ptr [ebx + ecx]. Wyrażenie w nawiasach [ ] określa tzw. tryb adresowania. Szczegóły są dosyć skomplikowane. Nam wystarczy intuicja, że suma wartości rejestrów (pomnożonnych ewentualnie przez potęgi 2) i niewielkich stałych może być zwykle użyta w takim wyrażeniu.
Snippet icon

Ćwiczenie 6. Zmodyfikuj powyższy przykład tak, by działał dla tablic elementów dwubajtowych indeksowanych zmienną dwubajtową. Zmienna k ma teraz typ const unsigned short a tablica tab ma elementy typu  unsigned short. Wskazówka może przydać się rozkaz: mov dx,word ptr [ebx + 2*ecx]

Organizacja pamięci programu

Z punktu widzenia programisty pamięć uruchomionego programu składa się przynajmniej z dwóch spójnych bloków pamięci. Są to segment kodu oraz segment danych. Segment kodu zawiera skompilowany program, w jego obrębie porusza się licznik rozkazów ip. Natomiast segment danych tworzą:

  • obszar danych statycznych - są to zmienne zadeklarowane globalnie, lub z modyfikatorem static. Rozmiar danych statycznych jest ustalany podczas kompilacji i nie zmienia się w toku wykonania programu.
  • sterta - obszar zarządzany przez tzw. alokator pamięci służący do dynamicznego przydziału pamięci, np. funkcją malloc. Wraz z biegiem programu zwykle zmienia się rozmiar sterty.
  • stos - struktura służąca przechowywaniu zmiennych lokalnych, parametrów i innych danych tymczasowych aktualnie wywołanych funkcji.
Przykład:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int N = 50;
 
int *alokuj(int num, int e) {
 int *tab;
 static int brzeg = 5;
 if( (num > brzeg) && (num <= N) )
 {
   tab = malloc(num*sizeof(int));
   *tab = e;
 }
 else tab = NULL;
 
 return tab;
}
W powyższym fragmencie kodu zmienne N oraz brzeg zostaną umieszczone w obszarze danych statycznych, num,e oraz wskaźnik tab na stosie w tzw. ramce funkcji alokuj a tablica dynamiczna wskazywana przez tab - na stercie.

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com