Assembler i platforma Intel

autor Michał Słapa


1.Wstęp

2.Notacja Intel vs AT&T

    1. Kolejność argumentów

    2. Prefiksy

    3. Specyfikacja rozmiaru argumentów

    4. Odwołania do pamięci

    5. Podsumowanie

3.Wstawki assemblera w C

    1. Podstawy

    2. Extended inline assembly

    3. Używanie dynamicznie przydzielanych rejestrów

    4. Symbol 'volatile'

4.Literatura

5.Materiały wykorzystane


1.Wstęp


Ta część prezentacji poświęcone będzie pewnemu skróconemu opisowi assemblera, takiego z jakim będziemy mieli styczność zarówno przy przeglądaniu kodu jądra, czy pisaniu niskopoziomowych aplikacji. Ma na celu zaznajomienie nas z podstawowymi zagadnieniami i konwencjami notacyjnymi pisania niskopoziomowego kodu na procesory rodziny Intel. Assembler bedzie przedstawiony w takim stopniu, by uczestnicy prezentacji mieli jakąś bazę pozwalającą na większe zrozumienie czytanego kodu, czy ogólne rozeznanie w linuksowym assemblerze na omawianą platformę.


2.Notacja Intel vs AT&T


Z assemblerem na platformę Intel związane są dwie podstawowe notacje: Intel oraz AT&T. Są to wyłącznie konwencje notacyjne i oczywiście oparte na nich kompilatory będą mieć taką samą funkcjonalność, a jedynie wymagać będą innego stylu pisania kodu. Interesować nas będzie głównie notacja AT&T, gdyż to ona jest używana przez GNU-Assembler (GAS), który z kolei jest wykorzystywany przez gcc jako narzędzie asemblacji. Dlatego to ze składnią AT&T spotkamy się przeglądając kod źródłowy linuksa- zarówno wstawki w bibliotekach C, jak i źródła asseblerowe jądra. Przykładowym kompilatorem korzystającym ze składni Intel będzie dla nas NASM. Poniżej zajmę się podstawowymi różnicami w składni obu konwencji.


2.1 Kolejność argumentów

W notacji AT&T argumenty ustawione są w kolejności najpierw źródło, następni cel, w notacji Intel przeciwnie: najpierw cel potem źródło. W przykładzie próbujemy przepisać zawartość eax na ebx:

AT&T: movl %eax, %ebx
Intel: mov ebx, eax


2.2 Prefiksy

W składni AT&T przed nazwami rejestrów stawiany jest znak '%'. Konwencja ta jest używana by uniknąć ewentualnych konfliktów z dołączanymi symbolami C. W AT&T przed wartościami stałymi, bądź nazwami zmiennych piszemy $. W notacji Intel nie stosuje się żadnych prefiksów, ale stałe podane w zapisie szesnastkowym kończymy dodając na końcu 'h', a stałe zero-jedynkowe 'b'.

AT&T:
	xorl %eax, %eax
	movl $0xd00d, %ebx
	movl $_zmiennaX, %ecx
Intel:
	xor eax, eax
	mov ebx, 0xd00dh
	mov ecx, _zmiennaX

2.3 Specyfikacja rozmiaru argumentów

Dla rozróżnienia rozmiarów argumentów w AT&T stosuje się sufiksy poleceń. Po nazwie polecenia musi wystąpić jedno z 'b'- dla operacji na 8-bitowych bajtach, 'w'- dla operacji na 16-bitowych słowach, 'l'- dla operacji na liczbach 32-bitowych. Kompilator GAS nie stosuje się w tak restrykcyjnym stopniu do powyższej konwencji i jeżeli nie zostaną sprecyzowane rozmiary argumentów, to będzie on próbował je zgadnąć. W notacji Intel sufiksy nie występują i gdy chcemy dokładnie sprecyzować typ argumentu po prostu podajemy go przed argumentem (odpowiednikami powyższych są 'byte' , 'word', 'dword').

AT&T:
	movb %bl,%al
	movl %ebx,%eax
	movl (%ebx),%eax
Intel:
	mov al,bl
	mov eax,ebx
	mov eax, dword ptr [ebx]

2.4 Odwołania do pamięci

Odwoływanie się do pamięci w składni Intal jest bardzo proste i polega na zastosowaniu nawiasów '[', ']'. W środku można umieścić dowolne wyrażenie arytmetyczne, które sprecyzuje adres pamięci do którego należy się odwołać. W AT&T adresowanie pamięci jest dużo mniej naturalne...

Podstawową formułą odwołującą się do pamięci jest:

adres(wskaznikbazowy,wskaznikdoindeksu,skalaindeksowania)

Odpowiada to następującemu wyrażeniu w konwencji notacyjnej Intel.

[wskaznikbazowy + wskaznikdoindeksu*skalaindeksowania + adres]

Czyli powyższe formuły wyliczą adres z obszaru:

wskaznikbazowy + wskaznikdoindeksu*skalaindeksowania + adres


Nie trzeba precyzować wszystkich tych pól, podany musi być jednak co najmniej adres, lub wskaznikbazowy. Dla lepszego zrozumienia polecam rzucić okiem na przykłady.

2.5 Podsumowanie

W dalszej części prezentacji opierać się będziemy na notacji AT&T, która jak już wspominałem jest wykorzystywana przez gcc. Warto myślę zwrócić uwagę, że istnieją programy potrafiące przetransformować kod napisany w jednej notacji na inną. Nowe wersje GAS potrafią już zresztą kompilować kod w notacji Intel, jest to jednak nowa własność kompilatora i jako taka jest jeszcze nie udokumentowana.

By zakończyć porównywanie notacji AT&T i Intel pragnę zachęcić do szybkiego przejrzenia jako przykładu źródeł programu Hello World, napisanych dla asseblera GAS, oraz NASM.



3.Wstawki assemblera w C


3.1 Podstawy


Podstawowa formuła pozwalająca na wstawienie do kodu asseblera jest następująca:

asm ("polecenie");


Czyli na przykład:

asm ("nop");


Stosuje się też notacje '__asm__', by uniknąć ewentualnych konfliktów ze zdefiniowanymi symbolami funkcyjnymi.


Podstawowej składni możemy swobodnie używać tak długo, aż nie zabierzemy się za zmienianie rejestrów:


__asm__ (
	"pushl %eax\n\t"
	"movl $0, %eax\n\t"
	"popl %eax");


Kod który w ten sposób napiszemy zostanie przez GCC wstawiony bezpośrednio do kodu powstałego po kompilacji, gotowego do assemblacji. Dlatego właśnie nie można zapominać o stosowaniu w kodzie znaków nowej linii '\n' i tabulacji '\t'. Jako doświadczenie proponuje obejrzeć plik inline1.s powstały po kompilacji inline1.c:

gcc -S inline1.c


Jeżeli jednak nasz wstawiony kod assemblera zabierze się za modyfikację rejestrów, może powstać problem. Najbardziej prawdopodobnym skutkiem nieoczekiwanej zmiany rejestrów będzie błąd programu, ale może zdarzyć się wszystko...


Dlatego gcc wykorzystuje:


3.2 Extended inline assembly


Extended inline assembly jest rozszerzeniem poprzedniej formuły o zależności w postaci: parametrów wejściowych, parametrów wynikowych, oraz rejestrów modyfikowanych.

Podstawowa składnia:


asm ( "polecenia" : parametry_wynikowe : parametry_wejściowe : zmienione_rejestry);


Ogólnie parametry są wypisanymi po przecinkach zależnościami. Każda zależność to podany w cudzysłowiu symbol rejestru, bądź innej pamięci której zależność dotyczy. Dodatkowo po zależnościach wynikowych i wejściowych występuje w nawiasie zmienna C, która będzie skojarzona z tą zależnością.
Oto najbardziej podstawowe z dostępnych zależności:

a eax

b ebx

c ecx

d edx

S esi

D edi

I stała (od 0 do 31)

m pamięć

q,r rejestr przydzielany dynamicznie (jeszcze o tym będzie)

g eax, ebx, ecx, edx lub adres w pamięci



Wszystko to najłatwiej będzie chyba omówić korzystając z przykładu, oto takowy:


#include <stdio.h>

int main(void) {
	unsigned long lie=2,to=21,me=3;
	asm (
		"mull %%ebx\n\t"
		"mull %%ecx"
		: "=a" (lie)
		: "a" (lie), "b" (to), "c" (me)
		: "edx"
	);
	printf("lie*to*me=%d\n",lie);
	return 0;
};


Przyjrzyjmy się wszystkiemu po kolei. Program ma pomnożyć trzy liczby i wypisać wynik.


	"mull %%ebx\n\t"
	"mull %%ecx"

Ponieważ operacja mnożenia 'mull %ebx' zapisuje wynik w edx:eax, nie można zapomnieć o tym by poinformować gcc, że dokonaliśmy zmiany w rejestrze edx. Argumentów oczekujemy w eax, ebx i ecx wynik znajdzie się w eax.

Zwróćmy uwagę, że na powyższym przykładzie widać, że przy stosowaniu Extended inline assembly, nazwy rejestrów należy poprzedzać przefiksem '%%'. Dlaczego? Niedługo się wyjaśni.


	: "=a" (lie)

Po wykonaniu naszego kodu wynik znajduje się w eax. Znak '=' jest obowiązkowy we wszystkich parametrach wynikowych.


: "a" (lie), "b" (to), "c" (me)

GCC ustawi nam przed wywołaniu kodu lie w eax, to w ebx i me w ecx.


	: "edx"

Ponieważ w tracie działania kodu wstawki rejestr edx zostanie wyzerowany, zaznaczamy gcc, że już dłużej nie może liczyć na wartość którą w nim przechowywał.

Jeżeli nasz assemblerowy kod dokona zmiany w jakiejś zmiennej znajdującej się w pamięci, należy dodatkowo dodać do zmienionych rejestrów symbol "m" (memory), co oznajmi gcc, że nie może liczyć na zmienną z pamięci która była wczytana do pewnego rejestru. Ponieważ gcc stosuje również optymalizację przy operacjach porównania i skoku, korzystając z aktualnego ustawienia flag, jeśli dokonamy ich zmiany (operacją porównania) należy dołączyć kolejny symbol "cc" (condition codes).


3.3 Używanie dynamicznie przydzielanych rejestrów


Dobrze, jak na razie wszystko działa. Zajmiemy się teraz usprawnieniami. W naszym przykładzie postanowiliśmy, że argumenty mają znaleźć się w eax, ebx i ecx. Lepszym rozwiązaniem będzie jeżeli pozwolimy GCC na samodzielne wybranie rejestrów, co umożliwi mu dalszą optymalizację kodu. Zrobimy to używając zależności "q", lub "r". Pozwolimy w ten sposób by GCC sam wybrał rejestry z których będziemy korzystać. Przy zależności "q" wybierze spośród eax, ebx, ecx, edx, "r" pozwoli mu wziąść również pod rozwagę rejestry esi, edi. By skorzystać z arbitralnie ustawionych rejestrów będziemy odwoływać się do nich wpisując w kodzie w miejsce ich wystąpienia %0, %1, %2... %<nr>, gdzie nr jest jednym z numerów przypisanych zmiennym w kolejności w jakiej się pojawiały w parametrach. Czyli pierwszej zmiennej jaka pojawiła się w parametrach używamy pisząc %0, drugiej %1 itd...


OK, pewnie pojawiły się już jakieś wątpliwości... powinna je rozwiać kontynuacja przykładu: inline2.c.


#include <stdio.h>

int main(void) {
	unsigned long lie=2,to=7,me=3;
	__asm__ (
		"mull %2\n\t"
		"mull %3"
		: "=a" (lie)
		: "a" (lie),"r" (to),"r" (me)
		: "edx"
	);
	printf("lie*to*me=%d\n",lie);
	return 0;
};


Przykład działa, co można sprawdzić. Warto zauważyć, że umieszczenie edx w rejestrach modyfikowanych zabroni GCC użycie go do przechowywania wartości 'to', lub 'me'. By się o tym przekonać polecam zamianę odpowiedniej linii kodu na:

: "a" (lie),"d" (to),"r" (me)



3.4 Symbol 'volatile'

Pozostaje jeszcze powiedzieć, że mamy możliwość zabronić GCC modyfikację naszego kodu, jaka normalnie odbywa się w ramach optymalizcji. Możliwe, że na przykład zależy nam na tym by nasze instrukcje koniecznie wykonały się bezpośrednio po sobie (powiedzmy, że na czas ich wykonania blokujemy przerwania komendą cli). W tym celu wstawka assemblerowa będzie miała formę:

asm volatile (...);

lub równoważnie:

__asm__ __volatile__ (...);

Kod assemblera tutaj umieszczony nie zostanie poddany optymalizacji i zostanie zachowany w takiej postaci w jakiej został zapisany.


4.Literatura

Jako źródło wiedzy na powyższy temat polecam obejrzenie strony:

http://linuxassembly.org

znaleźć tam można tony materiałów dotyczących assemblera w linuksie. Oczywiście w ostateczności zawsze pozostaje nam:

info nasm- assembler korzystający z notacji Intel.

info as- GNU assembler.

info gcc- wytrwali mogą dokopać się do materiałów dotyczyących łączenia C z kodem assemblera.

5.Materiały wykorzystane

Przy tworzeniu powyższej prezentacji korzystałem z wielu opracowań, do których odnośniki znaleźć można na http://linuxassembly.org. Były to:

    Linux Assembly HOWTO autor: Konstantin Boldyshev
    	(stąd też pochodzi wykorzystany przezemnie przykład 'hello.html')
    Brennan's Guide to Inline Assembly autor: Brennan "Bas" Underwood
    Linux Assembly "Hello World" Tutorial, CS 200 autor: Bjorn Chambless
    Introduction to GCC Inline Asm autor: Robin Miyagi
	http://linuxprogramming.c2c2c.net/linux-programming
    Using Assembly Language in Linux autor: Phillip
	(niestety materiał ten zawiera wiele błędów i nieścisłości)