Teraz napiszemy kilka programików które krok po kroku nauczą nas jak
pisać w assemblerze pod Linuxem za pomocą gcc.
Przykład pierwszy :
void main()
{
asm (
" nop "
);
}
Wszystko pomiędzy '(' i ')' jest traktowane jako instrukcje assemblerowe.
w tym wypadku instrukcja ta to 'nop' ktora jest odpowiednikiem 'instrukcji
pustej' z C, tzn. nie wykonująca żadnej akcji. Tak więc sam program oczywiście
również nic nie robi.
Efekt programu? Oczywiście żaden :-) Obejrzyjmy sobie tylko jego przekład na assembler
przez gcc.
gcc -S pierwszy.c -o pierwszy.s
i potem
oczywiście vi pierwszy.s. Podobnie najlepiej postąpisz z każdym programem, który
będziesz pisał, dzięki temu szybko będziesz w stanie poprawiać błędy i uczyć się
asma.
Przykład drugi:
asm("...." : : "jakaś litera" (nazwa zmiennej) , ...)
Na przykład chcąc mieć dostęp do zmiennej a, mogę napisać
asm("..":: "q" (a) ); . Do zmiennej tej mogę się odwoływać
za pomocą %(numer). Tutaj napisalibyśmy %0. Gdybyśmy napisali tak:
asm(".."::"q" (a) "g" (b) );
to do zmiennej a odwoływalibyśmy się
za pomocą %0 a do zmiennej b za pomocą %1. Proste nieprawdaż? Wyjaśnienia tylko wyjaśniają
tylko te literki których użyliśmy w cudzysłowiach. Za ich pomocą mówimy gcc
gdzie ma przechować nasze zmienne. I tak :
Poniższe oznaczenia znajdziesz w info na temat gcc. Napisz info gcc, najedź na
napis 'C Extensions', kliknij enter, najedź na napis 'Extended Asm' i tam znajdziesz
nieco po angielsku na temat rozszerzonego asma. Napisz info gcc ->Machine Desc ->
Constraints -> Machine Constraints i poszukaj 'Intel 386' tam znajdziesz opis (po angielsku)
tych oznaczeń.
Przypominam, że jeżeli używamy któregoś z trzech pól (zmienne wyjściowe, wejściowe,
zmodyfikowane rejestry), to mamy do czynienia z assemblerem rozszerzonym, i nazwy rejestrów musimy
poprzedzać '%%' a nie '%', czyli piszemy %%eax a nie %eax.
W naszym przykładzie nie używamy pola zmienne wyjściowe, więc zostawiamy je puste, nie używamy
pola zmodyfikowane rejestry więc je pomijamy.
drugi.c
#include <stdio.h>
void main()
{
int a=0;
printf("Wartość a: %d przed wykonaniem \n",a);
asm (
);
printf("I wartość tej samej zmiennej po wykonaniu %d\n",a);
}
Efektem wykonania będzie napis :
Wartość a: 0 przed wykonaniem
I wartość tej samej zmiennej po wykonaniu 0
Dlaczego ? Zmienna 'a' jest traktowana jako 'wejściowa', tzn z poziomu
inline-assemblera można się do niej odwoływać, ale nie można jej zmieniać.
Jeżeli zależy nam na tym, by zobaczyć efekt, napiszemy kolejny program, z wykorzystaniem
wartości "wyjściowych"
Przykład trzeci
asm("..." :"=q" (a) );
Jak widać wszystko podobnie jak
poprzednio , z tym że używamy znaku = zawsze przed określaniem gdzie ma być załadowana
zmienna. Odwołujmey się znowu za pomocą %0, %1 itd. Zawsze pamiętamy, że %0 odnosi się
do pierwszej zadeklarowanej w ten sposób zmiennej niezależnie od tego w którym polu się
znalazła. Format jest identyczny jak dla zmiennych wejściowych .
trzeci.c
#include <stdio.h>
void main()
{
int a=0;
int b=0;
printf("Wartość a: %d b: %d przed wykonaniem \n",a,b);
asm (
);
printf("I wartość tych samych zmiennych po wykonaniu a: %d b: %d\n",a,b);
}
Tym razem otrzymamy następujący wynik:
Wartość a: 0 b: 0 przed wykonaniem
I wartość tych samych zmiennych po wykonaniu a: 0 b: 1
Co się stało ? Zmienna 'a' została zadeklarowana jako wejściowa. Ponieważ
jest druga na liście więc odwołujemy się do niej przez %1. Zmienna 'b' jest jako wyjściowa
więc efekty jej zmian będzie widać na zewnątrz instrukcji assemblerowej. Odwołujemy się
do niej za pomocą %0, bo została zadeklarowana na pierwszym miejscu.
Załadowaliśmy liczbę '1' do zmiennej a (zmiany tej na zewnątrz nie widać,
bo a jest tylko wejściowa) a potem zmiennej 'b' przypisaliśmy tymczasową wartość
'a'. Kazaliśmy zarówno 'a' jak i 'b' przechować w rejestrach, ale nie obchodzi nas w jakich.
Jeżeli nas interesuje gdzie dokładnie, to wykonajmy instrukcję gcc -S trzeci.c
i obejrzyjmy plik trzeci.s , między #APP a #NOAPP będzie nasz wstawiony inline
assembler.
Slowko wyjasnienia: jest mozliwe oczywiscie, by zamiast tego napisac:
#include <stdio.h>
void main()
{
int a=0;
int b=0;
printf("Wartość a: %d b: %d przed wykonaniem \n",a,b);
asm (
);
printf("I wartość tych samych zmiennych po wykonaniu a: %d b: %d\n",a,b);
}
Nie robie tego dlatego, zeby po pierwsze, dokonac dokladnego przekladu a=b=1, a po drugie, zeby zademonstrowac, ze i tak mozna :). Ma to wplyw jednakze na szybkosc, o czym nizej.
Przykład czwarty
To samo co poprzednio, tyle tylko że a zadeklarujemy jako zmienną wejściową.
Wydawałoby się, że powinniśmy wyniki identyczne, prawda? Mi też na początku tak
się zdawało i dziesiątki razy popełniałem ten sam bład, przed którym teraz was
przestrzegę.
czwarty.c
#include <stdio.h>
void main()
{
int a=0;
int b=0;
printf("Wartość a: %d b: %d przed wykonaniem \n",a,b);
asm (
);
" movw %0,%1\n"
: "=r" (b), "=r" (a)
:
printf("I wartość tych samych zmiennych po wykonaniu a: %d b: %d\n",a,b);
}
i efekt :
Wartość a: 0 b: 0 przed wykonaniem
I wartość tych samych zmiennych po wykonaniu a: 134479873 b: 1
Dziwny nie ? Dlaczego tak się stało? To bardzo proste. Zmienna b została załadowana
do któregoś z rejestrów 32 bitowych. My, instrukcją movw $1,%0 (która rozwinęła
się do np movw $1,%eax) wprowadziliśmy tam 16 bitową liczbę. Czyli, jak łatwo się domyśleć,
zawartość ax będzie równa 1, a pozostała część eax będzie miało wartość nieokreśloną.
Można ten błąd naprawić na dwa sposoby:
pierwszy :
piaty.c
#include <stdio.h>
void main()
{
int a=0;
int b=0;
printf("Wartość a: %d b: %d przed wykonaniem \n",a,b);
asm (
Wartość a: 0 b: 0 przed wykonaniem I wartość tych samych zmiennych po wykonaniu a: 1 b: 1
W sposobie pierwszym
po prostu wyczyściliśmy rejestr, który akurat przechowywał
zmienną 'a', w związku z czym rejestr ten
nie zawierał żadnych przypadkowych wartości, w sposobie drugim
(lepszym) powiedzieliśmy gcc, że ma traktować argumenty jako liczby 32-bitowe
(movl zamiast movw).
Przy okazji : skompiluj ten sam plik szosty.c nastepująco :
gcc szosty.c -o a.out
A potem :
gcc -O2 szosty.c -o a1.out
i porównaj pliki wynikowe komendą ls -al a.out .
Widać małą różnicę rozmiaru, nieprawdaż? Warto więc znać opcje kompilatora.
Zazwyczaj ten kod będzie albo takiego samego rozmiaru jako poprzedni, albo nawet mniejszy. Dlaczego? Gdyz w naszym przykladzie pojawia sie zaleznosc miedzy dwoma rejestrami (wyjasnienie juz wkrotce w rozdziale o optymalizacji), podczas gdy kompilator automatycznie generuje kod ktory zaleznosci tej unika.
Pytanie więc znowu po co używać assemblera? Spokojnie. Ten przyklad pokazuje, ze programiki pisane na chybcika i bez zastanowienia moga byc gorsze od C.
Chyba nie zdziwiony? :-) Naprawdę zastanów się nim zaczniesz pisać coś w assemblerze,
i zawsze próbuj napisać równoważny kod w C. Potem porównaj szybkość działania,
wielkość pliku i dopiero wtedy wybierz lepszą wersję. A najlepiej obejrzyj kod wypluty przez gcc i popraw go by uzyskac lepszy wynik.
Przykład ósmy
Teraz napiszemy sobie mały programik, który posłuży nam do pokazania jednego
błędu i paru innych rzeczy. Programik będzie wpisywał
elementy jednej tablicy w odwrotnej kolejności do drugiej, tzn. przed jego
wykonaniem będziemy mieli tablicę
a={11,12} a po wykonaniu tablicę c={12,11}.
Kilka słów na temat użytych instrukcji. Instrukcja leal oblicza
adres pierwszego argumentu i umieszcza go w drugim. Np leal a,%%eax znaczy to
samo mniej więcej co movl $a,%%eax. .Instrukcja
mull mnoży swój
argument przez zawartość rejstru %%eax i wynik zwraca w parze rejestrów
%%eax:%%edx (%%eax młodszy bajt, %%edx starszy bajt). Instrukcja loop
powoduje skok do etykiety podanej jako argument, jeżeli %%ecx jest różne od zera,
i zmniejsza %%ecx o 1. Instrukcja subl powoduje odjęcie od pierwszego
argumentu durgiego i umieszcza wynik w drugim argumencie. Instrukcja push
umieszcza argument na stosie, a pop zdejmuje go stamtąd.
Wreszcie instrukcja dec podowuje zmniejszenie argumentu o jeden.
Dokładniejszego opisu tych instrukcji poszukaj na końcu tego tekstu .
osmy.c (wersja z błędem)
#include <stdio.h>
void main()
{
int a[2]={11,12};
int c[2];
printf("Przed wykonaniem instrukcji a[0]=%d, a[1]=%d",a[0],a[1]);
asm volatile(
"leal %1,%%ebx\n\t"
" movl $1,%%eax\n\t"
" movl $2,%%ecx\n\t"
" petla1:n\t"
" pushl %%eax\n\t"
"pushl %%ecx\n\t"
" movl $2,%%ecx\n\t"
" mull %%ecx\n\t"
" movl (%%edx,%%eax,2),%%ecx\n\t"
"pushl %%ebx\n\t"
" movl $2,%%ebx\n\t"
" subl %%eax,%%ebx\n\t"
" movl %%ebx,%%eax\n\t"
" popl %%ebx\n\t"
" movl %%ecx,(%%ebx,%%eax,2)\n\t"
"popl %%ecx\n\t"
"popl %%eax\n\t"
"decl %%eax\n\t"
" loop petla1\n\t"
: "=g" (a) , "=g" (c)
:: "ax", "cx", "dx" , "memory"
);
printf("Po wykonaniu instrukcji c[0]=%d, c[1]=%d\n",c[0],c[1]);
}
Niestety, ten program ma błąd. Spróbuj znaleźć go sam.
Jeżeli uda Ci się go znaleźć, to już
umiesz w miarę dobrze assemblera, a przynajmniej jesteś na dobrej drodze do
nauczenia się go.
Jeżeli nie wiesz gdzie jest błąd, doczytaj najpierw ten paragraf do końca. Po kolei
wyjaśnię co robią które komendy.
Najpierw obliczamy adres początku tablicy a ( która jest podana jako pierwszy argument, stąd %0, i gcc ma samo zadecydować gdzie ten argument ma być, stąd "=g") i adres ten ładujemy do rejestru %%edx. Tak samo obliczamy adres tablicy c, w której maja się znaleźć odwrócone elementy tablicy a. Adres tej drugiej tablicy, która została podana jako drugi argument (stąd %1) zostaje złożona do rejestru %ebx. Ponieważ używam rozszerzonego assemblera, nazwy wszsytkich rejestrów poprzedzam dodatkowym '%'. Ładuję jedynkę do %eax i dwójkę do %ecx. Ta dwójka to liczba pętli które wykona program. Kolejna instrukcja to etykieta o nazwie 'petla1'. Stare wartości %eax i %ecx odkładam na stos. Do %ecx ładuję dwójkę, i mnożę %eax przez %ecx. Wynik ma być w rejestrze %eax:%edx (już widzisz, gdzie jest błąd, prawda?).
Teraz przykład odwoływania się do adresów zmienej. Gdybym chciał się odwołać do pierwszego elementu tablicy 'a', napisałbym (%%edx) (bo do tego właśnie rejstru wcześniej załadowałem adres komendą leal %0,%%edx). Gdybym chciał odwoływać się do elementu drugiego tablicy, napisałbym 4(%%edx). Dlaczego? Dokonaj translacji osmy.c komenda gcc -S osmy.c na plik w asmie osmy.c i obejrzyj sobie go. Widzisz? Pierwszy element to -8(%%ebp), drugi to -4(%%ebp). Skoro %edx wskazuje na pierwszy element, wieć żeby dostać się do drugiego, muszę do niego dodać cztery bajty. Stąd to 4(%%edx). Dlaczego akurat cztery? Ponieważ typ int ma właśnie długość czterech bajtów. Gdyby to była tablica typu char, to dodałbym 1, czyli jeden bajt, bo długość typu char to jeden bajt. Proste prawda? (%%edx,%%eax,4) oznacza natomiast: element o adresie %edx+ %eax*4. Zamiast czwórki mógłbym wpisać jakąś inną potęgę dwójki, np: 1,2,4,8. W moim przypadku %eax zawiera w pierwszej pętli 2, a w drugiej zero, więc najpierw odwołam się do %edx+2*2 czyli tak samo jak 4(%edx), czyli drugiego elementu tablicy, a potem do %edx+0*2, czyli do pierwszego elementu tablicy. Reszty chyba już nie muszę tłumaczyć?
Teraz wyjaśnienie gdzie był błąd : sam się zresztą już chyba domyśliłeś. W rejestrze %edx
miałem przechowywać adres tablicy a, ale komenda mul, gdy jej argument jest 32-bitowy, umieszcza
wynik w parze rejestrów %eax:%edx. W naszym przypadku do %edx powędrowało zero, więc
więc zamiast odczytywać elementy z tablicy 'a', czytałem je gdzieś z kosmosu.
Jak go poprawić? Bardzo prosto. Dopisz przed komendą "mull %%ecx\n\t" komendą
"pushl %%edx\n\t" a bezpośrednio po niej komendę "popl %%edx\n\t".
#include
void main()
{
int a[2]={11,12};
// gdybym chciał dwie tablice, tu zadeklarowałbym int b[2]={0,0};
printf("Przed wykonaniem instrukcji a[0]=%d, a[1]=%d\n",a[0],a[1]);
asm volatile(
);
// tutaj dodałbym leal %1,%%ebx
"pushl (%%edx)\n\t"
"pushl 4(%%edx)\n\t"
//i wtedy dwie następne linijki zamieniłbym %%edx na %%ebx
"popl (%%edx)\n\t"
"popl 4(%%edx)\n\t"
: "=g" (a) // tu dodałbym "=g" (b)
::"dx" // a tu dodałby, "bx"
printf("Po wykonaniu instrukcji a[0]=%d, a[1]=%d\n",a[0],a[1]);
}
Aha, zapomniałem jeszcze wspomnieć po co jest lista zmodyfikowanych rejestrów.
W liście tej informujemy gcc, że dany rejestr jest używany przez nas, i kompilator
nie powinien już zakładać, że znajdujące się tam dane są takie, jak przed
wykonaniem instrukcji asm. Jeżeli sami zachowujemy wartość odpoweidnich
rejestrów, to wtedy ostatnie pola możemy pominąć. W ostatnim przykładzie modyfikowałem
rejestr %edx, więc poinformowałem kompilator o tym.