Asembler x86/Łączenie z językami wysokiego poziomu/Moduły w języku C++

Wprowadzenie

edytuj

W poniższym przykładzie użyjemy kompilatora GCC na systemie GNU/Linux oraz asemblera NASM. Napiszemy program, który będzie po prostu dodawał dwie liczby i wyświetlał wynik. Moglibyśmy oczywiście napisać bardziej poważną i rozbudowaną funkcję modułu w Asemblerze, ale nie chodzi o to co kodujemy ale jak. Program w C i C++ będzie pobierał od użytkownika dwie zmienne a następnie wywoływał funkcje dodawania z modułu napisanego w NASMie po czym wyświetli zwrócony wynik. Nasz moduł połaczymy z programem na dwa sposoby: linkowaniem statycznym oraz dynamicznym czyli zbudujemy bibliotekę współdzieloną (na GNU/Linuksie rozszerzenie *.so zaś na Microsoft Windowsie plik *.dll)

Moduł w Asemblerze

edytuj

Nasz kod będzie wyglądał następująco:

section .text

global sumuj

sumuj:
	;WPROWADZENIE:
	;[EBP+12] - pierwszy argument od prawej
	;[EBP+8] - drugi od prawej
	;[EBP+4] - adres powrotu z funkcji
	;[EBP] - stara wartość EBP
	
	;PRZYKŁAD DEKLARACJI FUNKCJI W C++ (przy linkowaniu statycznym)
	;int sumuj(int a, int b), zatem:
	;[EBP+12] = b
	;[EBP+8]  = a

	push ebp 	 ; tworzymy ramkę stosu
	mov ebp,esp      ; niech EBP ma teraz wartość ESP

	mov eax,[ebp+12] ; EAX=b
	add eax,[ebp+8]  ; EAX=EAX+a , zatem ostatecznie EAX=a+b

	mov esp,ebp 	 ; niszczmy ramkę 
	pop ebp     	 ; przywróć starego EBP
	ret		 ; koniec funkcji
; zwracana wartość funkcji przekazywana jest do programu poprzez rejestr EAX

section .data
napis db "<<<FUNCJA SUMUJ>>>",0Ah
offset equ $ - napis

Moduł ten będzie wyglądał tak samo czy to dla linkowania statycznego czy dynamicznego. Nie będziemy do niego wprowadzać żadnych zmian w związku z sposobem łączenia z programem głównym. Komentarze w kodzie źródłowym zapewne wyjaśniają wszystkie rozkazy.

Kompilacja do modułu statycznego

edytuj

Musimy po prostu uzyskać plik wyjściowy o rozszerzeniu *.o. W tym celu wydamy następującą komendę w terminalu:

    nasm -f elf funsumuj.asm -o funsumuj.o

W tej postaci zostawiamy plik aby potem później zlinkować z programem głównym.

Tworzenie biblioteki współdzielonej

edytuj

Tutaj sprawa nieco się komplikuje ponieważ musimy użyć dużo bardziej zaawansowanych flag kompilacji oraz użyć dodatkowo linker ld. Najpierw tworzymy zwykły skompilowany plik o rozszerzeniu *.o a potem poddamy go "obróbce".

    ld -shared funsumuj.o -o libfunsumuj.so

Przełącznik -shared oznacza, że kod który chcemy uzyskać ma być współdzielony. Możemy użyć programu nm aby wylistować wszystkie symbole z pliku objektowego takiego jak na przykład właśnie plik .so. Oto przykładowy wynik działania nm:

     
    00001f6c a _DYNAMIC
    00001ff4 a _GLOBAL_OFFSET_TABLE_
    00002013 A __bss_start
    00002013 A _edata
    00002014 A _end
    00002000 d napis
    00000013 a offset
    000001b0 T sumuj

Po opis opcji odsyłam na manuala programu nm.

Moduł programu głównego

edytuj

Tutaj już nastąpi małe rozdrobnienie, gdyż kod źródłowy w C i C++ oczywiście będzie wyglądał inaczej oraz każdy z języków będzie linkowany z modułem na dwa sposoby.

Kod źródłowy w C++ (linkowanie statyczne)

edytuj

Zaletą linkowania statycznego jest niewątpliwie prostota i przejrzystość kodu wywołującego linkowany moduł. Niestety w przypadku jakiejkolwiek zmiany w tym module konieczne jest ponowne statyczne linkowanie całego pliku wykonywalnego.

Wyjaśnię tylko kluczowe fragmenty kodu. Zakładam naturalnie, że znasz choćby podstawy dotyczące języka C++. Oto i kod źródłowy:

#include <iostream>
using namespace std;

extern "C" unsigned int sumuj (unsigned int, unsigned int);


int main()
{
 unsigned int a,b;

	cout<<"Podaj liczbe a: ";
	cin>>a;

	cout<<"Podaj liczbe b: ";
	cin>>b;
	


	cout<<"Wynik dodawania to: "<<sumuj(a,b)<<endl;

	return 0;
}

Jedyna nadzwyczajna linia kodu do deklaracja naszej funkcji dodawania dwóch zmiennych. Po pierwsze słówko extern mówi kompilatorowi, że danego symbolu (czyli nazwy zmiennej,funkcji, klasy etc) ma szukać poza plikiem z kodem źródłowym. Zaś zapis "C" oznacza, że dana funkcja ma być traktowana jak z języka C. Dlaczego ? Zaraz wyjaśnie. Naszą funkcję w pliku źródłowym w Asemblerze zapisaliśmy jako "suma", bez żadnych znaków podkreślenia z przodu. Normalnie kompilator g++ szukałby funkcji "_sumuj", gdyż zgodnie z standardem C++ takie "ozdobniki" posiadają funkcje. Pro prostu na etapie kompilacji, kompilator sam sobie nadaje takie dodatki bez naszej wiedzy. Zatem musimy napisać "C" aby wymusić na g++ aby niczego nie dodawał przed nazwę funkcji.

Łączenie z modułem funsumuj.o

edytuj

Aby zlinkować musisz wydać w terminalu następujące polecenia. Najpierw kompilacja modułu programu głównego:

    g++ -c sumuj.cpp

Następnie trzeba zlinkować statycznie całość. Zatem wydajemy polecenie:

    g++ sumuj.o funsumuj.o -o sumuj

Teraz możemy uruchomić program wydając w konsoli komendę:

    ./sumuj

Jeśli otrzymamy komunikat o braku uprawnień do pliku, oznacza to że nie mamy prawa do wykonywania programu. Nadajemy je sobie poleceniem:

     chmod +x sumuj

Gotowe. Napisałeś, zasemblowałeś, skompilowałeś i zlinkowałeś statycznie swój program z zewnętrznym modułem napisanym w Asemblerze!

Kod źródłowy w C++ (linkowanie dynamiczne)

edytuj

Linkowanie dynamiczne kosztem niewielkiej komplikacji kodu pozwala niezależnie kompilować zewnętrzne moduły bez konieczności każdorazowego linkowania ich z głównym programem. W przypadku większych aplikacji jest to znacznym ułatwieniem, gdyż pozwala przykładowo na niezależną instalację aplikacji od wykorzystywanych przez nią bibliotek. Taki zewnętrzny moduł nazwiemy wówczas biblioteką współdzieloną (z ang. shared library).

Aby na Linuksie używać bibliotek współdzielonych należy użyć specjalnego interfejsu pomiędzy jądrem systemu a programami w przestrzeni użytkownika. Taki interfejs daje nam plik nagłówkowy dlfcn.h. Zatem kod programu uległ znacznej zmianie:

#include <iostream>
#include <dlfcn.h>
using namespace std;
int (*funkcja)(int, int);
void *Biblioteka;
const char* blad;



int main()
{
 unsigned int a,b,wynik;

	cout<<"Podaj liczbe a: ";
	cin>>a;

	cout<<"Podaj liczbe b: ";
	cin>>b;
	
 Biblioteka = dlopen("libfunsumuj.so", RTLD_LAZY);
 blad = dlerror();

 *(void **)(&funkcja) = dlsym(Biblioteka, "sumuj");
 wynik = (*funkcja)(a,b);
	cout<<"Wynik dodawania to: "<<wynik<<endl;
dlclose(Biblioteka);

	return 0;
}

Linkowanie dynamiczne z modułem libfunsumuj.so

edytuj

Najpierw musimy skompilować plik sumuj.cpp:

    g++ -c sumuj.cpp

W ten sposób mamy skompilowany, ale nie zlinkowany plik binarny (przełącznik -c wyłącza automatyczne linkowanie). Teraz część główna, czyli łączenie z biblioteką współdzieloną:

    g++ -o sumuj sumuj.o -L . -lfunsumuj -ldl

Krótko wyjaśnię opcje kompilatora. Przełącznik -L podaje ścieżkę do pliku *.so. W naszym przypadku zamiast np " -L /lib/mojabiblioteka" dajemy kropkę co oznacza po prostu bieżący katalog. Jak widzisz g++ (cały pakiet GCC właściwie) ucina sobie nazwy bibliotek, zatem zamiast "-libfunsumuj" musimy podać "-lfunsumuj". Jednak pozostała jeszcze jedna flaga kompilacji. Po prostu dołączamy bibliotekę od pliku dlfcn.h. Resztę bibliotek dołączy kompilator.

Teraz możemy uruchomić program. Ale coś nie poszło prawda ? Masz taki (lub podobny komunikat):

    ./sumuj: error while loading shared libraries: libfunsumuj.so: cannot open shared object file: No such file or directory

Dlaczego tak się stało ? Ano dlatego, że nasza biblioteka nie znajduje się na standardowej ścieżce bibliotek jak /usr/lib czy /lib. Zawsze przeszukiwane są te lokacje oraz zmienna systemowa LD_LIBRARY_PATH oraz plik /etc/ld.so.conf Możemy albo dodać jako root naszą bibliotekę do tamtych folderów albo jako root skonfigurować ten plik albo ustawić tą zmienną systemową na naszym koncie. Wybierzemy to ostatnie rozwiązanie. Zatem wydajemy poniższe polecenie, ustawiające ścieżkę na katalog bieżący:

    export LD_LIBRARY_PATH="$LD_LIBRARY_PATH: `pwd`"

Teraz możemy spokojnie uruchomić program sumuj i cieszyć się jego działaniem. Bardzo dobrym pomysłem będzie uruchomienie programu ldd i podanie następującej opcji:

    ldd sumuj

Teraz możemy zobaczyć z czym jest łączony dynamicznie nasz program. U Ciebie wynik polecenia może się różnić:

        linux-gate.so.1 =>  (0xffffe000)
        libdl.so.2 => /lib/libdl.so.2 (0xb7f56000)
        libfunsumuj.so (0xb7f53000)
        libstdc++.so.6 => /usr/lib/gcc/i686-pc-linux-gnu/4.1.2/libstdc++.so.6 (0xb7e6c000)
        libm.so.6 => /lib/libm.so.6 (0xb7e46000)
        libgcc_s.so.1 => /usr/lib/gcc/i686-pc-linux-gnu/4.1.2/libgcc_s.so.1 (0xb7e3a000)
        libc.so.6 => /lib/libc.so.6 (0xb7d09000)
        /lib/ld-linux.so.2 (0xb7f6e000)