Cum se configurează smartphone-uri și PC-uri. Portal informativ

De ce este folosit un linker în bibliotecile grafice? Funcții de linker și încărcare

[din metode]

Definiția 9.22 Linker (editor de linkuri) „este un program conceput pentru a lega împreună fișierele obiect generate de compilator și fișierele de bibliotecă incluse în sistemul de programare.

Un fișier obiect nu poate fi lansat până când toate modulele și secțiunile din el nu sunt legate între ele. Ieșirea linkerului este un fișier executabil. Acest fișier include textul programului în limbajul codului mașinii. Când încercați să creați un fișier executabil, linkerul poate afișa un mesaj de eroare dacă nu găsește o componentă.

Mai întâi, linkerul selectează o secțiune de program din primul modul de obiect și îi atribuie o adresă de pornire. Secțiunile de program ale modulelor obiect rămase primesc adrese relativ la adresa de pornire în următoarea ordine. În acest caz, adresele secțiunilor de program pot fi aliniate. Concomitent cu îmbinarea textelor secțiunilor de program, se combină secțiunile de date, tabelele de identificatori și timpii externi. Legăturile transversale sunt permise.

Procedura de rezolvare a legăturilor se reduce la calcularea valorilor constantelor de adrese ale procedurilor, funcțiilor și variabilelor, ținând cont de mișcările secțiunilor față de începutul modulului de program asamblat. Dacă în același timp se găsesc referințe la variabile externe care nu sunt în lista modulelor obiect, editorul de linkuri organizează o căutare a acestora în bibliotecă; componenta necesară nu poate fi găsită și este generat un mesaj de eroare.

De obicei, linkerul creează un modul software simplu care este creat ca o singură unitate. Cu toate acestea, în cazuri mai complexe, linkerul poate crea alte module: module de program suprapuse structurate, module de obiecte de bibliotecă și module de bibliotecă de link-uri dinamice.

Linker (de asemenea link editor, linker - din limba engleză link editor, linker) - un program care realizează legături - ia ca intrare unul sau mai multe module obiect și asamblează un modul executabil din ele.

Pentru a lega module, linkerul folosește tabele de nume create de compilator în fiecare dintre modulele obiect. Astfel de nume pot fi de două tipuri:

Nume definite sau exportate - funcții și variabile definite într-un anumit modul și puse la dispoziție pentru utilizare de către alte module

Numele nedefinite sau importate sunt funcții și variabile la care se face referire de către un modul, dar nu sunt definite intern.

Sarcina linkerului este să rezolve referințele la nume nedefinite în fiecare modul. Pentru fiecare nume importat, definiția acestuia se găsește în alte module; mențiunea numelui este înlocuită cu adresa acestuia.

Linker-ul, în general, nu verifică tipurile și numărul de parametri ai procedurilor și funcțiilor. Dacă trebuie să combinați modulele obiect ale programelor scrise în limbi cu tastare puternică, atunci verificările necesare trebuie efectuate de un utilitar suplimentar înainte de a lansa editorul de linkuri.

Linker-ul (sau link editor) este proiectat pentru a lega împreună fișierele obiect generate de compilator, precum și fișierele de bibliotecă incluse în sistemul de programare.

Un fișier obiect (sau un set de fișiere obiect) nu poate fi executat până când toate modulele și secțiunile din el nu sunt legate între ele. Aceasta este ceea ce face editorul de linkuri (linker). Rezultatul muncii sale este un singur fișier numit modul de pornire.

Load module este un modul software potrivit pentru încărcare și execuție, obținut dintr-un modul obiect la editarea legăturilor și reprezentând un program sub forma unei secvențe de comenzi de mașină.

Linker-ul poate genera un mesaj de eroare dacă nu reușește să detecteze componentele necesare atunci când încearcă să asamblați fișiere obiect într-un singur întreg.

Funcția de linker este destul de simplă. Își începe activitatea selectând o secțiune de program din primul modul de obiect și atribuindu-i o adresă de pornire. Secțiunile de program ale modulelor obiect rămase primesc adrese relativ la adresa de pornire în următoarea ordine. În acest caz, poate fi îndeplinită și funcția de aliniere a adreselor de pornire ale secțiunilor de program. Concomitent cu îmbinarea textelor secțiunilor de program, se combină secțiunile de date, tabele de identificatori și nume externe. Legăturile transversale sunt permise.

Procedura de rezolvare a legăturilor se reduce la calcularea valorilor constantelor de adrese ale procedurilor, funcțiilor și variabilelor, ținând cont de mișcările secțiunilor față de începutul modulului de program asamblat. Dacă sunt detectate referiri la variabile externe care nu sunt în lista modulelor obiect, editorul de linkuri organizează o căutare a acestora în bibliotecile disponibile în sistemul de programare. Dacă componenta necesară nu poate fi găsită în bibliotecă, este generat un mesaj de eroare.

De obicei, linkerul creează un modul software simplu care este creat ca o singură unitate. Cu toate acestea, în cazuri mai complexe, linkerul poate crea alte module: module de program suprapuse structurate, module de obiecte de bibliotecă și module de bibliotecă de link-uri dinamice.

Majoritatea modulelor obiect din sistemele de programare moderne sunt construite pe baza așa-numitelor adrese relative. Compilatorul, care generează fișiere obiect, și apoi linkerul, care le combină într-un singur întreg, nu poate ști exact în ce zonă reală a memoriei computerului va fi localizat programul în momentul execuției sale. Prin urmare, ele nu funcționează cu adrese reale ale celulelor RAM, ci cu unele adrese relative. Astfel de adrese sunt numărate dintr-un anumit punct convențional, luat ca început al zonei de memorie ocupată de programul rezultat (de obicei acesta este punctul de început al primului modul de program).

Desigur, niciun program nu poate fi executat în aceste adrese relative. Prin urmare, este necesar un modul care să convertească adresele relative în adrese reale (absolute) imediat în momentul în care programul este lansat pentru execuție. Acest proces se numește traducerea adresei și este realizat de un modul special numit încărcător.

Cu toate acestea, încărcătorul de pornire nu este întotdeauna o parte integrantă a sistemului de programare, deoarece funcțiile pe care le îndeplinește depind foarte mult de arhitectura sistemului informatic țintă pe care este executat programul rezultat creat de sistemul de programare. În primele etape ale dezvoltării sistemului de operare, încărcătoarele de încărcare existau sub formă de module separate care efectuau traducerea adreselor și pregăteau programul pentru execuție - creând așa-numita „imagine a sarcinii”. Această schemă a fost tipică pentru multe sisteme de operare (de exemplu, RTOS pe un computer de tip SM-1, OS RSX/11 sau RAFOS pe un computer de tip SM-4 etc.). Imaginea sarcinii ar putea fi salvată pe un suport extern sau creată din nou de fiecare dată când programul a fost pregătit pentru execuție.

Odată cu dezvoltarea arhitecturii computerelor, a devenit posibilă efectuarea traducerii adreselor direct în momentul lansării programului pentru execuție. Pentru a face acest lucru, a fost necesar să includeți în fișierul executabil un tabel corespunzător care să conțină o listă de link-uri către adrese care trebuie traduse. La momentul lansării fișierului executabil, sistemul de operare a procesat acest tabel și a convertit adresele relative în adrese absolute. Această schemă, de exemplu, este tipică pentru un sistem de operare precum MS-DOS. În această schemă, nu există un modul bootloader ca atare (de fapt, face parte din sistemul de operare), iar sistemul de programare este responsabil doar pentru pregătirea tabelului de traducere a adreselor - această funcție este realizată de linker.

În sistemele de operare moderne, există metode complexe de conversie a adreselor care funcționează direct în timpul execuției programului. Aceste metode se bazează pe capabilitățile încorporate în hardware-ul arhitecturii sistemelor de calcul. Metodele de traducere a adreselor se pot baza pe organizarea memoriei segment, pagină și segment-pagină. Apoi, pentru a efectua traducerea adreselor, tabelele de sistem corespunzătoare trebuie pregătite în momentul lansării programului. Aceste funcții se încadrează în întregime pe modulele OS, deci nu sunt realizate în sistemele de programare.

Articole de citit:

Cum se face upgrade la Miui 9 Stable Global de la firmware-ul chinezesc? Deblocarea bootloader-ului

Acest articol.

Organizarea unui tabel de nume simbolice în asamblator.

Acest tabel conține informații despre simboluri și semnificațiile acestora colectate de către asamblator în timpul primei treceri. Asamblatorul accesează tabelul de nume simbolice în a doua trecere. Să ne uităm la modalități de a organiza un tabel de nume simbolice. Să ne imaginăm un tabel ca o memorie asociativă care stochează un set de perechi: nume simbolic - valoare. Memoria asociativă pentru un nume ar trebui să dea semnificația acestuia. În loc de un nume și o valoare, poate exista un pointer către nume și un indicator către valoare.

Asamblare secvențială.

Tabelul numelor simbolice este reprezentat ca rezultat al primei treceri ca o matrice de perechi nume-valoare. Căutarea caracterului necesar se efectuează prin scanarea secvenţială a tabelului până când este determinată o potrivire. Această metodă este destul de ușor de programat, dar funcționează lent.

Sorteaza dupa nume.

Numele sunt presortate alfabetic. Pentru a căuta nume, se folosește un algoritm de tăiere binar, care compară numele necesar cu numele elementului din mijloc al tabelului. Dacă simbolul dorit este situat alfabetic mai aproape de elementul din mijloc, atunci este în prima jumătate a tabelului și, dacă este mai departe, atunci în a doua jumătate a tabelului. Dacă numele dorit se potrivește cu numele elementului din mijloc, atunci căutarea se termină.

Algoritmul de tăiere binar este mai rapid decât scanarea tabelului secvenţial, dar elementele tabelului trebuie aranjate în ordine alfabetică.

Codare cache.

Cu această metodă, bazată pe tabelul original, este construită o funcție cache care mapează nume cu numere întregi în intervalul de la O la k–1 (Fig. 5.2.1, a). O funcție cache poate fi, de exemplu, o funcție care înmulțește toți biții unui nume reprezentat prin cod ASCII, sau orice altă funcție care oferă o distribuție uniformă a valorilor. După aceasta, este creat un tabel cache care conține k rânduri (sloturi). Fiecare linie conține (de exemplu, în ordine alfabetică) nume care au aceleași valori ale funcției cache (Fig. 5.2.1, b) sau număr de slot. Dacă tabelul cache conține n nume simbolice, atunci numărul mediu de nume din fiecare slot este n/k. Când n = k, găsirea numelui simbolic dorit în medie va necesita o singură căutare. Schimbând k, puteți varia dimensiunea mesei (numărul de sloturi) și viteza de căutare. Conectare și încărcare. Un program poate fi reprezentat ca un set de proceduri (subrutine). Asamblator unul câte unul difuzat o procedură după alta, creând module obiecteși plasându-le în memorie. Pentru a obține cod binar executabil, trebuie găsite următoarele și conectat toate procedurile traduse.

Funcțiile de legare și încărcare sunt efectuate de programe speciale numite linkeri, încărcătoare de linkuri, editori de linkuri sau linkerii.


Astfel, pentru a fi complet pregătit pentru a executa programul original, sunt necesari doi pași (Fig. 5.2.2):

● traducere implementată de compilator sau asamblator pentru fiecare procedură sursă în vederea obținerii unui modul obiect. La difuzare, are loc o tranziție din original limba în zi libera o limbă având comenzi și notații diferite;

● conectarea modulelor obiect realizată de linker pentru a produce cod binar executabil. Traducerea separată a procedurilor este cauzată de posibile erori sau de necesitatea modificării procedurilor. În aceste cazuri, va trebui să reconectați toate modulele obiect. Deoarece legarea este mult mai rapidă decât traducerea, efectuarea acestor doi pași (traducere și conectare) va economisi timp la finalizarea programului. Acest lucru este deosebit de important pentru programele care conțin sute sau mii de module. În sistemele de operare MS-DOS, Windows și NT, modulele obiect au extensia „.obj”, iar programele binare executabile au extensia „.exe”. Pe un sistem UNIX, modulele obiect au extensia „.o”, dar programele binare executabile nu au extensie.

Funcții de linker.

Înainte de a începe prima trecere de asamblare, contorul de adrese de instrucțiune este setat la 0. Acest pas este echivalent cu presupunerea că modulul obiect va fi localizat la adresa 0 în timpul rulării.

Scopul layout-ului este creați o mapare exactă a spațiului de adrese virtuale al programului executabil în interiorul linkerului și plasați toate modulele obiect la adresele corespunzătoare.


Să luăm în considerare caracteristicile de layout a patru module obiect (Fig. 5.2.3, a), presupunând că fiecare dintre ele este situat în celula cu adresa 0 și începe cu comanda de tranziție BRANCH la comanda MOVE din același modul. Înainte de a rula un program, linkerul plasează module obiect în memoria principală, producând o afișare a codului binar executabil. De obicei, o mică secțiune de memorie care începe de la adresa zero este utilizată pentru vectorii de întrerupere, interacțiunea cu sistemul de operare și alte scopuri.

Prin urmare, așa cum se arată în fig. 5.2.3, b, programele nu pornesc de la adresa zero, ci de la adresa 100. Deoarece fiecare modul obiect din Fig. 5.2.3, dar ocupă un spațiu de adrese separat, se pune problema redistribuirii memoriei. Toate comenzile de acces la memorie vor eșua din cauza adresei incorecte. De exemplu, comanda de apelare a modulului obiect B (Fig. 5.2.3, b), specificată în celula cu adresa 300 a modulului obiect A (Fig. 5.2.3, a), nu va fi executată din două motive:

● comanda CALL B este într-o celulă cu o adresă diferită (300, nu 200); ● Deoarece fiecare procedură este tradusă separat, asamblatorul nu poate determina ce adresă să insereze în CALL B. Adresa modulului obiect B nu este cunoscută înainte de legare. Această problemă se numește problema legaturilor externe. Ambele motive sunt eliminate folosind un linker care îmbină spațiile de adrese separate ale modulelor obiect într-un singur spațiu de adrese liniar, care se realizează prin:

● construiește un tabel cu module de obiecte și lungimile acestora;

● pe baza acestui tabel, atribuie adrese de pornire fiecărui modul obiect;

la memorie,și adaugă fiecăruia dintre ele o constantă de deplasare, care este egală cu adresa de pornire a acestui modul (în acest caz, 100);

● găsește toate comenzile care accesează la proceduri,și inserează în ele adresa acestor proceduri.
Mai jos este un tabel al modulelor obiect (Tabelul 5.2.6), construit în primul pas. Acesta oferă numele, lungimea și adresa de pornire a fiecărui modul. Spațiul de adrese după ce linkerul a finalizat toți pașii este afișat în tabel. 5.2.6 și în Fig. 5.2.3, c. Structura modulului obiect. Modulele obiect constau din următoarele părți:

numele modulului, unele informații suplimentare (de exemplu, lungimile diferitelor părți ale modulului, data asamblarii);

lista de simboluri definite într-un modul(nume simbolice) împreună cu semnificațiile acestora. Aceste simboluri pot fi accesate de alte module. Programatorul limbajului de asamblare folosește directiva PUBLIC pentru a specifica ce nume simbolice sunt considerate puncte de intrare;

lista de nume simbolice folosite, care sunt definite în alte module. Lista indică, de asemenea, denumirile simbolice utilizate de anumite instrucțiuni ale mașinii. Acest lucru permite linkerului să insereze adrese corecte în comenzile care folosesc nume externe. Acest lucru permite unei proceduri să apeleze alte proceduri traduse independent declarând (folosind directiva EXTERN) numele procedurilor apelate ca fiind externe. În unele cazuri, punctele de intrare și legăturile externe sunt combinate într-un singur tabel;

instrucțiuni și constante ale mașinii;

dicționar de mișcare. Comenzile care conțin adrese de memorie trebuie adăugate cu o constantă de deplasare (vezi Figura 5.2.3). Linkerul în sine nu poate determina ce cuvinte conțin instrucțiuni de mașină și care conțin constante. Prin urmare, acest tabel conține informații despre adresele care trebuie mutate. Acesta poate fi un tabel de biți, unde pentru fiecare bit există o adresă potențială de mutat, sau o listă explicită de adrese care trebuie mutate;

sfârşitul modulului, adresa de început,și verifica suma pentru a identifica erorile făcute în timpul citirii unui modul. Rețineți că instrucțiunile și constantele mașinii singura parte a unui modul obiect care va fi încărcată în memorie pentru execuție. Părțile rămase sunt folosite și eliminate de linker înainte ca programul să înceapă execuția. Cele mai multe linkere folosesc Două trecere:

● mai întâi, sunt citite toate modulele obiect și este construit un tabel cu nume și lungimi de module, precum și un tabel cu simboluri, care constă din toate punctele de intrare și referințele externe;

● Modulele sunt apoi citite din nou, mutate în memorie și legate. Despre mutarea programelor. Problema mutării programelor legate și în memorie se datorează faptului că, după ce sunt mutate, adresele stocate în tabele devin eronate. Pentru a lua o decizie cu privire la mutarea programelor, trebuie să cunoașteți momentul legării finale nume simbolice cu absolut adrese de memorie fizică.

Timp de decizie se numeste momentul determinarii adresei in memoria principala corespunzatoare numelui simbolic. Există diferite opțiuni pentru momentul deciziei obligatorii: când este scris program când program difuzat, compilat, descărcat sau când echipă, care contine adresa, efectuat. Metoda discutată mai sus asociază nume simbolice cu adrese fizice absolute. Din acest motiv, nu puteți muta programe după conectare.

La conectarea, se pot distinge două etape:

primul stadiul în care nume simbolice a lua legatura adrese virtuale. Când linkerul leagă spațiile de adrese individuale ale modulelor obiect într-un singur spațiu de adrese liniar, acesta creează efectiv un spațiu de adrese virtuale;

al doilea etapa când adrese virtuale a lua legatura adrese fizice. Numai după a doua operație procesul de legare poate fi considerat finalizat. Necesar condiție pentru mutarea programului este prezența unui mecanism care vă permite să schimbați maparea adreselor virtuale la adresele memoriei fizice principale (efectuați în mod repetat a doua etapă). Astfel de mecanisme includ:

● paginarea. Spațiul de adrese prezentat în fig. 5.2.3, în, conține adrese virtuale care sunt deja definite și corespund denumirilor simbolice A, B, C și D. Adresele lor fizice vor depinde de conținutul tabelului de pagini. Prin urmare, pentru a muta un program în memoria principală, este suficient să-i schimbi doar tabelul de pagini, dar nu și programul în sine;

● utilizarea registrul de mișcare. Acest registru indică adresa fizică a început programul curent, încărcat de sistemul de operare înainte de a muta programul. Folosind hardware, conținutul registrului de relocare este adăugat la toate adresele de memorie înainte de a fi încărcat în memorie. Procesul de mutare este transparent pentru fiecare program utilizator. Caracteristica mecanismului: spre deosebire de paginare, întregul program trebuie mutat. Dacă există registre separate (sau segmente de memorie, cum ar fi procesoarele Intel) pentru mutarea codului și a datelor, atunci programul trebuie mutat ca două componente;

● mecanism contestatii la memorie în raport cu contorul de programe. Cu acest mecanism, atunci când un program este mutat în memoria principală, este actualizat doar contorul de programe. Un program în care toate accesările la memorie sunt asociate cu contorul de programe (sau sunt absolute, cum ar fi accesele la registrele dispozitivului I/O în adrese absolute) este numit program independent de poziție. Un astfel de program poate fi plasat oriunde în spațiul de adrese virtuale fără a configura adrese. Legătura dinamică.

Metoda de conectare discutată mai sus are una particularitate: conexiunea cu toate procedurile necesare programului este stabilită înainte ca programul să înceapă să ruleze. Un mod mai eficient de a lega proceduri compilate separat, numit dinamic legarea constă în stabilirea unei legături cu fiecare procedură în timpul primului apel. A fost folosit pentru prima dată în sistemul MULTICS.

Legătura dinamică în sistemMULTICS. În spatele fiecăruia program asigurat segment de legare, conţinând un bloc de informaţii pentru fiecare procedură (Fig. 5.2.4).

Informațiile includ:

● cuvântul „Adresă indirectă”, rezervat adresei virtuale a procedurii;

● numele procedurii (Pământ, FOC etc.), care este salvat ca șir de caractere. Cu legarea dinamică, apelurile de procedură în limbajul de intrare sunt traduse în comenzi care, folosind adresare indirectă, accesează cuvântul „Adresă indirectă” al blocului corespunzător (Fig. 5.2.4). Compilatorul completează fie acest cuvânt adresă invalidă, sau un set special de biți, care provoacă o întrerupere a sistemului (cum ar fi capcane). După care:

● linkerul găsește numele procedurii (de exemplu, EARTH) și începe să caute un director de utilizator pentru procedura compilată cu acel nume;

● procedurii găsite i se atribuie o adresă virtuală „Adresă EARTH” (de obicei în propriul segment), care este scrisă peste adresa invalidă, așa cum se arată în Fig. 5.2.4;

● Comanda care a cauzat eroarea este apoi executată din nou. Acest lucru permite programului să continue să ruleze de unde era înainte de întreruperea sistemului. Toate apelurile ulterioare către procedura EARTH se vor executa fără eroare deoarece segmentul de legare conține acum adresa virtuală reală „Adresa EARTH” în loc de cuvântul „Adresă indirectă”. Astfel, linkerul este implicat doar atunci când o procedură este apelată pentru prima dată. Nu este nevoie să apelați linkerul după aceasta.

Conectare dinamică pe Windows.

Pentru legare, sunt utilizate biblioteci de legături dinamice (DLL), care conțin proceduri și (sau) date. Bibliotecile sunt formatate ca fișiere cu extensiile „.dll”, „.drv” (pentru bibliotecile de drivere) și „.fon” (pentru bibliotecile de fonturi). Acestea permit ca procedurile și datele lor să fie împărțite între mai multe programe (procese). Prin urmare, cea mai comună formă de DLL este o bibliotecă, constând dintr-un set de proceduri încărcate în memorie care pot fi accesate de mai multe programe în același timp. Ca exemplu în Fig. Figura 5.2.5 prezintă patru procese care partajează un fișier DLL care conține procedurile A, B, C și D. Programele 1 și 2 utilizează procedura A; programul 3 - procedura D, programul 4 - procedura B.
Fișierul DLL este construit de linker dintr-un set de fișiere de intrare. Principiul construcției este similar cu construcția codului binar executabil. Diferența este că atunci când un fișier DLL este construit, un steag special este transmis linkerului pentru a indica faptul că DLL-ul a fost creat. Fișierele DLL sunt de obicei construite dintr-o colecție de proceduri de bibliotecă care pot fi necesare mai multor procesoare. Exemplele obișnuite de fișiere DLL includ rutine de interfață cu biblioteca de apeluri de sistem Windows și biblioteci grafice mari. Utilizarea fișierelor DDL vă permite să:

● economisiți memorie și spațiu pe disc. De exemplu, dacă o bibliotecă ar fi asociată cu fiecare program care a folosit-o, atunci acea bibliotecă ar apărea în multe programe binare executabile în memorie și pe disc. Dacă utilizați fișiere DLL, atunci fiecare bibliotecă va apărea o dată pe disc și o dată în memorie;

●să faciliteze actualizarea procedurilor bibliotecii și, în plus, să efectueze actualizarea chiar și după ce programele care le folosesc au fost compilate și legate;

● remediați erorile detectate prin distribuirea de noi fișiere DLL (de exemplu, pe Internet). Acest lucru nu necesită nicio modificare a programelor binare subiacente. Diferența principalăîntre un fișier DLL și un program binar executabil este că fișierul DLL:

● nu poate porni și funcționa singur, deoarece nu are un program gazdă;

● conține alte informații în antet;

● are mai multe proceduri suplimentare care nu au legătură cu procedurile din bibliotecă, cum ar fi proceduri pentru alocarea memoriei și gestionarea altor resurse de care fișierul DLL are nevoie. Un program se poate conecta la un fișier DLL în două moduri: prin link implicit și prin link explicit. La legarea implicită programul utilizatorului este legat static la un fișier special numit biblioteca de import.

Această bibliotecă este creată de programul utilitar sau utilitate, prin extragerea anumitor informații dintr-un fișier DLL. O bibliotecă de import printr-un linker permite unui program utilizator să acceseze un fișier DLL și poate fi conectată la mai multe biblioteci de import. Windows, atunci când este legat implicit, controlează programul care este încărcat pentru execuție. Sistemul detectează ce fișiere DLL va folosi programul și dacă toate fișierele necesare sunt deja în memorie. Fișierele lipsă sunt imediat încărcate în memorie.

Se fac apoi modificări corespunzătoare structurilor de date ale bibliotecilor de import, astfel încât să poată fi determinată locația procedurilor apelate. Aceste modificări sunt mapate la spațiul de adrese virtuale al programului, după care programul utilizator poate apela proceduri din fișierele DLL ca și cum ar fi legate static la acesta și le poate rula.

La legături explicite nu sunt necesare biblioteci de import și nu trebuie încărcate fișiere DLL în același timp cu programul utilizatorului. În schimb, programul utilizatorului:

● face un apel explicit la runtime pentru a stabili o conexiune cu fișierul DLL;

● apoi efectuează apeluri suplimentare pentru a obține adresele procedurilor pe care le solicită;

● după aceasta, programul face un apel final pentru a întrerupe conexiunea cu fișierul DLL;

● Când ultimul proces se deconectează de la un fișier DLL, fișierul poate fi șters din memorie. Rețineți că, cu legătura dinamică, o procedură dintr-un fișier DLL rulează pe firul apelantului și utilizează stiva apelantului pentru variabilele sale locale. O diferență semnificativă între funcționarea procedurii în timpul legăturii dinamice (de la legarea statică) este metoda de stabilire a legăturii.

David Drysdale, Ghidul pentru începători pentru linkeri

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

Scopul acestui articol este de a ajuta programatorii C și C++ să înțeleagă esența a ceea ce face un linker. Am explicat acest lucru multor colegi în ultimii ani și, în sfârșit, am decis că este timpul să pun acest material pe hârtie, astfel încât să fie mai accesibil (și astfel nu trebuie să-l explic din nou). [Actualizare martie 2009: S-au adăugat mai multe informații despre aspectele legate de aspect în Windows, precum și mai multe detalii despre regula cu o singură definiție.

Un exemplu tipic de motiv pentru care oamenii au venit la mine pentru ajutor este următoarea eroare de aspect:

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

test1a.o(.text+0x18): În funcția „main”:

: referință nedefinită la `findmax(int, int)"

collect2: ld a returnat 1 stare de ieșire

Dacă reacția ta este „Probabil am uitat extern „C””, atunci cel mai probabil știi tot ce este dat în acest articol.

  • Definiții: ce este într-un fișier C?
  • Ce face compilatorul C?
  • Ce face linkerul: partea 1
  • Ce face sistemul de operare?
  • Ce face linkerul: partea 2
  • C++ pentru a completa imaginea
  • Biblioteci încărcate dinamic
  • În plus

Definiții: ce este într-un fișier C?

Acest capitol este un memento rapid al diferitelor componente ale unui fișier C. Dacă totul din lista de mai jos are sens pentru tine, atunci probabil că poți sări peste acest capitol și să mergi direct la următorul.

Mai întâi trebuie să înțelegeți diferența dintre o declarație și o definiție.

Definiția asociază un nume cu o implementare, care poate fi cod sau date:

  • Definirea unei variabile face ca compilatorul să rezerve o anumită zonă de memorie, dându-i probabil o anumită valoare.
  • Definirea unei funcții determină compilatorul să genereze cod pentru acea funcție

Declarația îi spune compilatorului că o funcție sau o definiție a variabilei (cu un anumit nume) există în altă parte a programului, probabil într-un alt fișier C. (Rețineți că o definiție este și o declarație - de fapt, este o declarație în care „celălalt loc” al programului este același cu cel curent.)

Există două tipuri de definiții pentru variabile:

  • variabile globale, care există pe tot parcursul ciclului de viață al programului („alocare statică”) și care sunt disponibile în diverse funcții;
  • variabile locale, care există doar în sfera unei anumite funcții de execuție („plasare locală”) și care sunt accesibile doar în cadrul acelei funcții.

În acest caz, termenul „disponibil” ar trebui înțeles ca „poate fi accesat prin numele asociat variabilei în momentul definiției”.

Există câteva cazuri speciale care pot să nu pară evidente la început:

  • variabile locale statice sunt de fapt globale deoarece există pe toată durata de viață a programului, chiar dacă sunt vizibile doar în cadrul unei singure funcții.
  • variabile globale statice sunt de asemenea globale, singura diferență fiind că sunt disponibile numai în același fișier în care sunt definite.

Este demn de remarcat faptul că prin definirea unei funcții ca fiind statică, pur și simplu reduceți numărul de locuri din care vă puteți referi la o anumită funcție prin nume.

Pentru variabilele globale și locale, putem distinge dacă variabila este inițializată sau nu, adică. dacă spațiul alocat pentru o variabilă din memorie va fi umplut cu o anumită valoare.

În cele din urmă, putem stoca informații în memorie care sunt alocate dinamic folosind malloc sau new . În acest caz, nu este posibilă accesarea memoriei alocate după nume, deci este necesar să folosiți pointeri - variabile denumite care conțin adresa unei zone de memorie fără nume. Această zonă de memorie poate fi, de asemenea, eliberată folosind free sau delete . În acest caz avem de-a face cu „alocarea dinamică”.

Să rezumăm:

Global

Local

Dinamic

Neinițierea

Neinițierea

Anunţ

int fn(int x);

extern int x;

extern int x;

Definiție

int fn(int x)

{ ... }

int x = 1;

(sfera de aplicare

Fişier)

int x;

(domeniu - dosar)

int x = 1;

(domeniu - funcție)

int x;

(domeniu - funcție)

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

Probabil că o modalitate mai ușoară de a învăța este să te uiți la un exemplu de program.

/* Definiția unei variabile globale neinițializate */

int x_global_uninit;

/* Definiția unei variabile globale inițializate */

int x_global_init = 1;

/* Definiția unei variabile globale neinițializate la care

static int y_global_uninit;

/* Definiția unei variabile globale inițializate la care

* poate fi adresat pe nume numai în cadrul acestui fișier C */

static int y_global_init = 2;

/* Declarația unei variabile globale care este definită undeva

* în altă parte a programului */

extern int z_global;

/* Declararea unei funcții care este definită în altă parte

* programe (Puteți adăuga „extern” în față, totuși acest lucru

*nu este necesar) */

int fn_a(int x, int y);

/* Definiția funcției. Cu toate acestea, fiind marcat ca static, poate fi

* apelați după nume numai în acest fișier C. */

static int fn_b(int x)

Întoarce x+1;

/* Definiția funcției. */

/* Parametrul funcției este considerat o variabilă locală. */

int fn_c(int x_local)

/* Definiția unei variabile locale neinițializate */

Int y_local_uninit;

/* Definiția unei variabile locale inițializate */

Int y_local_init = 3;

/* Cod care accesează variabilele locale și globale

* și, de asemenea, funcționează după nume */

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

Ce face compilatorul C?

Sarcina compilatorului C este de a converti text care este (de obicei) citibil de om în ceva pe care un computer îl poate înțelege. La ieșire, compilatorul produce un fișier obiect. Pe platformele UNIX, aceste fișiere au de obicei sufixul .o; pe Windows - suffix.obj. Conținutul unui fișier obiect este în esență două lucruri:

cod corespunzător definiției funcției din fișierul C

date corespunzătoare definiției variabilelor globale în fișierul C (pentru variabilele globale inițializate, valoarea inițială a variabilei trebuie să fie și ea stocată în fișierul obiect).

Codul și datele, în acest caz, vor avea nume asociate - numele funcțiilor sau variabilelor cu care sunt asociate prin definiție.

Codul obiect este o secvență de instrucțiuni de mașină (compuse în mod corespunzător) care corespund instrucțiunilor C scrise de programator: toate acele if, while și chiar goto. Aceste vrăji trebuie să manipuleze informații de un anumit fel, iar informațiile trebuie să fie undeva - de aceea avem nevoie de variabile. Codul poate face referire și la alt cod (în special la alte funcții C din program).

Oriunde codul se referă la o variabilă sau funcție, compilatorul o va permite doar dacă a văzut acea variabilă sau funcție declarată înainte. O declarație este o promisiune că o definiție există în altă parte în program.

Sarcina linkerului este să verifice aceste promisiuni. Cu toate acestea, ce face compilatorul cu toate aceste promisiuni atunci când generează fișierul obiect?

În esență, compilatorul lasă spații goale. Spațiul gol (link) are un nume, dar valoarea corespunzătoare acestui nume nu este încă cunoscută.

Având în vedere acest lucru, putem descrie fișierul obiect corespunzător programului de mai sus, după cum urmează:

Analizarea unui fișier obiect

Până acum am considerat totul la un nivel înalt. Cu toate acestea, este util să vedem cum funcționează acest lucru în practică. Instrumentul principal pentru noi va fi comanda nm, care oferă informații despre simbolurile unui fișier obiect pe platforma UNIX. Pe Windows, comanda dumpbin cu opțiunea /symbols este aproximativ echivalentă. Există, de asemenea, instrumente GNU binutils portate pe Windows, care includ nm.exe.

Să vedem ce iese nm pentru fișierul obiect obținut din exemplul nostru de mai sus:

Simboluri din c_parts.o:

Nume Valoare Clasă Tip Mărime Linie Secțiune

fn_a | | U | NOTIP| | |*UND*

z_global | | U | NOTIP| | |*UND*

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

x_global_init |00000000| D | OBIECT|00000004| |.date

y_global_uninit |00000000| b | OBIECT|00000004| |.bss

x_global_uninit |00000004| C | OBIECT|00000004| |*COM*

y_global_init |00000004| d | OBIECT|00000004| |.date

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

Rezultatul poate arăta ușor diferit pe diferite platforme (verificați pagina omului pentru detalii), dar informația cheie este clasa fiecărui personaj și dimensiunea acestuia (dacă există). Clasa poate avea semnificații diferite:

  • Clasa U înseamnă referințe nedefinite, acele „spații goale” menționate mai sus. Există două obiecte pentru această clasă: fn_a și z_global. (Unele versiuni de nm pot scoate o secțiune care ar fi *UND* sau UNDEF în acest caz.)
  • Clasele t și T indică un cod care este definit; diferența dintre t și T este dacă funcția este locală (t) fișierului sau nu (T), adică. dacă funcția a fost declarată ca fiind statică. Din nou, pe unele sisteme poate fi afișată o secțiune precum .text.
  • Clasele d și D conțin variabile globale inițializate. În acest caz, variabilele statice aparțin clasei d. Dacă informațiile secțiunii sunt prezente, acestea vor fi .data.
  • Pentru variabilele globale neinițializate, obținem b dacă sunt statice și B sau C în caz contrar. Secțiunea în acest caz va fi cel mai probabil .bss sau *COM*.

Este posibil să vedeți și simboluri care nu fac parte din codul sursă C. Nu ne vom concentra atenția asupra acestui lucru, deoarece de obicei face parte din mecanismul intern al compilatorului, astfel încât programul dvs. poate fi conectat mai târziu.

Scopul acestui articol este de a ajuta programatorii C și C++ să înțeleagă esența a ceea ce face un linker. Am explicat acest lucru multor colegi în ultimii ani și, în sfârșit, am decis că este timpul să pun acest material pe hârtie, astfel încât să fie mai accesibil (și astfel nu trebuie să-l explic din nou). [Actualizare martie 2009: S-au adăugat mai multe informații despre aspectele legate de aspect în Windows, precum și mai multe detalii despre regula cu o singură definiție.

Un exemplu tipic de motiv pentru care oamenii au venit la mine pentru ajutor este următoarea eroare de aspect:
g++ -o test1 test1a.o test1b.o test1a.o(.text+0x18): În funcția `main":: referință nedefinită la `findmax(int, int)" collect2: ld a returnat 1 stare de ieșire
Dacă reacția ta este „Probabil am uitat extern „C””, atunci cel mai probabil știi tot ce este dat în acest articol.

Definiții: ce este într-un fișier C?

Acest capitol este un memento rapid al diferitelor componente ale unui fișier C. Dacă totul are sens pentru tine, atunci cel mai probabil poți sări peste acest capitol și să mergi direct la.

Mai întâi trebuie să înțelegeți diferența dintre o declarație și o definiție. Definiție asociază un nume cu o implementare, care poate fi cod sau date:

  • Definirea unei variabile face ca compilatorul să rezerve o anumită zonă de memorie, dându-i probabil o anumită valoare.
  • Definirea unei funcții determină compilatorul să genereze cod pentru acea funcție
Anunţ spune compilatorului că o definiție a unei funcții sau variabile (cu un anumit nume) există în altă parte a programului, probabil într-un alt fișier C. (Rețineți că o definiție este și o declarație - de fapt, este o declarație în care „celălalt loc” al programului este același cu cel curent.)

Există două tipuri de definiții pentru variabile:

  • variabile globale, care există pe tot parcursul ciclului de viață al programului („alocare statică”) și care sunt disponibile în diverse funcții;
  • variabile locale, care există doar în sfera unei anumite funcții de execuție („plasare locală”) și care sunt accesibile doar în cadrul acelei funcții.
În acest caz, termenul „disponibil” ar trebui înțeles ca „poate fi accesat prin numele asociat variabilei în momentul definiției”.

Există câteva cazuri speciale care pot să nu pară evidente la început:

  • Variabilele locale statice sunt de fapt globale, deoarece există pe toată durata de viață a programului, chiar dacă sunt vizibile doar într-o singură funcție.
  • Variabilele globale statice sunt, de asemenea, globale, singura diferență fiind că sunt disponibile numai în același fișier în care sunt definite.
Este demn de remarcat faptul că prin definirea unei funcții ca fiind statică, pur și simplu reduceți numărul de locuri din care vă puteți referi la o anumită funcție prin nume.

Pentru variabilele globale și locale, putem distinge dacă variabila este inițializată sau nu, adică. dacă spațiul alocat pentru o variabilă din memorie va fi umplut cu o anumită valoare.

În cele din urmă, putem stoca informații în memorie care sunt alocate dinamic folosind malloc sau new . În acest caz, nu este posibil să accesați memoria alocată după nume, deci este necesar să folosiți pointeri - variabile numite care conțin adresa unei zone de memorie fără nume. Această zonă de memorie poate fi, de asemenea, eliberată folosind free sau delete . În acest caz avem de-a face cu „plasare dinamică”.

Să rezumăm:

Probabil că o modalitate mai ușoară de a învăța este să te uiți la un exemplu de program.
/* Definiția unei variabile globale neinițializate */ int x_global_uninit; /* Definiția unei variabile globale inițializate */ int x_global_init = 1; /* Definiți o variabilă globală neinițializată care poate * fi accesată după nume numai în acest fișier C */ static int y_global_uninit; /* Definiția unei variabile globale inițializate care * poate fi accesată după nume numai în acest fișier C */ static int y_global_init = 2; /* Declarația unei variabile globale care este definită undeva * în altă parte în program */ extern int z_global; /* Declararea unei funcții care este definită în altă parte * în program (Puteți adăuga „extern”, dar acest * este opțional) */ int fn_a(int x, int y); /* Definiția funcției. Cu toate acestea, atunci când este marcat ca static, * poate fi numit după nume numai în acel fișier C. */ static int fn_b(int x) ( return x+1; ) /* Definiția funcției. */ /* Un parametru de funcție este considerat o variabilă locală. */ int fn_c(int x_local) ( /* Definiția unei variabile locale neinițializate */ int y_local_uninit; /* Definiția unei variabile locale inițializate */ int y_local_init = 3; /* Cod care accesează variabilele locale și globale * și funcționează prin nume */ 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); )

Ce face compilatorul C?

Sarcina compilatorului C este de a converti text care este (de obicei) citibil de om în ceva pe care un computer îl poate înțelege. La ieșire, compilatorul produce fișier obiect. Pe platformele UNIX, aceste fișiere au de obicei sufixul .o; pe Windows - suffix.obj. Conținutul unui fișier obiect este în esență două lucruri:

Codul și datele, în acest caz, vor avea nume asociate - numele funcțiilor sau variabilelor cu care sunt asociate prin definiție.

Codul obiect este o secvență de instrucțiuni de mașină (compuse în mod corespunzător) care corespund instrucțiunilor C scrise de programator: toate acele if, while și chiar goto. Aceste vrăji trebuie să manipuleze informații de un anumit fel, iar informațiile trebuie să fie undeva - de aceea avem nevoie de variabile. Codul poate face referire și la alt cod (în special la alte funcții C din program).

Oriunde codul se referă la o variabilă sau funcție, compilatorul o permite doar dacă a văzut acea variabilă sau funcție înainte. O declarație este o promisiune că o definiție există în altă parte în program.

Sarcina linkerului este să verifice aceste promisiuni. Cu toate acestea, ce face compilatorul cu toate aceste promisiuni atunci când generează fișierul obiect?

În esență, compilatorul lasă spații goale. Spațiul gol (link) are un nume, dar valoarea corespunzătoare acestui nume nu este încă cunoscută.

Având în vedere acest lucru, putem descrie fișierul obiect corespunzător lui , după cum urmează:

Analizarea unui fișier obiect

Până acum am considerat totul la un nivel înalt. Cu toate acestea, este util să vedem cum funcționează acest lucru în practică. Instrumentul principal pentru noi va fi echipa nm, care oferă informații despre simbolurile unui fișier obiect pe platforma UNIX. Pentru comanda Windows gunoi cu opțiunea /symbols este un echivalent aproximativ. Există, de asemenea, instrumente GNU binutils care includ nm.exe.

Să vedem ce produce nm pentru un fișier obiect obținut din:
Simboluri din c_parts.o: Nume Valoare Clasa Tip Mărime Linie Secțiune fn_a | | U | NOTIP| | |*UND* z_global | | U | NOTIP| | |*UND* fn_b |00000000| t | FUNC|00000009| |.text x_global_init |00000000| D | OBIECT|00000004| |.data y_global_uninit |00000000| b | OBIECT|00000004| |.bss x_global_uninit |00000004| C | OBIECT|00000004| |*COM* y_global_init |00000004| d | OBIECT|00000004| |.data fn_c |00000009| T | FUNC|00000055| |.text
Rezultatul poate arăta ușor diferit pe diferite platforme (consultați pagina de manual pentru detalii), dar informația cheie este clasa fiecărui caracter și dimensiunea acestuia (dacă este prezentă). Clasa poate avea semnificații diferite:

  • Clasă U denotă legături nedefinite, aceleași „spatii goale” menționate mai sus. Există două obiecte pentru această clasă: fn_a și z_global. (Unele versiuni de nm pot ieși secțiune, ceea ce ar fi *UND* sau UNDEFîn acest caz.)
  • Clase tȘi T indica un cod care este definit; diferență între tȘi T este dacă funcția este locală ( t) în dosar sau nu ( T), adică dacă funcția a fost declarată ca static. Din nou, pe unele sisteme, poate fi afișată o secțiune, de ex. .text.
  • Clase dȘi D conțin variabile globale inițializate. În acest caz, variabilele statice aparțin clasei d. Dacă informațiile de secțiune sunt prezente, va fi .date.
  • Pentru variabilele globale neinițializate, obținem b, dacă sunt statice și B sau C in caz contrar. Secțiunea în acest caz va fi cel mai probabil .bss sau *COM*.
Este posibil să vedeți și simboluri care nu fac parte din codul sursă C. Nu ne vom concentra atenția asupra acestui lucru, deoarece de obicei face parte din mecanismul intern al compilatorului, astfel încât programul dvs. poate fi conectat mai târziu.

Ce face linkerul: partea 1

Am spus mai devreme că declararea unei funcții sau variabile este o promisiune pentru compilator că există o definiție a acelei funcții sau variabile în altă parte în program și că sarcina linkerului este să-și îndeplinească această promisiune. Privind la , putem descrie acest proces ca „completarea spațiilor libere”.

Să ilustrăm acest lucru cu un exemplu, uitându-ne la un alt fișier C în plus față de cel care .
/* Variabilă globală inițializată */ int z_global = 11; /* A doua variabilă globală numită y_global_init, dar ambele sunt statice */ static int y_global_init = 2; /* Declarația altei variabile globale */ extern int x_global_init; int fn_a(int x, int y) ( return(x+y); ) int main(int argc, char *argv) ( const char *message = "Bună ziua, lume"; return fn_a(11,12); )

Din ambele diagrame, putem vedea că toate punctele pot fi conectate (dacă nu, linker-ul ar arunca un mesaj de eroare). Fiecare lucru are locul lui, și fiecare loc are locul lui. De asemenea, linkerul poate completa orice spații goale, așa cum se arată aici (pe sistemele UNIX, procesul de conectare este de obicei apelat cu comanda ld).

Pentru C situația este mai puțin evidentă. Trebuie să existe exact o definiție pentru orice funcție și variabilă globală inițializată, dar definiția unei variabile neinițializate poate fi tratată ca determinarea prealabilă. Limbajul C permite astfel (sau cel puțin nu împiedică) diferite fișiere sursă să conțină predefiniții ale aceluiași obiect.

Cu toate acestea, linkerii trebuie să fie capabili să se ocupe și cu alte limbi decât C și C++, pentru care regula unei singure definiții nu se aplică neapărat. De exemplu, este normal ca Fortran să aibă o copie a fiecărei variabile globale din fiecare fișier care face referire la aceasta. Apoi, linkerul trebuie să elimine duplicatele selectând o copie (cel mai mare reprezentant, dacă acestea diferă ca dimensiune) și eliminând toate celelalte. Acest model este numit uneori „modelul comun” de aspect datorită cuvântului cheie COMUN al limbajului Fortran.

Drept urmare, este destul de obișnuit ca linkerii UNIX să nu se plângă de simboluri duplicate, cel puțin dacă acestea sunt simboluri duplicate ale variabilelor globale neinițializate (acest model de legare este uneori numit „model cuplat liber” [ aproximativ traducere Aceasta este traducerea mea gratuită a modelului relaxat ref/def. Sunt binevenite sugestii mai bune]). Dacă acest lucru vă îngrijorează (și probabil ar trebui), consultați documentația linkerului pentru a găsi o opțiune --work-right care să-și îmblânzească comportamentul. De exemplu, în lanțul de instrumente GNU, opțiunea compilatorului -fno-common forțează ca o variabilă neinițializată să fie plasată în segmentul BBS în loc să genereze blocuri COMUNE.

Ce face sistemul de operare?

Acum că linker-ul a produs un fișier executabil, oferind fiecărei referințe de simbol definiția corespunzătoare, puteți face o scurtă pauză pentru a înțelege ce face sistemul de operare când rulați programul.

Rularea unui program implică, desigur, executarea unui cod de mașină, de exemplu. În mod evident, sistemul de operare trebuie să transfere codul mașinii al fișierului executabil de pe hard disk în memoria de operare, de unde CPU-ul îl poate prelua. Aceste porțiuni sunt numite segment de cod sau segment de text.

Codul fără date în sine este inutil. Prin urmare, toate variabilele globale au nevoie și de spațiu în memoria computerului. Cu toate acestea, există o diferență între variabilele globale inițializate și neinițializate. Variabilele inițiale au anumite valori de pornire, care trebuie stocate și în fișiere obiect și executabile. Când programul pornește, sistemul de operare copiază aceste valori în spațiul virtual al programului, în segmentul de date.

Pentru variabilele neinițializate, sistemul de operare poate presupune că toate au 0 ca valoare inițială, adică. nu este nevoie să copiați nicio valoare. Piesa de memorie care este inițializată cu zerouri este cunoscută sub numele de segment bss.

Aceasta înseamnă că spațiu pentru variabilele globale poate fi alocat într-un fișier executabil stocat pe disc; variabilele inițiale trebuie să aibă valorile inițiale păstrate, dar cele neinițializate trebuie doar să-și păstreze dimensiunea.

După cum probabil ați observat, până acum în toate discuțiile despre fișierele obiect și linker am vorbit doar despre variabile globale; În același timp, nu am menționat variabilele locale și memoria ocupată dinamic.

Aceste date nu au nevoie de nicio intervenție a linkerului, deoarece durata de viață începe și se termină în timpul execuției programului - mult după ce linkerul și-a făcut treaba. Cu toate acestea, de dragul completității, vom sublinia pe scurt că:

  • variabilele locale sunt situate într-o zonă de memorie numită grămadă, care crește și se contractă pe măsură ce sunt numite și executate diferite funcții.
  • Memoria alocată dinamic este preluată dintr-o zonă de memorie cunoscută ca o gramada, iar funcția malloc controlează accesul la spațiul liber din această zonă.
Pentru a completa imaginea, merită să adăugați cum arată spațiul de memorie al procesului de rulare. Deoarece grămada și stiva își pot schimba dimensiunile în mod dinamic, este destul de comun ca stiva să crească într-o direcție și grămada să crească în direcția opusă. Astfel, programul va elimina o eroare de memorie liberă numai dacă stiva și heap-ul se întâlnesc undeva la mijloc (caz în care spațiul de memorie al programului va fi de fapt plin).

Ce face linkerul? partea 2

Acum că am acoperit ceea ce face linkerul, ne putem scufunda în părțile mai complexe - aproximativ în ordinea cronologică a modului în care au fost adăugate la linker.

Principala observație care afectează funcționalitatea linkerului este următoarea: dacă un număr de programe diferite fac aproximativ aceleași lucruri (ieșire pe ecran, citirea fișierelor de pe hard disk etc.), atunci este evident că are sens să izolați acest lucru. codificați într-un anumit loc și dați-l altor programe pentru a-l folosi.

O soluție posibilă ar fi să folosiți aceleași fișiere obiect, dar ar fi mult mai convenabil să păstrați întreaga colecție de... fișiere obiect într-o locație ușor accesibilă: bibliotecă.

Deoparte tehnică: Acest capitol omite complet o caracteristică importantă a linkerului: redirecționare(relocare). Programele diferite au dimensiuni diferite, de ex. dacă o bibliotecă partajată este mapată în spațiul de adrese ale diferitelor programe, va avea adrese diferite. Acest lucru înseamnă, la rândul său, că toate funcțiile și variabilele din bibliotecă vor fi în locuri diferite. Acum, dacă toate referințele la adrese sunt relative („valoare +1020 octeți de aici”) mai degrabă decât absolute („valoare la 0x102218BF”), atunci aceasta nu este o problemă, dar nu este întotdeauna cazul. În astfel de cazuri, toate adresele absolute trebuie adăugate cu un offset adecvat - aceasta este relocare. Nu voi reveni la acest subiect din nou, dar voi adăuga că, deoarece acest lucru este aproape întotdeauna ascuns de programatorul C/C++, este foarte rar ca problemele de layout să fie cauzate de dificultăți de redirecționare.

Biblioteci statice

Cea mai simplă implementare a bibliotecii este static bibliotecă. În capitolul anterior s-a menționat că puteți partaja cod pur și simplu prin reutilizarea fișierelor obiect; aceasta este esența bibliotecilor statice.

Pe sistemele UNIX, comanda pentru a construi o bibliotecă statică este de obicei ar, iar fișierul de bibliotecă rezultat are extensia *.a. De asemenea, aceste fișiere au de obicei un prefix „lib” în numele lor și sunt transmise linker-ului cu opțiunea „-l” urmată de numele bibliotecii fără prefix și extensie (adică „-lfred” va prelua fișierul „ libfred.a").
(În trecut, un program numit ranlib era, de asemenea, necesar pentru bibliotecile statice pentru a genera o listă de simboluri în partea din față a bibliotecii. În prezent, instrumentele ar fac acest lucru singure.)

Pe Windows, bibliotecile statice au extensia .LIB și sunt construite de instrumentele LIB, dar acest fapt poate induce în eroare, deoarece aceeași extensie este folosită pentru „biblioteca de import”, care conține doar o listă a ceea ce este în DLL - vedea

Pe măsură ce linker-ul iterează printr-o colecție de fișiere obiect pentru a le combina împreună, menține o listă de simboluri care nu pot fi încă implementate. Odată ce toate fișierele obiect specificate în mod explicit au fost procesate, linkerul are acum un nou loc pentru a căuta simboluri care rămân în listă - în bibliotecă. Dacă un simbol neimplementat este definit într-unul dintre obiectele bibliotecii, atunci obiectul este adăugat, la fel ca și cum ar fi fost adăugat la lista de fișiere obiect de către utilizator, iar legătura continuă.

Observați granularitatea a ceea ce este adăugat din bibliotecă: dacă este necesară o definiție a unui simbol, atunci întregul obiect, care conține definiția simbolului, vor fi incluse. Aceasta înseamnă că acest proces poate fi fie un pas înainte, fie un pas înapoi - un obiect nou adăugat poate fie să rezolve o referință nedefinită, fie să introducă o întreagă colecție de noi referințe nerezolvate.

Un alt detaliu important este Ordin evenimente; bibliotecile sunt invocate numai atunci când conectarea normală este completă și sunt procesate în Bine de la stanga la dreapta. Aceasta înseamnă că, dacă ultimul obiect preluat din bibliotecă necesită un simbol din bibliotecă mai devreme în linia de comandă a linkului, linkerul nu îl va găsi automat.

Să dăm un exemplu pentru a clarifica situația; Să presupunem că avem următoarele fișiere obiect și o linie de comandă link care conține a.o, b.o, -lx și -ly .


Odată ce linkerul a procesat a.o și b.o , referințele la b2 și a3 vor fi rezolvate, în timp ce x12 și y22 vor fi încă nerezolvate. În acest moment, linkerul verifică prima bibliotecă, libx.a, pentru simboluri lipsă și găsește că poate include x1.o pentru a compensa referința la x12; totuși, făcând acest lucru, x23 și y12 sunt adăugate la lista de referințe nedefinite (lista arată acum ca y22, x23, y12).

Linker-ul încă se ocupă de libx.a, deci referința la x23 este ușor compensată prin includerea x2.o din libx.a. Cu toate acestea, aceasta adaugă y11 la lista nedefinită (care a devenit y22, y12, y11). Niciuna dintre aceste legături nu poate fi rezolvată folosind libx.a , așa că se presupune că linkerul este liby.a .

Același lucru se întâmplă și aici și linkerul include y1.o și y2.o . Primul obiect adăugat este o referință la y21 , dar din moment ce y2.o va fi inclus în continuare, această referință este rezolvată simplu. Rezultatul acestui proces este că toate referințele nedefinite sunt rezolvate și unele (dar nu toate) obiectele bibliotecii sunt incluse în executabilul final.

Rețineți că situația se schimbă oarecum dacă spunem că b.o avea și o legătură către y32 . Dacă acesta ar fi cazul, atunci legarea libx.a ar avea loc în același mod, dar procesarea liby.a ar implica includerea y3.o . Prin includerea acestui obiect vom adăuga x31 la lista de simboluri nerezolvate și această referință va rămâne nerezolvată - în această etapă linkerul a terminat deja procesarea libx.a și, prin urmare, nu va mai găsi definiția acestui simbol (în x3.o) .

(Apropo, acest exemplu are o dependență circulară între libx.a și liby.a; acesta este de obicei un lucru rău)

Biblioteci partajate dinamice

Pentru bibliotecile populare, cum ar fi biblioteca standard C (de obicei libc), a fi o bibliotecă statică are un dezavantaj distinct - fiecare program executabil va avea o copie a aceluiași cod. Într-adevăr, dacă fiecare fișier executabil ar avea o copie a printf , fopen și altele asemenea, atunci o cantitate nerezonabil de mare de spațiu pe disc ar fi consumată.

Un dezavantaj mai puțin evident este că într-un program legat static codul este fix pentru totdeauna. Dacă cineva găsește și remediază o eroare în printf , fiecare program va trebui reconectat pentru a obține codul corect.

Pentru a scăpa de aceste și alte probleme, au fost introduse biblioteci partajate dinamic (de obicei au extensia .so sau .dll pe Windows și .dylib pe Mac OS X). Pentru acest tip de bibliotecă, linkerul nu conectează neapărat toate punctele. În schimb, linkerul emite un cupon de tipul „IOU” (Îți sunt dator) și amână încasarea acestui cupon până când programul rulează.

La ce se rezumă totul este că, dacă linkerul detectează că definiția unui anumit simbol se află într-o bibliotecă partajată, nu include acea definiție în executabilul final. În schimb, linkerul scrie numele simbolului și biblioteca de unde se așteaptă să provină simbolul.

Când un program este chemat pentru execuție, sistemul de operare se asigură că părțile rămase ale procesului de conectare sunt finalizate la timp înainte ca programul să înceapă să ruleze. Înainte ca funcția principală să fie apelată, o versiune mică a linkerului (deseori numită ld.so) parcurge lista de promisiuni și efectuează actul final de conectare la fața locului - introducerea codului bibliotecii și conectarea punctelor.

Aceasta înseamnă că niciun fișier executabil nu conține o copie a codului printf. Dacă este disponibilă o nouă versiune de printf, o puteți utiliza pur și simplu schimbând libc.so - data viitoare când rulați programul, noul printf va fi apelat.

Există o altă mare diferență între modul în care funcționează bibliotecile dinamice în comparație cu cele statice și aceasta vine sub forma granularității legăturilor. Dacă un anumit simbol este preluat dintr-o anumită bibliotecă dinamică (să spunem printf din libc.so), atunci întregul conținut al bibliotecii este plasat în spațiul de adrese al programului. Aceasta este principala diferență față de bibliotecile statice, unde sunt adăugate doar obiecte specifice legate de un simbol nedefinit.

Cu alte cuvinte, bibliotecile partajate în sine sunt obținute ca rezultat al muncii linkerului (și nu ca formarea unui morman mare de obiecte, cum face ar), care conțin referințe între obiectele din bibliotecă în sine. Din nou, nm este un instrument util pentru a ilustra ce se întâmplă: va produce mai multe ieșiri pentru fiecare fișier obiect individual atunci când este rulat pe o versiune statică a bibliotecii, dar pentru o versiune partajată a bibliotecii, liby.so are doar un x31 nedefinit. simbol. De asemenea, în exemplul cu ordinea includerii bibliotecilor la sfârșit, nu vor fi nici probleme: adăugarea unui link către y32 în b.c nu va implica nicio modificare, deoarece toate conținuturile y3.o și x3.o au deja fost folosit.

Deci, apropo, un alt instrument util este ldd; pe platforma Unix, arată toate bibliotecile partajate de care depinde binarul executabil (sau altă bibliotecă partajată), împreună cu o indicație despre unde pot fi găsite acele biblioteci. Pentru ca programul să se lanseze cu succes, încărcătorul trebuie să găsească toate aceste biblioteci împreună cu toate dependențele lor. (De obicei, încărcătorul caută biblioteci în lista de directoare specificată în variabila de mediu 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)
Motivul pentru o mai mare granularitate este că sistemele de operare moderne sunt suficient de inteligente pentru a vă permite să faceți mai mult decât să salvați elemente duplicate pe disc, lucru de care suferă bibliotecile statice. Diferite procese de execuție care folosesc aceeași bibliotecă partajată pot partaja și un segment de cod (dar nu un segment de date sau un segment bss - de exemplu, două procese diferite pot fi în locuri diferite când se utilizează, de exemplu, strtok). Pentru a realiza acest lucru, întreaga bibliotecă trebuie să fie abordată dintr-o singură lovitură, astfel încât toate legăturile interne să fie aliniate într-un mod unic. Într-adevăr, dacă un proces preia a.o și c.o , iar un altul procesează b.o și c.o , atunci sistemul de operare nu va putea folosi nicio potrivire.

Windows DLL

Chiar dacă principiile generale ale bibliotecilor partajate sunt aproximativ aceleași atât pe platformele Unix, cât și pe Windows, există totuși câteva detalii în care începătorii pot fi prinși.

Simboluri exportate

Cea mai mare diferență este că în bibliotecile Windows, simbolurile nu sunt exportate automat. Pe Unix, toate simbolurile tuturor fișierelor obiect care au fost legate la o bibliotecă partajată sunt vizibile pentru utilizatorul acelei biblioteci. Pe Windows, programatorul trebuie să facă vizibile în mod explicit unele caractere, de exemplu. le exporta.

Există trei moduri de a exporta un simbol și un DLL Windows (și toate aceste trei metode pot fi amestecate în aceeași bibliotecă).

  • În codul sursă, declarați simbolul ca __declspec(dllexport) , ceva de genul acesta:
    __declspec(dllexport) int my_exported_function(int x, double y)
  • Când executați comanda linker, utilizați opțiunea de export LINK.EXE: simbol_pentru_export
    LINK.EXE /dll /export:funcția_exportată_mea
  • Introduceți fișierul de definire a modulului (DEF) către linker (folosind opțiunea /DEF: def_file), prin includerea unei secțiuni EXPORT în acest fișier, care conține simbolurile de exportat.
    EXPORTĂ funcția_me_exportată_funcția_mee_other_exported
Odată ce C++ este implicat în această mizerie, prima dintre aceste opțiuni devine cea mai simplă, deoarece în acest caz compilatorul își asumă responsabilitatea de a avea grijă de

.LIB și alte fișiere legate de bibliotecă

Ajungem la a doua dificultate cu bibliotecile Windows: informațiile despre simbolurile exportate pe care linkerul trebuie să le lege la alte simboluri nu sunt conținute în DLL-ul în sine. În schimb, aceste informații sunt conținute în fișierul .LIB corespunzător.

Fișierul LIB asociat cu DLL descrie simbolurile (exportate) în DLL împreună cu locația lor. Orice binar care folosește un DLL trebuie să acceseze fișierul .LIB pentru a lega corect simbolurile.

Pentru a face lucrurile și mai confuze, extensia .LIB este folosită și pentru bibliotecile statice.

De fapt, există o serie de fișiere diferite care pot fi legate într-un fel de bibliotecile Windows. Împreună cu fișierul .LIB, precum și cu fișierul (opțional) .DEF, puteți vedea toate următoarele fișiere asociate cu biblioteca dvs. Windows.

Aceasta este o mare diferență față de Unix, unde aproape toate informațiile conținute în toate aceste fișiere suplimentare sunt pur și simplu adăugate bibliotecii în sine.

Simboluri importate

Pe lângă faptul că solicită DLL-urilor să declare în mod explicit , Windows permite și binarelor care utilizează cod de bibliotecă să declare în mod explicit simbolurile care urmează să fie importate. Acest lucru nu este obligatoriu, dar oferă o oarecare optimizare a vitezei datorită proprietăților istorice ale ferestrelor pe 16 biți.

Putem urmări aceste liste, folosind din nou nm . Luați în considerare următorul fișier C++:
clasa Fred ( privat: int x; int y; public: Fred() : x(1), y(2) () Fred(int z): x(z), y(3) () ); Fred theFred; Fred celalaltFred(55);
Pentru acest cod ( Nu decorat) ieșirea nm arată astfel:
Simboluri din global_obj.o: Nume Valoare Clasa Tip Mărime Linie Secțiune __gxx_personality_v0| | U | NOTIP| | |*UND* __static_initialization_and_distruction_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 | OBIECT|00000008| |.bss theOtherFred |00000008| B | OBIECT|00000008| |.bss constructori globali cu cheie pentru Fred |0000003a| t | FUNC|0000001a| |.text
Ca de obicei, putem vedea o grămadă de lucruri diferite aici, dar unul dintre ele care este cel mai interesant pentru noi este postările de clasă W(care înseamnă un simbol „slab”), precum și intrări cu un nume de secțiune precum „.gnu.linkonce.t. chestie„. Aceștia sunt markeri pentru constructorii obiectelor globale și vedem că câmpul „Nume” corespunzător arată la ce ne-am putea aștepta de fapt acolo - fiecare dintre cei doi constructori este implicat.

Șabloane

Anterior, am oferit trei implementări diferite ale funcției max, fiecare dintre ele a luat diferite tipuri de argumente. Cu toate acestea, vedem că codul corpului funcției este identic în toate cele trei cazuri. Și știm că duplicarea aceluiași cod este o practică proastă de programare.

C++ introduce concepte șablon(șabloane), care vă permite să utilizați codul de mai jos pentru toate cazurile simultan. Putem crea un fișier antet max_template.h cu o singură copie a codului funcției max:
șablon T max(T x, T y) ( dacă (x>y) returnează x; altfel returnează y; )
și includeți acest fișier în fișierul sursă pentru a încerca funcția șablon:
#include "max_template.h" int main() ( int a=1; int b=2; int c; c = max(a,b); // Compilatorul determină automat ce maxim este necesar (int,int) dublu x = 1,1; float y = 2,2; dublu z; z = max (X y); // Compilatorul nu poate determina, așa că avem nevoie de max (dublu,dublu) return 0; )
Acest cod, scris în C++, folosește max (int,int) și max (dublu dublu) . Cu toate acestea, un alt cod ar putea folosi alte instanțe ale acestui model. Ei bine, să spunem max (float,float) sau chiar max (MyFloatingPointClass,MyFloatingPointClass) .

Fiecare dintre aceste instanțe diferite produce cod de mașină diferit. Astfel, în momentul în care programul este în sfârșit conectat, compilatorul și linkerul trebuie să se asigure că codul pentru fiecare instanță de șablon utilizată este inclus în program (și că nu este inclusă nicio instanță de șablon neutilizată, pentru a nu mări dimensiunea programului).

Cum se face asta? În mod obișnuit, există două căi de acțiune: fie reducerea instanțelor duplicate, fie amânarea instanțierii până la etapa de legătură (de obicei mă refer la aceste abordări ca fiind modul inteligent și modul Sun).

Metoda de subțire a instanțelor repetate implică faptul că fiecare fișier obiect conține codul tuturor șabloanelor întâlnite. De exemplu, pentru fișierul de mai sus, conținutul fișierului obiect arată astfel:
Simboluri din max_template.o: Nume Valoare Clasă Tip Mărime Linie Secțiune __gxx_personality_v0 | | U | NOTIP| | |*UND* dublu max (dublu, dublu) |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| |.text
Și vedem prezența ambelor instanțe max (int,int) și max (dublu dublu) .

Ambele definiții sunt marcate ca personaje slabe, iar acest lucru înseamnă că linkerul, atunci când creează fișierul executabil final, poate elimina toate instanțele duplicate ale aceluiași șablon și poate lăsa doar una (și dacă consideră că este necesar, poate verifica dacă toate instanțele duplicate ale șablonului se potrivesc efectiv la același cod). Cel mai mare dezavantaj al acestei abordări este creșterea dimensiunii fiecărui fișier obiect individual.

O altă abordare (care este folosită în Solaris C++) este de a nu include definiții de șablon în fișierele obiect, ci de a le marca ca simboluri nedefinite. Când vine vorba de etapa de conectare, linkerul poate colecta toate simbolurile nedefinite care aparțin de fapt instanțelor șablon și apoi poate genera cod de mașină pentru fiecare dintre ele.

Acest lucru reduce cu siguranță dimensiunea fiecărui fișier obiect, dar dezavantajul acestei abordări este că linkerul trebuie să țină evidența unde se află codul sursă și trebuie să poată rula compilatorul C++ la momentul conexiunii (ceea ce poate încetini întregul proces). )

Biblioteci încărcate dinamic

Ultima caracteristică pe care o vom discuta aici este încărcarea dinamică a bibliotecilor partajate. Am văzut cum utilizarea bibliotecilor partajate amână legarea finală până când programul rulează efectiv. În sistemele de operare moderne acest lucru este posibil chiar și în etapele ulterioare.

Acest lucru este realizat printr-o pereche de apeluri de sistem, dlopen și dlsym (echivalentele aproximative pe Windows se numesc LoadLibrary și, respectiv, GetProcAddress). Primul ia numele bibliotecii partajate și îl încarcă în spațiul de adrese al procesului care rulează. Desigur, această bibliotecă poate avea și simboluri nerezolvate, așa că apelarea dlopen poate implica încărcarea altor biblioteci partajate.

Dlopen oferă posibilitatea de a șterge toate problemele nerezolvate imediat ce biblioteca este încărcată (RTLD_NOW) sau de a rezolva simbolurile după cum este necesar (RTLD_LAZY). Prima metodă înseamnă că apelarea dlopen poate dura destul de mult, dar a doua metodă introduce un anumit risc ca în timpul execuției programului să se găsească o referință nedefinită care nu poate fi rezolvată, moment în care programul se va termina.

Desigur, simbolurile dintr-o bibliotecă încărcată dinamic nu pot avea un nume. Cu toate acestea, acest lucru poate fi rezolvat cu ușurință, la fel cum se rezolvă alte probleme de programare, prin adăugarea unui strat suplimentar de soluții. În acest caz, se folosește un pointer către spațiul de caractere. Apelul la dlsym preia un parametru literal care specifică numele simbolului care trebuie găsit și returnează un pointer la locația sa (sau NULL dacă simbolul nu este găsit).

Interoperabilitate cu C++

Procesul de încărcare dinamică este destul de simplu, dar cum interacționează cu diferitele caracteristici C++ care afectează comportamentul general al linkerului?

Prima observație se referă la decorarea numelor. Când apelați dlsym , numele simbolului care trebuie găsit este trecut. Aceasta înseamnă că aceasta trebuie să fie versiunea vizibilă pentru linker a numelui, de exemplu. nume decorat.

Deoarece procesul de decorare poate varia de la platformă la platformă și de la compilator la compilator, aceasta înseamnă că este practic imposibil să găsiți dinamic un simbol C++ folosind o metodă universală. Chiar dacă lucrați doar cu un singur compilator și vă adânciți în lumea lui interioară, există și alte probleme - în afară de funcțiile simple asemănătoare C, există o grămadă de alte lucruri (tabele de metode virtuale și altele asemenea) de care trebuie luate în considerare. .

Pentru a rezuma cele de mai sus, de obicei este mai bine să aveți un punct de intrare extern „C” care poate fi găsit de dlsym „. Acest punct de intrare ar putea fi o metodă din fabrică care returnează pointeri către toate instanțele clasei C++, permițând accesul la toate deliciile C++.

Compilatorul ar putea foarte bine să se ocupe de constructorii de obiecte globale într-o bibliotecă încărcată de dlopen , deoarece există câteva simboluri speciale care pot fi adăugate bibliotecii și care vor fi apelate de linker (indiferent la încărcare sau execuție). timp) dacă biblioteca este încărcată sau descărcată dinamic - atunci există apeluri necesare către constructori sau destructori se pot întâmpla aici. Pe Unix, acestea sunt funcțiile _init și _fini, sau pentru sistemele mai noi care folosesc setul de instrumente GNU există funcții etichetate __attribute__((constructor)) sau __attribute__((destructor)) . Pe Windows, funcția corespunzătoare este DllMain cu un DWORD fdwReason egal cu DLL_PROCESS_ATTACH sau DLL_PROCESS_DETACH .

Și în concluzie, vom adăuga că încărcarea dinamică face o treabă excelentă de „subțire a instanțelor repetate” atunci când vine vorba de instanțierea șabloanelor; și totul pare ambiguu cu „amânarea instanției”, deoarece „etapa de conectare” are loc după ce programul a fost deja rulat (și foarte posibil pe o altă mașină care nu stochează sursele). Consultați documentația compilatorului și linkerului pentru a găsi o soluție la această situație.

În plus

Acest articol a omis în mod intenționat multe detalii despre cum funcționează linkerul, deoarece cred că ceea ce este scris acoperă 95% din problemele de zi cu zi cu care se confruntă un programator atunci când își leagă programul.

Dacă doriți să aflați mai multe, puteți obține informații din linkurile de mai jos:

Mulțumiri lui Mike Capp și Ed Wilson pentru sugestiile utile despre această pagină.

Copyright 2004-2005,2009-2010 David Drysdale

Se acordă permisiunea de a copia, distribui și/sau modifica acest document în conformitate cu termenii Licenței de documentare liberă GNU, Versiunea 1.1 sau orice versiune ulterioară publicată de Free Software Foundation; fără secțiuni invariante, fără texte de pe copertă și fără texte de pe copertă din spate. O copie a licenței este disponibilă.

Etichete: Adăugați etichete

Cele mai bune articole pe această temă