Kako podesiti pametne telefone i računare. Informativni portal

Zašto se linker koristi u grafičkim bibliotekama? Funkcije linkera i loadera

[od metoda]

Definicija 9.22 Linker (uređivač linkova) “je program dizajniran da poveže objektne datoteke koje generiše kompajler i datoteke biblioteke uključene u programski sistem.

Objektni fajl se ne može pokrenuti sve dok svi moduli i sekcije u njemu nisu međusobno povezani. Izlaz linkera je izvršna datoteka. Ova datoteka uključuje tekst programa na jeziku strojnog koda. Prilikom pokušaja kreiranja izvršne datoteke, linker može prikazati poruku o grešci ako ne pronađe komponentu.

Prvo, povezivač bira programsku sekciju iz prvog modula objekta i dodeljuje mu početnu adresu. Programski dijelovi preostalih objektnih modula primaju adrese u odnosu na početnu adresu sljedećim redoslijedom. U tom slučaju, adrese programskih sekcija se mogu poravnati. Istovremeno sa spajanjem tekstova programskih sekcija kombinuju se sekcije podataka, tabele identifikatora i eksterna vremena. Poprečne veze su dozvoljene.

Procedura rješavanja veza svodi se na izračunavanje vrijednosti adresnih konstanti procedura, funkcija i varijabli, uzimajući u obzir kretanja sekcija u odnosu na početak sastavljenog programskog modula. Ako se u isto vrijeme pronađu reference na vanjske varijable koje se ne nalaze na listi objektnih modula, uređivač linkova organizira njihovu pretragu u biblioteci; potrebna komponenta se ne može pronaći i generira se poruka o grešci.

Tipično, linker kreira jednostavan softverski modul koji je kreiran kao jedna jedinica. Međutim, u složenijim slučajevima, povezivač može kreirati druge module: module programa sa preklapanjem, objektne module biblioteke i module biblioteke dinamičke veze.

Linker (također link editor, linker - od engleskog link editor, linker) - program koji vrši povezivanje - uzima jedan ili više objektnih modula kao ulaz i sastavlja od njih izvršni modul.

Za povezivanje modula, linker koristi tabele imena koje je kreirao kompajler u svakom od modula objekata. Takva imena mogu biti dva tipa:

Definirana ili izvezena imena - funkcije i varijable definirane u datom modulu i dostupne za korištenje drugim modulima

Nedefinirana ili uvezena imena su funkcije i varijable koje referencira modul, ali nisu interno definirane.

Posao povezivača je da razriješi reference na nedefinirana imena u svakom modulu. Za svako uvezeno ime, njegova definicija se nalazi u drugim modulima; spominjanje imena zamjenjuje se njegovom adresom.

Linker općenito ne provjerava tipove i broj parametara procedura i funkcija. Ako trebate kombinirati objektne module programa napisanih na jezicima s jakim kucanjem, tada dodatni uslužni program mora izvršiti potrebne provjere prije pokretanja uređivača veza.

Linker (ili editor linkova) je dizajniran da poveže zajedno objektne datoteke koje generiše kompajler, kao i datoteke biblioteke uključene u sistem programiranja.

Objektni fajl (ili skup objektnih datoteka) ne može se izvršiti sve dok svi moduli i sekcije u njemu nisu međusobno povezani. To je ono što radi editor linkova (linker). Rezultat njegovog rada je jedan fajl koji se zove modul za pokretanje.

Modul za učitavanje je softverski modul pogodan za učitavanje i izvršavanje, dobijen iz objektnog modula prilikom uređivanja linkova i predstavlja program u obliku niza mašinskih naredbi.

Povezivač može generirati poruku o grešci ako ne uspije otkriti bilo koju potrebnu komponentu kada pokušava sastaviti objektne datoteke u jednu cjelinu.

Funkcija povezivača je prilično jednostavna. Počinje svoj rad odabirom programske sekcije iz prvog modula objekta i dodjeljivanjem početne adrese. Programski dijelovi preostalih objektnih modula primaju adrese u odnosu na početnu adresu sljedećim redoslijedom. U ovom slučaju može se izvršiti i funkcija poravnanja početnih adresa programskih sekcija. Istovremeno sa spajanjem tekstova programskih sekcija kombinuju se sekcije podataka, tabele identifikatora i eksterna imena. Poprečne veze su dozvoljene.

Procedura rješavanja veza svodi se na izračunavanje vrijednosti adresnih konstanti procedura, funkcija i varijabli, uzimajući u obzir kretanja sekcija u odnosu na početak sastavljenog programskog modula. Ako se otkriju reference na eksterne varijable koje se ne nalaze na listi objektnih modula, uređivač linkova organizuje pretragu za njima u bibliotekama dostupnim u programskom sistemu. Ako se potrebna komponenta ne može pronaći u biblioteci, generira se poruka o grešci.

Tipično, linker kreira jednostavan softverski modul koji je kreiran kao jedna jedinica. Međutim, u složenijim slučajevima, povezivač može kreirati druge module: module programa sa preklapanjem, objektne module biblioteke i module biblioteke dinamičke veze.

Većina objektnih modula u savremenim sistemima programiranja izgrađena je na osnovu takozvanih relativnih adresa. Kompajler, koji generiše objektne fajlove, a zatim linker, koji ih kombinuje u jednu celinu, ne mogu tačno znati u kojoj će se stvarnoj oblasti računarske memorije program nalaziti u trenutku njegovog izvršavanja. Stoga ne rade sa stvarnim adresama RAM ćelija, već sa nekim relativnim adresama. Takve adrese se računaju od određene konvencionalne tačke, koja se uzima kao početak memorijske oblasti koju zauzima rezultujući program (obično je to početna tačka prvog programskog modula).

Naravno, nijedan program se ne može izvršiti na ovim relativnim adresama. Stoga je potreban modul koji bi konvertovao relativne adrese u stvarne (apsolutne) adrese odmah u trenutku kada se program pokrene za izvršenje. Ovaj proces se naziva translacija adresa i provodi ga poseban modul koji se zove loader.

Međutim, pokretač nije uvijek sastavni dio programskog sistema, budući da funkcije koje obavlja veoma zavise od arhitekture ciljnog računarskog sistema na kojem se izvršava rezultujući program kreiran od strane programskog sistema. U prvim fazama razvoja OS-a, pokretači pokretanja sistema postojali su u obliku zasebnih modula koji su vršili translaciju adresa i pripremali program za izvršenje – kreirajući takozvanu „sliku zadatka“. Ova šema je bila tipična za mnoge operativne sisteme (na primjer, RTOS na računaru tipa SM-1, OS RSX/11 ili RAFOS na računaru tipa SM-4, itd.). Slika zadatka se može sačuvati na eksternom mediju ili ponovo kreirati svaki put kada se program priprema za izvršenje.

Razvojem računarske računarske arhitekture postalo je moguće izvršiti prevod adrese direktno u trenutku kada je program pokrenut za izvršenje. Da biste to učinili, bilo je potrebno uključiti u izvršnu datoteku odgovarajuću tabelu koja sadrži listu veza do adresa koje je potrebno prevesti. U trenutku kada je izvršna datoteka pokrenuta, OS je obradio ovu tabelu i konvertovao relativne adrese u apsolutne. Ova šema, na primjer, tipična je za operativni sistem kao što je MS-DOS. U ovoj shemi ne postoji modul pokretačkog programa kao takav (u stvari, on je dio OS-a), a sistem programiranja je odgovoran samo za pripremu tablice prijevoda adresa - ovu funkciju obavlja linker.

U modernim operativnim sistemima postoje složene metode konverzije adresa koje rade direktno tokom izvršavanja programa. Ove metode se zasnivaju na mogućnostima ugrađenim u hardver arhitekture računarskih sistema. Metode prevođenja adresa mogu se zasnivati ​​na organizaciji memorije segmenta, stranice i segment-stranica. Zatim, da bi se izvršila translacija adresa, odgovarajuće sistemske tabele moraju biti pripremljene u trenutku kada se program pokrene. Ove funkcije u potpunosti padaju na OS module, tako da se ne izvode u sistemima za programiranje.

Članci za čitanje:

Kako nadograditi na Miui 9 Stable Global sa kineskog firmvera? Otključavanje pokretača

Ovaj članak.

Organizacija tabele simboličkih imena u asembleru.

Ova tabela sadrži informacije o simbolima i njihovim značenjima koje sakuplja asembler tokom prvog prolaza. Asembler pristupa tabeli simboličkih imena u drugom prolazu. Pogledajmo načine organiziranja tablice simboličkih imena. Zamislimo tabelu kao asocijativnu memoriju koja pohranjuje skup parova: simboličko ime - vrijednost. Asocijativna memorija za ime treba da odaje njegovo značenje. Umjesto imena i vrijednosti, može postojati pokazivač na ime i pokazivač na vrijednost.

Sekvencijalno sklapanje.

Tabela simboličkih imena je predstavljena kao rezultat prvog prolaza kao niz parova ime-vrijednost. Traženje traženog karaktera vrši se uzastopnim skeniranjem tabele dok se ne utvrdi podudaranje. Ovu metodu je prilično lako programirati, ali radi sporo.

Poredaj po imenu.

Imena su prethodno sortirana po abecednom redu. Za traženje imena koristi se binarni algoritam skraćivanja, koji upoređuje traženo ime sa imenom srednjeg elementa tabele. Ako se željeni simbol nalazi abecedno bliže srednjem elementu, onda se nalazi u prvoj polovini tabele, a ako dalje, onda u drugoj polovini tabele. Ako se željeni naziv poklapa sa imenom srednjeg elementa, pretraga se završava.

Algoritam binarnog skraćivanja brži je od sekvencijalnog skeniranja tablice, ali elementi tabele moraju biti raspoređeni po abecednom redu.

Cache kodiranje.

Sa ovom metodom, na osnovu originalne tabele, izgrađena je funkcija keš memorije koja preslikava imena u cele brojeve u opsegu od O do k–1 (slika 5.2.1, a). Funkcija keš memorije može biti, na primjer, funkcija koja množi sve bitove imena predstavljenog ASCII kodom, ili bilo koja druga funkcija koja daje uniformnu distribuciju vrijednosti. Nakon toga, kreira se keš tabela koja sadrži k redova (slotova). Svaki red sadrži (na primjer, abecednim redom) imena koja imaju iste vrijednosti funkcije keš memorije (slika 5.2.1, b) ili broj utora. Ako tabela keš memorije sadrži n simboličkih imena, tada je prosječan broj imena u svakom slotu n/k. Kada je n = k, pronalaženje željenog simboličkog imena u prosjeku će zahtijevati samo jedno pretraživanje. Promjenom k, možete mijenjati veličinu tablice (broj slotova) i brzinu pretraživanja. Povezivanje i učitavanje. Program se može predstaviti kao skup procedura (potprograma). Asembler jedan po jedan emitovanje jedna procedura za drugom, stvaranje objektni moduli i stavljanje u memoriju. Da biste dobili izvršni binarni kod, potrebno je pronaći sljedeće i povezan sve prevedene procedure.

Funkcije povezivanja i učitavanja izvode se posebnim programima tzv linkeri, učitavači linkova, uređivači linkova ili linkeri.


Dakle, da biste bili potpuno spremni za izvršavanje originalnog programa, potrebna su dva koraka (slika 5.2.2):

● prevod implementiran od strane kompajlera ili asemblera za svaku izvornu proceduru kako bi se dobio objektni modul. Prilikom emitiranja dolazi do prijelaza od originala jezik u slobodan dan jezik koji ima različite komande i notaciju;

● povezivanje objektnih modula koje izvodi linker za proizvodnju izvršnog binarnog koda. Odvojeni prijevod procedura uzrokovan je mogućim greškama ili potrebom za promjenom procedura. U ovim slučajevima, morat ćete ponovo povezati sve objektne module. Budući da je povezivanje mnogo brže od prevođenja, izvođenje ova dva koraka (prevođenje i povezivanje) uštedjet će vrijeme pri finalizaciji programa. Ovo je posebno važno za programe koji sadrže stotine ili hiljade modula. U operativnim sistemima MS-DOS, Windows i NT, objektni moduli imaju ekstenziju “.obj”, a izvršni binarni programi imaju ekstenziju “.exe”. Na UNIX sistemu, objektni moduli imaju ekstenziju ".o", ali izvršni binarni programi nemaju ekstenziju.

Funkcije linkera.

Prije nego što započne prvi montažni prolaz, brojač adresa instrukcija je postavljen na 0. Ovaj korak je ekvivalentan pretpostavci da će objektni modul biti lociran na adresi 0 u vrijeme izvođenja.

Svrha izgleda je kreirajte tačno mapiranje virtuelnog adresnog prostora izvršnog programa unutar linkera i postavite sve objektne module na odgovarajuće adrese.


Razmotrimo karakteristike rasporeda četiri objektna modula (slika 5.2.3, a), pod pretpostavkom da se svaki od njih nalazi u ćeliji sa adresom 0 i počinje sa naredbom za prelazak BRANCH na komandu MOVE u istom modulu. Prije pokretanja programa, povezivač postavlja objektne module u glavnu memoriju, proizvodeći prikaz izvršnog binarnog koda. Tipično, mali dio memorije koji počinje na adresi nula se koristi za vektore prekida, interakciju s operativnim sistemom i druge svrhe.

Stoga, kao što je prikazano na sl. 5.2.3, b, programi ne počinju od nulte adrese, već od adrese 100. Pošto svaki modul objekta na Sl. 5.2.3, ali zauzima poseban adresni prostor, javlja se problem preraspodjele memorije. Sve naredbe za pristup memoriji neće uspjeti zbog pogrešnog adresiranja. Na primjer, naredba za pozivanje modula objekta B (slika 5.2.3, b), navedena u ćeliji sa adresom 300 objektnog modula A (slika 5.2.3, a), neće biti izvršena iz dva razloga:

● komanda CALL B je u ćeliji sa drugom adresom (300, a ne 200); ● Pošto se svaka procedura prevodi zasebno, asembler ne može odrediti koju adresu da ubaci u CALL B. Adresa objektnog modula B nije poznata prije povezivanja. Ovaj problem se zove problem sa eksternom vezom. Oba razloga se eliminišu korišćenjem povezivača koji spaja odvojene adresne prostore objektnih modula u jedan linearni adresni prostor, što čini:

● pravi tabelu objektnih modula i njihovih dužina;

● na osnovu ove tabele, dodeljuje početne adrese svakom objektnom modulu;

za uspomenu, i svakom od njih dodaje konstantu pomaka, koja je jednaka početnoj adresi ovog modula (u ovom slučaju 100);

● pronalazi sve komande kojima pristupa na procedure, i ubacuje adresu ovih procedura u njih.
Ispod je tabela objektnih modula (Tabela 5.2.6), izgrađena u prvom koraku. Daje naziv, dužinu i početnu adresu svakog modula. Adresni prostor nakon što je linker završio sve korake prikazan je u tabeli. 5.2.6 i na sl. 5.2.3, c. Struktura objektnog modula. Objektni moduli se sastoje od sljedećih dijelova:

naziv modula, neke dodatne informacije (na primjer, dužine različitih dijelova modula, datum sklapanja);

lista simbola definisanih u modulu(simbolička imena) zajedno sa njihovim značenjima. Ovim simbolima mogu pristupiti drugi moduli. Programer asemblerskog jezika koristi direktivu PUBLIC da specificira koja se simbolička imena smatraju ulaznim tačkama;

spisak korištenih simboličkih naziva, koji su definisani u drugim modulima. Lista također ukazuje na simboličke nazive koje koriste određene strojne upute. Ovo omogućava povezivaču da ubaci ispravne adrese u naredbe koje koriste vanjska imena. Ovo omogućava proceduri da pozove druge nezavisno prevedene procedure deklarisanjem (koristeći EXTERN direktivu) imena pozvanih procedura kao eksterna. U nekim slučajevima, ulazne tačke i eksterne veze su kombinovane u jednoj tabeli;

strojne upute i konstante;

rječnik pokreta. Naredbe koje sadrže memorijske adrese moraju se dodati konstantom pomaka (vidi sliku 5.2.3). Sam linker ne može odrediti koje riječi sadrže strojne upute, a koje konstante. Stoga ova tabela sadrži informacije o tome koje adrese treba premjestiti. Ovo može biti tabela bitova, gdje za svaki bit postoji potencijalna adresa koju treba premjestiti, ili eksplicitna lista adresa koje treba premjestiti;

kraj modula, početna adresa, i ček suma da se identifikuju greške napravljene tokom čitanja modula. Zapiši to strojne upute i konstante jedini dio objektnog modula koji će biti učitan u memoriju za izvršenje. Preostale dijelove koristi i odbacuje linker prije nego što program počne izvršavanje. Većina linkera koristi dva prolaz:

● prvo se čitaju svi objektni moduli i pravi se tabela naziva i dužina modula, kao i tabela simbola, koja se sastoji od svih ulaznih tačaka i eksternih referenci;

● Moduli se zatim ponovo čitaju, premeštaju u memoriju i povezuju. O pokretnim programima. Problem premeštanja povezanih programa i programa u memoriji nastaje zbog činjenice da nakon njihovog premeštanja adrese pohranjene u tabelama postaju pogrešne. Da biste doneli odluku o premeštanju programa, potrebno je da znate trenutak konačnog vezivanja simbolična imena sa apsolutnim adrese fizičke memorije.

Vrijeme odluke se naziva trenutak određivanja adrese u glavnoj memoriji koja odgovara simboličkom imenu. Postoje različite opcije za vrijeme donošenja obavezujuće odluke: kada je napisano program kada program emitovanje, sastavljeno, preuzeto ili kada tim, koji sadrži adresu, izvedeno. Metoda o kojoj smo gore govorili povezuje simbolička imena sa apsolutnim fizičkim adresama. Iz tog razloga ne možete premještati programe nakon povezivanja.

Prilikom povezivanja mogu se razlikovati dvije faze:

prvo faza u kojoj simbolična imena kontakt virtuelne adrese. Kada linker poveže pojedinačne adresne prostore objektnih modula u jedan linearni adresni prostor, on efektivno kreira virtuelni adresni prostor;

sekunda faza kada virtuelne adrese kontakt fizičke adrese. Tek nakon druge operacije proces vezivanja se može smatrati završenim. Neophodno uslov za pomeranje programa je prisutnost mehanizma koji vam omogućava da promijenite mapiranje virtuelnih adresa u adrese glavne fizičke memorije (uzastopno izvođenje druge faze). Takvi mehanizmi uključuju:

● paginacija. Adresni prostor prikazan na sl. 5.2.3, u, sadrži virtuelne adrese koje su već definisane i odgovaraju simboličkim imenima A, B, C i D. Njihove fizičke adrese će zavisiti od sadržaja tabele stranica. Prema tome, da biste premestili program u glavnu memoriju, dovoljno je promeniti samo njegovu tabelu stranica, ali ne i sam program;

● koristiti registar kretanja. Ovaj registar ukazuje na fizičku adresu počeo trenutni program, koji je učitao operativni sistem prije premještanja programa. Koristeći hardver, sadržaj registra relokacije se dodaje svim memorijskim adresama prije nego što se učita u memoriju. Proces premještanja je transparentan za svaki korisnički program. Karakteristika mehanizma: za razliku od paginacije, cijeli program mora biti premješten. Ako postoje odvojeni registri (ili memorijski segmenti, kao što su Intel procesori) za pomeranje koda i premeštanje podataka, tada se program mora premestiti kao dve komponente;

● mehanizam žalbe u memoriju u odnosu na programski brojač. Sa ovim mehanizmom, kada se program premjesti u glavnu memoriju, ažurira se samo brojač programa. Program u kojem su svi pristupi memoriji povezani sa programskim brojačem (ili su apsolutni, kao što su pristupi registrima I/O uređaja na apsolutnim adresama) naziva se program nezavisan od pozicije. Takav program se može postaviti bilo gdje u virtuelnom adresnom prostoru bez konfigurisanja adresa. Dinamičko povezivanje.

Metoda povezivanja o kojoj smo gore govorili ima jednu posebnost: veza sa svim procedurama potrebnim programu se uspostavlja prije nego što program počne da radi. Efikasniji način povezivanja zasebno kompajliranih procedura, tzv dinamičan vezivanje se sastoji od uspostavljanja veze sa svakom procedurom tokom prvog poziva. Prvi put je korišten u MULTICS sistemu.

Dinamičko povezivanje u sistemuMULTICS. Iza svakog program osiguran vezivni segment, koji sadrži blok informacija za svaku proceduru (slika 5.2.4).

Informacije uključuju:

● riječ “Indirektna adresa”, rezervirana za virtuelnu adresu postupka;

● naziv procedure (EARTH, FIRE, itd.), koji se čuva kao niz znakova. Kod dinamičkog povezivanja, pozivi procedura na ulaznom jeziku se prevode u komande koje, koristeći indirektno adresiranje, pristupaju reči „Indirektna adresa“ odgovarajućeg bloka (slika 5.2.4). Prevodilac popunjava i ovu riječ nevažeća adresa, ili poseban set bitova,što uzrokuje sistemski prekid (npr zamke). Nakon toga:

● povezivač pronalazi ime procedure (na primjer, EARTH) i počinje tražiti korisnički direktorij za kompajliranu proceduru s tim imenom;

● pronađenoj proceduri se dodeljuje virtuelna adresa “EARTH Address” (obično u svom segmentu), koja se upisuje preko nevažeće adrese, kao što je prikazano na Sl. 5.2.4;

● Komanda koja je izazvala grešku se zatim ponovo izvršava. Ovo omogućava programu da nastavi sa radom tamo gde je bio pre prekida sistema. Svi naredni pozivi EARTH procedure će se izvršiti bez greške jer segment povezivanja sada sadrži stvarnu virtuelnu adresu "EARTH Address" umjesto riječi "Indirect Address". Dakle, linker je uključen samo kada je procedura pozvana prvi put. Nakon ovoga nema potrebe pozvati linker.

Dinamičko povezivanje na Windows-u.

Za povezivanje se koriste biblioteke dinamičkog povezivanja (DLL), koje sadrže procedure i (ili) podatke. Biblioteke su formatirane kao datoteke sa ekstenzijama “.dll”, “.drv” (za biblioteke drajvera) i “.fon” (za biblioteke fontova). Oni omogućavaju da se njihove procedure i podaci podijele između nekoliko programa (procesa). Stoga je najčešći oblik DLL-a biblioteka, koja se sastoji od skupa procedura učitanih u memoriju kojima može pristupiti nekoliko programa u isto vrijeme. Kao primjer na sl. Slika 5.2.5 prikazuje četiri procesa koji dijele DLL datoteku koja sadrži procedure A, B, C i D. Programi 1 i 2 koriste proceduru A; program 3 - postupak D, program 4 - postupak B.
DLL datoteku gradi povezivač iz skupa ulaznih datoteka. Princip konstrukcije je sličan konstrukciji izvršnog binarnog koda. Razlika je u tome što kada se izgradi DLL datoteka, specijalna zastavica se prosljeđuje povezivaču koja označava da je DLL kreiran. DLL datoteke se obično konstruiraju iz zbirke bibliotečkih procedura koje mogu biti potrebne višestrukim procesorima. Uobičajeni primjeri DLL datoteka uključuju rutine povezivanja sa bibliotekom Windows sistemskih poziva i velikim grafičkim bibliotekama. Korištenje DDL datoteka vam omogućava da:

● uštedite memoriju i prostor na disku. Na primjer, ako je biblioteka povezana sa svakim programom koji je koristi, tada bi se ta biblioteka pojavila u mnogim izvršnim binarnim programima u memoriji i na disku. Ako koristite DLL datoteke, svaka biblioteka će se pojaviti jednom na disku i jednom u memoriji;

●da se olakša ažuriranje bibliotečkih procedura i, pored toga, da se izvrši ažuriranje čak i nakon što su programi koji ih koriste prevedeni i povezani;

● popravite otkrivene greške distribucijom novih DLL datoteka (na primjer, preko Interneta). Ovo ne zahtijeva nikakve promjene osnovnih binarnih programa. Glavna razlika između DLL datoteke i izvršnog binarnog programa je da DLL datoteka:

● ne može pokrenuti i raditi samostalno, jer nema host program;

● sadrži druge informacije u zaglavlju;

● ima nekoliko dodatnih procedura koje nisu povezane sa procedurama u biblioteci, kao što su procedure za dodelu memorije i upravljanje drugim resursima koji su potrebni DLL datoteci. Program se može povezati sa DLL datotekom na dva načina: implicitnim povezivanjem i eksplicitnim povezivanjem. At implicitno vezivanje korisnički program je statički povezan sa posebnom datotekom pod nazivom import biblioteku.

Ovu biblioteku kreira uslužni program ili korisnost, izdvajanjem određenih informacija iz DLL datoteke. Biblioteka za uvoz preko povezivača omogućava korisničkom programu da pristupi DLL datoteci i može se povezati sa više biblioteka za uvoz. Windows, kada je implicitno povezan, kontrolira program koji se učitava za izvršenje. Sistem otkriva koje će DLL datoteke program koristiti i da li su sve potrebne datoteke već u memoriji. Fajlovi koji nedostaju se odmah učitavaju u memoriju.

Zatim se prave odgovarajuće promjene u strukturama podataka uvoznih biblioteka tako da se može odrediti lokacija pozvanih procedura. Ove promjene se mapiraju u virtuelni adresni prostor programa, nakon čega korisnički program može pozvati procedure u DLL datotekama kao da su statički povezane s njim i pokrenuti ih.

At eksplicitno povezivanje nisu potrebne biblioteke za uvoz i DLL datoteke se ne moraju učitavati u isto vrijeme kada i korisnički program. Umjesto toga, korisnički program:

● vrši eksplicitni poziv tokom vremena izvođenja da uspostavi vezu sa DLL datotekom;

● zatim vrši dodatne pozive da dobije adrese procedura koje su mu potrebne;

● nakon toga, program vrši konačni poziv da prekine vezu sa DLL datotekom;

● Kada se posljednji proces prekine sa DLL datotekom, datoteka se može ukloniti iz memorije. Imajte na umu da se kod dinamičkog povezivanja, procedura u DLL datoteci pokreće na niti pozivaoca i koristi stek pozivaoca za svoje lokalne varijable. Značajna razlika između rada procedure tokom dinamičkog povezivanja (od statičkog povezivanja) je način uspostavljanja veze.

David Drysdale, početnički vodič za linkere

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

Svrha ovog članka je pomoći C i C++ programerima da shvate suštinu onoga što linker radi. Objasnio sam to mnogim kolegama u proteklih nekoliko godina i konačno odlučio da je vrijeme da ovaj materijal stavim na papir kako bi bio pristupačniji (i da ga ne moram ponovo objašnjavati). [Ažuriranje marta 2009: Dodato više informacija o izgledu u Windowsu, kao i više detalja o pravilu jedne definicije.

Tipičan primjer zašto su mi ljudi došli za pomoć je sljedeća greška u izgledu:

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

test1a.o(.text+0x18): U funkciji `main":

: nedefinirana referenca na `findmax(int, int)"

collect2: ld vratio 1 izlazni status

Ako je vaša reakcija “vjerovatno sam zaboravio ekstern “C””, onda najvjerovatnije znate sve što je dato u ovom članku.

  • Definicije: šta se nalazi u C datoteci?
  • Šta radi C kompajler?
  • Šta radi linker: 1. dio
  • Šta radi operativni sistem?
  • Šta radi linker: 2. dio
  • C++ za kompletiranje slike
  • Dinamički učitane biblioteke
  • Dodatno

Definicije: šta se nalazi u C datoteci?

Ovo poglavlje je brzi podsjetnik na različite komponente C datoteke. Ako vam sve u listi ispod ima smisla, onda vjerovatno možete preskočiti ovo poglavlje i preći direktno na sljedeće.

Prvo morate razumjeti razliku između deklaracije i definicije.

Definicija povezuje ime sa implementacijom, koja može biti ili kod ili podaci:

  • Definiranje varijable uzrokuje da kompajler rezerviše neko područje memorije, možda mu dajući određenu vrijednost.
  • Definiranje funkcije uzrokuje da kompajler generiše kod za tu funkciju

Deklaracija govori kompajleru da funkcija ili definicija varijable (sa određenim imenom) postoji negde drugde u programu, verovatno u drugoj C datoteci. (Imajte na umu da je definicija također deklaracija – u stvari, to je deklaracija u kojoj je "drugo mjesto" programa isto kao i trenutno.)

Postoje dvije vrste definicija za varijable:

  • globalne varijable, koji postoje tokom životnog ciklusa programa ("statička alokacija") i koji su dostupni u različitim funkcijama;
  • lokalne varijable, koji postoje samo u okviru neke izvršne funkcije ("lokalni smještaj") i koji su dostupni samo unutar te funkcije.

U ovom slučaju, termin „dostupan“ treba shvatiti kao „može mu se pristupiti preko imena povezanog sa varijablom u trenutku definicije“.

Postoji nekoliko posebnih slučajeva koji na prvi pogled možda neće izgledati očigledni:

  • statičke lokalne varijable su zapravo globalne jer postoje tokom cijelog života programa, čak i ako su vidljive samo unutar jedne funkcije.
  • statičke globalne varijable su takođe globalne sa jedinom razlikom što su dostupne samo unutar iste datoteke u kojoj su definisane.

Vrijedi napomenuti da definiranjem funkcije kao statične jednostavno smanjujete broj mjesta s kojih se možete pozvati na datu funkciju po imenu.

Za globalne i lokalne varijable možemo razlikovati da li je varijabla inicijalizirana ili ne, tj. da li će prostor dodijeljen varijabli u memoriji biti popunjen određenom vrijednošću.

Konačno, možemo pohraniti informacije u memoriju koje se dinamički dodjeljuju pomoću malloc-a ili new . U ovom slučaju nije moguće pristupiti dodijeljenoj memoriji po imenu, pa je potrebno koristiti pokazivače - imenovane varijable koje sadrže adresu neimenovanog memorijskog područja. Ovo memorijsko područje se također može osloboditi korištenjem free ili delete. U ovom slučaju radi se o "dinamičkom plasmanu".

Hajde da rezimiramo:

Global

Lokalno

Dynamic

Ne-inicijacija

Ne-inicijacija

Najava

int fn(int x);

extern int x;

extern int x;

Definicija

int fn(int x)

{ ... }

int x = 1;

(opseg

fajl)

int x;

(opseg - fajl)

int x = 1;

(opseg - funkcija)

int x;

(opseg - funkcija)

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

Vjerovatno je lakši način za učenje samo pogledati primjer programa.

/* Definicija neinicijalizirane globalne varijable */

int x_global_uninit;

/* Definicija inicijalizirane globalne varijable */

int x_global_init = 1;

/* Definicija neinicijalizirane globalne varijable kojoj

static int y_global_uninit;

/* Definicija inicijalizirane globalne varijable kojoj

* može se adresirati imenom samo unutar ovog C fajla */

static int y_global_init = 2;

/* Deklaracija globalne varijable koja je negdje definirana

* drugdje u programu */

extern int z_global;

/* Deklarisanje funkcije koja je negdje drugdje definirana

* programi (možete dodati "extern" ispred, međutim ovo

* nije potrebno) */

int fn_a(int x, int y);

/* Definicija funkcije. Međutim, može biti označeno kao statičko

* pozivanje po imenu samo unutar ove C datoteke. */

statički int fn_b(int x)

Vrati x+1;

/* Definicija funkcije. */

/* Funkcijski parametar se smatra lokalnom varijablom. */

int fn_c(int x_local)

/* Definicija neinicijalizirane lokalne varijable */

Int y_local_uninit;

/* Definicija inicijalizirane lokalne varijable */

Int y_local_init = 3;

/* Kod koji pristupa lokalnim i globalnim varijablama

* i također funkcije po imenu */

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);

Šta radi C kompajler?

Posao C kompajlera je da konvertuje tekst koji je (obično) čoveku čitljiv u nešto što računar može da razume. Na izlazu, kompajler proizvodi objektni fajl. Na UNIX platformama, ove datoteke obično imaju sufiks .o; na Windows-u - sufiks.obj. Sadržaj objektne datoteke su u suštini dvije stvari:

kod koji odgovara definiciji funkcije u C datoteci

podaci koji odgovaraju definiciji globalnih varijabli u C datoteci (za inicijalizirane globalne varijable, početna vrijednost varijable također mora biti pohranjena u objektnoj datoteci).

Kod i podaci će, u ovom slučaju, imati imena povezana s njima - imena funkcija ili varijabli s kojima su po definiciji pridruženi.

Objektni kod je niz (odgovarajuće sastavljenih) mašinskih instrukcija koje odgovaraju C instrukcijama koje je napisao programer: sve one if i while, pa čak i goto. Ove čarolije moraju manipulirati informacijama određene vrste, a informacije moraju biti negdje - zato su nam potrebne varijable. Kod također može referencirati drugi kod (posebno druge C funkcije u programu).

Gdje god se kod odnosi na varijablu ili funkciju, kompajler će to dozvoliti samo ako je vidio tu varijablu ili funkciju ranije deklariranu. Deklaracija je obećanje da definicija postoji negdje drugdje u programu.

Posao povezivača je da provjeri ova obećanja. Međutim, šta prevodilac radi sa svim ovim obećanjima kada generiše objektni fajl?

U suštini kompajler ostavlja prazna mesta. Prazan prostor (link) ima ime, ali vrijednost koja odgovara ovom imenu još nije poznata.

S obzirom na ovo, možemo prikazati objektni fajl koji odgovara gore navedenom programu na sljedeći način:

Parsiranje objektne datoteke

Do sada smo sve razmatrali na visokom nivou. Međutim, korisno je vidjeti kako to funkcionira u praksi. Glavni alat za nas će biti naredba nm, koja pruža informacije o simbolima objektne datoteke na UNIX platformi. U Windowsu, naredba dumpbin sa opcijom /symbols je otprilike ekvivalentna. Postoje i GNU binutils alati preneseni na Windows koji uključuju nm.exe.

Hajde da vidimo šta nm izlazi za objektni fajl dobijen iz našeg primera iznad:

Simboli sa c_parts.o:

Naziv Vrijednost Klasa Tip Veličina Linija Presjek

fn_a | | U | NOTYPE| | |*UND*

z_global | | U | NOTYPE| | |*UND*

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

x_global_init |00000000| D | OBJEKT|00000004| |.podaci

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

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

y_global_init |00000004| d | OBJEKT|00000004| |.podaci

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

Rezultat može izgledati malo drugačije na različitim platformama (provjerite stranicu čovjeka za detalje), ali ključna informacija je klasa svakog znaka i njegova veličina (ako postoji). Klasa može imati različita značenja:

  • Klasa U označava nedefinisane reference, one "prazne prostore" koji su gore pomenuti. Postoje dva objekta za ovu klasu: fn_a i z_global. (Neke verzije nm-a mogu dati dio koji bi u ovom slučaju bio *UND* ili UNDEF.)
  • Klase t i T označavaju kod koji je definiran; razlika između t i T je da li je funkcija lokalna (t) na fajl ili nije (T), tj. da li je funkcija deklarirana kao statična. Opet, na nekim sistemima se može prikazati odjeljak kao što je .text.
  • Klase d i D sadrže inicijalizirane globalne varijable. U ovom slučaju, statičke varijable pripadaju klasi d. Ako su informacije o sekciji prisutne, to će biti .data.
  • Za neinicijalizirane globalne varijable, dobijamo b ako su statične i B ili C u suprotnom. Odjeljak u ovom slučaju će najvjerovatnije biti .bss ili *COM*.

Također možete vidjeti simbole koji nisu dio C izvornog koda. Nećemo fokusirati našu pažnju na ovo, pošto je to obično deo internog mehanizma kompajlera, tako da se vaš program može kasnije povezati.

Svrha ovog članka je pomoći C i C++ programerima da shvate suštinu onoga što linker radi. Objasnio sam to mnogim kolegama u proteklih nekoliko godina i konačno odlučio da je vrijeme da ovaj materijal stavim na papir kako bi bio pristupačniji (i da ga ne moram ponovo objašnjavati). [Ažuriranje marta 2009: Dodato više informacija o izgledu u Windowsu, kao i više detalja o pravilu jedne definicije.

Tipičan primjer zašto su mi ljudi došli za pomoć je sljedeća greška u izgledu:
g++ -o test1 test1a.o test1b.o test1a.o(.text+0x18): U funkciji `main": : nedefinirana referenca na `findmax(int, int)" collect2: ld vratio 1 izlazni status
Ako je vaša reakcija “vjerovatno sam zaboravio ekstern “C””, onda najvjerovatnije znate sve što je dato u ovom članku.

Definicije: šta se nalazi u C datoteci?

Ovo poglavlje je brzi podsjetnik na različite komponente C datoteke. Ako vam sve ima smisla, onda najvjerovatnije možete preskočiti ovo poglavlje i preći direktno na.

Prvo morate razumjeti razliku između deklaracije i definicije. Definicija povezuje ime sa implementacijom, koja može biti ili kod ili podaci:

  • Definiranje varijable uzrokuje da kompajler rezerviše neko područje memorije, možda mu dajući određenu vrijednost.
  • Definiranje funkcije uzrokuje da kompajler generiše kod za tu funkciju
Najava govori kompajleru da definicija funkcije ili varijable (sa određenim imenom) postoji negdje drugdje u programu, vjerovatno u drugoj C datoteci. (Imajte na umu da je definicija također deklaracija – u stvari, to je deklaracija u kojoj je "drugo mjesto" programa isto kao i trenutno.)

Postoje dvije vrste definicija za varijable:

  • globalne varijable, koji postoje tokom životnog ciklusa programa ("statička alokacija") i koji su dostupni u različitim funkcijama;
  • lokalne varijable, koji postoje samo u okviru neke izvršne funkcije ("lokalni smještaj") i koji su dostupni samo unutar te funkcije.
U ovom slučaju, termin „dostupan“ treba shvatiti kao „može mu se pristupiti preko imena povezanog sa varijablom u trenutku definicije“.

Postoji nekoliko posebnih slučajeva koji na prvi pogled možda neće izgledati očigledni:

  • statičke lokalne varijable su zapravo globalne jer postoje za cijeli život programa, čak i ako su vidljive samo unutar jedne funkcije.
  • statičke globalne varijable su također globalne, s jedinom razlikom što su dostupne samo unutar iste datoteke u kojoj su definirane.
Vrijedi napomenuti da definiranjem funkcije kao statične jednostavno smanjujete broj mjesta s kojih se možete pozvati na datu funkciju po imenu.

Za globalne i lokalne varijable možemo razlikovati da li je varijabla inicijalizirana ili ne, tj. da li će prostor dodijeljen varijabli u memoriji biti popunjen određenom vrijednošću.

Konačno, možemo pohraniti informacije u memoriju koje se dinamički dodjeljuju pomoću malloc-a ili new . U ovom slučaju nije moguće pristupiti dodijeljenoj memoriji po imenu, pa je potrebno koristiti pokazivače - imenovane varijable koje sadrže adresu neimenovanog memorijskog područja. Ovo memorijsko područje se također može osloboditi korištenjem free ili delete. U ovom slučaju radi se o "dinamičkom plasmanu".

Hajde da rezimiramo:

Vjerovatno je lakši način za učenje samo pogledati primjer programa.
/* Definicija neinicijalizirane globalne varijable */ int x_global_uninit; /* Definicija inicijalizirane globalne varijable */ int x_global_init = 1; /* Definirajte neinicijaliziranu globalnu varijablu kojoj se * može pristupiti samo po imenu unutar ove C datoteke */ static int y_global_uninit; /* Definicija inicijalizirane globalne varijable kojoj * se može pristupiti po imenu samo unutar ove C datoteke */ static int y_global_init = 2; /* Deklaracija globalne varijable koja je * definisana negde drugde u programu */ extern int z_global; /* Deklarisanje funkcije koja je definisana negdje drugdje * u programu (Možete dodati "extern", ali ovo * je opciono) */ int fn_a(int x, int y); /* Definicija funkcije. Međutim, kada je označen kao statičan, može se * pozvati po imenu samo unutar te C datoteke. */ static int fn_b(int x) ( return x+1; ) /* Definicija funkcije. */ /* Funkcijski parametar se smatra lokalnom varijablom. */ int fn_c(int x_local) ( /* Definicija neinicijalizirane lokalne varijable */ int y_local_uninit; /* Definicija inicijalizirane lokalne varijable */ int y_local_init = 3; /* Kod koji pristupa lokalnim i globalnim varijablama * i funkcijama putem ime */ 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)_

Šta radi C kompajler?

Posao C kompajlera je da konvertuje tekst koji je (obično) čoveku čitljiv u nešto što računar može da razume. Na izlazu kompajler proizvodi objektni fajl. Na UNIX platformama, ove datoteke obično imaju sufiks .o; na Windows-u - sufiks.obj. Sadržaj objektne datoteke su u suštini dvije stvari:

Kod i podaci će, u ovom slučaju, imati imena povezana s njima - imena funkcija ili varijabli s kojima su po definiciji pridruženi.

Objektni kod je niz (odgovarajuće sastavljenih) mašinskih instrukcija koje odgovaraju C instrukcijama koje je napisao programer: sve one if i while, pa čak i goto. Ove čarolije moraju manipulirati informacijama određene vrste, a informacije moraju biti negdje - zato su nam potrebne varijable. Kod također može referencirati drugi kod (posebno druge C funkcije u programu).

Gdje god se kod odnosi na varijablu ili funkciju, kompajler to dozvoljava samo ako je vidio tu varijablu ili funkciju prije. Deklaracija je obećanje da definicija postoji negdje drugdje u programu.

Posao povezivača je da provjeri ova obećanja. Međutim, šta prevodilac radi sa svim ovim obećanjima kada generiše objektni fajl?

U suštini kompajler ostavlja prazna mesta. Prazan prostor (link) ima ime, ali vrijednost koja odgovara ovom imenu još nije poznata.

S obzirom na ovo, možemo prikazati objektni fajl koji odgovara , na sljedeći način:

Parsiranje objektne datoteke

Do sada smo sve razmatrali na visokom nivou. Međutim, korisno je vidjeti kako to funkcionira u praksi. Glavni alat za nas će biti tim nm, koji pruža informacije o simbolima objektne datoteke na UNIX platformi. Za Windows komandu dumpbin sa opcijom /symbols je približan ekvivalent. Postoje i GNU binutils alati koji uključuju nm.exe.

Hajde da vidimo šta nm proizvodi za objektni fajl dobijen od:
Simboli iz c_parts.o: Naziv Vrijednost Klasa Tip Veličina Linija Sekcija fn_a | | U | NOTYPE| | |*UND* z_global | | U | NOTYPE| | |*UND* fn_b |00000000| t | FUNC|00000009| |.text x_global_init |00000000| D | OBJEKT|00000004| |.data y_global_uninit |00000000| b | OBJEKT|00000004| |.bss x_global_uninit |00000004| C | OBJEKT|00000004| |*COM* y_global_init |00000004| d | OBJEKT|00000004| |.data fn_c |00000009| T | FUNC|00000055| |.tekst
Rezultat može izgledati malo drugačije na različitim platformama (pogledajte man stranicu za detalje), ali ključne informacije su klasa svakog znaka i njegova veličina (ako postoji). Klasa može imati različita značenja:

  • Klasa U označava nedefinisane veze, one iste „prazne prostore“ pomenute gore. Postoje dva objekta za ovu klasu: fn_a i z_global. (Neke verzije nm mogu da izlaze odjeljak, što bi bilo *UND* ili UNDEF u ovom slučaju.)
  • Casovi t I T označiti šifru koja je definisana; razlika između t I T je li funkcija lokalna ( t) u fajlu ili ne ( T), tj. da li je funkcija deklarirana kao statična. Opet na nekim sistemima može se prikazati odjeljak, npr. .text.
  • Casovi d I D sadrže inicijalizirane globalne varijable. U ovom slučaju, statičke varijable pripadaju klasi d. Ako su informacije o sekciji prisutne, bit će .data.
  • Za neinicijalizirane globalne varijable dobijamo b, ako su statične i B ili C inače. Odjeljak će u ovom slučaju najvjerovatnije biti .bss ili *COM*.
Također možete vidjeti simbole koji nisu dio C izvornog koda. Nećemo fokusirati našu pažnju na ovo, pošto je to obično deo internog mehanizma kompajlera, tako da se vaš program može kasnije povezati.

Šta radi linker: 1. dio

Ranije smo rekli da je deklarisanje funkcije ili varijable obećanje kompajleru da postoji definicija te funkcije ili varijable negdje drugdje u programu i da je posao povezivača da ispuni to obećanje. Posmatrajući , ovaj proces možemo opisati kao “popunjavanje praznina”.

Ilustrirajmo ovo na primjeru, gledajući još jedan C fajl pored onog koji .
/* Inicijalizirana globalna varijabla */ int z_global = 11; /* Druga globalna varijabla pod nazivom y_global_init, ali su obje statične */ static int y_global_init = 2; /* Deklaracija druge globalne varijable */ extern int x_global_init; int fn_a(int x, int y) ( return(x+y); ) int main(int argc, char *argv) ( const char *message = "Zdravo, svijete"; return fn_a(11,12); )

Iz oba dijagrama možemo vidjeti da se sve tačke mogu povezati (ako ne, linker bi izbacio poruku o grešci). Svaka stvar ima svoje mjesto, i svako mjesto ima svoju stvar. Povezivač također može popuniti sve prazne prostore kao što je prikazano ovdje (na UNIX sistemima, proces povezivanja se obično poziva naredbom ld).

Za C situacija je manje očigledna. Mora postojati tačno jedna definicija za bilo koju funkciju i inicijaliziranu globalnu varijablu, ali definicija neinicijalizirane varijable može se tretirati kao preliminarno utvrđivanje. Jezik C stoga dozvoljava (ili barem ne sprječava) različite izvorne datoteke da sadrže predefinacije istog objekta.

Međutim, povezivači moraju biti u stanju da rade i sa jezicima koji nisu C i C++, za koje pravilo jedne definicije ne važi nužno. Na primjer, normalno je da Fortran ima kopiju svake globalne varijable u svakoj datoteci koja je referencira. Linker tada mora ukloniti duplikate odabirom jedne kopije (najveći predstavnik, ako se razlikuju po veličini) i odbacivanjem svih ostalih Ovaj model se ponekad naziva "uobičajeni model" rasporeda zbog ključne riječi COMMON jezika Fortran.

Kao rezultat toga, prilično je uobičajeno da se UNIX povezivači ne žale na duple simbole, barem ako su to dupli simboli neinicijaliziranih globalnih varijabli (ovaj model povezivanja se ponekad naziva "labavo-spregnuti model" [ cca. prevod Ovo je moj besplatni prijevod opuštenog ref/def modela. Bolji prijedlozi su dobrodošli]). Ako vas ovo brine (a vjerovatno bi trebalo), konsultujte dokumentaciju vašeg linkera da pronađete opciju --work-right koja ukroti njegovo ponašanje. Na primjer, u GNU lancu alata, opcija kompajlera -fno-common prisiljava neinicijaliziranu varijablu da se stavi u BBS segment umjesto da generiše COMMON blokove.

Šta radi operativni sistem?

Sada kada je linker proizveo izvršnu datoteku, dajući svakoj referenci simbola odgovarajuću definiciju, možete nakratko pauzirati da shvatite šta operativni sistem radi kada pokrenete program.

Pokretanje programa, naravno, podrazumeva izvršavanje mašinskog koda, tj. OS očigledno treba da prenese mašinski kod izvršne datoteke sa čvrstog diska u operativnu memoriju, gde ga CPU može pokupiti. Ovi dijelovi se nazivaju segment koda ili segment teksta.

Sam kod bez podataka je beskoristan. Stoga je svim globalnim varijablama potreban i prostor u memoriji računara. Međutim, postoji razlika između inicijaliziranih i neinicijaliziranih globalnih varijabli. Inicijalizirane varijable imaju određene početne vrijednosti, koje također moraju biti pohranjene u objektnim i izvršnim datotekama. Kada se program pokrene, OS kopira ove vrijednosti u virtualni prostor programa, u segment podataka.

Za neinicijalizirane varijable, OS može pretpostaviti da sve imaju 0 kao početnu vrijednost, tj. nema potrebe za kopiranjem vrijednosti. Komad memorije koji je inicijaliziran nulama poznat je kao bss segment.

To znači da se prostor za globalne varijable može dodijeliti u izvršnoj datoteci pohranjenoj na disku; Inicijalizirane varijable moraju imati sačuvane početne vrijednosti, ali neinicijalizirane samo trebaju sačuvati svoju veličinu.

Kao što ste možda primijetili, do sada smo u svim raspravama o objektnim datotekama i linkeru govorili samo o globalnim varijablama; U isto vrijeme nismo spomenuli lokalne varijable i dinamički zauzetu memoriju.

Ovim podacima nije potrebna nikakva intervencija povezivača jer njihov životni vijek počinje i završava se za vrijeme izvršavanja programa—dugo nakon što linker obavi svoj posao. Ipak, radi kompletnosti, ukratko ćemo istaći da:

  • lokalne varijable nalaze se u memorijskom području tzv stog, koji raste i skuplja kako se različite funkcije pozivaju i izvršavaju.
  • Dinamički dodijeljena memorija uzima se iz područja memorije poznatog kao gomila, a malloc funkcija kontrolira pristup slobodnom prostoru u ovoj oblasti.
Da biste upotpunili sliku, vrijedi dodati kako izgleda memorijski prostor pokrenutog procesa. Budući da hrpa i stek mogu dinamički mijenjati svoju veličinu, prilično je uobičajeno da stog raste u jednom smjeru, a hrpa raste u suprotnom smjeru. Prema tome, program će samo izbaciti grešku kada nema slobodne memorije ako se stek i hrpa sretnu negde na sredini (u tom slučaju će memorijski prostor programa biti pun).

Šta radi linker? dio 2

Sada kada smo pokrili šta radi linker, možemo zaroniti u složenije dijelove - otprilike hronološkim redoslijedom kako su dodani linkeru.

Glavno zapažanje koje utječe na funkcionalnost povezivača je sljedeće: ako veliki broj različitih programa radi približno iste stvari (izlaz na ekran, čitanje datoteka sa tvrdog diska, itd.), onda očigledno ima smisla izolirati ovo kod na određenom mjestu i dati ga drugim programima da ga koriste.

Jedno moguće rješenje bi bilo korištenje istih objektnih datoteka, ali bi bilo mnogo praktičnije držati cijelu kolekciju... objektnih datoteka na jednoj lako dostupnoj lokaciji: biblioteka.

Tehnički na stranu: Ovo poglavlje potpuno izostavlja važnu karakteristiku povezivača: preusmjeravanje(preseljenje). Različiti programi imaju različite veličine, tj. ako je zajednička biblioteka mapirana u adresni prostor različitih programa, ona će imati različite adrese. To zauzvrat znači da će sve funkcije i varijable u biblioteci biti na različitim mjestima. Sada, ako su sve adrese adresa relativne („vrijednost +1020 bajtova odavde“) a ne apsolutne („vrijednost na 0x102218BF“), onda to nije problem, ali to nije uvijek slučaj. U takvim slučajevima, sve apsolutne adrese moraju biti dodate sa odgovarajućim pomakom - to je preseljenje. Neću se ponovo vraćati na ovu temu, ali ću dodati da, pošto je ovo skoro uvek skriveno od C/C++ programera, vrlo je retko da su problemi sa rasporedom uzrokovani poteškoćama u preusmeravanju.

Statičke biblioteke

Najjednostavnija implementacija biblioteke je statički biblioteka. U prethodnom poglavlju je spomenuto da možete dijeliti kod jednostavno ponovnom upotrebom objektnih datoteka; ovo je suština statičkih biblioteka.

Na UNIX sistemima, naredba za izgradnju statičke biblioteke je obično ar, a rezultirajuća datoteka biblioteke ima ekstenziju *.a. Također, ove datoteke obično imaju prefiks "lib" u svom nazivu i prosljeđuju se linkeru s opcijom "-l", nakon čega slijedi naziv biblioteke bez prefiksa i ekstenzije (tj. "-lfred" će pokupiti datoteku " libfred.a").
(U prošlosti je takođe bio potreban program nazvan ranlib da statičke biblioteke generišu listu simbola na prednjoj strani biblioteke. Ovih dana ar alati to rade sami.)

Na Windows-u, statičke biblioteke imaju ekstenziju .LIB i napravljene su pomoću LIB alata, ali ova činjenica može da zavara, jer se ista ekstenzija koristi za „biblioteku uvoza“, koja sadrži samo listu onoga što se nalazi u DLL-u - vidi

Dok povezivač prolazi kroz kolekciju objektnih datoteka kako bi ih spojio zajedno, on održava listu simbola koji se još ne mogu implementirati. Nakon što su svi eksplicitno specificirani objektni fajlovi obrađeni, linker sada ima novo mjesto za traženje simbola koji ostaju na listi – u biblioteci. Ako je neimplementirani simbol definiran u jednom od objekata biblioteke, tada se objekt dodaje, kao da ga je korisnik dodao na listu objektnih datoteka, a povezivanje se nastavlja.

Obratite pažnju na granularnost onoga što se dodaje iz biblioteke: ako je potrebna definicija nekog simbola, onda ceo objekat, koji sadrži definiciju simbola, biće uključen. To znači da ovaj proces može biti ili korak naprijed ili korak nazad - novododani objekt može ili riješiti nedefiniranu referencu ili uvesti cijelu kolekciju novih neriješenih referenci.

Još jedan važan detalj je red događaji; biblioteke se pozivaju samo kada je normalno povezivanje završeno i kada se obrađuju uredu s lijeva na desno. To znači da ako posljednji objekt preuzet iz biblioteke zahtijeva simbol iz biblioteke ranije u komandnoj liniji veze, linker ga neće automatski pronaći.

Dajemo primjer da razjasnimo situaciju; Recimo da imamo sljedeće objektne datoteke i komandnu liniju veze koja sadrži a.o, b.o, -lx i -ly .


Jednom kada linker obradi a.o i b.o, reference na b2 i a3 će biti riješene, dok će x12 i y22 i dalje biti neriješene. U ovom trenutku, povezivač provjerava prvu biblioteku, libx.a, za nedostajuće simbole i otkriva da može uključiti x1.o da kompenzira referencu na x12; međutim, na ovaj način, x23 i y12 se dodaju na listu nedefiniranih referenci (lista sada izgleda kao y22, x23, y12).

Linker se još uvijek bavi libx.a, tako da se referenca na x23 lako kompenzira uključivanjem x2.o iz libx.a. Međutim, ovo dodaje y11 na nedefinisanu listu (koja je postala y22, y12, y11). Nijedna od ovih veza se ne može riješiti korištenjem libx.a, tako da se pretpostavlja da je linker liby.a.

Ista stvar se dešava ovde i linker uključuje y1.o i y2.o. Prvi dodan objekt je referenca na y21, ali pošto će y2.o i dalje biti uključen, ova referenca se jednostavno rješava. Rezultat ovog procesa je da su sve nedefinirane reference riješene i neki (ali ne svi) objekti biblioteke su uključeni u konačni izvršni fajl.

Imajte na umu da se situacija donekle mijenja ako kažemo da je b.o također imao vezu sa y32. Da je to slučaj, onda bi se povezivanje libx.a odvijalo na isti način, ali bi obrada liby.a uključivala uključivanje y3.o . Uključivanjem ovog objekta dodaćemo x31 na listu neriješenih simbola i ova referenca će ostati neriješena - u ovoj fazi linker je već završio obradu libx.a i stoga više neće pronaći definiciju ovog simbola (u x3.o) .

(Usput, ovaj primjer ima kružnu ovisnost između libx.a i liby.a; ovo je obično loša stvar)

Dinamičke dijeljene biblioteke

Za popularne biblioteke kao što je standardna biblioteka C (obično libc), kao statička biblioteka ima jasan nedostatak - svaki izvršni program će imati kopiju istog koda. Zaista, ako bi svaka izvršna datoteka imala kopiju printf, fopen i slično, tada bi se potrošila nerazumno velika količina prostora na disku.

Manje očigledan nedostatak je taj što je kod statički povezanog programa zauvijek fiksiran. Ako neko pronađe i popravi grešku u printf-u, svaki program će morati ponovo da se poveže da bi dobio ispravan kod.

Da bi se riješili ovih i drugih problema, uvedene su dinamički dijeljene biblioteke (obično imaju ekstenziju .so ili .dll na Windows-u i .dylib na Mac OS X-u). Za ovu vrstu biblioteke, linker ne mora nužno povezati sve tačke. Umjesto toga, linker izdaje kupon tipa “IOU” (dužan sam vam) i odlaže unovčenje ovog kupona dok se program ne pokrene.

Ono na šta se sve ovo svodi je da ako linker otkrije da je definicija određenog simbola u dijeljenoj biblioteci, ne uključuje tu definiciju u konačni izvršni fajl. Umjesto toga, linker upisuje ime simbola i biblioteku iz koje se očekuje da simbol dolazi.

Kada se program pozove na izvršenje, OS osigurava da su preostali dijelovi procesa povezivanja završeni na vrijeme prije nego što program počne raditi. Prije nego što se pozove glavna funkcija, mala verzija povezivača (često nazvana ld.so) prolazi kroz listu obećanja i izvodi završni čin povezivanja na licu mjesta - ubacivanje koda biblioteke i povezivanje tačaka.

To znači da nijedna izvršna datoteka ne sadrži kopiju printf koda. Ako je dostupna nova verzija printf-a, može se koristiti jednostavno promjenom libc.so - sljedeći put kada se program pokrene, novi printf će biti pozvan.

Postoji još jedna velika razlika između načina na koji dinamičke biblioteke rade u odnosu na statičke, a to dolazi u obliku granularnosti povezivanja. Ako je određeni simbol preuzet iz određene dinamičke biblioteke (recimo printf iz libc.so), tada se cijeli sadržaj biblioteke stavlja u adresni prostor programa. Ovo je glavna razlika u odnosu na statičke biblioteke, gdje se dodaju samo specifični objekti koji se odnose na nedefinirani simbol.

Drugim rečima, same deljene biblioteke se dobijaju kao rezultat rada povezivača (a ne kao formiranje velike gomile objekata, kao što to čini ar), koje sadrže reference između objekata u samoj biblioteci. Opet, nm je koristan alat za ilustraciju onoga što se događa: proizvešće više izlaza za svaki pojedinačni objektni fajl kada se pokrene na statičkoj verziji biblioteke, ali za dijeljenu verziju biblioteke, liby.so ima samo jedan nedefinirani x31 simbol. Takođe, u primeru sa redosledom uključivanja biblioteka na kraju, takođe neće biti problema: dodavanje linka na y32 u b.c neće podrazumevati nikakve promene, pošto je sav sadržaj y3.o i x3.o već korišteno.

Usput, još jedan koristan alat je ldd; na Unix platformi, prikazuje sve deljene biblioteke od kojih zavisi izvršna binarna datoteka (ili druga deljena biblioteka), zajedno sa indikacijom gde se te biblioteke mogu naći. Da bi se program uspješno pokrenuo, učitavač mora pronaći sve ove biblioteke zajedno sa svim njihovim ovisnostima. (Uobičajeno, učitavač traži biblioteke na listi direktorija navedenih u varijabli okruženja 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) libXm.so.6 .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 .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)
Razlog za veću granularnost je taj što su moderni operativni sistemi dovoljno pametni da vam omoguće više od samo spremanja duplih elemenata na disk, od čega pate statičke biblioteke. Različiti izvršni procesi koji koriste istu zajedničku biblioteku također mogu dijeliti segment koda (ali ne segment podataka ili bss segment - na primjer, dva različita procesa mogu biti na različitim mjestima kada se koristi, recimo, strtok). Da bi se to postiglo, čitava biblioteka mora biti adresirana jednim potezom, tako da su sve interne veze usklađene na jedinstven način. Zaista, ako jedan proces pokupi a.o i c.o, a drugi obradi b.o i c.o, tada OS neće moći koristiti nijedno podudaranje.

Windows DLL

Iako su opći principi dijeljenih biblioteka otprilike isti i na Unix i na Windows platformama, još uvijek postoji nekoliko detalja u koje se početnici mogu uhvatiti.

Izvezeni simboli

Najveća razlika je u tome što u Windows bibliotekama simboli nisu izvezeno automatski. Na Unixu, svi simboli svih objektnih fajlova koji su povezani sa deljenom bibliotekom vidljivi su korisniku te biblioteke. Na Windows-u, programer mora eksplicitno učiniti neke znakove vidljivim, tj. izvozi ih.

Postoje tri načina za izvoz simbola i Windows DLL-a (i sve tri ove metode mogu se pomiješati u istoj biblioteci).

  • U izvornom kodu, deklarirajte simbol kao __declspec(dllexport) , otprilike ovako:
    __declspec(dllexport) int my_exported_function(int x, double y)
  • Kada izvršavate naredbu povezivača, koristite opciju izvoza LINK.EXE: simbol_za_izvoz
    LINK.EXE /dll /export:my_exported_function
  • Ubacite datoteku definicije modula (DEF) u linker (koristeći /DEF opciju: def_file), uključivanjem odjeljka EXPORT u ovu datoteku, koja sadrži simbole za izvoz.
    IZVOZ my_exported_function my_other_exported_function
Kada se C++ uključi u ovaj nered, prva od ovih opcija postaje najjednostavnija, jer u ovom slučaju kompajler preuzima odgovornost za brigu o

.LIB i druge datoteke povezane s bibliotekom

Dolazimo do druge poteškoće sa Windows bibliotekama: informacije o izvezenim simbolima koje linker mora povezati sa drugim simbolima nisu sadržane u samom DLL-u. Umjesto toga, ove informacije su sadržane u odgovarajućoj .LIB datoteci.

LIB datoteka povezana s DLL-om opisuje koji (izvezeni) simboli se nalaze u DLL-u zajedno s njihovom lokacijom. Svaki binarni fajl koji koristi DLL mora pristupiti .LIB datoteci da bi ispravno povezao simbole.

Da stvari budu još zbunjujuće, ekstenzija .LIB se također koristi za statičke biblioteke.

U stvari, postoji niz različitih datoteka koje mogu na neki način biti povezane sa Windows bibliotekama. Uz .LIB datoteku, kao i (opcionalnu) .DEF datoteku, možete vidjeti sve sljedeće datoteke povezane s vašom Windows bibliotekom.

Ovo je velika razlika u odnosu na Unix, gdje se gotovo sve informacije sadržane u svim ovim dodatnim datotekama jednostavno dodaju u samu biblioteku.

Uvezeni simboli

Osim što zahtijeva da DLL-ovi eksplicitno deklariraju , Windows također dozvoljava binarne datoteke koje koriste kod biblioteke da eksplicitno deklariraju simbole za uvoz. Ovo nije obavezno, ali pruža određenu optimizaciju brzine zbog istorijskih svojstava 16-bitnih prozora.

Možemo pratiti ove liste, opet koristeći nm. Razmotrite sljedeću C++ datoteku:
klasa Fred ( privatno: int x; int y; javno: Fred() : x(1), y(2) () Fred(int z): x(z), y(3) () ); Fred theFred; Fred TheOtherFred(55);
Za ovaj kod ( Ne ukrašen) nm izlaz izgleda ovako:
Simboli iz global_obj.o: Ime Vrijednost Klasa Tip Veličina Linija Odjeljak __gxx_personality_v0| | U | NOTYPE| | |*UND* __static_inicialization_and_destruction_0(int, int) |00000000| t | FUNC|00000039| |.text Fred::Fred(int) |00000000| W | FUNC|00000017| |.text._ZN4FredC1Ei Fred::Fred() |00000000| W | FUNC|00000018| |.text._ZN4FredC1Ev theFred |00000000| B | OBJEKAT|00000008| |.bss theOtherFred |00000008| B | OBJEKAT|00000008| |.bss globalni konstruktori s ključem za theFred |0000003a| t | FUNC|0000001a| |.tekst
Kao i obično, ovdje možemo vidjeti gomilu različitih stvari, ali jedna od njih koja nam je najzanimljivija su postovi iz razreda W(što znači „slab“ simbol) kao i unosi sa nazivom sekcije kao što je „.gnu.linkonce.t. stvari". Ovo su markeri za konstruktore globalnih objekata i vidimo da odgovarajuće polje "Name" pokazuje šta zapravo možemo očekivati ​​tamo - svaki od dva konstruktora je uključen.

Predlošci

Ranije smo dali tri različite implementacije max funkcije, od kojih je svaka uzimala različite vrste argumenata. Međutim, vidimo da je kod tijela funkcije identičan u sva tri slučaja. A znamo da je dupliranje istog koda loša praksa programiranja.

C++ uvodi koncepte šablon(šabloni), koji vam omogućava da koristite kod u nastavku za sve slučajeve odjednom. Možemo kreirati datoteku zaglavlja max_template.h sa samo jednom kopijom koda funkcije max:
šablon T max(T x, T y) (ako (x>y) vrati x; inače vrati y; )
i uključite ovu datoteku u izvornu datoteku da isprobate funkciju predloška:
#include "max_template.h" int main() ( int a=1; int b=2; int c; c = max(a,b); // Kompajler automatski određuje šta je tačno max potrebno (int,int) dupli x = 1,1; float y = 2,2; dupli z; z = max (x,y); // Kompajler ne može odrediti, pa nam je potrebno max (double,double) return 0; )
Ovaj kod, napisan u C++, koristi max (int,int) i maks (dvostruki, dupli) . Međutim, neki drugi kod bi mogao koristiti druge instance ovog uzorka. Pa, recimo max (float,float) ili čak max (MyFloatingPointClass,MyFloatingPointClass) .

Svaka od ovih različitih instanci proizvodi drugačiji strojni kod. Dakle, u trenutku kada je program konačno povezan, kompajler i linker moraju osigurati da je kod za svaku korišćenu instancu predloška uključen u program (i da nijedna neiskorištena instanca šablona nije uključena, kako ne bi povećala veličinu programa).

Kako se to radi? Obično postoje dva pravca akcije: ili smanjivanje duplih instanci ili odlaganje instanciranja do faze povezivanja (obično ove pristupe nazivam pametnim putem i načinom Sunca).

Metoda razrjeđivanja ponavljajućih instanci podrazumijeva da svaki objektni fajl sadrži kod svih naišlih šablona. Na primjer, za gornju datoteku, sadržaj objektne datoteke izgleda ovako:
Simboli iz max_template.o: Ime Vrijednost Klasa Tip Veličina Linija Odjeljak __gxx_personality_v0 | | U | NOTYPE| | |*UND* duplo max (dvostruki, dupli) |00000000| W | FUNC|00000041| |.text _Z3maxIdET_S0_S0_ int max (int, int) |00000000| W | FUNC|00000021| |.text._Z3maxIiET_S0_S0_ main |00000000| T | FUNC|00000073| |.tekst
I vidimo prisustvo obe instance max (int,int) i maks (dvostruki, dupli) .

Obje definicije su označene kao slabi karakteri, a to znači da linker prilikom kreiranja konačne izvršne datoteke može izbaciti sve duple instance istog predloška i ostaviti samo jednu (i ako smatra da je potrebno, može provjeriti da li se sve duple instance predloška stvarno mapiraju u isti kod). Najveći nedostatak ovog pristupa je povećanje veličine svakog pojedinačnog objektnog fajla.

Drugi pristup (koji se koristi u Solaris C++) je da se definicije predložaka uopće ne uključuju u objektne datoteke, već da se one označavaju kao nedefinirani simboli. Kada je u pitanju faza povezivanja, linker može prikupiti sve nedefinirane simbole koji zapravo pripadaju instancama šablona, ​​a zatim generirati strojni kod za svaku od njih.

Ovo definitivno smanjuje veličinu svake objektne datoteke, ali loša strana ovog pristupa je da povezivač mora pratiti gdje se nalazi izvorni kod i mora biti u mogućnosti da pokrene C++ kompajler u vrijeme povezivanja (što može usporiti cijeli proces )

Dinamički učitane biblioteke

Posljednja karakteristika o kojoj ćemo ovdje raspravljati je dinamičko učitavanje dijeljenih biblioteka. U nastavku smo vidjeli kako korištenje zajedničkih biblioteka odgađa konačno povezivanje dok se program stvarno ne pokrene. U modernim OS-ima to je čak moguće u kasnijim fazama.

Ovo se postiže parom sistemskih poziva, dlopen i dlsym (približni ekvivalenti na Windows-u se zovu LoadLibrary i GetProcAddress, respektivno). Prvi uzima ime dijeljene biblioteke i učitava ga u adresni prostor pokrenutog procesa. Naravno, ova biblioteka može imati i neriješene simbole, tako da pozivanje dlopen-a može dovesti do učitavanja drugih zajedničkih biblioteka.

Dlopen nudi izbor ili brisanja svih neriješenih čim se biblioteka učita (RTLD_NOW) ili rješavanja simbola po potrebi (RTLD_LAZY). Prvi metod znači da pozivanje dlopen-a može potrajati dosta vremena, ali drugi metod uvodi određeni rizik da će se tokom izvršavanja programa pronaći nedefinirana referenca koja se ne može riješiti, nakon čega će se program prekinuti.

Naravno, simboli iz dinamički učitane biblioteke ne mogu imati ime. Međutim, ovo se može lako riješiti, kao što se rješavaju i drugi programski problemi, dodavanjem dodatnog sloja zaobilaznih rješenja. U ovom slučaju se koristi pokazivač na prostor znakova. Poziv dlsym-a uzima literalni parametar koji specificira ime simbola koji treba pronaći i vraća pokazivač na njegovu lokaciju (ili NULL ako simbol nije pronađen).

Interoperabilnost sa C++

Proces dinamičkog učitavanja je prilično jednostavan, ali kako je u interakciji s različitim C++ karakteristikama koje utiču na cjelokupno ponašanje povezivača?

Prvo zapažanje se odnosi na ukrašavanje imena. Prilikom pozivanja dlsym-a, prosljeđuje se ime simbola koji treba pronaći. To znači da ovo mora biti verzija imena koja je vidljiva linkeru, tj. ukrašeno ime.

Pošto se proces dekoracije može razlikovati od platforme do platforme i od kompajlera do kompajlera, to znači da je praktično nemoguće dinamički pronaći C++ simbol koristeći univerzalnu metodu. Čak i ako radite samo s jednim kompajlerom i zadubite se u njegov unutarnji svijet, postoje i drugi problemi - osim jednostavnih funkcija sličnih C, postoji i gomila drugih stvari (tablice virtualnih metoda i slično) o kojima se također treba pobrinuti .

Da sumiramo gore navedeno, obično je bolje imati jednu eksternu "C" ulaznu tačku koju može pronaći dlsym ". Ova ulazna tačka može biti fabrička metoda koja vraća pokazivače na sve instance C++ klase, omogućavajući pristup svim čari C++-a.

Kompajler bi mogao biti u stanju da se nosi sa globalnim konstruktorima objekata u biblioteci koju učitava dlopen, pošto postoji nekoliko specijalnih simbola koji se mogu dodati u biblioteku, a koje će pozvati linker (bez obzira na učitavanje ili izvršavanje vrijeme) ako se biblioteka dinamički učitava ili ispušta - tada postoje neophodni pozivi konstruktorima ili destruktorima mogu se dogoditi ovdje. Na Unixu su to funkcije _init i _fini, ili za novije sisteme koji koriste GNU komplet alata postoje funkcije označene __attribute__((konstruktor)) ili __attribute__((destructor)) . Na Windows-u, odgovarajuća funkcija je DllMain sa DWORD fdwReason jednakim DLL_PROCESS_ATTACH ili DLL_PROCESS_DETACH .

I kao zaključak, dodaćemo da dinamičko učitavanje radi odličan posao „razrjeđivanja ponavljajućih instanci“ kada je u pitanju instanciranje predložaka; i sve izgleda dvosmisleno sa “odgađanjem instanciranja”, pošto se “faza povezivanja” javlja nakon što je program već pokrenut (i vrlo moguće na drugoj mašini koja ne pohranjuje izvore). Pogledajte dokumentaciju kompajlera i linkera kako biste pronašli rješenje za ovu situaciju.

Dodatno

Ovaj članak je namjerno izostavio mnoge detalje o tome kako linker radi jer vjerujem da ono što je napisano pokriva 95% svakodnevnih problema sa kojima se programer suočava kada povezuje svoj program.

Ukoliko želite da saznate više, informacije možete dobiti na linkovima ispod:

Veliko hvala Mikeu Cappu i Edu Wilsonu na korisnim prijedlozima o ovoj stranici.

Autorska prava 2004-2005,2009-2010 David Drysdale

Dozvoljava se kopiranje, distribucija i/ili modifikacija ovog dokumenta pod uslovima GNU licence za slobodnu dokumentaciju, verzije 1.1 ili bilo koje novije verzije koju je objavila Free Software Foundation; bez nepromjenjivih odjeljaka, bez teksta na prednjoj korici i bez teksta na stražnjoj korici. Kopija licence je dostupna.

Oznake: Dodajte oznake

Najbolji članci na ovu temu