Asembler i C - standard cdecl w 32 bit
- Wprowadzenie
- Standard cdecl
- Kompilacja na Linuxie 64-bitowym
- Wywołanie funkcji języka C z asemblera
- Wywołanie z C funkcji zaimplementowanej w asemblerze
- Zadania
Wprowadzenie
Pisanie większych programów wyłącznie w asemblerze jest bardzo nieefektywne. Dlatego najczęściej asembler jest używany do tworzenia wybranych funkcji, wołanych z programów pisanych w językach wysokiego poziomu. Aby takie wywołania (np. pomiędzy asemblerem a C) poprawnie działały, należy przestrzegać konwencji wywołania funkcji przyjętych w odpowiednich standardach (definiujących sposób przekazywania argumentów, odbioru wyniku, zachowywania rejestrów itd). Standardy te różnią się w zależności od architektury procesora, systemu operacyjnego, języka programowania (czy nawet jego wersji).
Standard cdecl
Konwencja cdecl (stosowana przez język C w trybie 32-bitowym) w wielkim skrócie:
- Argumenty są przekazywane do funkcji za pośrednictwem stosu i umieszczane w kolejności od prawej do lewej (RLO).
- Wartość w zależności od typu i rozmiaru zwracana jest w rejestrze
EAX
: wskaźniki oraz liczby całkowite np.int
,char
,short
EDX:EAX
: małe strukturyST0
: liczby zmiennoprzecinkowe:float
,double
,long double
- Rejestry
EBX
,ESI
,EDI
,EBP
,CS
,DS
,SS
,ES
po wyjściu z funkcji powinny mieć niezmienione wartości.
Więcej szczegółów można znaleźć w materiałach z wykładu.
Kompilacja na Linuxie 64-bitowym
Aby kompilować programy 32 bitowe w systemie Linux 64 bitowym przeważnie trzeba doinstalować odpowiednie 32 bitowe pakiety gcc-multilib
i lib32stdc++-XX-dev
(gdzie XX to wersja kompilatora g++). Przykładowo w Ubuntu
sudo apt install gcc-multilib lib32stdc++10-dev
Kompilując i linkując musimy dodawać odpowiednie opcje -felf32
i -m32
informujące, że chcemy otrzymać kod binarny 32 bitowy. Przykładowo dla plików zródłowych main.cpp i funkcja.asm mamy
g++ -m32 -c main.cpp -o main.o
nasm -felf32 funkcja.asm -o funkcja.o
g++ -m32 main.o funkcja.o -o main
Wywołanie funkcji języka C z asemblera
Na Listingu 1 pokazano kod programu asemblerowego wywołującego funkcję printf
.
W Windows nazwy funkcji poprzedzane są znakiem _. Aby skompilować przykłady należy dodać podkreślenia do etykiet main
i printf
.
Listing 1 : c_asm.asm
; KOMPILACJA: plik źródłowy c_asm.asm
; nasm -o c_asm.obj -felf c_asm.asm
; gcc -m32 -no-pie c_asm.obj -o c_asm
section .text
extern printf ; definicja funkcji printf znajduje się w bibliotece standardowej C
global main
main: ; punkt wejścia - funkcja main
enter 0, 0
; printf("Liczba jeden to: %d\n", 1);
push dword 1 ; drugi argument
push dword napis ; pierwszy argument
; UWAGA: kolejność argumentów RLO (od prawej do lewej)
call printf ; uruchomienie funkcji
add esp, 2*4 ; posprzątanie stosu - rejestr ESP wskazuje to samo,
; co przed wywołaniem funkcji printf
xor eax, eax ; return 0;
leave
ret ; wyjście z programu
section .data
napis: db "Liczba jeden to: %d", 10, 0
Wywołanie z C funkcji zaimplementowanej w asemblerze
Na Listingu 2 pokazano kod programu napisanego w C, wołającego funkcję asemblerową przedstawioną na Listingu 3.
Listing 2 main.c
// KOMPILACJA - kod źródłowy C w main.c, kod źródłowy ASM w suma.asm
// LINUX :
// nasm -felf32 suma.asm -o suma.o
// gcc -m32 -no-pie -o main.o -c main.c
// gcc -m32 -no-pie main.o suma.o -o suma
#include <stdio.h>
int suma (int a, int b); /* prototyp funkcji */
int main(){
int c=1, d=2;
scanf("%d %d",&c,&d);
printf("%d\n", suma(c,d));
return 0;
}
Listing 3 : suma.asm
BITS 32
section .text
global suma ; funkcja suma ma być widziana w innych modułach aplikacji
suma:
enter 0, 0 ; tworzymy ramkę stosu na początku funkcji
; ENTER 0,0 = PUSH EBP / MOV EPB, ESP
; po wykonaniu enter 0,0
; w [ebp] znajduje się stary EBP
; w [ebp+4] znajduje się adres powrotny z procedury
; w [ebp+8] znajduje się pierwszy parametr,
; w [ebp+12] znajduje się drugi parametr
; itd.
; pomocnicze makrodefinicje
%idefine a [ebp+8]
%idefine b [ebp+12]
; tu zaczyna się właściwy kod funkcji
mov eax, a
add eax, b
; tu kończy się właściwy kod funkcji
leave ; usuwamy ramkę stosu LEAVE = MOV ESP, EBP / POP EBP
ret ; wynik zwracany jest w rejestrze eax
Zadania
Zadanie 1
Napisać w assemblerze funkcję main, która wczytuje dwie liczby całkowite ze znakiem przy użyciu funkcji scanf z biblioteki standardowej języka C i wypisujący na ekran ich iloraz przy użyciu funkcji printf. Należy przysłać tylko plik ASM.
Input | Output |
---|---|
2 1 | 2 |
-4 -2 | 2 |
-6 2 | -3 |
Zadanie 2
Napisać aplikację 32 bitową wyliczającą iloczyn elementów tablicy liczb int.
Aplikacja ma być złożona z dwóch modułów:
- w C (inicjalizacja tablicy, operacje wejścia/wyjścia),
-
w ASM (funkcja licząca iloczyn) parametrami funkcji są ilość elementów tablicy i wskaźnik na pierwszy element tablicy.
int iloczyn(int n, int * tab);
Zadbaj o to aby funkcja iloczyn
była zgodna ze standardem cdecl.
Zadanie 3
Napisać funkcję o nagłówku
void sortuj( int * a, int *b, int * c);
sortującą malejąco wartości trzech podanych zmiennych. Po wywołaniu funkcji wartości zmiennych powinny zostać odpowiednio pozamieniane.
Na przykład
int x=5, y=3, z=4;
sortuj( &x, &y, &z);
printf(" %d %d %d \n", x, y, z);
powinno wypisać
5 4 3
Zadanie 4
Napisać moduł asemblerowy implementujący 32 bitowyą funkcję minmax
MinMax minmax( int N, ...);
wyliczającą minimalny i maksymalny spośród argumentów funkcji. Pierwszym argumentem funkcji jest liczba całkowita N>0, po której następuje N argumentów całkowitych.
Wyniki mają być zwracane jako struktura MinMax.
typedef struct{
int min;
int max;
} MinMax;
Sposób zwracania zależy od systemu operacyjnego i wersji kompilatora:
- Linux: Jako pierwszy argument funkcji minmax zostanie przekazany wskaźnik na obiekt typu MinMax, który należy uzupełnić.
- Windows, starsze gcc pod Linuxem: Struktura MinMax mieści się w sumie rejestrów EDX:EAX i tam powinna być zwrócona.
Aplikacja ma być złożona z dwóch modułów:
- w C (operacje I/O, wywołanie funkcji),
- w ASM implementacja funkcji minmax (tylko ten plik przesyłamy jako rozwiązanie).
#include <stdio.h>
typedef struct{
int min;
int max;
} MinMax;
int main(){
MinMax wynik = minmax(7, 1, -2, 4 , 90, 4, -11, 101); // -11 101
printf("min = %d, max = %d \n", wynik.min, wynik.max);
wynik = minmax(5, 1, -2, 4 , 90, 4, -11, 101); // -2 90
printf("min = %d, max = %d \n", wynik.min, wynik.max);
wynik = minmax(1, 0); // 0 0
printf("min = %d, max = %d \n", wynik.min, wynik.max);
return 0;
}