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:
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:
float funkcja(int a, float b)
{
return a*a+b*b*b;
}
Kod źródłowy w Asemblerze:
.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.
#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:
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:
.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:
#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:
.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 :-)