Kompilacja, linkowanie, biblioteki statyczne i dynamiczne

Wielu programistów, mimo że bardzo dobrze obeznanych z pisaniem programów, nie do końca orientuje się, co za magia stoi za linkowaniem bibliotek zewnętrznych do ich aplikacji, a także jest trochę wystraszona, gdy ma takową bibliotekę dołączyć do swojego projektu, gdy nie ma dokładnego opisu jak to zrobić.

Zacznijmy od bardzo prostego programu.

#include "stdio.h"
void main()
{
	printf("Hello World!");
}

Gdybyśmy chcieli skompilować na Linuxie powyższy kod, to zastosowalibyśmy takie oto polecenie.

$ gcc -o hello hello.c

Powyższe polecenie wygenerowałoby plik wykonywalny o nazwie hello (na Linuxie pliki wykonywalne najczęściej nie mają rozszerzenia). Plik wykonywalny jest następstwem 3 czynności.

1. Sprawdzenia poprawności kodu.
2. Kompilacji. Podczas tego procesu kompilator generuje plik obiektowy zawierający kod maszynowy odpowiadający naszemu kodowi źródłowemu. Wszystkie odwołania do funkcji zewnętrznych (w naszym przypadku to odwołanie do funkcji printf) zostają odpowiednio wyróżnione.
3. Linkowanie. Teraz linker (w Linuxie nazywa się on ld) próbuje odnaleść zastosowane przez nas funkcje zewnętrzne w dostępnych bibliotekach. Nasza funkcja printf zostaje odnaleziona w bibliotece statycznej libc.a (lub dynamicznej libc.so) na Linuxie lub libc.lib (lub libc.dll) na Windowsie.

Powyższą operację kompilacji i linkowania można rozbić na 2 czynności.

gcc -c hello.c
ld -lc -o hello hello.c

Najpierw przeprowadzamy samą kompilację (parametr -c) pliku hello.c. Następnie linkujemy plik obiektowy wraz z dołączeniem biblioteki libc.a (parametr -lc).

Ten rodzaj linkowania nazwy się linkowaniem statycznym. Kod z biblioteki zostaje wpleciony w plik wykonywalny czyniąc jego rozmiar większym. Dodatkowo, przy uruchamianiu cały ten kod zostaje załadowany do pamięci zwiększając ilość pamięci jaką wykorzystuje program.

Konieczne było wypracowanie innej metody używania bibliotek, który by nie powodował takiego wzrostu koniecznego miejsca na dysku i pamięci. Dodatkowo mechanizm ten powinien rozwiązywać problem ze stosowaniem nowszych wersji bibliotek bez konieczności ponownej kompilacji programu. Taki mechanizm nazywa się dynamicznym linkowaniem.

Istnieją dwa sposoby dynamicznego linkowania.

Pierwszy sposób to niejawne linkowanie dynamiczne z zastosowaniem biblioteki importu (import library). W takim przypadku potrzebujemy zarówno biblioteki dynamicznej (*.dll, *.so), która zawiera funkcje, które chcemy użyć. Dodatkowo, potrzebujemy biblioteki importu (która ma rozszerzenie identyczne, jak biblioteka statyczna (*.lib, *.a). Linker linkuje plik obiektowy razem z bibioteką importu, która to dostarcza wskaźniki na odpowiednie funkcje w bibiotekach dynamicznych. Podczas uruchamiania aplikacji, biblioteki dynamiczne są ładowane do pamięci, a wskaźniki w pliku wykonywalnym naszego programu są zastępowane rzeczywistymi adresami pamięci, gdzie znajdują się załadowane funkcje z biblioteki dynamicznej. Warte uwagi jest to, że przy tym rodzaju dynamicznego linkowania, nasz program się nie uruchomi, gdy brakuje odpowiedniej biblioteki dynamicznej.

Drugi sposób to jawne linkowanie dynamiczne w trakcie działania programu (run-time dynamic linking). W takim przypadku nie jest potrzebna ani biblioteka importu, ani biblioteka statyczna. Jedyne co jest potrzebne to biblioteka dynamiczna. W jawnym linkowaniu nie ma wiązania naszego pliku wykonywalnego z biblioteką podczas kompilacji i linkowania. Biblioteka dynamiczna nie jest też ładowana do pamięci podczas uruchomienia programu, więc nawet, gdy jej nie ma w systemie to program i tak się uruchomi. Biblioteka jest ładowana do pamięci programowo poprzez wywołanie specjalnej funkcji systemowej (dla Windows to LoadLibrary, a dla Linux dlopen), która załaduje bibliotekę do przestrzeni adresowej naszej aplikacji. Dalej wywołujemy funkcję, która zwróci adres konkretnej funkcji wewnątrz biblioteki (dla Windows to GetProcAddress, a dla Linux to dlsym). W tym wypadku, po załadowaniu biblioteki sami musimy pozyskać adresy wszystkich funkcji, które będziemy wykorzystywać.

Z wykorzystywaniem bibliotek wiąże się pewna kwestie – ich sposób wywoływania. Biblioteki mogły zostać utworzone przez różne kompilatory, więc mogą mieć różne sposoby przekazywania argumentów (kolejność na stosie itp.) i zwracania wartości. Kiedy korzystamy z bibliotek musimy wiedzieć jaki sposób wywoływania funkcji biblioteka wykorzystuje i umieścić odpowiednie dyrektywy podczas ich wywoływania. Przykład poniżej.

Windosowe funkcje systemowe WinAPI wykorzystują konwencję wywoływania zwaną std_call. Prototypy, które inkludujemy wyglądają więc mniej więcej tak:

#define WINAPI __stdcall
int WINAPI WinMain ( HINSTANCE instance, HINSTANCE prev_instance, PSTR cmd_line, int cmd_show )