Aby zrozumieć konsolidatory, warto najpierw zrozumieć, co dzieje się „pod maską”, gdy konwertujesz plik źródłowy (taki jak plik C lub C ++) na plik wykonywalny (plik wykonywalny to plik, który można wykonać na komputerze lub czyjś komputer z tą samą architekturą).
Pod maską, gdy program jest kompilowany, kompilator konwertuje plik źródłowy na kod bajtowy obiektu. Ten kod bajtowy (czasami nazywany kodem obiektowym) to instrukcje mnemoniczne zrozumiałe tylko dla architektury twojego komputera. Tradycyjnie te pliki mają rozszerzenie .OBJ.
Po utworzeniu pliku obiektowego do gry wchodzi konsolidator. Najczęściej prawdziwy program, który robi coś pożytecznego, będzie musiał odwoływać się do innych plików. Na przykład w C prosty program wyświetlający twoje imię na ekranie składałby się z:
printf("Hello Kristina!\n");
Gdy kompilator skompilował twój program do pliku obj, po prostu umieszcza odniesienie do pliku printf
funkcji. Konsolidator rozwiązuje to odwołanie. Większość języków programowania ma standardową bibliotekę procedur, które obejmują podstawowe rzeczy, których oczekuje się od tego języka. Konsolidator łączy twój plik OBJ z tą standardową biblioteką. Konsolidator może również połączyć twój plik OBJ z innymi plikami OBJ. Możesz tworzyć inne pliki OBJ, które mają funkcje, które mogą być wywoływane przez inny plik OBJ. Linker działa prawie jak kopiowanie i wklejanie w edytorze tekstu. „Kopiuje” wszystkie niezbędne funkcje, do których program się odwołuje i tworzy pojedynczy plik wykonywalny. Czasami inne skopiowane biblioteki są zależne od jeszcze innych plików OBJ lub plików bibliotek. Czasami linker musi być dość rekurencyjny, aby wykonać swoją pracę.
Zwróć uwagę, że nie wszystkie systemy operacyjne tworzą jeden plik wykonywalny. Na przykład system Windows używa bibliotek DLL, które przechowują wszystkie te funkcje w jednym pliku. Zmniejsza to rozmiar pliku wykonywalnego, ale uzależnia plik wykonywalny od tych konkretnych bibliotek DLL. DOS używał rzeczy zwanych nakładkami (pliki .OVL). Miało to wiele celów, ale jednym z nich było przechowywanie powszechnie używanych funkcji w jednym pliku (innym celem, na wypadek gdybyś się zastanawiał, było umieszczanie dużych programów w pamięci. DOS ma ograniczenia pamięci i nakładki mogą być „wyładowane” z pamięci, a inne nakładki mogą być „załadowane” na wierzch tej pamięci, stąd nazwa „nakładki”). Linux ma współdzielone biblioteki, co jest zasadniczo tym samym pomysłem, co DLL (znani mi twardogłowi ludzie z Linuksa powiedzieliby mi, że jest WIELE DUŻYCH różnic).
Mam nadzieję, że to pomoże Ci zrozumieć!
Przykład minimalnego relokacji adresu
Przenoszenie adresów jest jedną z kluczowych funkcji linkowania.
Przyjrzyjmy się więc, jak to działa na minimalnym przykładzie.
0) Wprowadzenie
Podsumowanie: relokacja edytuje
.text
sekcję plików obiektowych do przetłumaczenia:Musi to zrobić konsolidator, ponieważ kompilator widzi tylko jeden plik wejściowy naraz, ale musimy wiedzieć o wszystkich plikach obiektowych naraz, aby zdecydować, jak:
.text
i.data
sekcji wielu plików obiektowychWymagania wstępne: minimalne zrozumienie:
Łączenie nie ma w szczególności nic wspólnego z C lub C ++: kompilatory po prostu generują pliki obiektowe. Następnie konsolidator przyjmuje je jako dane wejściowe, nigdy nie wiedząc, w jakim języku je skompilował. Równie dobrze mógłby to być Fortran.
Aby zmniejszyć problem, przyjrzyjmy się NASM x86-64 ELF Linux hello world:
skompilowany i złożony z:
z NASM 2.10.09.
1). Tekst .o
Najpierw dekompilujemy
.text
sekcję pliku obiektowego:co daje:
kluczowe linie to:
który powinien przenieść adres ciągu hello world do
rsi
rejestru, który jest przekazywany do wywołania systemowego write.Ale poczekaj! Skąd kompilator może wiedzieć, gdzie
"Hello world!"
znajdzie się w pamięci po załadowaniu programu?Cóż, nie może, szczególnie po połączeniu kilku
.o
plików z wieloma.data
sekcjami.Tylko konsolidator może to zrobić, ponieważ tylko on będzie miał wszystkie te pliki obiektowe.
Więc kompilator po prostu:
0x0
na skompilowanym wyjściuTe „dodatkowe informacje” są zawarte w
.rela.text
sekcji pliku obiektowego2) .rela.text
.rela.text
oznacza „przeniesienie sekcji .text”.Stosowane jest słowo relokacja, ponieważ linker będzie musiał przenieść adres z obiektu do pliku wykonywalnego.
Możemy zdemontować
.rela.text
sekcję za pomocą:który zawiera;
Format tej sekcji jest udokumentowany pod adresem : http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Każdy wpis mówi linkerowi o jednym adresie, który ma zostać przeniesiony, tutaj mamy tylko jeden dla łańcucha.
Trochę upraszczając, dla tej konkretnej linii mamy następujące informacje:
Offset = C
: jaki jest pierwszy bajt.text
tego wpisu.Jeśli spojrzymy wstecz na zdekompilowany tekst, znajduje się on dokładnie w środku krytycznym
movabs $0x0,%rsi
, a ci, którzy znają kodowanie instrukcji x86-64, zauważą, że koduje to 64-bitową część adresową instrukcji.Name = .data
: adres wskazuje na.data
sekcjęType = R_X86_64_64
, który określa dokładnie, jakie obliczenia należy wykonać, aby przetłumaczyć adres.To pole jest w rzeczywistości zależne od procesora i dlatego jest udokumentowane w AMD64 System V ABI, sekcja 4.4 „Relokacja”.
Ten dokument mówi, że
R_X86_64_64
tak:Field = word64
: 8 bajtów, czyli00 00 00 00 00 00 00 00
adres at0xC
Calculation = S + A
S
jest więc wartością pod adresem, który ma zostać przeniesiony00 00 00 00 00 00 00 00
A
jest dodatkiem, który jest0
tutaj. To jest pole wpisu relokacji.A więc
S + A == 0
zostaniemy przeniesieni pod pierwszy adres.data
sekcji.3). Tekst .out
Teraz spójrzmy na obszar tekstowy
ld
wygenerowanego dla nas pliku wykonywalnego :daje:
Więc jedyną rzeczą, która zmieniła się w pliku obiektowym, są linie krytyczne:
które teraz wskazują adres
0x6000d8
(d8 00 60 00 00 00 00 00
w little-endian) zamiast0x0
.Czy to właściwa lokalizacja dla
hello_world
ciągu?Aby zdecydować, musimy sprawdzić nagłówki programu, które mówią Linuksowi, gdzie załadować każdą sekcję.
Demontujemy je za pomocą:
co daje:
To mówi nam, że
.data
sekcja, która jest drugą, zaczyna się odVirtAddr
=0x06000d8
.Jedyną rzeczą w sekcji danych jest nasz ciąg hello world.
Poziom bonusowy
PIE
łączenie: Jaka jest opcja -fPIE dla plików wykonywalnych niezależnych od pozycji w gcc i ld?źródło
W językach takich jak `` C '' poszczególne moduły kodu są tradycyjnie kompilowane osobno w bloki kodu obiektowego, który jest gotowy do wykonania pod każdym innym względem, poza tym, że wszystkie odniesienia, które moduł tworzy poza sobą (tj. Do bibliotek lub innych modułów) mają nie zostały jeszcze rozwiązane (tj. są puste, oczekują, że ktoś przyjdzie i dokona wszystkich połączeń).
To, co robi konsolidator, to przeglądanie wszystkich modułów razem, sprawdzanie, co każdy moduł musi łączyć się ze sobą, i patrzenie na wszystkie rzeczy, które eksportuje. Następnie naprawia to wszystko i tworzy ostateczny plik wykonywalny, który można następnie uruchomić.
Tam, gdzie zachodzi również dynamiczne łączenie, dane wyjściowe konsolidatora nadal nie mogą zostać uruchomione - nadal istnieją pewne odniesienia do bibliotek zewnętrznych, które nie zostały jeszcze rozwiązane i są one rozwiązywane przez system operacyjny w momencie ładowania aplikacji (lub prawdopodobnie nawet później podczas biegu).
źródło
Gdy kompilator tworzy plik obiektu, zawiera wpisy dla symboli, które są zdefiniowane w tym pliku obiektowym, oraz odwołania do symboli, które nie są zdefiniowane w tym pliku obiektów. Konsolidator bierze je i składa razem, więc (kiedy wszystko działa poprawnie) wszystkie odniesienia zewnętrzne z każdego pliku są obsługiwane przez symbole zdefiniowane w innych plikach obiektowych.
Następnie łączy wszystkie te pliki obiektowe razem i przypisuje adresy do każdego z symboli, a gdy jeden plik obiektowy ma odniesienie zewnętrzne do innego pliku obiektowego, wypełnia adres każdego symbolu wszędzie tam, gdzie jest używany przez inny obiekt. W typowym przypadku utworzy również tabelę wszystkich użytych adresów bezwzględnych, więc program ładujący może / będzie "naprawiać" adresy podczas ładowania pliku (tj. Doda adres podstawowego obciążenia do każdego z nich adresy, więc wszystkie odnoszą się do prawidłowego adresu pamięci).
Całkiem sporo nowoczesnych linkerów może również wykonywać część (w kilku przypadkach dużo ) innych "rzeczy", takich jak optymalizacja kodu w sposób, który jest możliwy tylko wtedy, gdy wszystkie moduły są widoczne (np. Usuwanie funkcji, które zostały dołączone ponieważ było możliwe, że jakiś inny moduł może je wywołać, ale po złożeniu wszystkich modułów widać, że nic ich nie wywołuje).
źródło