Jaký je účel linkeru? Funkce Linker a Loader

Chci přesně porozumět tomu, na jakou část kompilátoru programu se linker dívá a na kterou odkazuje. Napsal jsem tedy následující kód:

#zahrnout pomocí jmenného prostoru std; #zahrnout void FunctionTemplate (paramType val) ( i = val ) ); void Test::DefinedCorrectFunction(int val) ( i = val; ) void Test::DefinedIncorrectFunction(int val) ( i = val ) void main() ( Test testObject(1); //testObject.NonDefinedFunction(2); / /testObject.FunctionTemplate (2); }

Mám tři funkce:

  • DefinedCorrectFunction je normální funkce, deklarovaná a definovaná správně.
  • DefinedIncorrectFunction - tato funkce je deklarována správně, ale implementace je nesprávná (chybí;)
  • NonDefinedFunction je pouze deklarace. Žádná definice.
  • FunctionTemplate - šablona funkce.

    Nyní, když zkompiluji tento kód, dostanu chybu kompilátoru pro chybějící ";" v DefinedIncorrectFunction.
    Řekněme, že to opravím a poté zakomentuji testObject.NonDefinedFunction(2). Nyní se mi zobrazuje chyba linkeru. Nyní zakomentujte testObject.FunctionTemplate(2). Nyní dostávám chybu kompilátoru pro chybějící ";".

Pokud jde o šablony funkcí, chápu to tak, že jsou kompilátorem nedotčeny, pokud nejsou volány v kódu. Takže chybějící ";" Kompilátor si nestěžuje, dokud nezavolám testObject.FunctionTemplate(2).

U testObject.NonDefinedFunction(2) si kompilátor nestěžoval, ale linker ano. Pokud jsem pochopil, celý kompilátor měl vědět, že byla deklarována funkce NonDefinedFunction. O realizaci se nestaral. Linker si pak stěžoval, protože nemohl najít implementaci. Zatím je vše dobré.

Takže opravdu nechápu, co přesně dělá kompilátor a co dělá linker. Moje chápání komponent tvůrce odkazů s jejich voláními. Když je tedy volána NonDefinedFunction, vyhledá zkompilovanou implementaci NonDefinedFunction a stěžuje si. Kompilátor se ale nestaral o implementaci NonDefinedFunction, ale pro DefinedIncorrectFunction ano.

Opravdu bych ocenil, kdyby to někdo vysvětlil nebo poskytl nějaké reference.

8 odpovědí

Funkcí kompilátoru je zkompilovat vámi napsaný kód a převést jej do objektových souborů. Takže v případě, že jste to nestihli; nebo použili nedefinovanou proměnnou, kompilátor si bude stěžovat, protože se jedná o syntaktické chyby.

Pokud je kompilace úspěšná bez jakýchkoli selhání, vytvoří se soubory .object. Objektové soubory mají složitou strukturu, ale v zásadě obsahují pět věcí

  • Záhlaví – informace o souboru
  • Objektový kód – kód strojového jazyka (tento kód nelze ve většině případů spustit samostatně)
  • Informace o stěhování. Které části kódu budou muset při skutečném spuštění změnit adresy.
  • tabulka symbolů. Znaky, na které kód odkazuje. Mohou být definovány v tomto kódu, importovány z jiných modulů nebo definovány linkerem
  • Ladicí informace – používají debuggery

Kompilátor zkompiluje kód a naplní tabulku symbolů každým symbolem, na který narazí. Symboly odkazují na proměnné a funkce. Odpověď na tuto otázku vysvětluje tabulku symbolů.

To obsahuje kolekci spustitelného kódu a dat, které může linker zpracovat v produkční aplikaci nebo sdílené knihovně. Objektový soubor má datovou strukturu zvanou tabulka symbolů, která mapuje různé prvky v objektovém souboru na názvy, kterým linker rozumí.

Poznámka bod

Pokud zavoláte funkci z vašeho kódu, kompilátor nevloží koncovou adresu rutiny do souboru objektu. Místo toho vloží do kódu zástupnou hodnotu a přidá poznámku, která říká linkeru, aby hledal odkaz v různých tabulkách symbolů ze všech souborů objektů, které zpracovává, a vložil tam konečné umístění.

Vygenerované soubory objektů jsou zpracovány linkerem, který vyplní mezery v tabulkách symbolů, propojí jeden modul s druhým a nakonec vytvoří spustitelný kód, který lze načíst zavaděčem.

Takže ve vašem konkrétním případě -

  • DefinedIncorrectFunction() - Kompilátor obdrží definici funkce a začne ji kompilovat, aby vytvořil kód objektu a vložil odpovídající odkaz do tabulky symbolů. Kompilace se nezdařila kvůli chybě syntaxe, takže se kompilátor přeruší s chybou.
  • NonDefinedFunction() - Kompilátor obdrží deklaraci, ale nemá žádnou definici, takže přidá položku do tabulky symbolů a vloží linker, aby přidal příslušné hodnoty (protože linker zpracovává spoustu objektových souborů, je možné, že tato definice je přítomen v nějakém jiném objektovém souboru). Ve vašem případě nezadáte žádný jiný soubor, takže linker selže s nedefinovaným odkazem na chybu NonDefinedFunction, protože nemůže najít odkaz na odpovídající položku tabulky symbolů.

Abychom to pochopili, řekněme znovu, že váš kód je strukturován takto

#zahrnout #zahrnout class Test ( private: int i; public: Test(int val) (i=val ;) void DefinedCorrectFunction(int val); void DefinedIncorrectFunction(int val); void NonDefinedFunction(int val); šablona void FunctionTemplate (paramType val) ( i = val; ) );

try.cpp soubor

#include "try.h" void Test::DefinedCorrectFunction(int val) ( i = val; ) void Test::DefinedIncorrectFunction(int val) ( i = val; ) int main() ( Test testObject(1); testObject. NonDefinedFunction(2); //testObject.FunctionTemplate (2); návrat 0; )

Nejprve zkopírujeme a sestavíme kód, ale nepropojujeme jej

$g++ -c try.cpp -o try.o $

Tento krok probíhá bez problémů. Takže máte objektový kód v try.o. Zkuste to a připojte to.

$g++ try.o try.o: Ve funkci `main": try.cpp:(.text+0x52): nedefinovaný odkaz na `Test::NonDefinedFunction(int)" collect2: ld vrátil 1 stav ukončení

Zapomněli jste definovat Test::NonDefinedFunction. Pojďme jej definovat v samostatném souboru.

Soubor-zkus1.cpp

#include "try.h" void Test::NonDefinedFunction(int val) ( i = val; )

Pojďme to zkompilovat do objektového kódu

$ g++ -c try1.cpp -o try1.o $

Opět je to úspěšné. Zkusme propojit pouze tento soubor

$ g++ try1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.5/../../../../lib64/crt1.o: Ve funkci `_start": (.text+ 0x20 ): nedefinovaný odkaz na „hlavní“ collect2: ld vrátil 1 návratový stav

Není hlavní tak vyhráno; t odkaz!!

Nyní máte dva samostatné objektové kódy, které mají všechny potřebné komponenty. Stačí je předat OBĚ linkeru a nechat to udělat zbytek

$ g++ try.o try1.o $

Žádné chyby! Je to proto, že linker najde definice všech funkcí (i když jsou rozptýleny v různých objektových souborech) a vyplní mezery v kódech objektů odpovídajícími hodnotami

Řekněte, že chcete jíst polévku, tak jděte do restaurace.

Hledáte polévkové menu. Pokud ji v nabídce nenajdete, opustíte restauraci. (asi jako když si kompilátor stěžuje, že nemůže najít funkci). Pokud nějaké najdete, co uděláte?

Zavoláte číšníka, aby přišel s vaší polévkou. To, že je na jídelním lístku, však neznamená, že ho mají i v kuchyni. Možná je jídelní lístek zastaralý, možná někdo zapomněl kuchaři říct, že má udělat polévku. Takže zase odejdete. (například chyba z linkeru, že nemohl najít symbol)

Věřím, že toto je vaše otázka:

Kde jsem se zmátl, bylo, když si kompilátor stěžoval na DefinedIncorrectFunction. Nehledala implementaci NonDefinedFunction, ale prošla DefinedIncorrectFunction.

Kompilátor se pokusil analyzovat DefinedIncorrectFunction (protože jste zadali definici v tomto zdrojovém souboru) a došlo k chybě syntaxe (chybějící středník). Na druhou stranu kompilátor nikdy neviděl definici NonDefinedFunction, protože v tomto modulu prostě nebyl žádný kód. Možná jste zadali definici NonDefinedFunction v jiném zdrojovém souboru, ale kompilátor to neví. Kompilátor se dívá vždy pouze na jeden zdrojový soubor (a jeho zahrnuté hlavičkové soubory).

Kompilátor kontroluje, zda je zdrojový kód vhodný pro daný jazyk, a řídí se sémantikou jazyka. Výstupem kompilátoru je objektový kód.

Linker spojuje různé objektové moduly dohromady a tvoří exe. V této fázi jsou umístěny definice funkcí a v této fázi je přidán příslušný kód pro jejich volání.

Kompilátor zkompiluje kód do překladových jednotek. Zkompiluje veškerý kód obsažený ve zdrojovém souboru .cpp.
DefinedIncorrectFunction() je definována ve vašem zdrojovém souboru, takže kompilátor kontroluje jeho jazykovou správnost.
NonDefinedFunction() má nějakou definici ve zdrojovém souboru, takže kompilátor ji nemusí kompilovat, pokud je definice přítomna v nějakém jiném zdrojovém souboru, funkce bude zkompilována jako součást této překladové jednotky a později se linker odkáže na pokud linker v definici kroku propojení nenalezne, vyvolá chybu propojení.

Co dělá kompilátor a co dělá linker, závisí na implementaci: legální implementace může jednoduše uložit tokenizovaný zdroj do "kompilátoru" a dělat vše v linkeru. Moderní implementace kladou stále větší důraz na linker, kvůli lepší optimalizaci. A mnoho raných implementací šablon se ani nepodívá na kód šablony až do referenčního času, jiné než odpovídající složené závorky stačí k tomu, abychom věděli, kde šablona skončila. Z pohledu uživatele vás spíše zajímá, zda chyba vyžaduje „diagnózu“ (kterou může vybrat kompilátor nebo linker) nebo není definována.

V případě DefinedIncorrectFunction poskytnete zdrojový text, který je vyžadován pro analýzu. Tento text obsahuje chybu, která vyžaduje diagnostiku. V případě NonDefinedFunction: pokud je funkce použita, neposkytnutí definice (nebo poskytnutí více než jedné definice) v kompletním programu je porušením jednoho definičního pravidla, což je nedefinované chování. Není potřeba žádná diagnostika (ale nedovedu si představit, která neobsahovala nějakou chybějící definici funkce, která byla použita).

V praxi jsou chyby, které lze snadno odhalit pouhým prozkoumáním textového vstupu jedné překladové jednotky, definovány standardem „požadovaná diagnostika“ a budou detekovány kompilátorem. Chyby, které nelze odhalit zkoumáním jedné překladové jednotky (například chybějící definice, která může být přítomna v jiné překladové jednotce), mají formálně nedefinované chování v mnoha případech mohou být chyby odhaleny linkerem a v takových případech implementací ve skutečnosti vyvolá chybu.

Toto je poněkud upraveno v případech, jako jsou inline funkce, kde je povoleno opakovat definici v každé překladové jednotce, a upraveno šablonami, protože mnoho chyb nelze zjistit, dokud není vytvořena instance. V případě šablon má standardní implementační list velkou volnost: kompilátor musí přinejmenším analyzovat šablonu natolik, aby určil, kde šablona končí. přidané standardní věci, jako je název typu, však umožňují podstatně více analýzy před vytvořením. V závislých kontextech však nemusí být některé chyby detekovány, dokud není instance vytvořena, k čemuž může dojít v době kompilace nebo v době propojení; rané implementace preferované rozložení doby spojení; Doba kompilace je dnes a používají se VC++ a g++.

[z metod]

Definice 9.22 Linker (editor odkazů) „je program určený k propojení objektových souborů generovaných kompilátorem a souborů knihoven zahrnutých v programovacím systému.

Soubor objektu nelze spustit, dokud nejsou všechny moduly a sekce v něm propojeny. Výstupem linkeru je spustitelný soubor. Tento soubor obsahuje text programu v jazyce strojového kódu. Při pokusu o vytvoření spustitelného souboru může linker zobrazit chybovou zprávu, pokud nenalezne součást.

Nejprve linker vybere programovou sekci z prvního objektového modulu a přiřadí mu počáteční adresu. Programové sekce zbývajících objektových modulů obdrží adresy vzhledem k počáteční adrese v následujícím pořadí. V tomto případě lze adresy programových sekcí zarovnat. Současně se slučováním textů programových úseků dochází ke spojení datových úseků, tabulek identifikátorů a externích časů. Průřezové vazby jsou povoleny.

Postup pro řešení vazeb je redukován na výpočet hodnot adresových konstant procedur, funkcí a proměnných s přihlédnutím k pohybům sekcí vzhledem k začátku sestaveného programového modulu. Pokud jsou zároveň nalezeny odkazy na externí proměnné, které nejsou v seznamu modulů objektů, editor odkazů zorganizuje jejich hledání v knihovně, nelze nalézt potřebnou komponentu a vygeneruje se chybové hlášení.

Linker obvykle vytváří jednoduchý softwarový modul, který je vytvořen jako jedna jednotka. Ve složitějších případech však může linker vytvořit další moduly: překryvné strukturované programové moduly, moduly knihovních objektů a moduly dynamických knihoven.

Linker (také link editor, linker – z anglického link editor, linker) – program provádějící linkování – jako vstup vezme jeden nebo více objektových modulů a sestaví z nich spustitelný modul.

K propojení modulů používá linker tabulky názvů vytvořené kompilátorem v každém z objektových modulů. Taková jména mohou být dvou typů:

Definované nebo exportované názvy - funkce a proměnné definované v daném modulu a zpřístupněné pro použití jinými moduly

Nedefinované nebo importované názvy jsou funkce a proměnné, na které odkazuje modul, ale nejsou definovány interně.

Úkolem linkeru je vyřešit odkazy na nedefinované názvy v každém modulu. U každého importovaného jména je jeho definice v jiných modulech nahrazena jeho adresou.

Linker obecně nekontroluje typy a počet parametrů procedur a funkcí. Pokud potřebujete zkombinovat objektové moduly programů napsaných v jazycích se silným psaním, je nutné před spuštěním editoru odkazů provést potřebné kontroly pomocí dalšího nástroje.

Štítky: Linker, linker, objektový soubor, statická knihovna, dynamická knihovna, provádění programu, definice, deklarace

Průvodce pro začátečníky linkery. Část 1

Překlad článku Průvodce pro začátečníky linkery s příklady a doplňky.

Následující pojmy se používají zaměnitelně: linker a linker, definice a definice, deklarace a deklarace. Přílohy s příklady jsou zvýrazněny šedě.

Pojmenování komponent: co je uvnitř souboru C

Nejprve musíte pochopit rozdíl mezi deklarací a definicí. Definice spojuje název s implementací tohoto názvu, což může být buď data, nebo kód:

  • Definování proměnné způsobí, že jí kompilátor přidělí paměť a případně ji naplní nějakou počáteční hodnotou
  • Definování funkce způsobí, že kompilátor vygeneruje kód pro tuto funkci

Deklarace říká kompilátoru C, že někde v programu, možná v jiném souboru, existuje definice spojená s tímto jménem (všimněte si, že definice může být okamžitě deklarace, pro kterou je definice na stejném místě).

Pro proměnné existují dva typy definic

  • Globální proměnné, které existují po dobu životnosti programu (statická alokace) a jsou obvykle přístupné z mnoha funkcí
  • Lokální proměnné, které existují pouze během provádění funkce, ve které jsou deklarovány (lokální umístění) a jsou přístupné pouze v rámci ní

Pro srozumitelnost „přístupný“ znamená, že na proměnnou lze odkazovat jménem, ​​které je spojeno s její definicí.

Existuje několik případů, kdy věci nejsou tak zřejmé.

  • Statické lokální proměnné jsou ve skutečnosti globální, protože existují po celou dobu životnosti programu, i když jsou přístupné v rámci jediné funkce.
  • Stejně jako statické proměnné jsou globální proměnné, které jsou přístupné pouze ve stejném souboru, kde jsou deklarovány.

Okamžitě stojí za to připomenout, že deklarování funkce jako statické redukuje její rozsah na soubor, ve kterém je definována (konkrétně funkce z tohoto souboru k ní mají přístup).

Lokální a globální proměnné lze také rozdělit na neinicializované a inicializované (které jsou předem vyplněny nějakou hodnotou).

Můžeme totiž pracovat s proměnnými vytvořenými dynamicky pomocí funkce malloc (nebo operátoru new v C++). Není možné přistupovat k oblasti paměti podle jména, takže používáme ukazatele - pojmenované proměnné, které ukládají adresu nepojmenované oblasti paměti. Tato oblast může být také uvolněna pomocí free (nebo delete), takže paměť je považována za dynamicky alokovanou.

Pojďme to teď dát dohromady

Kód Data
Globální Místní Dynamický
Inicializováno Neinicializováno Inicializováno Neinicializováno
Prohlášení int fn(int x); externí int x; externí int x; N/A N/A N/A
Definice int fn(int x) ( ... ) int x = 1;
(v rozsahu souboru)
int x;
(v rozsahu souboru)
int x = 1;
(v rozsahu funkce)
int x;
(v rozsahu funkce)
(int* p = malloc(sizeof(int));)

Je jednodušší se na tento program podívat

/* Toto je definice neinicializované globální proměnné */ int x_global_uninit; /* Toto je definice inicializované globální proměnné */ int x_global_init = 1; /* Toto je definice neinicializované globální proměnné, ale lze k ní přistupovat podle jména pouze ze stejného souboru C */ static int y_global_uninit; /* Toto je definice inicializované globální proměnné, ale lze k ní přistupovat pouze jménem ze stejného souboru C */ static int y_global_init = 2; /* Toto je deklarace globální proměnné, která existuje někde jinde v programu */ extern int z_global; /* Toto je deklarace funkce, která je definována někde jinde v programu. Můžete přidat servisní slovo extern. Ale to je jedno */ int fn_a(int x, int y); /* Toto je definice funkce, ale protože je definována slovem static, je k dispozici pouze ve stejném souboru C */ static int fn_b(int x) ( return x+1; ) /* Toto je definice funkce . S jejími parametry se zachází jako s lokálními proměnnými */ int fn_c(int x_local) ( /* Toto je definice neinicializované lokální proměnné */ int y_local_uninit; /* Toto je definice inicializované lokální proměnné */ int y_local_init = 3; /* Tento kód odkazuje na lokální a globální proměnné a funkce jménem */ x_global_uninit = fn_a(x_local, x_global_init = fn_a(x_local, y_local_init);

Nechť se tento soubor jmenuje file.c. Složíme to takto

Cc -g -O -c soubor.c

Pojďme získat soubor objektu file.o

Co dělá kompilátor C?

Úkolem kompilátoru C je přeměnit kódový soubor z něčeho, čemu (někdy) mohou lidé porozumět, na něco, čemu rozumí počítač. Výstupem kompilátoru je objektový soubor, který má obvykle příponu .o na platformě UNIX a .obj na Windows. Obsahem objektového souboru jsou v podstatě dva typy objektů

  • Definice funkcí porovnávání kódu
  • Data odpovídající globálním proměnným definovaným v souboru (pokud jsou předinicializovány, pak je tam uložena i jejich hodnota)

Instance těchto objektů budou mít svá jména - názvy proměnných nebo funkcí, jejichž definice vedly k jejich generování.

Objektový kód je posloupnost (vhodně zakódovaných) strojových instrukcí, které odpovídají instrukcím v jazyce C – všechny ty if, while a dokonce gotos. Všechny tyto příkazy pracují s různými druhy informací a tyto informace musí být někde uloženy (to vyžaduje proměnné). Kromě toho mají přístup k dalším částem kódu, které jsou v souboru definovány.

Pokaždé, když kód přistupuje k funkci nebo proměnné, kompilátor mu to umožní pouze v případě, že viděl deklaraci této proměnné nebo funkce. Deklarace je příslib kompilátoru, že někde v programu existuje definice.

Úkolem linkeru je splnit tyto sliby, ale co by měl kompilátor dělat, když narazí na nedefinované entity?

Kompilátor v podstatě zanechá pouze útržek. Útržek (odkaz) má název, ale hodnota s ním spojená ještě není známa.

Nyní si můžeme zhruba popsat, jak bude náš program vypadat

Analýza objektového souboru

Dosud jsme pracovali s abstraktním programem; Teď je důležité vidět, jak to vypadá v praxi. Na platformě UNIX můžete použít nástroj nm. V systému Windows je ekvivalentem dumpbin s příznakem /symbols, ačkoli existuje také port GNU binutils, který obsahuje nm.exe.

Podívejme se, co nám dává nm pro výše napsaný program:

00000000 b .bss 00000000 d .data 00000000 N .debug_abbrev 00000000 N .debug_aranges 00000000 N .0bug_info 00000000000.drectve 00000000 r .eh_frame 00000000 r .rdata$zzz 00000000 t .text U_fn_a 00000000 T _fn_c 00000000 D _x_global_init 00000004 C _x_global_uninit U _z_global

Pro dříve zkompilovaný soubor file.o

Nm soubor.o

Výstup se může lišit systém od systému, ale klíčovou informací je třída každého znaku a jeho velikost (pokud je k dispozici). Třída může mít následující hodnoty

  • Třída U znamená neznámý nebo útržek, jak je uvedeno výše. Existují pouze dva takové objekty: fn_a a z_global (některé verze nm mohou také vydávat sekci, která by v tomto případě byla *UND* nebo UNDEF)
  • Třída t nebo T označuje, že kód je definován - t je lokální nebo T je statická funkce. Výstupem lze také sekci .text
  • Třídy da D označují inicializovanou globální proměnnou, d lokální proměnnou, D nelokální proměnnou. Segment pro variabilní data je obvykle .data
  • Pro neinicializované globální proměnné se používá třída b, pokud je statická/místní, nebo B a C, pokud ne. Obvykle je to segment.bss nebo *COM*

Existují další hanebné třídy, které představují jakýsi vnitřní mechanismus kompilátoru.

Co dělá linker? Část 1

Jak jsme definovali dříve, deklarace proměnné nebo funkce je příslibem kompilátoru, že někde existuje definice této proměnné nebo funkce a že úkolem linkera je tyto sliby splnit. V našem diagramu objektových souborů to může být také nazýváno "vyplnění prázdných míst".

Pro ilustraci je zde další soubor C kromě prvního:

/* Inicializovaná globální proměnná */ int z_global = 11; /* Druhá globální proměnná s názvem y_global_init, ale obě jsou statické */ static int y_global_init = 2; /* Deklarace další globální proměnné */ extern int x_global_init; int fn_a(int x, int y) ( return(x+y); ) int main(int argc, char *argv) ( const char *message = "Ahoj světe"; return fn_a(11,12); )

Nechť se tento soubor jmenuje main.c. Sestavujeme to jako předtím

Cc –g –O –c main.c

S těmito dvěma diagramy nyní vidíme, že všechny body lze propojit (a pokud ne, linker vyhodí chybu). Každá věc má své místo a každé místo má nějakou věc a linker může nahradit všechny pahýly, jak je znázorněno na obrázku.


Pro dříve zkompilované main.o a file.o sestavení spustitelného souboru

Cc -o out.exe main.o soubor.o

nm výstup pro spustitelný soubor (v našem případě out.exe):

Symboly z sample1.exe: Název Hodnota Třída Typ Velikost Část řádku _Jv_RegisterClasses | | w | NETYP| | |*UND* __gmon_start__ | | w | NETYP| | |*UND* __libc_start_main@@GLIBC_2.0| | U | FUNC|000001ad| |*UND* _init |08048254| T | FUNC| | |.init _start |080482c0| T | FUNC| | |.text __do_global_dtors_aux|080482f0| t | FUNC| | |.textový rámeček_dummy |08048320| t | FUNC| | |.text fn_b |08048348| t | FUNC|00000009| |.text fn_c |08048351| T | FUNC|00000055| |.text fn_a |080483a8| T | FUNC|0000000b| |.text hlavní |080483b3| T | FUNC|0000002c| |.text __libc_csu_fini |080483e0| T | FUNC|00000005| |.text __libc_csu_init |080483f0| T | FUNC|00000055| |.text __do_global_ctors_aux|08048450| t | FUNC| | |.text_fini |08048478| T | FUNC| | |.fini_fp_hw |08048494| R | OBJEKT|00000004| |.rodata_IO_stdin_used |08048498| R | OBJEKT|00000004| |.rodata __FRAME_END__ |080484ac| r | OBJEKT| | |.eh_frame __CTOR_LIST__ |080494b0| d | OBJEKT| | |.ctors __init_array_end |080494b0| d | NETYP| | |.ctors __init_array_start |080494b0| d | NETYP| | |.ctors __CTOR_END__ |080494b4| d | OBJEKT| | |.ctors __DTOR_LIST__ |080494b8| d | OBJEKT| | |.dtors __DTOR_END__ |080494bc| d | OBJEKT| | |.dtors __JCR_END__ |080494c0| d | OBJEKT| | |.jcr __JCR_LIST__ |080494c0| d | OBJEKT| | |.jcr_DYNAMIC |080494c4| d | OBJEKT| | |.dynamický _GLOBAL_OFFSET_TABLE_|08049598| d | OBJEKT| | |.got.plt __data_start |080495ac| D | NETYP| | |.data data_start |080495ac| W | NETYP| | |.data __dso_handle |080495b0| D | OBJEKT| | |.data str.5826 |080495b4| d | OBJEKT| | |.data x_global_init |080495b8| D | OBJEKT|00000004| |.data y_global_init |080495bc| d | OBJEKT|00000004| |.data z_global |080495c0| D | OBJEKT|00000004| |.data y_global_init |080495c4| d | OBJEKT|00000004| |.data __bss_start |080495c8| A | NETYP| | |*ABS* _edata |080495c8| A | NETYP| | |*ABS* dokončeno.5828 |080495c8| b | OBJEKT|00000001| |.bss y_global_uninit |080495cc| b | OBJEKT|00000004| |.bss x_global_uninit |080495d0| B | OBJEKT|00000004| |.bss_end |080495d4| A | NETYP| | |*ABS*

Zde jsou shromážděny všechny symboly z obou objektů a všechny nedefinované odkazy byly vyčištěny. Symboly byly také přeuspořádány, aby udržely podobné třídy pohromadě, a bylo přidáno několik dalších entit, které pomáhají operačnímu systému považovat celou věc za spustitelný program.

Chcete-li vyčistit výstup v systému UNIX, můžete odstranit vše, co začíná podtržítkem.

Duplicitní postavy

V předchozí části jsme se dozvěděli, že pokud linker nemůže najít definici pro deklarovaný symbol, vyvolá chybu. Co se stane, když jsou během propojování nalezeny dvě definice symbolu?

V C++ je vše jednoduché - podle normy musí mít symbol vždy jednu definici (tzv. pravidlo jedné definice) oddílu 3.2 jazykové normy.

Pro si je vše méně jasné. Funkce nebo inicializovaná globální proměnná musí mít vždy pouze jednu definici. Ale definování neinicializované globální proměnné lze považovat za předběžné. C v tomto případě umožňuje (alespoň nezakazuje) různé soubory kódu mít své vlastní předběžné definice pro stejný objekt.

Linkery se však musí vypořádat i s jinými programovacími jazyky, pro které pravidlo jedné definice neplatí. Například pro Fortran je zcela normální mít kopii každé globální proměnné v každém souboru, kde se k ní přistupuje. Linker je nucen zbavit se všech kopií, vybrat jednu (obvykle nejvyšší verzi, pokud mají různé velikosti) a zbytek zahodit. Tento model je často nazýván COMMON model sestavy, kvůli funkčnímu slovu FORTRAN COMMON.

Výsledkem je, že UNIXový linker si obvykle nestěžuje na duplicitní definice symbolů, alespoň pokud je duplicitní symbol neinicializovanou globální proměnnou (tento model je známý jako uvolněný ref/def model propojení). Pokud vám to vadí (a mělo by!), vyhledejte v dokumentaci kompilátoru klíč, který chování zpřísní. Například –fno-common pro kompilátor GNU vynutí umístění neinicializovaných proměnných do segmentu BSS namísto generování společných bloků.

Co dělá operační systém?

Nyní, poté, co linker sestavil spustitelný program a propojil všechny symboly s nezbytnými definicemi, se musíme trochu zastavit a pochopit, co operační systém dělá, když spouští program.

Spuštění programu vede ke spuštění strojového kódu, takže je samozřejmě potřeba přesunout program z pevného disku do operační paměti, kde s ním již může pracovat centrální procesor. Část paměti pro program se nazývá segment kódu nebo textový segment.

Kód je bez dat bezcenný, takže všechny globální proměnné musí mít také nějaké místo v RAM pro sebe. Zde je rozdíl mezi inicializovanými a neinicializovanými globálními proměnnými. Inicializované proměnné již mají svou vlastní hodnotu, která je uložena v objektových i spustitelných souborech. Po spuštění programu se zkopírují z pevného disku do paměti do datového segmentu.

U neinicializovaných proměnných OS nebude kopírovat hodnoty z paměti (protože žádné nejsou) a vše vyplní nulami. Část paměti inicializovaná na 0 se nazývá segment bss.

Počáteční hodnota inicializovaných proměnných je uložena na disku, ve spustitelném souboru; U neinicializovaných proměnných je uložena jejich velikost.


Vezměte prosím na vědomí, že celou tu dobu mluvíme pouze o globálních proměnných a nikdy jsme nezmínili lokální nebo dynamicky vytvářené objekty.

Tato data nepotřebují, aby linker fungoval, protože jeho životnost začíná při spuštění programu - dlouho poté, co linker dokončil svou práci. Nicméně pro úplnost ještě jednou naznačíme

  • Lokální proměnné jsou umístěny v části paměti známé jako zásobník, který roste a zmenšuje se, jak se funkce začíná nebo dokončuje.
  • Dynamická paměť je alokována v oblasti známé jako halda; výběr bude řešen funkcí malloc

Nyní můžeme přidat tyto chybějící oblasti paměti do našeho diagramu. Vzhledem k tomu, že halda i zásobník mohou měnit svou velikost za běhu programu, aby se vyřešily problémy, rostou směrem k sobě. Tímto způsobem dojde k výpadku paměti pouze tehdy, když se setkají (a to vyžaduje použití velkého množství paměti).


Co dělá linker? Část 2

Poté, co jsme se naučili základy toho, jak linker funguje, začněme se posouvat vpřed a studovat jeho další schopnosti v pořadí, v jakém historicky vznikaly a byly přidány k linkeru.

První pozorování, které vedlo k vývoji linkeru: často se znovu používají stejné části kódu (vstup/výstup dat, matematické funkce, čtení souborů atd.). Proto bych je rád přidělil na samostatné místo a používal je společně s mnoha programy.

Obecně je docela snadné použít stejný objektový soubor k sestavení různých programů, ale mnohem lepší je shromáždit podobné objektové soubory dohromady a vytvořit knihovnu.

Statické knihovny

Nejjednodušší forma knihovny je statická. V předchozí části bylo uvedeno, že mezi programy můžete jednoduše sdílet jeden objektový soubor. Ve skutečnosti je statická knihovna o něco více než jen objektový soubor.

V systémech UNIX se statická knihovna obvykle generuje pomocí příkazu ar a samotný soubor knihovny má příponu .a. Tyto soubory také obvykle začínají prefixem lib a jsou předány linkeru s parametrem –l, za kterým následuje název knihovny bez prefixu lib a bez přípony (například pro soubor libfred.a byste přidali -lfred).

Ar rcs libfile.a soubor.o gcc main.o libfile.a -o out.exe

Složitější příklad, mějme tři soubory

A.c int a_f(int a) ( vrátit a + 1; ) b.c int b_f (int a) ( vrátit a + 1; ) c.c int c_f(int a) ( vrátit a + 1; )

A hlavní soubor

ABC.c #zahrnout int main() ( int a = a_f(0); int b = a_f(1); int c = a_f(2); printf("%d %d %d", a, b, c); return 0; )

Pojďme shromáždit a.c, b.c a c.c do knihovny libabc.a. Nejprve zkompilujeme všechny soubory (můžete to udělat samostatně, společně je to rychlejší)

Gcc –g –O –c a.c b.c c.c abc.c

Obdržíme čtyři objektové soubory. Poté shromážděme a, b a c do jednoho souboru

Ar rcs libabc.a a.o b.o c.o

a nyní můžeme program zkompilovat

Gcc -o abc.exe libabc.a abc.o

Vezměte prosím na vědomí, že se snaží sestavit takto

Gcc -o abc.exe abc.o libabc.a

povede k chybě – linker si začne stěžovat na nevyřešené symboly.

Ve Windows mají statické knihovny obvykle příponu .lib a jsou generovány obslužným programem LIB, ale matoucí je, že stejné rozšíření platí také pro importní knihovny, které jednoduše obsahují seznam věcí dostupných v dynamické knihovně (dll) .

Když linker prochází kolekcí souborů objektů, aby je dal dohromady, sestavuje seznam symbolů, které ještě nebyly vyřešeny. Po zpracování seznamu všech explicitně deklarovaných objektů má nyní linker ještě jedno místo, kde hledat nedeklarované symboly: knihovnu. Pokud je v knihovně nevyřešený objekt, je přidán přesně tak, jako by jej uživatel zadal na příkazovém řádku.

Věnujte pozornost úrovni detailů objektů: pokud je potřeba specifický symbol, přidá se celý objekt obsahující tento symbol. Linker se tedy může ocitnout v situaci, kdy udělá jeden krok vpřed a dva kroky zpět, protože nový objekt zase může obsahovat vlastní, nevyřešené symboly.

Dalším důležitým detailem je pořadí událostí. Knihovny jsou dotazovány až po normálním propojení a jsou zpracovávány v pořadí zleva doprava. To znamená, že pokud knihovna vyžaduje symbol, který byl dříve v předchozí připojené knihovně, linker jej nebude schopen automaticky najít.

K podrobnějšímu pochopení by měl pomoci příklad. Mějme objektové soubory a.o, b.o a knihovny libx.a, liby.b.

Soubor a.o b.o libx.a Libye
Objekt a.o b.o x1.o x2.o x3.o y1.o y2.o y3.o
Definice a1, a2, a3 b1, b2 x11, x12, x13 x21, x22, x23 x31, x32 y11, y12 y21, y22 y31, y32
Nedefinované reference b2, x12 a3, y22 x23, y12 y11 y21 x31

Po zpracování souborů a.o a b.o linker vyřeší odkazy b2 a a3, přičemž x12 a y22 zůstanou nedefinované. V tomto okamžiku linker začne kontrolovat první knihovnu libx.a a zjistí, že může vytáhnout x1.o, který definuje symbol x12; Poté linker přijme nedefinované symboly x23 a y12 deklarované v x1.o (tj. seznam nedefinovaných symbolů zahrnuje y22, x23 a y23).

Linker stále kontroluje libx.a, takže může snadno vyřešit symbol x23 jeho stažením z knihovny x2.o libx.a. Ale toto x2.o přidá y11 (které se nyní skládá z y11, y22 a y12). Ani jedno z toho nelze dále vyřešit pomocí libx.a, takže linker přejde na liby.a.

Zde se děje téměř to samé a linker vytáhne y1.o a y2.o. První přidá y21, ale to lze snadno vyřešit, protože y2.o je již vydáno na světlo. Výsledkem veškeré práce je, že linker byl schopen vyřešit všechny symboly a načíst téměř všechny soubory objektů, které by byly umístěny do konečného spustitelného souboru.

Všimněte si, že pokud by b.o například obsahovalo odkaz na y32, pak by vše probíhalo podle jiného scénáře. Zpracování libx.a by bylo stejné, ale zpracování liby.a vytáhlo y3.o obsahující x31 referenci, která je definována v libx.a. Protože libx.a již dokončila zpracování, linker by vyvolal chybu.

Toto je příklad kruhové závislosti mezi dvěma knihovnami libx a liby.

Sdílené knihovny

Populární standardní C knihovny (obvykle libc) mají zjevnou nevýhodu - každý spustitelný soubor bude mít svou vlastní kopii stejného. Pokud má každý program kopii printf, fopen a podobně, zabere se spousta místa na disku.

Další, méně zřejmou nevýhodou je, že po statickém propojení se programový kód nezmění. Pokud někdo najde a opraví chybu v printf, pak všechny programy využívající tuto knihovnu budou muset být přestavěny.

Aby se tyto problémy vyhnuly, byly zavedeny sdílené knihovny (obvykle mají příponu .so nebo .dll na Windows nebo .dylib na Mac OS X). Při práci s takovými knihovnami není nutné, aby linker spojil všechny prvky do jednoho obrázku. Místo toho nechá něco jako směnku a platbu odloží na dobu spuštění programu.

Stručně řečeno: pokud linker zjistí, že nedefinovaný symbol je ve sdílené knihovně, nepřidá definici do spustitelného souboru. Místo toho linker zapíše do programu název symbolu a knihovnu, ve které je údajně definován.

Během provádění programu operační systém určí, že tyto chybějící bity jsou propojeny „právě včas“. Před spuštěním hlavní funkce projde menší verze linkeru (často ld.so) seznamy dlužníků a dokončí závěrečnou část práce – vytáhne kód z knihovny a sestaví puzzle.

To znamená, že žádný spustitelný program nemá kopii printf. Nová opravená verze libc může jednoduše nahradit starou a při opětovném spuštění si ji každý z programů převezme.

Mezi dynamickou a statickou knihovnou je ještě jeden důležitý rozdíl, a to se odráží v úrovni detailů odkazů. Pokud je určitý symbol načten ze sdílené knihovny (například printf z libc), celý obsah této knihovny je mapován do adresního prostoru. To se velmi liší od statické knihovny, ze které se stahuje pouze objektový soubor, který obsahuje definici deklarovaného symbolu.

Jinými slovy, sdílená knihovna je výsledkem linkeru (nejen ar-assembled objektových souborů) s vyřešenými referencemi v rámci objektů v tomto souboru. Ještě jednou: nm to úspěšně ilustruje. U statické knihovny nm zobrazí sadu jednotlivých objektových souborů. U sdílené knihovny bude liby.so specifikovat pouze nedefinovaný symbol x31. Také pro náš příklad s pořadím rozložení a kruhovým odkazem nebudou žádné problémy, protože veškerý obsah y3.o a x3.o je již vytažen.

Existuje také další užitečný nástroj nazvaný ldd. Zobrazuje všechny sdílené knihovny, na kterých závisí spustitelný soubor nebo knihovna, s informacemi o tom, kde je lze nalézt. Aby program úspěšně běžel, musí být nalezeny všechny tyto knihovny spolu se všemi jejich závislostmi (na UNIXových systémech zavaděč obvykle hledá knihovny v seznamu složek, který je uložen v proměnné prostředí LD_LIBRARY_PATH).

/usr/bin:ldd xeyes linux-gate.so.1 => (0xb7efa000) libXext.so.6 => /usr/lib/libXext.so.6 (0xb7edb000) libXmu.so.6 => /usr/lib /libXmu.so.6 (0xb7ec6000) libXt.so.6 => /usr/lib/libXt.so.6 (0xb7e77000) libX11.so.6 => /usr/lib/libX11.so.6 (0xb7d93000) libSM .so.6 => /usr/lib/libSM.so.6 (0xb7d8b000) libICE.so.6 => /usr/lib/libICE.so.6 (0xb7d74000) libm.so.6 => /lib/libm .so.6 (0xb7d4e000) libc.so.6 => /lib/libc.so.6 (0xb7c05000) libXau.so.6 => /usr/lib/libXau.so.6 (0xb7c01000) libxcb-xlib.so .0 => /usr/lib/libxcb-xlib.so.0 (0xb7bff000) libxcb.so.1 => /usr/lib/libxcb.so.1 (0xb7be8000) libdl.so.2 => /lib/libdl .so.2 (0xb7be4000) /lib/ld-linux.so.2 (0xb7efb000) libXdmcp.so.6 => /usr/lib/libXdmcp.so.6 (0xb7bdf000)

Například ve Windows

Ldd C:\Windows\System32\rundll32.exe ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x77100000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x763a00000.dll) KERNELBASE00.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x73e10000) apphelp.dll => /c/WINDOWS/system32/apphelp.dll (0x71ec0000) AcLayers.DLL => /c/WINDOWS/AppPatch/AcLayers.DLL (0x78000 ) msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x74ef0000) USER32.dll => /c/WINDOWS/System32/USER32.dll (0x76fb0000) win32u.dll => /c/WINDOWS/System3 .dll (0x74060000) GDI32.dll => /c/WINDOWS/System32/GDI32.dll (0x74b00000) gdi32full.dll => /c/WINDOWS/System32/gdi32full.dll (0x741e0000) =SHINDELLWS3200 = SHINDELLWS3200 /System32/SHELL32.dll (0x74fc0000) cfgmgr32.dll => /c/WINDOWS/System32/cfgmgr32.dll (0x74900000) windows.storage.dll => /c/WINDOWS/System32/windows.743000 (0x) .dll => /c/WINDOWS/System32/combase.dll (0x76490000) ucrtbase.dll => /c/WINDOWS/System32/ucrtbase.dll (0x74100000) RPCRT4.dll => /c/WINDOWS/System32/RPCRT4.dll (0x76b50000) bcryptPrimitives.dll => /c/WINDOWS/System32/bcryptPrimitives.dll (0x74940000) powrprof.dll => /c/WINDOWS/System32/powrprof.dll (0x73c2003WS/200) /WINDOWS/2000) adva =WpiO3WS/200. m32 /advapi32.dll (0x76ad0000) sechost.dll => /c/WINDOWS/System32/sechost.dll (0x76440000) shlwapi.dll => /c/WINDOWS/System32/shlwapi.dll (0x76d30000) dllkernelapp.dll > /c/WINDOWS/System32/kernel.appcore.dll (0x73c10000) shcore.dll => /c/WINDOWS/System32/shcore.dll (0x76c20000) profapi.dll => /c/WINDOWS/System32/profapi.dll ( 0x73c70000 ) OLEAUT32.dll => /c/WINDOWS/System32/OLEAUT32.dll (0x76e20000) msvcp_win.dll => /c/WINDOWS/System32/msvcp_win.dll =0x032WIND0800System /System SETUPA P.I. .dll (0x766c0000) MPR.dll => /c/WINDOWS/SYSTEM32/MPR.dll (0x6cac0000) sfc.dll => /c/WINDOWS/SYSTEM32/sfc.dll (0x2380000) WINSPOOL => /cDR. /WINDOWS /SYSTEM32/WINSPOOL.DRV (0x6f2f0000) bcrypt.dll => /c/WINDOWS/SYSTEM32/bcrypt.dll (0x73b70000) sfc_os.DLL => /c/WINDOWS/SYSTEM32/sfc_os.0DLL (0x68eDLL)DLL (0x68eDLL) => /c/WINDOWS/System32/IMM32.DLL (0x76d90000) imagehlp.dll => /c/WINDOWS/System32/imagehlp.dll (0x749a0000)

Důvodem této větší fragmentace je skutečnost, že operační systém je dostatečně chytrý, že můžete duplikovat místo na disku pomocí více než jen statických knihoven. Různé prováděcí procesy mohou také sdílet jeden segment kódu (ale ne segmenty data/bss). K tomu je třeba namapovat celou knihovnu v jednom průchodu, aby se všechny interní reference seřadily do jedné řady: pokud jeden proces vytáhne a.o a c.o a druhý vytáhne b.o a c.o, nenajde se žádná shoda pro operační systém.

David Drysdale, průvodce pro začátečníky linkery

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

Účelem tohoto článku je pomoci programátorům C a C++ pochopit podstatu toho, co linker dělá. Během několika posledních let jsem to vysvětlil mnoha kolegům a nakonec jsem se rozhodl, že je čas dát tento materiál na papír, aby byl dostupnější (a abych to nemusel znovu vysvětlovat). [Aktualizace z března 2009: Přidány další informace o aspektech rozvržení ve Windows a také více podrobností o pravidle jedné definice.

Typickým příkladem, proč jsem byl požádán o pomoc, je následující chyba rozvržení:

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

test1a.o(.text+0x18): Ve funkci „main“:

: nedefinovaný odkaz na „findmax(int, int)“

collect2: ld vrátil 1 výstupní stav

Pokud je vaše reakce „pravděpodobně jsem zapomněl externí „C“, pak pravděpodobně víte vše, co je uvedeno v tomto článku.

  • Definice: co je v souboru C?
  • Co dělá kompilátor C?
  • Co dělá Linker: Část 1
  • Co dělá operační systém?
  • Co dělá Linker: Část 2
  • C++ pro doplnění obrázku
  • Dynamicky načítané knihovny
  • dodatečně

Definice: co je v souboru C?

Tato kapitola je rychlou připomínkou různých součástí souboru C. Pokud vám vše ve výpisu níže dává smysl, pak pravděpodobně můžete tuto kapitolu přeskočit a přejít rovnou k další.

Nejprve musíte pochopit rozdíl mezi deklarací a definicí.

Definice spojuje název s implementací, kterou může být kód nebo data:

  • Definování proměnné způsobí, že si kompilátor vyhradí určitou oblast paměti, což jí možná dá nějakou konkrétní hodnotu.
  • Definování funkce způsobí, že kompilátor vygeneruje kód pro tuto funkci

Deklarace říká kompilátoru, že definice funkce nebo proměnné (s konkrétním názvem) existuje jinde v programu, pravděpodobně v jiném souboru C. (Všimněte si, že definice je také deklarace – ve skutečnosti je to deklarace, ve které je „jiné místo“ programu stejné jako to aktuální.)

Existují dva typy definic proměnných:

  • globální proměnné, které existují po celou dobu životního cyklu programu ("statická alokace") a které jsou dostupné v různých funkcích;
  • lokální proměnné, které existují pouze v rámci nějaké vykonávací funkce ("místní umístění") a které jsou přístupné pouze v rámci této funkce.

V tomto případě by měl být termín „dostupný“ chápán jako „lze přistupovat pod názvem spojeným s proměnnou v době definice“.

Existuje několik speciálních případů, které se na první pohled nemusí zdát zřejmé:

  • statické lokální proměnné jsou ve skutečnosti globální, protože existují po celou dobu životnosti programu, i když jsou viditelné pouze v rámci jediné funkce.
  • statické globální proměnné jsou také globální s jediným rozdílem, že jsou dostupné pouze v rámci stejného souboru, kde jsou definovány.

Stojí za zmínku, že definováním funkce jako statické jednoduše snížíte počet míst, ze kterých můžete na danou funkci odkazovat jménem.

U globálních a lokálních proměnných můžeme rozlišit, zda je proměnná inicializována či nikoliv, tzn. zda bude prostor přidělený pro proměnnou v paměti vyplněn konkrétní hodnotou.

Nakonec můžeme do paměti uložit informace, které jsou dynamicky alokovány pomocí malloc nebo new . V tomto případě není možné přistupovat k alokované paměti jménem, ​​proto je nutné použít ukazatele - pojmenované proměnné obsahující adresu nepojmenované oblasti paměti. Tato paměťová oblast může být také uvolněna pomocí free nebo delete . V tomto případě máme co do činění s „dynamickým umístěním“.

Pojďme si to shrnout:

Globální

Místní

Dynamický

Nezasvěcení

Nezasvěcení

Oznámení

int fn(int x);

externí int x;

externí int x;

Definice

int fn (int x)

{ ... }

int x = 1;

(rozsah

Soubor)

int x;

(rozsah - soubor)

int x = 1;

(rozsah - funkce)

int x;

(rozsah - funkce)

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

Pravděpodobně jednodušší způsob, jak se to naučit, je podívat se na ukázkový program.

/* Definice neinicializované globální proměnné */

int x_global_uninit;

/* Definice inicializované globální proměnné */

int x_global_init = 1;

/* Definice neinicializované globální proměnné, ke které

static int y_global_uninit;

/* Definice inicializované globální proměnné, ke které

* lze adresovat jménem pouze v tomto souboru C */

static int y_global_init = 2;

/* Deklarace globální proměnné, která je někde definována

* jinde v programu */

extern int z_global;

/* Deklarace funkce, která je definována někde jinde

* programy (Můžete přidat "externí" dopředu, nicméně toto

* není nezbytné) */

int fn_a(int x, int y);

/* Definice funkce. Nicméně, být označen jako statický, může být

* volání podle jména pouze v tomto souboru C. */

statický int fn_b(int x)

Návrat x+1;

/* Definice funkce. */

/* Parametr funkce je považován za lokální proměnnou. */

int fn_c(int x_local)

/* Definice neinicializované lokální proměnné */

Int y_local_uninit;

/* Definice inicializované lokální proměnné */

Int y_local_init = 3;

/* Kód, který přistupuje k lokálním a globálním proměnným

* a funguje také podle jména */

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 dělá kompilátor C?

Úkolem kompilátoru C je převést text, který je (obvykle) čitelný pro člověka, na něco, čemu rozumí počítač. Na výstupu kompilátor vytvoří objektový soubor. Na platformách UNIX mají tyto soubory obvykle příponu .o; v systému Windows - přípona.obj. Obsahem objektového souboru jsou v podstatě dvě věci:

kód odpovídající definici funkce v souboru C

data odpovídající definici globálních proměnných v souboru C (u inicializovaných globálních proměnných musí být v objektovém souboru uložena i počáteční hodnota proměnné).

Kód a data v tomto případě budou mít svá jména – názvy funkcí nebo proměnných, se kterými jsou podle definice spojeny.

Objektový kód je sekvence (vhodně sestavených) strojových instrukcí, které odpovídají instrukcím C napsaným programátorem: všechny ty if a while a dokonce goto. Tato kouzla musí manipulovat s informacemi určitého druhu a informace musí někde být – proto potřebujeme proměnné. Kód může také odkazovat na jiný kód (zejména na další funkce C v programu).

Kdekoli kód odkazuje na proměnnou nebo funkci, kompilátor to povolí pouze tehdy, pokud viděl tuto proměnnou nebo funkci deklarovanou dříve. Deklarace je příslib, že definice existuje někde jinde v programu.

Úkolem linkera je tyto sliby ověřit. Co však kompilátor udělá se všemi těmito sliby, když generuje soubor objektu?

Kompilátor v podstatě ponechává prázdná místa. Prázdné místo (odkaz) má název, ale hodnota odpovídající tomuto názvu zatím není známa.

Vzhledem k tomu můžeme znázornit objektový soubor odpovídající výše uvedenému programu takto:

Analýza souboru objektu

Až dosud jsme vše považovali na vysoké úrovni. Je však užitečné vidět, jak to funguje v praxi. Hlavním nástrojem pro nás bude příkaz nm, který poskytuje informace o symbolech objektového souboru na platformě UNIX. V systému Windows je příkaz dumpbin s možností /symbols zhruba ekvivalentní. Existují také nástroje GNU binutils portované na Windows, které zahrnují nm.exe.

Podívejme se, jaké výstupy nm pro objektový soubor byly získány z našeho příkladu výše:

Symboly z c_parts.o:

Název Hodnota Třída Typ Velikost Řádek

fn_a | | U | NETYP| | |*UND*

z_globální | | U | NETYP| | |*UND*

fn_b |00000000| t | FUNC|00000009| |.text

x_global_init |00000000| D | OBJEKT|00000004| |.údaje

y_global_uninit |00000000| b | OBJEKT|00000004| |.bss

x_global_uninit |00000004| C | OBJEKT|00000004| |*COM*

y_global_init |00000004| d | OBJEKT|00000004| |.údaje

fn_c |00000009| T | FUNC|00000055| |.text

Výsledek může na různých platformách vypadat mírně odlišně (podrobnosti najdete u muže), ale klíčovou informací je třída každé postavy a její velikost (pokud je přítomna).

  • Třída U znamená nedefinované odkazy, výše zmíněná „prázdná místa“. Pro tuto třídu existují dva objekty: fn_a a z_global. (Některé verze nm mohou mít na výstupu sekci, která by v tomto případě byla *UND* nebo UNDEF.)
  • Třídy ta T označují kód, který je definován; rozdíl mezi t a T je, zda je funkce lokální (t) k souboru nebo ne (T), tzn. zda byla funkce deklarována jako statická. Opět platí, že na některých systémech může být zobrazena sekce jako .text.
  • Třídy da D obsahují inicializované globální proměnné. V tomto případě statické proměnné patří do třídy d. Pokud jsou k dispozici informace o sekci, bude to .data.
  • Pro neinicializované globální proměnné dostaneme b, pokud jsou statické, a B nebo C jinak. Sekce v tomto případě bude s největší pravděpodobností .bss nebo *COM*.

Můžete také vidět symboly, které nejsou součástí zdrojového kódu C. Nebudeme na to zaměřovat naši pozornost, protože je to obvykle součást vnitřního mechanismu kompilátoru, takže váš program lze později propojit.



rozdělení programu do modulů C++ (7)

Chci pochopit, na jakou část kompilátoru programu se dívá a na co odkazuje linker. Napsal jsem tedy následující kód:

#zahrnout pomocí jmenného prostoru std ; #zahrnout class Test ( private : int i ; public : Test ( int val ) ( i = val ;) void DefinedCorrectFunction ( int val ); void DefinedIncorrectFunction ( int val ); void NonDefinedFunction ( int val ); šablona< class paramType >void FunctionTemplate (paramType val ) ( i = val ) ); void Test :: DefinedCorrectFunction ( int val ) ( i = val ; ) void Test :: DefinedIncorrectFunction ( int val ) ( i = val ) void main () ( Test testObject ( 1 ); //testObject.NonDefinedFunction(2);//testObject.FunctionTemplate (2); }

Mám tři funkce:

  • DefinedCorrectFunction je normální funkce, deklarovaná a definovaná správně.
  • DefinedIncorrectFunction - tato funkce je deklarována správně, ale implementace je nesprávná (chybí;)
  • NonDefinedFunction je pouze deklarace. Žádná definice.
  • FunctionTemplate - šablona funkce.

    Nyní, když zkompiluji tento kód, dostanu chybu kompilátoru pro chybějící ";" v DefinedIncorrectFunction.
    Řekněme, že to opravím a poté zakomentuji testObject.NonDefinedFunction(2). Nyní se mi zobrazuje chyba linkeru. Nyní zakomentujte testObject.FunctionTemplate(2). Nyní dostávám chybu kompilátoru pro chybějící ";".

Pokud jde o šablony funkcí, chápu to tak, že jsou kompilátorem nedotčeny, pokud nejsou volány v kódu. Takže chybějící ";" kompilátor si nestěžuje, dokud nezavolám testObject.FunctionTemplate(2).

U testObject.NonDefinedFunction(2) si kompilátor nestěžoval, ale linker ano. Pokud jsem pochopil, celý kompilátor měl vědět, že byla deklarována funkce NonDefinedFunction. O realizaci se nestaral. Linker si pak stěžoval, protože nemohl najít implementaci. Zatím je vše dobré.

Takže moc nerozumím tomu, co přesně dělá kompilátor a co dělá linker. Moje chápání komponent tvůrce odkazů s jejich voláními. Když je tedy volána NonDefinedFunction, vyhledá zkompilovanou implementaci NonDefinedFunction a stěžuje si. Kompilátor se ale nestaral o implementaci NonDefinedFunction, ale pro DefinedIncorrectFunction ano.

Opravdu bych ocenil, kdyby to někdo vysvětlil nebo poskytl nějaké reference.

Co dělá kompilátor a co dělá linker, závisí na implementaci: legální implementace by mohla jednoduše uložit tokenizovaný zdroj do „kompilátoru“ a dělat vše v linkeru. Moderní implementace Všechno více je odloženo pro linker, pro lepší optimalizaci. A mnoho raných implementací šablon se na kód šablony ani nepodívalo, dokud referenční čas, kromě odpovídajících složených závorek, nestačil k tomu, aby zjistil, kde šablona končí. Z pohledu uživatele vás spíše zajímá, zda je chyba „povinnou diagnostikou“ (kterou může vybrat kompilátor nebo linker) nebo jde o nedefinované chování.

V případě DefinedIncorrectFunction máte zdrojový text, který je vyžadován pro analýzu. Tento text obsahuje chybu, která vyžaduje diagnostiku. V případě NonDefinedFunction: pokud je funkce použita, neposkytnutí definice (nebo poskytnutí více než jedné definice) v kompletním programu je porušením jednoho definičního pravidla, což je nedefinované chování. Není potřeba žádná diagnostika (ale nedovedu si představit implementaci, která by neposkytla žádnou chybějící definici používané funkce).

V praxi jsou chyby, které lze snadno odhalit pouhým prozkoumáním textového vstupu do jednoho překladového bloku, definovány standardem „vyžadovat diagnostiku“ a budou detekovány kompilátorem. Chyby, které nelze odhalit kontrolou jedné překladové jednotky (například chybějící definice, která může být přítomna v jiné překladové jednotce), jsou formálně nedefinovaným chováním – v mnoha případech může chyby detekovat linker a v takových případech implementace ve skutečnosti vyvolá chybu.

Toto je poněkud upraveno v případech, jako jsou vestavěné funkce, kde je povoleno opakovat definici v každé překladové jednotce a enormně upravovat šablony, protože mnoho chyb nelze zachytit, dokud není instance vytvořena. V případě šablon ponechává standard implementacím velkou volnost: kompilátor musí přinejmenším analyzovat šablonu natolik, aby určil, kde šablona končí. Standard však přidal věci jako typename , aby umožnil mnohem více analýzy před vytvořením instance. V závislých kontextech však nemusí být některé chyby detekovány, dokud není vytvořena konkretizace, k čemuž může dojít v době kompilace nebo v době časné implementace, která upřednostňuje konkretizaci; časová kompilace je dnes dominantní a používají ji VC++ a g++.

Věřím, že toto je vaše otázka:

Kde jsem se zmátl, byl kompilátor, který si stěžoval na DefinedIncorrectFunction. Nehledala implementaci NonDefinedFunction, ale prošla DefinedIncorrectFunction.

Kompilátor se pokusil analyzovat DefinedIncorrectFunction (protože jste zadali definici v tomto zdrojovém souboru) a došlo k chybě syntaxe (chybějící středník). Na druhou stranu kompilátor nikdy neviděl definici NonDefinedFunction, protože v tomto modulu prostě nebyl žádný kód. Možná jste definovali NonDefinedFunction v jiném zdrojovém souboru, ale kompilátor to neví. Kompilátor pouze vypadá jeden zdrojový soubor (a jeho zahrnuté hlavičkové soubory) najednou.

Neplatný středník je syntaktická chyba, takže kód by se neměl zkompilovat. To se může stát i v implementaci šablony. V podstatě se jedná o krok analýzy, a přestože je člověku zřejmé, jak „opravit a obnovit“, kompilátor by to dělat neměl. Nemůže jen "předstírat, že je tu dvojtečka, protože to jste měl na mysli" a jít dál.

Kompilátor hledá definice funkcí, které lze volat tam, kde jsou potřeba. To zde není vyžadováno, takže není žádná stížnost. V tomto souboru není žádná chyba, protože i kdyby byl potřeba, nemohl by být implementován v tomto konkrétním kompilačním bloku. Komponenta je zodpovědná za shromažďování různých kompilačních bloků, tj. jejich „propojování“ dohromady.

Kompilátor kontroluje, zda zdrojový kód odpovídá jazyku a dodržuje sémantiku jazyka. Výstupem kompilátoru je objektový kód.

Linker spojuje různé moduly objektů dohromady a tvoří exe. V této fázi jsou umístěny definice funkcí a v této fázi je přidán příslušný kód pro jejich volání.

Kompilátor zkompiluje kód do překladových jednotek. Zkompiluje veškerý kód obsažený ve zdrojovém souboru .cpp.
DefinedIncorrectFunction() je definována ve vašem zdrojovém souboru, takže kompilátor ji zkontroluje, aby zjistil, zda je pravdivá.
NonDefinedFunction() má nějakou definici ve zdrojovém souboru, takže kompilátor ji nemusí kompilovat, pokud je definice přítomna v nějakém jiném zdrojovém souboru, funkce bude zkompilována jako součást této překladové jednotky a později se na ni linker propojí, pokud ve fázi propojení linker definici nenajde, pak vyvolá chybu propojení.

Řekni, že chceš jíst polévku, tak jdi do restaurace.

Hledáte polévkové menu. Pokud ji v nabídce nenajdete, opustíte restauraci. (asi jako když si kompilátor stěžuje, že nemůže najít funkci). Pokud nějaké najdete, co uděláte?

Zavoláš číšníka, aby dostal polévku. To, že je to v nabídce, však neznamená, že jsou také v kuchyni. Možná je jídelní lístek zastaralý, možná někdo zapomněl kuchaři říct, že má udělat polévku. Takže zase odejdete. (například chyba z linkeru, že nemohl najít symbol)

Aha, ale můžete mít NonDefinedFunction(int) v jiném kompilačním bloku.

Kompilátor vytváří některá data pro linker, který v podstatě říká následující (mimo jiné):

  • Jaké symboly (funkce/proměnné/atd.) jsou definovány.
  • Které symboly jsou specifikovány, ale nejsou definovány. V tomto případě musí linker vyřešit odkazy prohledáním dalších souvisejících modulů. Pokud to není možné, zobrazí se chyba linkeru.

Kompilátor se musí propojit s kódem definovaným (možná) v externích modulech – knihovnách nebo objektových souborech, které použijete spolu s tímto konkrétním zdrojovým souborem k vygenerování kompletního spustitelného souboru. Pokud tedy máte deklaraci, ale žádnou definici, váš kód se zkompiluje, protože kompilátor ví, že linker může najít chybějící kód někde jinde a zprovoznit jej. Takže v tomto případě dostanete chybu z linkeru a ne kompilátoru.

Pokud je na druhé straně ve vašem kódu chyba syntaxe, kompilátor nemůže ani zkompilovat a v tomto bodě dostanete chybu. Makra a šablony se mohou chovat odlišně, aniž by způsobovaly chyby, pokud se nepoužívají (šablony jsou přibližně stejné jako makra s trochu hezčím rozhraním), ale to také závisí na závažnosti chyby. Pokud to pokazíte natolik, že kompilátor nedokáže zjistit, kde končí vzor zástupných znaků/makro a začíná normální kód, nebude schopen kompilovat.

Při použití běžného kódu musí kompilátor zkompilovat i mrtvý kód (kód není specifikován ve zdrojovém souboru), protože někdo může chtít použít tento kód z jiného zdrojového souboru propojením vašeho .o souboru s jeho kódem. Proto musí být nešablonový/makro kód syntakticky správný, i když není použit přímo ve stejném zdrojovém souboru.