VKontakte Facebooku Świergot Kanał RSS

Jakim programem jest Adobe Illustrator? Historia Adobe Illustratora. Dlaczego potrzebujesz programu Adobe Illustrator?

Ten artykuł.

Organizacja tabeli nazw symbolicznych w asemblerze.

Ta tabela zawiera informacje o symbolach i ich znaczeniach zebrane przez asembler podczas pierwszego przebiegu. Asembler uzyskuje dostęp do tablicy nazw symbolicznych w drugim przebiegu. Przyjrzyjmy się sposobom zorganizowania tabeli nazw symbolicznych. Wyobraźmy sobie tabelę jako pamięć skojarzeniową przechowującą zestaw par: nazwa symboliczna - wartość. Pamięć skojarzeniowa imienia powinna ujawniać jego znaczenie. Zamiast nazwy i wartości może znajdować się wskaźnik do nazwy i wskaźnik do wartości.

Montaż sekwencyjny.

Tabela nazw symbolicznych jest reprezentowana jako wynik pierwszego przebiegu jako tablica par nazwa-wartość. Wyszukiwanie wymaganego znaku odbywa się poprzez sekwencyjne skanowanie tabeli, aż do ustalenia dopasowania. Ta metoda jest dość łatwa do zaprogramowania, ale działa powoli.

Sortuj według nazwy.

Nazwy są wstępnie posortowane alfabetycznie. Do wyszukiwania nazw używany jest binarny algorytm czyszczenia, który porównuje wymaganą nazwę z nazwą środkowego elementu tabeli. Jeśli żądany symbol znajduje się alfabetycznie bliżej środkowego elementu, to znajduje się w pierwszej połowie tabeli, a jeśli dalej, to w drugiej połowie tabeli. Jeśli żądana nazwa pasuje do nazwy środkowego elementu, wyszukiwanie zostaje zakończone.

Algorytm czyszczenia binarnego jest szybszy niż sekwencyjne skanowanie tabeli, ale elementy tabeli muszą być ułożone w kolejności alfabetycznej.

Kodowanie pamięci podręcznej.

Dzięki tej metodzie, w oparciu o oryginalną tabelę, budowana jest funkcja pamięci podręcznej, która odwzorowuje nazwy na liczby całkowite z zakresu od O do k–1 (ryc. 5.2.1, a). Funkcja pamięci podręcznej może być na przykład funkcją mnożącą wszystkie bity nazwy reprezentowanej przez kod ASCII lub dowolną inną funkcją zapewniającą równomierny rozkład wartości. Następnie tworzona jest tabela pamięci podręcznej zawierająca k wierszy (miejsc). Każda linia zawiera (na przykład w kolejności alfabetycznej) nazwy, które mają te same wartości funkcji pamięci podręcznej (ryc. 5.2.1, b) lub numer gniazda. Jeśli tabela pamięci podręcznej zawiera n nazw symbolicznych, wówczas średnia liczba nazw w każdym gnieździe wynosi n/k. Gdy n = k, znalezienie żądanej nazwy symbolicznej będzie wymagało średnio tylko jednego wyszukiwania. Zmieniając k, możesz zmieniać rozmiar tabeli (liczbę miejsc) i szybkość wyszukiwania. Łączenie i ładowanie. Program można przedstawić jako zbiór procedur (podprogramów). Asembler jeden po drugim audycja jedna procedura po drugiej, tworzenie moduły obiektowe i umieszczenie ich w pamięci. Aby uzyskać wykonywalny kod binarny, należy znaleźć następujące elementy i połączony wszystkie przetłumaczone procedury.

Funkcje łączenia i ładowania realizują specjalne programy tzw linkery, moduły ładujące linki, edytory linków Lub linkery.


Zatem, aby być całkowicie gotowym do wykonania oryginalnego programu, wymagane są dwa kroki (ryc. 5.2.2):

● tłumaczenie realizowane przez kompilator lub asembler dla każdej procedury źródłowej w celu uzyskania modułu obiektowego. Podczas nadawania następuje przejście od oryginału język w dzień wolny od pracy język mający różne polecenia i notację;

● łączenie modułów obiektowych wykonywane przez linker w celu wytworzenia wykonywalnego kodu binarnego. Oddzielne tłumaczenie procedur nazywa się możliwe błędy lub konieczność zmiany procedur. W takich przypadkach konieczne będzie ponowne połączenie wszystkich modułów obiektowych. Ponieważ łączenie jest znacznie szybsze niż tłumaczenie, wykonanie tych dwóch kroków (tłumaczenie i łączenie) pozwoli zaoszczędzić czas podczas finalizowania programu. Jest to szczególnie ważne w przypadku programów zawierających setki lub tysiące modułów. W systemach operacyjnych MS-DOS, Windows i NT moduły obiektowe mają rozszerzenie „.obj”, a wykonywalne programy binarne mają rozszerzenie „.exe”. W systemie UNIX moduły obiektowe mają rozszerzenie „.o”, ale wykonywalne programy binarne nie mają rozszerzenia.

Funkcje linkera.

Przed rozpoczęciem pierwszego przebiegu montażu licznik adresów instrukcji jest ustawiany na 0. Ten krok jest równoznaczny z założeniem, że moduł obiektowy będzie zlokalizowany pod adresem 0 w czasie wykonywania.

Celem układu jest utwórz dokładne odwzorowanie wirtualnej przestrzeni adresowej programu wykonywalnego wewnątrz linkera i umieść wszystkie moduły obiektowe pod odpowiednimi adresami.


Rozważmy cechy układu czterech modułów obiektowych (rys. 5.2.3, a), zakładając, że każdy z nich znajduje się w komórce o adresie 0 i zaczyna się od polecenia przejścia BRANCH do polecenia MOVE w tym samym module. Przed uruchomieniem programu linker umieszcza moduły obiektowe w pamięci głównej, tworząc wyświetlacz wykonywalnego kodu binarnego. Zwykle niewielka część pamięci zaczynająca się od adresu zerowego jest używana do wektorów przerwań i interakcji system operacyjny i inne cele.

Dlatego też, jak pokazano na rys. 5.2.3, b, programy rozpoczynają się nie od adresu zerowego, ale od adresu 100. Ponieważ każdy moduł obiektowy na ryc. 5.2.3, ale zajmuje odrębną przestrzeń adresową, pojawia się problem redystrybucji pamięci. Wszystkie polecenia dostępu do pamięci nie powiodą się z powodu nieprawidłowego adresowania. Przykładowo polecenie wywołania modułu obiektowego B (rys. 5.2.3, b), podane w komórce o adresie 300 modułu obiektowego A (rys. 5.2.3, a), nie zostanie wykonane z dwóch powodów:

● komenda CALL B znajduje się w komórce o innym adresie (300, a nie 200); ● Ponieważ każda procedura jest tłumaczona osobno, asembler nie może określić, który adres wstawić do CALL B. Adres modułu obiektowego B nie jest znany przed powiązaniem. Ten problem nazywa się problem z łączem zewnętrznym. Obydwa powody są eliminowane przy użyciu linkera, który łączy oddzielne przestrzenie adresowe modułów obiektowych w jedną liniową przestrzeń adresową, co odbywa się poprzez:

● buduje tabelę modułów obiektowych i ich długości;

● na podstawie tej tabeli przydziela adresy początkowe każdemu modułowi obiektowemu;

do pamięci, i dodaje do każdego z nich stałą przemieszczenia, która jest równa adresowi początkowemu tego modułu (w tym przypadku 100);

● wyszukuje wszystkie polecenia umożliwiające dostęp do procedur, i wstawia do nich adres tych procedur.
Poniżej znajduje się tabela modułów obiektowych (tabela 5.2.6), zbudowanych w pierwszym kroku. Podaje nazwę, długość i adres początkowy każdego modułu. Przestrzeń adresowa po wykonaniu przez linker wszystkich kroków jest pokazana w tabeli. 5.2.6 i na ryc. 5.2.3, ok. Struktura modułu obiektowego. Moduły obiektowe składają się z następujących części:

nazwa modułu, Niektóre dodatkowe informacje(np. długości poszczególnych części modułu, data montażu);

lista symboli zdefiniowanych w module(nazwy symboliczne) wraz z ich znaczeniem. Dostęp do tych symboli mogą uzyskać inne moduły. Programista języka asemblera używa dyrektywy PUBLIC do określenia, które nazwy symboliczne są uważane za punkty wejścia;

lista używanych nazw symbolicznych, które są zdefiniowane w innych modułach. Lista wskazuje także symboliczne nazwy używane w niektórych instrukcjach maszynowych. Umożliwia to linkerowi wstawianie poprawnych adresów do poleceń korzystających z nazw zewnętrznych. Pozwala to procedurze wywoływać inne, niezależnie przetłumaczone procedury, deklarując (przy użyciu dyrektywy EXTERN) nazwy wywoływanych procedur jako zewnętrzne. W niektórych przypadkach punkty wejścia i linki zewnętrzne są łączone w jedną tabelę;

instrukcje maszynowe i stałe;

słownik ruchu. Do poleceń zawierających adresy pamięci należy dołączyć stałą przemieszczenia (patrz rysunek 5.2.3). Sam linker nie może określić, które słowa zawierają instrukcje maszynowe, a które stałe. Dlatego ta tabela zawiera informacje o tym, które adresy należy przenieść. Może to być tablica bitów, gdzie dla każdego bitu jest potencjalny adres do przeniesienia, lub jawna lista adresów do przeniesienia;

koniec modułu, adres początkowy, taj suma kontrolna w celu zidentyfikowania błędów popełnionych podczas odczytu modułu. Zauważ to instrukcje maszynowe i stałe jedyna część modułu obiektowego, która zostanie załadowana do pamięci w celu wykonania. Pozostałe części są wykorzystywane i odrzucane przez linker przed rozpoczęciem wykonywania programu. Większość linkerów używa dwa przejście:

● w pierwszej kolejności odczytywane są wszystkie moduły obiektowe i budowana jest tabela nazw i długości modułów oraz tablica symboli, na którą składają się wszystkie punkty wejścia i odniesienia zewnętrzne;

● Moduły są następnie ponownie odczytywane, przenoszone do pamięci i łączone. O przenoszeniu programów. Problem przenoszenia programów połączonych i znajdujących się w pamięci polega na tym, że po ich przeniesieniu adresy zapisane w tabelach stają się błędne. Aby podjąć decyzję o przeniesieniu programów, trzeba znać moment ostatecznego związania nazwy symboliczne z absolutem adresy pamięci fizycznej.

Czas decyzji nazywa się momentem ustalenia adresu w pamięci głównej odpowiadającego nazwie symbolicznej. Tam są różne opcje w odniesieniu do terminu podjęcia wiążącej decyzji: kiedy jest napisane program kiedy program nadawane, kompilowane, pobierane lub kiedy zespół, zawierający adres, biegnie. Omówiona powyżej metoda wiąże nazwy symboliczne z bezwzględnymi adresami fizycznymi. Z tego powodu nie można przenosić programów po połączeniu.

Podczas łączenia można wyróżnić dwa etapy:

Pierwszy etap, na którym nazwy symboliczne kontakt adresy wirtualne. Kiedy linker łączy indywidualne przestrzenie adresowe modułów obiektowych w pojedynczą liniową przestrzeń adresową, skutecznie tworzy wirtualną przestrzeń adresową;

drugi etap, kiedy adresy wirtualne kontakt adresy fizyczne. Dopiero po drugiej operacji proces wiązania można uznać za zakończony. Niezbędny warunek przeniesienia programu to obecność mechanizmu umożliwiającego zmianę mapowania adresów wirtualnych na adresy głównej pamięci fizycznej (wielokrotnie wykonaj drugi etap). Do takich mechanizmów należą:

● paginacja. Przestrzeń adresowa pokazana na rys. 5.2.3, in zawiera adresy wirtualne, które są już zdefiniowane i odpowiadają nazwom symbolicznym A, B, C i D. Ich adresy fizyczne będą zależeć od zawartości tablicy stron. Dlatego, aby przenieść program do pamięci głównej, wystarczy zmienić tylko jego tablicę stron, ale nie sam program;

● używać rejestr ruchu. Rejestr ten wskazuje adres fizyczny zaczął bieżący program, ładowany przez system operacyjny przed przeniesieniem programu. Za pomocą sprzętu zawartość rejestru relokacji jest dodawana do wszystkich adresów pamięci przed załadowaniem ich do pamięci. Proces przenoszenia jest przejrzysty dla każdego programu użytkownika. Cecha mechanizmu: w przeciwieństwie do paginacji, cały program musi zostać przeniesiony. Jeśli istnieją oddzielne rejestry (lub segmenty pamięci, takie jak in Procesory Intela) aby przenieść kod i przenieść dane, wówczas w tym przypadku program musi zostać przeniesiony jako dwa komponenty;

● mechanizm apelacje do pamięci względem licznika programu. Dzięki temu mechanizmowi, gdy program jest przenoszony do pamięci głównej, aktualizowany jest tylko licznik programu. Program, w którym wszystkie dostępy do pamięci są powiązane z licznikiem programu (lub mają charakter bezwzględny, jak np. dostęp do rejestrów urządzeń we/wy w adresach bezwzględnych), nazywa się program niezależny od pozycji. Taki program można umieścić w dowolnym miejscu wirtualnej przestrzeni adresowej bez konieczności konfigurowania adresów. Linkowanie dynamiczne.

Omówiona powyżej metoda łączenia ma jedną osobliwość: komunikacja dotycząca wszystkich procedur wymaganych przez program jest ustanawiana przed rozpoczęciem działania programu. Bardziej efektywny sposób łączenia oddzielnie skompilowanych procedur, tzw dynamiczny wiązanie polega na nawiązaniu połączenia z każdą procedurą podczas pierwszego wywołania. Po raz pierwszy zastosowano go w systemie MULTICS.

Dynamiczne łączenie w systemieMULTIK. Za każdym program zabezpieczone segment wiążący, zawierający blok informacji dla każdej procedury (rys. 5.2.4).

Informacje obejmują:

● słowo „Adres pośredni” zarezerwowane dla wirtualnego adresu procedury;

● nazwa procedury (EARTH, FIRE itp.), która zapisywana jest w postaci ciągu znaków. W przypadku wiązania dynamicznego wywołania procedur w języku wejściowym są tłumaczone na polecenia, które przy użyciu adresowania pośredniego uzyskują dostęp do słowa „Adres pośredni” odpowiedniego bloku (ryc. 5.2.4). Kompilator również wypełnia to słowo nieprawidłowy adres, Lub specjalny zestaw bitów, co powoduje przerwanie systemowe (takie jak majdan). Po tym:

● linker znajduje nazwę procedury (na przykład EARTH) i rozpoczyna wyszukiwanie katalogu użytkownika dla skompilowanej procedury o tej nazwie;

● znalezionej procedurze przypisany jest adres wirtualny „Adres EARTH” (zwykle w jego własnym segmencie), który jest nadpisywany na nieprawidłowy adres, jak pokazano na rys. 5.2.4;

● Polecenie, które spowodowało błąd, jest następnie wykonywane ponownie. Dzięki temu program może kontynuować działanie od miejsca, w którym znajdował się przed przerwaniem systemowym. Wszystkie kolejne wywołania procedury EARTH zostaną wykonane bez błędów, ponieważ segment wiązania zawiera teraz rzeczywisty adres wirtualny „Adres EARTH” zamiast słowa „Adres pośredni”. Zatem linker jest używany tylko wtedy, gdy procedura jest wywoływana po raz pierwszy. Po tym nie ma potrzeby wywoływania linkera.

Dynamiczne łączenie w systemie Windows.

Do łączenia wykorzystywane są biblioteki dołączane dynamicznie (DLL), które zawierają procedury i (lub) dane. Biblioteki są sformatowane jako pliki z rozszerzeniami „.dll”, „.drv” (dla bibliotek sterowników) i „.fon” (dla bibliotek czcionek). Umożliwiają podział swoich procedur i danych pomiędzy kilka programów (procesów). Dlatego najpowszechniejszą formą biblioteki DLL jest biblioteka składająca się z zestawu procedur załadowanych do pamięci, do których może uzyskać dostęp kilka programów jednocześnie. Jako przykład na ryc. Rysunek 5.2.5 przedstawia cztery procesy współdzielące plik DLL zawierający procedury A, B, C i D. Programy 1 i 2 korzystają z procedury A; program 3 - procedura D, program 4 - procedura B.
Plik DLL jest budowany przez linker z zestawu plików wejściowych. Zasada konstrukcji jest podobna do konstrukcji wykonywalnego kodu binarnego. Różnica polega na tym, że podczas budowania pliku DLL do linkera przekazywana jest specjalna flaga wskazująca, że ​​biblioteka DLL została utworzona. Pliki DLL są zwykle tworzone na podstawie zbioru procedur bibliotecznych, które mogą być potrzebne wielu procesorom. Typowe przykłady Pliki DLL to procedury umożliwiające współpracę z biblioteką wywołań systemowych systemu Windows i dużymi bibliotekami graficznymi. Korzystanie z plików DDL umożliwia:

● oszczędzaj pamięć i miejsce na dysku. Na przykład, gdyby biblioteka była powiązana z każdym programem, który jej używał, wówczas biblioteka ta pojawiałaby się w wielu wykonywalnych programach binarnych w pamięci i na dysku. Jeśli korzystasz z plików DLL, każda biblioteka pojawi się raz na dysku i raz w pamięci;

●ułatwienie aktualizacji procedur bibliotecznych, a ponadto przeprowadzenie aktualizacji nawet po skompilowaniu i powiązaniu programów, które z nich korzystają;

● napraw wykryte błędy poprzez dystrybucję nowych plików DLL (na przykład przez Internet). Nie wymaga to żadnych zmian w podstawowych programach binarnych. Główna różnica pomiędzy plikiem DLL a wykonywalnym programem binarnym jest to, że plik DLL:

● nie może uruchomić się i pracować samodzielnie, ponieważ nie posiada programu hosta;

● zawiera inne informacje w nagłówku;

● posiada kilka dodatkowych procedur, które nie są związane z procedurami w bibliotece, np. procedury alokacji pamięci i zarządzania innymi zasobami, których potrzebuje plik DLL. Program może łączyć się z plikiem DLL na dwa sposoby: poprzez niejawne łączenie i jawne łączenie. Na niejawne wiązanie program użytkownika jest statycznie powiązany ze specjalnym plikiem o nazwie importuj bibliotekę.

Ta biblioteka jest tworzona przez program narzędziowy lub pożytek, poprzez wyodrębnienie pewnych informacji z pliku DLL. Biblioteka importu poprzez linker umożliwia programowi użytkownika dostęp do pliku DLL i może być połączona z wieloma bibliotekami importu. Windows, gdy jest niejawnie połączony, kontroluje program ładowany do wykonania. System wykrywa, jakich plików DLL użyje program i czy wszystkie wymagane pliki znajdują się już w pamięci. Brakujące pliki są natychmiast ładowane do pamięci.

Następnie wprowadzane są odpowiednie zmiany w strukturach danych bibliotek importu, tak aby można było określić lokalizację wywoływanych procedur. Zmiany te są mapowane do wirtualnej przestrzeni adresowej programu, po czym program użytkownika może wywoływać procedury z plików DLL tak, jakby były z nim statycznie połączone i uruchamiać je.

Na wyraźne powiązanie nie są wymagane żadne biblioteki importowe i nie trzeba ładować plików DLL w tym samym czasie, co program użytkownika. Zamiast tego program użytkownika:

● wykonuje jawne wywołanie w czasie wykonywania w celu nawiązania połączenia z plikiem DLL;

● następnie wykonuje dodatkowe telefony w celu uzyskania adresów wymaganych procedur;

● następnie program wykonuje ostatnie wywołanie, aby przerwać połączenie z plikiem DLL;

● Kiedy ostatni proces rozłączy się z plikiem DLL, plik ten może zostać usunięty z pamięci. Należy zauważyć, że w przypadku łączenia dynamicznego procedura w pliku DLL działa w wątku osoby wywołującej i używa stosu osoby wywołującej jako zmiennych lokalnych. Istotną różnicą pomiędzy działaniem procedury podczas łączenia dynamicznego (od łączenia statycznego) jest sposób ustanawiania połączenia.

David Drysdale, Przewodnik dla początkujących po linkerach

(http://www.lurklurk.org/linkers/linkers.html).

Celem tego artykułu jest pomoc programistom C i C++ w zrozumieniu istoty działania linkera. Wyjaśniałem to przez ostatnie kilka lat duża liczba kolegów i w końcu zdecydowałem, że nadszedł czas, aby przelać ten materiał na papier, aby stał się bardziej przystępny (i żebym nie musiał go ponownie tłumaczyć). [Aktualizacja z marca 2009 r.: Dodano więcej informacji na temat zagadnień związanych z układem w systemie Windows, a także więcej szczegółów na temat reguły jednej definicji.

Typowym przykładem tego, dlaczego ludzie zwrócili się do mnie o pomoc, jest następujący błąd w układzie:

g++ -o test1 test1a.o test1b.o

test1a.o(.text+0x18): W funkcji `main":

: niezdefiniowane odwołanie do `findmax(int, int)"

Collect2: ld zwrócił 1 status wyjścia

Jeśli Twoja reakcja brzmi: „Prawdopodobnie zapomniałem zewnętrznego „C”, to najprawdopodobniej wiesz wszystko, co jest podane w tym artykule.

  • Definicje: co znajduje się w pliku C?
  • Co robi kompilator C?
  • Co robi linker: część 1
  • Co robi system operacyjny?
  • Co robi linker: część 2
  • C++, aby uzupełnić obraz
  • Biblioteki ładowane dynamicznie
  • Dodatkowo

Definicje: co znajduje się w pliku C?

Ten rozdział jest krótkim przypomnieniem różnych komponentów pliku C. Jeśli wszystko na poniższej liście ma dla Ciebie sens, prawdopodobnie możesz pominąć ten rozdział i przejść od razu do następnego.

Najpierw musisz zrozumieć różnicę między deklaracją a definicją.

Definicja kojarzy nazwę z implementacją, którą może być kod lub dane:

  • Zdefiniowanie zmiennej powoduje, że kompilator rezerwuje pewien obszar pamięci, być może nadając mu jakąś konkretną wartość.
  • Zdefiniowanie funkcji powoduje, że kompilator generuje kod dla tej funkcji

Deklaracja informuje kompilator, że definicja funkcji lub zmiennej (o określonej nazwie) istnieje w innym miejscu programu, prawdopodobnie w innym pliku C. (Zauważ, że definicja jest także deklaracją - w rzeczywistości jest to deklaracja, w której „inne miejsce” programu jest takie samo, jak bieżące.)

Istnieją dwa rodzaje definicji zmiennych:

  • zmienne globalne, które istnieją w całym tekście cykl życia programy („rozmieszczenie statyczne”) i które są dostępne w różnych funkcjach;
  • zmienne lokalne, które istnieją tylko w ramach jakiejś funkcji wykonawczej („lokowanie lokalne”) i które są dostępne tylko w ramach tej właśnie funkcji.

W tym przypadku termin „dostępny” należy rozumieć jako „dostępny po nazwie powiązanej ze zmienną w momencie jej zdefiniowania”.

Istnieje kilka szczególnych przypadków, które na pierwszy rzut oka mogą nie wydawać się oczywiste:

  • statyczne zmienne lokalne są w rzeczywistości globalne, ponieważ istnieją przez cały czas istnienia programu, nawet jeśli są widoczne tylko w ramach jednej funkcji.
  • statyczne zmienne globalne są również globalne, z tą tylko różnicą, że są dostępne tylko w tym samym pliku, w którym zostały zdefiniowane.

Warto zaznaczyć, że definiując funkcję jako statyczną, po prostu zmniejsza się ilość miejsc, z których można odwołać się do tej funkcji po nazwie.

W przypadku zmiennych globalnych i lokalnych możemy rozróżnić, czy zmienna jest inicjowana, czy nie, tj. czy miejsce przydzielone zmiennej w pamięci zostanie wypełnione określoną wartością.

Wreszcie możemy przechowywać informacje w pamięci, która jest dynamicznie alokowana przy użyciu malloc lub new . W takim przypadku nie ma możliwości dostępu do przydzielonej pamięci po nazwie, dlatego konieczne jest użycie wskaźników – nazwanych zmiennych zawierających adres nienazwanego obszaru pamięci. Ten obszar pamięci można również zwolnić za pomocą polecenia free lub usuń. W tym przypadku mamy do czynienia z „alokacją dynamiczną”.

Podsumujmy:

Światowy

Lokalny

Dynamiczny

Brak inicjacji

Brak inicjacji

Ogłoszenie

int fn(int x);

zewnętrzny int x;

zewnętrzny int x;

Definicja

int fn(int x)

{ ... }

int x = 1;

(zakres

Plik)

int x;

(zakres - plik)

int x = 1;

(zakres - funkcja)

int x;

(zakres - funkcja)

int* p = malloc(rozmiar(int));

Prawdopodobnie łatwiejszym sposobem nauki jest po prostu spojrzenie na przykładowy program.

/* Definicja niezainicjowanej zmiennej globalnej */

int x_global_uninit;

/* Definicja zainicjowanej zmiennej globalnej */

int x_global_init = 1;

/* Definicja niezainicjowanej zmiennej globalnej, do której

statyczny int y_global_uninit;

/* Definicja zainicjowanej zmiennej globalnej, do której

* można adresować po nazwie tylko w tym pliku C */

statyczny int y_global_init = 2;

/* Deklaracja zmiennej globalnej, która jest gdzieś zdefiniowana

*w innym miejscu programu */

extern int z_global;

/* Deklaracja funkcji zdefiniowanej gdzie indziej

* programy (Możesz dodać „extern” z przodu, jednak to

* opcjonalnie) */

int fn_a(int x, int y);

/* Definicja funkcji. Jednak może być oznaczony jako statyczny

* wywołaj po imieniu tylko w tym pliku C. */

statyczny int fn_b(int x)

Zwróć x+1;

/* Definicja funkcji. */

/* Parametr funkcji jest uważany za zmienną lokalną. */

int fn_c(int x_local)

/* Definicja niezainicjowanej zmiennej lokalnej */

Int y_local_uninit;

/* Definicja zainicjowanej zmiennej lokalnej */

Int y_local_init = 3;

/* Kod uzyskujący dostęp do zmiennych lokalnych i globalnych

* a także działa według nazwy */

X_global_uninit = fn_a(x_local, x_global_init);

Y_local_uninit = fn_a(x_local, y_local_init);

Y_local_uninit += fn_b(z_global);

Return(x_global_uninit + y_local_uninit);

Co robi kompilator C?

Zadaniem kompilatora C jest konwersja tekstu (zwykle) czytelnego dla człowieka na tekst zrozumiały dla komputera. Na wyjściu kompilator tworzy plik obiektowy. Na platformach UNIX pliki te mają zwykle przyrostek .o; w systemie Windows - przyrostek.obj. Zawartość pliku obiektowego to zasadniczo dwie rzeczy:

kod odpowiadający definicji funkcji w pliku C

dane odpowiadające definicji zmiennych globalnych w pliku C (w przypadku zainicjalizowanych zmiennych globalnych wartość początkowa zmiennej musi być również zapisana w pliku obiektowym).

Kod i dane w tym przypadku będą miały skojarzone z nimi nazwy - nazwy funkcji lub zmiennych, z którymi są z definicji powiązane.

Kod obiektowy to ciąg (odpowiednio skomponowanych) instrukcji maszynowych, które odpowiadają instrukcjom C napisanym przez programistę: wszystkim tym, jeśli i while, a nawet goto. Zaklęcia te muszą manipulować określonym rodzajem informacji, a informacja musi gdzieś się znajdować – dlatego potrzebujemy zmiennych. Kod może również odwoływać się do innego kodu (szczególnie innych funkcji C w programie).

Gdziekolwiek kod odwołuje się do zmiennej lub funkcji, kompilator zezwoli na to tylko wtedy, gdy widział wcześniej zadeklarowaną tę zmienną lub funkcję. Deklaracja jest obietnicą, że definicja istnieje gdzie indziej w programie.

Zadaniem linkera jest weryfikacja tych obietnic. Co jednak kompilator robi ze wszystkimi tymi obietnicami, generując plik obiektowy?

Zasadniczo kompilator pozostawia puste przestrzenie. Pusta przestrzeń (link) ma nazwę, ale wartość odpowiadająca tej nazwie nie jest jeszcze znana.

Biorąc to pod uwagę, możemy przedstawić plik obiektowy odpowiadający powyższemu programowi w następujący sposób:

Parsowanie pliku obiektowego

Do tej pory sprawdziliśmy wszystko wysoki poziom. Warto jednak sprawdzić, jak to działa w praktyce. Głównym narzędziem dla nas będzie polecenie nm, które dostarcza informacji o symbolach pliku obiektowego na platformie UNIX. W systemie Windows polecenie dumpbin z opcją /symbols jest mniej więcej równoważne. Istnieją również narzędzia GNU binutils przeniesione do systemu Windows, które obejmują nm.exe.

Zobaczmy, co nm wyświetli dla pliku obiektowego uzyskanego z naszego przykładu powyżej:

Symbole z c_parts.o:

Nazwa Wartość Klasa Typ Rozmiar Przekrój linii

fn_a | | Ty | TYP| | |*UND*

z_global | | Ty | TYP| | |*UND*

fn_b |00000000| t | FUNC|00000009| |.tekst

x_global_init |00000000| D | OBIEKT|00000004| |.dane

y_global_uninit |00000000| b | OBIEKT|00000004| |.bss

x_global_uninit |00000004| C | OBIEKT|00000004| |*COM*

y_global_init |00000004| d | OBIEKT|00000004| |.dane

fn_c |00000009| T | FUNC|00000055| |.tekst

Wynik może wyglądać nieco inaczej na różnych platformach (sprawdź szczegóły u mężczyzny), ale kluczową informacją jest klasa każdej postaci i jej rozmiar (jeśli występuje). Klasa może mieć różne znaczenia:

  • Klasa U oznacza niezdefiniowane odniesienia, czyli wspomniane „puste przestrzenie”. Dla tej klasy istnieją dwa obiekty: fn_a i z_global. (Niektóre wersje nm mogą wyprowadzać sekcję, która w tym przypadku będzie miała postać *UND* lub UNDEF.)
  • Klasy t i T wskazują kod, który jest zdefiniowany; różnica między t i T polega na tym, czy funkcja jest lokalna (t) w pliku, czy nie (T), tj. czy funkcja została zadeklarowana jako statyczna. Ponownie, w niektórych systemach może zostać wyświetlona sekcja taka jak .text.
  • Klasy d i D zawierają zainicjowane zmienne globalne. W tym przypadku zmienne statyczne należą do klasy d. Jeśli obecne są informacje o sekcji, będzie to plik .data.
  • W przypadku niezainicjowanych zmiennych globalnych otrzymujemy b, jeśli są statyczne, oraz B lub C w przeciwnym razie. Sekcja w tym przypadku będzie najprawdopodobniej .bss lub *COM*.

Możesz także zobaczyć symbole, które nie są częścią kodu źródłowego C. Nie będziemy na tym skupiać naszej uwagi, ponieważ zwykle jest to część wewnętrznego mechanizmu kompilatora, dzięki czemu Twój program będzie mógł zostać później powiązany.



2024 O komforcie w domu. Gazomierze. System ogrzewania. Zaopatrzenie w wodę. System wentylacji