Wstawki asemblerowe w C++

Składnia AT&T

    1. Zmieniona kolejność operandów : najpierw źródło potem cel

    2. OpCode źrodlo cel

    3. Nazwy rejestrów poprzedzamy % np. %eax, %ebx

    4. Stałe poprzedzamy znakiem $, stałe szesnastkowe przez $0x

    5. $1, $322, $0xffff

    6. Rozmiar operandów określamy dodając do OpCode przyrostek b (8 bit), w (16 bit), l (32 bit)

    7. movl zmienna, %ebc

    8. Adresowanie pamięci wykorzystuje () zamiast []

    9. movl (%ebx), %eax

    10. Adresowaniu skalowanemu w stylu Intela

    11. [base + index*scale + disp]

    12. w AT&T odpowiada

    13. disp(base, index, scale)

    14. np.

    15. movl %eax, -0xf4(%ebx, %ecx, 4)

    16. Tutaj stałych nie należy poprzedzać znakiem $.

Wstawki asemblerowe

Do umieszczania prostych wstawek używamy instrukcji

asm( "Instrukcje asemblerowe");

np.

asm("movl %%ecx, %%eax \n\t addl %%ebx, %%eax");

Powoduje to proste wklejenie danego tekstu (kodu asemblerowego) do pliku generowanego przez gcc,

bez żadnego sprawdzenia poprawności. Dlatego też konieczne jest np. wstawienie znaków nowej linii po każdej instrukcji.

Ogólna postać wstawki asemblerowej ma postać

asm ( "szablon instrukcji asemblerowych"

: wyjściowe operandy /* optional */

: wejściowe operandy /* optional */

: lista niszczonych rejestrów /* optional */

);

Operandy podaje się jako listę w elementów postaci "xxx" (wartosc), gdzie xxx określa miejsce umieszczenia wartości np.

"a" = eax, "b"=ebx, ..., "S" = esi, "D" = edi, "r" = jakiś rejestr, "m" = pamięć, "i" = stała całkowita, "f" = rejestr zmiennoprzecinowy, "t" = ST0, ...

Operandy te są dostępne pod literałami %0, %1, %2, ... (liczy się najpierw wyjście potem wejście)

Przed operandami wejściowymi dodatkowo dajemy znak = np. "=r" (wyjscie)

Przykład

int x=2, y=3, result;

asm( "addl %%ebx, %%eax" // % jest symbolem specjalnym dlatego nazwy rejestrów to np. %%eax

: "=a"(result) // wynik z rejestru eax nalezy umieścić w zmiennej result

: "a"(x), "b"(y) // na wejściu x będzie w rejestrze eax, a zmienna y w ebx

);

Ze względów optymalizacyjnych dobrze jest wybór rejestru do przekazywania parametrów pozostawić kompilatorowi,

można też przekazywać zmienne w pamięci.

int x=2, y=3, result;

asm( "movl %1, %%eax;"

"addl %2, %%eax;"

"movl %%eax, %0"

: "=r"(result) // wynik będzie zwrócony w rejestrze %0 i zapisany do zmiennej result

: "r"(x), // x będzie w którymś z rejestrów dostępnym pod nazwą %1,

"m"(y) // y będzie w pamięci pod adresem %2

: "eax" // informujemy kompilator, że nasz kod zmienia eax i kompilator nie może

); // juz ufać wartości tego rejestru

Więcej informacji można znaleźć na stronie: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

Obowiązkowo z tego zestawu należy zrobić zadanie 2 (oraz zadania z następnego zestawu).

Zadanie 1

Wykorzystując operacje łańcuchowe i przedrostek REP zaimplementuj w C++ wykorzystując wstawkę asemblerową funkcję

// kopiuje n liczb typu int z zrodla do celu

void kopiuj(int * cel, int * zrodlo, unsigned int n);

Zadanie 2

Napisz w C++ funkcję

char * dodaj(char * tab1, char * tab2, int n)

dodającą do siebie odpowiednie elementy dwóch tablic o rozmiarze n i zwracającą wynik w nowej tablicy utworzonej na stercie.

Dodawanie powinno być z wysyceniem i powinno wykorzystywać instrukcje SSE: PADDSB (Packed add with saturation bytes).

W tym celu użyj odpowiednią wstawkę asemblerową.

Zadanie 3

Napisz klasę Przedział o końcach typu double.

Posługując się wstawkami asemblerowymi zaimplementuj operatory + i - : dodający i odejmujący poprawnie dwa przedziały.

W ścisłych obliczeniach arytmetycznych zamiast liczb zmiennoprzecinkowych wykorzystuje się przedziały,

określając dla nich zwykłe operacje arytmetyczne w ten sposób, że wynikiem działania jest możliwie najmniejszy przedział,

który zawiera wszystkie możliwe wyniki pomiędzy elementami pierwszego i drugiego przedziału.

Np. dla dodawania [a,b] + [c,d] = [a+c, b+d].

Niestety liczby a+c i b+d mogą nie być reprezentowalne na komputerze, dlatego aby otrzymać własność zawierania musimy obliczyć a+c zaokrąglając w dół, a b+d zaokrąglając w górę. Można tego dokonać ustawiając przed dodawaniem odpowiednie flagi FPU tzw control word lub SSE (w zależności jakiego trybu używamy i na jakiej jednostce zamierzamy liczyć).

Niestety z poziomu C++ nie mamy dostępu do tych flag i musimy to zrobić z poziomu asemblera.

Do zmiany control word służą instrukcje

fstcw mem - zapisuje w pamięci control word

fldcw mem - wczytuje z pamięci control word

Za zaokrąglanie są odpowiedzialne bity 11 i 12

00 - zaokrąglanie do najbliższej

01 - zaokrąglanie w dół (wartość 0x0400)

10 - zaokrąglanie w górę (wartość 0x800)

#include <iostream> using namespace std; class Interval{ double left, right; public: Interval(double left, double right) : left(left), right(right){ } double inf() { return left; } double sup() { return right; } friend Interval operator+ (const Interval & a, const Interval &b); friend Interval operator- (const Interval & a, const Interval &b); } int main(){ Interval a(1.,1.); Interval b(1e-20,1e-20); Interval c = a + b; if( (c.inf() == c.sup()) or (c.sup() <= 1.0) or (c.inf() != 1.0)) cout << "Blad operatora +!\n"; c = a - b; if( (c.inf() == c.sup()) or (c.sup() != 1.0) or (c.inf() >= 1.0)) cout << "Blad operatora +!\n"; return 0; }