1. Dostęp do funkcji języka C z poziomu Asemblera
Aby wywołać funkcję napisaną w C z kodu Asemblerowego należy umieścić jej argumenty całkowite (liczby lub wskaźniki na adresy w pamięci) kolejno w rejestrach RDI, RSI, RDX, RCX, R8, R9, argumenty zmiennoprzecinkowe w rejestrach XMM0-XMM7 i wywołać tą funkcję korzystając z rozkazu call
. Jeśli przekazujemy argumenty zmiennoprzecinkowe, wtedy do rejestru RAX musimy również wpisać ich ilość.
Przykład wywołania funkcji:
1 2 3 4 5 6 7 8 |
mov $1, %rax # Ilość argumentów zmiennoprzecinkowych # - przesyłany jest jeden parametr i znajdzie się on # w rejestrze XMM0 mov $25, %rsi # Pierwszy parametr - typu całkowitego mov $75, %rdi # Drugi parametr - typu całkowitego movss liczba, %xmm0 # Trzeci parametr - typu zmiennoprzecinkowego # skopiowany z 4-bajtowej komórki w pamięci call funkcja # Wywołanie funkcji |
Kod należy także odpowiednio skompilować i zlinkować. Możemy wykonać tą operację jednocześnie korzystając z polecenia:
gcc kod_asm.s kod_c.c -o plik_wykonywalny -g
W kodzie Asemblerowym możemy korzystać także z innych funkcji C np. z biblioteki stdio.h
.
W ramach zadania z zajęć należało wczytać od użytkownika dwie liczby – całkowitą i zmiennoprzecinkową, przekazać je do funkcji napisanej w C zwracającej wynik działania a^2+b^3
, gdzie a
i b
to kolejne liczby – całkowita i zmiennoprzecinkowa, a następnie korzystając z funkcji printf wyświetlić ten wynik.
Kod źródłowy w C:
1 2 3 4 |
float funkcja(int a, float b) { return a*a+b*b*b; } |
Kod źródłowy w Asemblerze:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
.data format_d: .asciz "%d", # Łańcuchy znaków wykorzystywane do format_f: .asciz "%f", # wywoływania funkcji scanf i printf nowa_linia: .asciz "\n" .bss .comm liczba1, 4 # Bufory na liczby typu integer i float .comm liczba2, 4 .text .global main main: # # Pobranie od użytkownika pierwszej liczby - typu całkowitego # # Odpowiednik poniższego kodu w C: scanf(&liczba1, "%d"); mov $0, %rax # Przesyłamy 0 parametrów zmiennoprzecinkowych mov $format_d, %rdi # Pierwszy parametr całkowity dla scanf # - format w jakim ma zostać zapisany # wynik w buforze mov $liczba1, %rsi # Drugi parametr całkowity dla scanf # - adres bufora do które zapisany # ma zostać wynik call scanf # Wywołanie funkcji scanf z biblioteki stdio.h # # Pobranie od użytkownika drugiej liczby - typu zmiennoprzecinkowego # mov $0, %rax # Przesyłamy 0 parametrów zmiennoprzecinkowych mov $format_f, %rdi # Pierwszy parametr całkowity dla scanf # - format w jakim ma zostać zapisany # wynik w buforze mov $liczba2, %rsi # Drugi parametr całkowity dla scanf # - adres bufora do które zapisany # ma zostać wynik call scanf # Wywołanie funkcji scanf z biblioteki stdio.h # # Wywołanie funkcji napisanej w C z parametrami # stało i zmiennoprzecinkowymi # mov $1, %rax # Ilość argumentów zmiennoprzecinkowych # - przesyłany jest jeden parametr w rejestrze XMM0 # jeśli było by ich więcej musiały by one zostać # umieszczone w kolejnych rejestrach XMM mov $0, %rdi # Czyszczenie rejestru RDI - do jego młodszych # czterech bajtów wpisana zostanie wartość # wczytanej liczby typu int mov $0, %rcx # Licznik na potrzeby adresacji pamięci niżej mov liczba1(, %rcx, 4), %edi # Przeniesienie pierwszego parametru # - typu całkowitego do rejestru RDI movss liczba2, %xmm0 # Przeniesienie drugiego parametru # - typu zmiennoprzecinkowego do rejestru XMM0 call funkcja # Wywołanie funkcji cvtps2pd %xmm0, %xmm0 # Konwersja wyniku na double aby możliwe było # wyświetlenie go przez funkcje printf # # Wyświetlenie wyniku z użyciem funkcji printf # mov $1, %rax # Przesyłamy jeden parametr zmiennoprzecinkowy # - liczbę do wyświetlenia (w rejestrze XMM0) mov $format_f, %rdi # Pierwszy parametr typu całkowitego # - format w jakim wyświetlona ma zostać liczba sub $8, %rsp # Workaround, aby printf nie zmienił wartości # ostatniej komórki na stosie. Jest to potrzebne tylko # przy wyświetlaniu liczb zmiennoprzecinkowych. # Wskaźnik na stos należy przesunąć o wielokrotność # liczby 8 równą ilości parametrów ZP (8*RAX). call printf # Wywołanie funkcji printf add $8, %rsp # Workaround -||- # # Wyświetlenie znaku nowej linii # mov $0, %rax # Nie przesyłamy żadnych parametrów ZP mov $nowa_linia, %rdi # Pierwszy parametr typu całkowitego # - wskaźnik na ciąg znaków do wyświetlenia # - znak nowej linii call printf # Wywołanie funkcji printf # # Zwrot wartości EXIT_SUCCESS # mov $0, %rax # Brak parametrów zmiennoprzecinkowych call exit |
2. Wstawka Asemblerowa w kodzie C
Wstawki w kodzie C wykonuje się korzystając ze słowa kluczowego asm()
. W kolejnych liniach, w cudzysłowach, wpisuje rozkazy Asemblerowe. Każdą z linii zawierających rozkaz musi zostać ujęta w cudzysłowy oraz kończyć się znakiem nowej linii – \n
, przed zamknięciem cudzysłowu. W następnych liniach, zaczynając od dwukropka podajemy kolejno: zmienne do których skopiowana zostanie wartość z rejestrów po zakończeniu wstawki, zmienne których wartości trafią do rejestrów przed wykonaniem rozkazów oraz rejestry których będziemy używać.
Wartości wejściowe i wyjściowe umieszczone będą w rejestrach wybranych przez kompilator. Aby uzyskać do nich dostęp należy skorzystać z aliasów, wpisując znak %
oraz numer parametru – np. %5
. Aby uzyskać dostęp do rejestrów korzystając z ich nazw należy użyć podwójnego znaku %
oraz nazwy rejestru – np. %%RAX
.
Dostęp do stałych zadeklarowanych wcześniej w kodzie jest możliwy wprost po podaniu ich nazwy.
Poniżej kod z zajęć i zarazem przykład wykorzystania kodu Asemblerowego wewnątrz kodu C. Program ma za zadanie zmienić wielkość liter (na małe) we wcześniej zadeklarowanej zmiennej typu string.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#include <stdio.h> // Deklaracja zmiennej przechowującej ciąg znaków do konwersji char str[] = "AbCdEfGh"; // Stała przechowująca długość tego ciągu const int len = 8; int main(void) { // // Wstawka Asemblerowa // asm( "mov $0, %%rbx \n" // Zerowanie rejestru RBX - licznika do pętli. // Każdy mnemonik rejestru należy poprzedzić znakami %%. "petla: \n" // Etykieta powrotu do pętli "mov (%0, %%rbx, 1), %%al \n" // Skopiowanie n-tej komórki stringa // do rejestru Al. %0 to alias rejestru w którym kompilator C umieści // pierwszy parametr wejściowy (wskaźnik na pierwszą komórkę stringa). "and $223, %%al \n" // Wyzerowanie 5 bity kodu znaku ASCII // (zamiana na duża literę) "add $32, %%al \n" // Dodanie do kodu litery wartości 2^5 // (zamiana na małą literę) "mov %%al, (%0, %%rbx, 1) \n" // Zapisanie zmienionej wartości do stringa "inc %%rbx \n" // Zwiększenie licznika pętli "cmp len, %%ebx \n" // Porównanie licznika pętli ze stałą "len" // zadeklarowaną w kodzie C "jl petla \n" // Powrót na początek pętli aż do wykonania operacji // dla każdego znaku ze stringa : // Nie mamy żadnych parametrów wyjściowych. Jeśli by takie były // należało by je zadeklarować podobnie jak w lini poniżej, jednak // zamiast "r" należało by użyć "r=". Spowodowało by to przeniesienie // wartości z rejestru oznaczonego w kodzie jako %0, %1 itp. do zmiennej // po wykonaniu wstawki. :"r"(&str) // Lista parametrów wejściowych - zmiennych które zostaną // zapisane do rejestrów i będzie możliwy ich odczyt w kodzie Asemblerowym. // Podobnie jak wyżej - są one dostępne jako aliasy na rejestry - %0, %1 itp. :"%rax", "%rbx" // Rejestry których będziemy używać w kodzie Asemblerowym. ); // // Wyświetlenie wyniku // printf("Wynik: %s\n", str); // // Zwrot wartości EXIT_SUCCESS // return 0; } |
3. Dostęp do funkcji Asemblerowych z kodu w języku C
Aby móc używać funkcji napisanych w innym języku i dołączonych dopiero na etapie kompilacji, musimy zapowiedzieć ich ich dołączenie, podać typ zawracany oraz typy przyjmowanych parametrów. Służy do tego słowo kluczowe extern
za którym umieszcza się typowy prototyp funkcji:
1 |
extern int nazwa_funkcji(int parametr_pierwszy, float parametr_drugi); |
W języku Asembler funkcję również należy zadeklarować na początku kodu (w sekcji text) korzystając z poniższej składni:
1 2 3 |
.global nazwa_funkcji1, nazwa_funkcji2 .type nazwa_funkcji1, @function .type nazwa_funkcji2, @function |
Następnie należy umieścić etykiety o nazwach identycznych jak nazwy funkcji i kończących się rozkazem ret
. Parametry wywołania z C zostaną umieszczone w rejestrach opisanych w punkcie pierwszym. Wartość zwracaną należy umieścić w rejestrze RAX w przypadku wartości całkowitych lub XMM0 w przypadku wartości zmiennoprzecinkowych.
Kod źródłowy w C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> // Deklaracja funkcji które dołączone zostaną // do programu dopiero na etapie linkowania kodu extern void szyfr_cezara(char * str, int len); // Deklaracja zmiennych char txt[] = "aBcDefGG"; int len = 8; int main(void) { // Wywołanie funkcji Asemblerowej szyfr_cezara(&txt, len); // Wyświetlenie wyniku printf("Wynik: %s\n", txt); // Zwrot wartości EXIT_SUCCESS na wyjściu programu return 0; } |
Kod źródłowy w Asemblerze:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
.data .text # Zadeklarowane tutaj funkcje będą możliwe do wykorzystania # w języku C po zlinkowaniu plików wynikowych kompilacji obu kodów .global szyfr_cezara .type szyfr_cezara, @function # # Funkcja szyfrująca podany ciąg znaków szyfrem cezara # z przesunięciem o 3 # # Deklaracja w C: szyfr_cezara(&txt, len); # szyfr_cezara: # Odłożenie rejestru bazowego na stos i skopiwanie obecnej # wartości wskaźnika stosu do rejestru bazowego push %rbp mov %rsp, %rbp # Parametry wywołania funkcji umieszczone zostaną # w rejestach RDI i RSI. # * W rejestrze RDI znajdzie się wskaźnik na pierwszą # komórkę stringa. # * W rejestrze RSI znajdzie się długość tego stringa. # Pętla wykonująca się dla każdego znaku mov $0, %rax petla_glowna: # Skopiowanie n-tego znaku stringa do rejestru BL mov (%rdi, %rax, 1), %bl # Wykonanie szyfrowania cmp $'Z', %bl jle duze male: add $3, %bl cmp $'z', %bl jle zapisz sub $26, %bl jmp zapisz duze: add $3, %bl cmp $'Z', %bl jle zapisz sub $26, %bl # Zapis zaszyfrowanego znaku do stringa zapisz: mov %bl, (%rdi, %rax, 1) # Instrukcje sterujące dla pętli inc %rax cmp %rsi, %rax jl petla_glowna # Przywrócenie poprzedniej wartości rejestru bazowego # i wskaźnika stosu mov %rbp, %rsp pop %rbp ret # Powrót do miejsca wywołania funkcji |
Specjalne podziękowania dla Mateusza Gniewkowskiego za pomoc w przygotowaniu tego wpisu :-)