Asembler w kodzie linuxa | ||
---|---|---|
<<< Wstecz | Dalej >>> |
Najwygodniejszą metodą używania asemblera (gdy nie chcemy całego projektu pisać tak niskopoziomowo) jest wprowadzanie asemblerowych fragmentów do programów napisanych w innym języku. W kodzie jądra linuxa asembler umieszczany jest wewnątrz funkcji napisanych w C i takiemu rozwiązaniu poświęcony jest ten rozdział.
Dwa głowne problemy przy łączeniu asemblera z kodem w języku wyższego poziomu, to przekazanie danych i wyników, oraz zapobieganie wzajemnemu wymazywaniu zawartości rejestrów.
Język C udostępnia specjalną konstrukcję składniową, umożliwiającą wprowadzenie asemblera. Jej budowa jest następująca:
__asm__ { "instrukcja asemblera\n" "następna instrukcja\n" ... "ostatnia instrukcja\n" : wyjściowe zmienne (opcjonalne) : wejściowe wartości (opcjonalne) : niszczone rejestry (opcjonalne) }; |
Podkreślenia wokół asm nie są konieczne, ale pomagają uniknąć konfliktu nazw. Instrukcje asemblera są zwykłymi napisami których konkatenację kompilator C po wstępnym przetworzeniu przekaże do kompilatora asemblera.
Do kompilacji wstawek asemblerowych kompilator gcc używa Gnu Assembler'a (gas) , stosującego składnię AT&T. Nie wymagne są żadne dodatkowe parametry.
Poniższy kod zamienia wartości zmiennych 'x' i 'y' (typu int):
__asm__ { "movl %2, %%eax\n" //kopiujemy "x wejściowe" do eax "movl %3, %0\n" //kopiujemy "y wejściowe" od razu do "x wyjściowego" "movl %%eax, %1\n" //kopiujemy eax do "y wyjściowego" : "=r"(x),"=r"(y) //wykona się 7 kopiowań! : "r"(x),"r"(y) : "%eax" //podczas instrukcji __asm__ "przepadła" zawartość eax }; |
W instrukcjach asemblera odwołujemy się do argumentów wejściowych i wyjściowych za pomocą ich numerów porządkowych poprezedzonych '%'. Kompilator przerabia napisy zawierające instrukcje asemblera, napotykając znak % próbuje zinterpretować to co się za nim znajduje. Jeśli jest to nazwa rejestru (np. %eax, stąd piszemy %%eax), jest ona przekazywana wprost. Jeśli jednak jest to liczba, do asemblera przekazywana jest lokalizacja odpowiedniego parametru instrukcji __asm__ (nazwa rejestru, lub adres w pamięci).
Elementy list wartości wejściowych i zmiennych wyjściowych oddzielamy przecinkami. Każdy element ma określony sposób przekazywania.
"r" - za pomocą dowolnego rejestru
"m" - poprzez adres w pamięci
"a", "b", "c", "d", "S", "D" - w rejestrach eax, ebx, ecx, edx, esi lub edi (lub odpowiednich mniejszych). Gcc po wielkości argumentu rozpozna jak duży rejestr jest potrzebny.
Oczywiście, poprzez wymuszenie użycia konkretnych rejestrów do przechowania parametrów można zamianę wartości zmiennych zapisać prościej.
Kompilator uważa, że dane wejściowe będą odczytywane przed zapisaniem wyników i chętnie używa do obydwu tych rzeczy tych samych rejestrów. Można wyrazić chęć zachowania wejściowych wartości aż do końca wykonywania bloku asemblera poprzez dopisanie & po znaku równości przy parametrze wyjściowym. Nadmierne stosowanie tego oznaczenia może jednak prowadzić do wyczerpania ilości rejestrów.
Lista rejestrów o niszczonej zawartości jest podwójne istotna. Mówi ona kompilatorowi C, aby:
nie używał tych rejestrów do przechowywania argumentów __asm__ opatrzonych parametrem "r"
po instrukcji __asm__ nie zakładał niczego o zawartości tych rejestrów...
Gdy w instrukcj asemblera chcemy skorzystać z adresu zmiennej to piszemy jej nazwę poprzedzonąznaiem $. Na przykład:
__asm__("movl $xxx, %%eax":::"%eax"); |
umieści adres zmiennej xxx w rejestrze %%eax. Rejestr ten został wskazany jako zmieniany. Operacja ta powiedzie się jednak tylko dla globalnych zmiennych statycznych, których adres wirtualny znany będzie już w trakcie kompilacji. Dla pozostałych zmiennych niezbędne jest korzystanie z list wartości wejściowych i zmiennych wyjściowych.
Przydatne jest pisanie __volatile__ po __asm__, dla zachowania kontroli nad tym, co robi, a czego nie robi nasz asembler. Dla gcc oznaczać to będzie nieoptymalizowanie tego fragmentu kodu. Gcc może uznać, że nasz asembler tutaj nic istotnego nie wnosi (czytaj: nie zmienia wartości zmiennej, ani nie wywoluje funkcji) i usunąć go z końcowego kodu.
Gdy chcemy mięć dobrą kontrolę nad kształtem kodu wykonywalnego, ale nie zależy nam na pisaniu go od podstaw w asemblerze, możemy wygenerować kod z kompilatora wyższego poziomu do asemblera.
Standardowym sposobem jest skompilowanie z flagą "-s" ( w gcc "-S"). Bardziej przejrzysty kod można uzyskać dodając jeszcze flagę "-fverbose-asm". Isntnieje jeszcze wiele rożnych opcji, właściwych poszczególnym kompilatorom.
W przypadku gcc kod generowany przez kompilator będzie wyrażnie oddzielony komentarzami od wprowadzonego za pomocą instrukcji asm.
<<< Wstecz | Spis treści | Dalej >>> |
Okna rejestrów | Przykłady |