Care este scopul unui linker? Funcții de conectare și încărcare

Vreau să înțeleg exact ce parte a compilatorului programului este privită și referită de linker. Așa că am scris următorul cod:

#include folosind namespace std; #include void FunctionTemplate (paramType val) ( i = val ) ); void Test::DefinedCorrectFunction(int val) ( i = val; ) void Test::DefinedIncorrectFunction(int val) ( i = val ) void main() ( Test testObject(1); //testObject.NonDefinedFunction(2); / /testObject.FunctionTemplate (2); }

Am trei functii:

  • DefinedCorrectFunction este o funcție normală, declarată și definită corect.
  • DefinedIncorrectFunction - această funcție este declarată corect, dar implementarea este incorectă (lipsește;)
  • NonDefinedFunction este doar o declarație. Fără definiție.
  • FunctionTemplate - șablon de funcție.

    Acum, dacă compilez acest cod, primesc o eroare de compilator pentru ";" lipsă. în DefinedIncorrectFunction.
    Să presupunem că rezolv acest lucru și apoi comentez testObject.NonDefinedFunction(2). Acum primesc o eroare de linker. Acum comentați testObject.FunctionTemplate(2). Acum primesc o eroare de compilator pentru lipsa „;”.

Pentru șabloanele de funcție, înțeleg că acestea nu sunt atinse de compilator decât dacă sunt apelate în cod. Deci, lipsa „;” Compilatorul nu se plânge până nu am sunat testObject.FunctionTemplate(2).

Pentru testObject.NonDefinedFunction(2), compilatorul nu sa plâns, dar linkerul a făcut-o. Din câte am înțeles, întregul compilator ar fi trebuit să știe că a fost declarată o funcție NonDefinedFunction. Nu-i păsa de implementare. Linkerul s-a plâns apoi pentru că nu a găsit o implementare. Până acum, bine.

Deci, nu înțeleg exact ce face compilatorul și ce face linkerul. Înțelegerea mea despre componentele generatorului de legături cu apelurile lor. Deci, atunci când NonDefinedFunction este apelată, caută implementarea compilată a NonDefinedFunction și se plânge. Dar compilatorului nu i-a păsat implementarea NonDefinedFunction, dar a făcut-o pentru DefinedIncorrectFunction.

Aș aprecia cu adevărat dacă cineva ar putea explica acest lucru sau oferi o referință.

8 răspunsuri

Funcția compilatorului este de a compila codul pe care îl scrieți și de a-l converti în fișiere obiect. Deci, în cazul în care ați ratat-o; sau a folosit o variabilă nedefinită, compilatorul se va plânge deoarece acestea sunt erori de sintaxă.

Dacă compilarea reușește fără eșecuri, sunt create fișiere .object. Fișierele obiect au o structură complexă, dar practic conțin cinci lucruri

  • Anteturi - informații despre fișier
  • Cod obiect - cod în limbajul mașinii (acest cod nu poate rula singur în majoritatea cazurilor)
  • Informații despre mutare. Ce părți ale codului vor trebui să schimbe adresele atunci când sunt executate efectiv.
  • tabelul de simboluri. Caracterele la care face referire cod. Ele pot fi definite în acest cod, importate din alte module sau definite de linker
  • Informații de depanare - utilizate de depanatori

Compilatorul compilează codul și completează tabelul de simboluri cu fiecare simbol pe care îl întâlnește. Simbolurile se referă la variabile și funcții. Răspunsul la Această întrebare explică tabelul cu simboluri.

Acesta conține o colecție de cod executabil și date pe care linkerul le poate procesa într-o aplicație de producție sau într-o bibliotecă partajată. Un fișier obiect are o structură de date numită tabel de simboluri, care mapează diferitele elemente din fișierul obiect cu nume pe care linkerul le poate înțelege.

Observați punctul

Dacă apelați o funcție din codul dvs., compilatorul nu pune adresa finală a rutinei în fișierul obiect. În schimb, pune o valoare de substituent în cod și adaugă o notă care îi spune linkerului să caute o referință în diferite tabele de simboluri din toate fișierele obiect pe care le procesează și să introducă acolo locația finală.

Fișierele obiect generate sunt procesate de linker, care completează golurile din tabelele de simboluri, leagă un modul la altul și, în final, produce cod executabil care poate fi încărcat de încărcător.

Deci, în cazul tău specific -

  • DefinedIncorrectFunction() - Compilatorul primește definiția funcției și începe să o compileze pentru a produce cod obiect și a introduce referința corespunzătoare în tabelul de simboluri. Compilarea a eșuat din cauza unei erori de sintaxă, așa că compilatorul se anulează cu o eroare.
  • NonDefinedFunction() - Compilatorul primește declarația, dar nu are definiție, așa că adaugă o intrare la tabelul de simboluri și pune linkerul să adauge valorile corespunzătoare (deoarece linker-ul procesează o grămadă de fișiere obiect, este posibil ca această definiție este prezent într-un alt fișier obiect). În cazul dvs., nu specificați niciun alt fișier, astfel încât linkerul eșuează cu o referință nedefinită la eroarea NonDefinedFunction, deoarece nu poate găsi o referință la intrarea corespunzătoare din tabelul de simboluri.

Pentru a înțelege acest lucru, să spunem din nou că codul dvs. este structurat astfel

#include #include clasa Test (privat: int i; public: Test(int val) (i=val ;) void DefinedCorrectFunction(int val); void DefinedIncorrectFunction(int val); void NonDefinedFunction(int val); șablon void FunctionTemplate (paramType val) ( i = val; ) );

try.cpp fișier

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

întoarce 0; )

Mai întâi să copiem și să asamblam codul, dar să nu îl conectăm

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

Acest pas continuă fără probleme. Deci aveți cod obiect în try.o. Încercați și conectați-l.

$g++ try.o try.o: În funcția `main": try.cpp:(.text+0x52): referință nedefinită la `Test::NonDefinedFunction(int)" collect2: ld a returnat 1 stare de ieșire

Ați uitat să definiți Test::NonDefinedFunction. Să-l definim într-un fișier separat.

File-try1.cpp

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

Să-l compilam în cod obiect

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

Din nou, acest lucru are succes. Să încercăm să conectăm doar acest fișier

$ g++ try1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.5/../../../../lib64/crt1.o: În funcția `_start": (.text+ 0x20 ): referință nedefinită la „principal” collect2: ld a returnat 1 stare de ieșire

Nu există principal așa câștigat; t link!!

Acum aveți două coduri obiect separate care au toate componentele necesare. Doar transmiteți-le pe AMBELE către linker și lăsați-le pe restul să o facă

$ g++ încercați.o încercați1.o $

Fără greșeală! Acest lucru se datorează faptului că linkerul găsește definițiile tuturor funcțiilor (chiar dacă acestea sunt împrăștiate în fișiere obiect diferite) și umple spațiile din codurile obiectelor cu valorile adecvate.

Cauți un meniu de supă. Dacă nu îl găsești în meniu, pleci din restaurant. (un fel ca compilatorul care se plânge că nu a putut găsi o funcție). Daca gasesti unul, ce faci?

Sunați chelnerul să vină cu supa. Totuși, doar pentru că este în meniu nu înseamnă că îl au și în bucătărie. Poate meniul este depășit, poate cineva a uitat să-i spună bucătarului că ar trebui să facă supă. Deci din nou pleci. (de exemplu, o eroare de la linker că nu a putut găsi simbolul)

Cred că aceasta este întrebarea ta:

Unde am fost confuz a fost atunci când compilatorul s-a plâns de DefinedIncorrectFunction. Nu a căutat o implementare a NonDefinedFunction, ci a trecut prin DefinedIncorrectFunction.

Compilatorul a încercat să analizeze DefinedIncorrectFunction (pentru că ați furnizat o definiție în acel fișier sursă) și a apărut o eroare de sintaxă (lipsește punct și virgulă). Pe de altă parte, compilatorul nu a văzut niciodată o definiție pentru NonDefinedFunction, deoarece pur și simplu nu exista cod în acel modul. Este posibil să fi specificat o definiție NonDefinedFunction într-un alt fișier sursă, dar compilatorul nu știe acest lucru. Compilatorul analizează un singur fișier sursă (și fișierele de antet incluse) la un moment dat.

Compilatorul verifică dacă codul sursă este adecvat pentru limbaj și urmează semantica limbajului. Ieșirea compilatorului este cod obiect.

Linker-ul leagă diverse module obiect împreună pentru a forma un exe. Definițiile funcțiilor sunt situate în această fază, iar codul adecvat pentru a le apela este adăugat în această fază.

Compilatorul compilează codul în unități de traducere. Acesta va compila tot codul care este inclus în fișierul sursă .cpp.
DefinedIncorrectFunction() este definit în fișierul sursă, astfel încât compilatorul verifică corectitudinea limbii.
NonDefinedFunction() are o anumită definiție în fișierul sursă, deci compilatorul nu trebuie să o compileze, dacă definiția este prezentă într-un alt fișier sursă, funcția va fi compilată ca parte a acelei unități de traducere și mai târziu linkerul se va conecta la dacă în definiția pasului de legătură nu a fost găsită de linker, atunci va arunca o eroare de legătură.

Ce face compilatorul și ceea ce face linkerul depinde de implementare: o implementare legală ar putea pur și simplu să stocheze sursa tokenizată în „compilator” și să facă totul în linker. Implementările moderne pun tot mai mult accent pe linker, pentru o mai bună optimizare. Și multe implementări timpurii ale șablonului nici măcar nu se uită la codul șablonului până la ora de referință, în afară de acoladele potrivite, este suficient pentru a ști unde s-a terminat șablonul. Din perspectiva unui utilizator, sunteți mai interesat dacă eroarea necesită „diagnostic” (care poate fi selectată de compilator sau linker) sau nedefinită.

În cazul DefinedIncorrectFunction, furnizați textul sursă care este necesar pentru analiză. Acest text conține o eroare care necesită diagnosticare. În cazul NonDefinedFunction: dacă funcția este utilizată, eșecul de a furniza o definiție (sau de a furniza mai mult de o definiție) în programul complet este o încălcare a unei reguli de definiție, care este un comportament nedefinit. Nu sunt necesare diagnostice (dar nu îmi pot imagina care nu includea o definiție lipsă a funcției care a fost folosită).

În practică, erorile care pot fi detectate cu ușurință prin simpla examinare a textului introdus dintr-o singură unitate de traducere sunt definite de standardul „diagnostic required” și vor fi detectate de compilator. Erorile care nu pot fi detectate prin examinarea unei singure unități de traducere (de exemplu, o definiție lipsă care poate fi prezentă într-o altă unitate de traducere) au un comportament nedefinit în multe cazuri, erorile pot fi detectate de linker și, în astfel de cazuri, implementarea; de fapt aruncă o eroare.

Acest lucru este oarecum modificat în cazuri precum funcțiile inline, în care vi se permite să repetați definiția în fiecare unitate de traducere și modificat de șabloane, deoarece multe erori nu pot fi detectate până la instanțiere. În cazul șabloanelor, foaia de implementare standard are multă libertate: cel puțin, compilatorul trebuie să analizeze șablonul suficient pentru a determina unde se termină șablonul. lucruri standard adăugate, cum ar fi typename, permit totuși o analiză semnificativ mai mare înainte de creare. Cu toate acestea, în contexte dependente, este posibil ca unele erori să nu fie detectate până când instanța este creată, ceea ce poate apărea în timpul compilării sau în momentul legăturii; implementările timpurii au preferat aspectul timpului de legătură; Timpul de compilare este astăzi și sunt folosite VC++ și g++.

[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ă sunt detectate 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 structurate prin suprapunere, module obiect bibliotecă și module bibliotecă cu link dinamic.

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, linker-ul 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.

Etichete: Linker, linker, fișier obiect, bibliotecă statică, bibliotecă dinamică, execuție program, definiție, declarație

Un ghid pentru începători pentru linkuri. Partea 1

Traducerea articolului Ghidul pentru începători pentru linkeri cu exemple și completări.

Următoarele concepte sunt folosite în mod interschimbabil: linker și linker, definiție și definiție, declarație și declarație. Inserțiile cu exemple sunt evidențiate cu gri.

Denumirea componentelor: ce se află în interiorul unui fișier C

În primul rând, trebuie să înțelegeți diferența dintre o declarație și o definiție. O definiție asociază un nume cu o implementare a acelui nume, care poate fi fie date, fie cod:

  • Definirea unei variabile face ca compilatorul să aloce memorie pentru aceasta și, eventual, să o umple cu o valoare inițială
  • Definirea unei funcții determină compilatorul să genereze cod pentru acea funcție

Declarația îi spune compilatorului C că undeva în program, poate într-un alt fișier, există o definiție asociată cu acest nume (rețineți că definiția poate fi imediat o declarație pentru care definiția este în același loc).

Pentru variabile, există două tipuri de definiții

  • Variabile globale care există pe durata de viață a programului (alocare statică) și sunt de obicei accesate din multe funcții
  • Variabile locale care există doar în timpul execuției funcției în care sunt declarate (plasare locală) și sunt accesibile doar în cadrul acesteia

Pentru claritate, „accesibil” înseamnă că o variabilă poate fi referită printr-un nume care este asociat cu definiția sa.

Există câteva cazuri în care lucrurile nu sunt atât de evidente.

  • Variabilele locale statice sunt de fapt globale, deoarece există pe toată durata de viață a programului, deși sunt accesibile într-o singură funcție.
  • La fel ca variabilele statice, variabilele globale care sunt accesibile numai în același fișier în care sunt declarate sunt, de asemenea, globale.

Merită să reamintim imediat că declararea unei funcții statice își reduce domeniul de aplicare la fișierul în care este definită (și anume, funcțiile din acest fișier o pot accesa).

Variabilele locale și globale pot fi, de asemenea, împărțite în neinițializate și inițializate (care sunt pre-completate cu o anumită valoare).

La urma urmei, putem lucra cu variabile create dinamic folosind funcția malloc (sau noul operator în C++). Este imposibil să accesezi o zonă de memorie după nume, așa că folosim pointeri - variabile numite care stochează adresa unei zone de memorie fără nume. Această zonă poate fi eliberată și folosind free (sau ștergere), astfel încât memoria este considerată a fi alocată dinamic.

Să punem totul împreună acum

Cod Date
Global Local Dinamic
Inițializat Neinițializat Inițializat Neinițializat
Declaraţie int fn(int x); extern int x; extern int x; N / A N / A N / A
Definiție int fn(int x) ( ... ) int x = 1;
(la domeniul de aplicare al fișierului)
int x;
(la domeniul de aplicare al fișierului)
int x = 1;
(la domeniul de aplicare al funcției)
int x;
(la domeniul de aplicare al funcției)
(int* p = malloc(sizeof(int));)

Este mai ușor să te uiți la acest program

/* Aceasta este definiția unei variabile globale neinițializate */ int x_global_uninit; /* Aceasta este definiția unei variabile globale inițializate */ int x_global_init = 1; /* Aceasta este definiția unei variabile globale neinițializate, dar poate fi accesată după nume doar din același fișier C */ static int y_global_uninit; /* Aceasta este definiția unei variabile globale inițializate, dar poate fi accesată după nume numai din același fișier C */ static int y_global_init = 2; /* Aceasta este o declarație a unei variabile globale care există în altă parte în program */ extern int z_global; /* Aceasta este o declarație de funcție care este definită în altă parte în program. Puteți adăuga cuvântul de serviciu extern. Dar asta nu contează */ int fn_a(int x, int y); /* Aceasta este o definiție a funcției, dar deoarece este definită cu cuvântul static, este disponibilă numai în același fișier C */ static int fn_b(int x) ( return x+1; ) /* Aceasta este o definiție a funcției . Parametrii săi sunt tratați ca variabile locale */ int fn_c(int x_local) ( /* Aceasta este definiția unei variabile locale neinițializate */ int y_local_uninit; /* Aceasta este definiția unei variabile locale inițializate */ int y_local_init = 3; /* Acest cod se referă la variabile și funcții locale și globale prin nume */ x_global_uninit = fn_a(x_local, x_global_init = fn_a(x_local, y_local_init);

Lăsați acest fișier să se numească fișier.c. Hai să-l asamblam așa

Cc -g -O -c fișier.c

Să obținem fișierul obiect file.o

Ce face un compilator C?

Sarcina unui compilator C este să transforme un fișier de cod din ceva care poate fi (uneori) înțeles de un om în ceva care poate fi înțeles de un computer. Ieșirea compilatorului este un fișier obiect, care are de obicei extensia .o pe platforma UNIX și .obj pe Windows. Conținutul unui fișier obiect este în esență două tipuri de obiecte

  • Definițiile funcției de potrivire a codului
  • Date corespunzătoare variabilelor globale definite în fișier (dacă sunt pre-inițializate, atunci valoarea lor este de asemenea stocată acolo)

Instanțele acestor obiecte vor avea nume asociate - numele variabilelor sau funcțiilor ale căror definiții au condus la generarea lor.

Codul obiect este o secvență de instrucțiuni de mașină (codificate corespunzător) care corespund instrucțiunilor C - toate acele if, whiles și chiar gotos. Toate aceste comenzi operează pe diferite tipuri de informații, iar aceste informații trebuie stocate undeva (aceasta necesită variabile). În plus, pot accesa alte bucăți de cod care sunt definite în fișier.

De fiecare dată când codul accesează o funcție sau o variabilă, compilatorul îi permite să facă acest lucru numai dacă a văzut declarația acelei variabile sau funcție. O declarație este o promisiune pentru compilator că există o definiție undeva în program.

Sarcina linkerului este de a îndeplini aceste promisiuni, dar ce ar trebui să facă compilatorul când întâlnește entități nedefinite?

În esență, compilatorul lasă doar un stub. Un stub (link) are un nume, dar valoarea asociată cu acesta nu este încă cunoscută.

Acum putem descrie aproximativ cum va arăta programul nostru

Analiza fișierului obiect

Până acum am lucrat cu un program abstract; Acum este important să vedem cum arată în practică. Pe o platformă UNIX, puteți utiliza utilitarul nm. Pe Windows, echivalentul este dumpbin cu steag-ul /symbols, deși există și un port al GNU binutils care include nm.exe.

Să vedem ce ne oferă nm pentru programul scris mai sus:

00000000 b .bss 00000000 d .data 00000000 N .debug_abbrev 00000000 N .debug_aranges 00000000 N .debug_info 00000000 N .000000000000000000 0000 i .drectve 00000000 r .eh_frame 00000000 r .rdata$zzz 00000000 t .text U_fn_a 00000000 T _fn_c 00000000 D _x_global_init 00000004 C _x_global_uninit U _z_global

Pentru un fișier fișier compilat anterior.o

Nm dosar.o

Ieșirea poate varia de la sistem la sistem, dar informația cheie este clasa fiecărui caracter și dimensiunea acestuia (dacă este disponibilă). Clasa poate avea următoarele valori

  • Clasa U înseamnă necunoscut sau ciot, așa cum s-a menționat mai sus. Există doar două astfel de obiecte: fn_a și z_global (unele versiuni de nm pot scoate și o secțiune, care în acest caz va fi *UND* sau UNDEF)
  • Clasa t sau T indică faptul că codul este definit - t este local sau T este o funcție statică. Secțiunea .text poate fi, de asemenea, afișată
  • Clasele d și D reprezintă o variabilă globală inițializată, d o variabilă locală, D o variabilă nelocală. Segmentul pentru date variabile este de obicei .data
  • Pentru variabilele globale neinițializate, clasa b este utilizată dacă este statică/locală sau B și C dacă nu. De obicei, acesta este segment.bss sau *COM*

Există și alte clase nefaste care reprezintă un fel de mecanism intern al compilatorului.

Ce face linkerul? Partea 1

După cum am definit mai devreme, declararea unei variabile sau funcție este o promisiune pentru compilator că există o definiție a acelei variabile sau funcție undeva și că sarcina linkerului este să-și îndeplinească aceste promisiuni. În diagrama noastră de fișier obiect, aceasta poate fi numită și „umplerea golurilor”.

Pentru a ilustra acest lucru, iată un alt fișier C în plus față de primul:

/* 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ă, lume"; return fn_a(11,12); )

Lăsați acest fișier să se numească main.c. Îl compilam ca înainte

Cc –g –O –c principal.c

Cu aceste două diagrame, vedem acum că toate punctele pot fi conectate (și dacă nu, linkerul va arunca o eroare). Fiecare lucru are locul său și fiecare loc are un lucru, iar linkerul poate înlocui toate stuburile așa cum se arată în figură.


Pentru main.o și file.o compilate anterior, construirea executabilului

Cc -o out.exe main.o file.o

ieșire nm pentru fișierul executabil (out.exe în cazul nostru):

Simboluri din sample1.exe: Nume Valoare Clasă Tip Mărime Linie Secțiune _Jv_RegisterClasses | | w | NOTIP| | |*UND* __gmon_start__ | | w | NOTIP| | |*UND* __libc_start_main@@GLIBC_2.0| | U | FUNC|000001ad| |*UND* _init |08048254| T | FUNC| | |.init _start |080482c0| T | FUNC| | |.text __do_global_dtors_aux|080482f0| t | FUNC| | |.text frame_dummy |08048320| t | FUNC| | |.text fn_b |08048348| t | FUNC|00000009| |.text fn_c |08048351| T | FUNC|00000055| |.text fn_a |080483a8| T | FUNC|0000000b| |.text principal |080483b3| T | FUNC|0000002c| |.text __libc_csu_fini |080483e0| T | FUNC|00000005| |.text __libc_csu_init |080483f0| T | FUNC|00000055| |.text __do_global_ctors_aux|08048450| t | FUNC| | |.text_fini |08048478| T | FUNC| | |.fini_fp_hw |08048494| R | OBIECTUL|00000004| |.rodata_IO_stdin_used |08048498| R | OBIECTUL|00000004| |.rodata __FRAME_END__ |080484ac| r | OBIECT| | |.eh_frame __CTOR_LIST__ |080494b0| d | OBIECT| | |.ctors __init_array_end |080494b0| d | NOTIP| | |.ctors __init_array_start |080494b0| d | NOTIP| | |.ctors __CTOR_END__ |080494b4| d | OBIECT| | |.ctors __DTOR_LIST__ |080494b8| d | OBIECT| | |.dtors __DTOR_END__ |080494bc| d | OBIECT| | |.dtors __JCR_END__ |080494c0| d | OBIECT| | |.jcr __JCR_LIST__ |080494c0| d | OBIECT| | |.jcr_DYNAMIC |080494c4| d | OBIECT| | |.dynamic _GLOBAL_OFFSET_TABLE_|08049598| d | OBIECT| | |.got.plt __data_start |080495ac| D | NOTIP| | |.data data_start |080495ac| W | NOTIP| | |.data __dso_handle |080495b0| D | OBIECT| | |.date p.5826 |080495b4| d | OBIECT| | |.data x_global_init |080495b8| D | OBIECTUL|00000004| |.data y_global_init |080495bc| d | OBIECTUL|00000004| |.data z_global |080495c0| D | OBIECTUL|00000004| |.data y_global_init |080495c4| d | OBIECTUL|00000004| |.data __bss_start |080495c8| A | NOTIP| | |*ABS* _edata |080495c8| A | NOTIP| | |*ABS* finalizat.5828 |080495c8| b | OBIECT|00000001| |.bss y_global_uninit |080495cc| b | OBIECTUL|00000004| |.bss x_global_uninit |080495d0| B | OBIECTUL|00000004| |.bss_end |080495d4| A | NOTIP| | |*ABS*

Toate simbolurile de la ambele obiecte sunt colectate aici și toate referințele nedefinite au fost curățate. Simbolurile au fost, de asemenea, reordonate pentru a menține clase similare împreună și au fost adăugate câteva entități suplimentare pentru a ajuta sistemul de operare să trateze totul ca pe un program executabil.

Pentru a curăța rezultatul pe UNIX, puteți elimina tot ceea ce începe cu un caracter de subliniere.

Caractere duplicate

În secțiunea anterioară, am aflat că, dacă linkerul nu poate găsi o definiție pentru un simbol declarat, va arunca o eroare. Ce se întâmplă dacă se găsesc două definiții pentru un simbol în timpul conectării?

În C++, totul este simplu - conform standardului, un simbol trebuie să aibă întotdeauna o definiție (așa-numita regulă de definiție) din secțiunea 3.2 a standardului de limbaj.

Pentru si, totul este mai putin clar. O funcție sau o variabilă globală inițializată trebuie să aibă întotdeauna o singură definiție. Dar definirea unei variabile globale neinițializate poate fi considerată preliminară. C în acest caz permite (cel puțin nu interzice) fișierelor de cod diferite să aibă propriile lor definiții preliminare pentru același obiect.

Cu toate acestea, linkerii trebuie să se ocupe și cu alte limbaje de programare pentru care nu se aplică regula unei singure definiții. De exemplu, este destul de normal ca Fortran să aibă o copie a fiecărei variabile globale în fiecare fișier în care este accesat. Linker-ul este forțat să scape de toate copiile, alegând una (de obicei cea mai înaltă versiune, dacă sunt de dimensiuni diferite) și eliminând restul. Acest model este adesea numit model de asamblare COMUN, datorită cuvântului de funcție FORTRAN COMMON.

Ca rezultat, linkerul UNIX de obicei nu se plânge de definițiile simbolurilor duplicate, cel puțin atâta timp cât simbolul duplicat este o variabilă globală neinițializată (acest model este cunoscut ca modelul relaxat ref/def de legare). Dacă acest lucru vă deranjează (și ar trebui!), căutați în documentația compilatorului o cheie care face comportamentul mai strict. De exemplu, –fno-common pentru compilatorul GNU forțează ca variabilele neinițializate să fie plasate în segmentul BSS, în loc să genereze blocuri comune.

Ce face sistemul de operare?

Acum, după ce linkerul a asamblat programul executabil, legând toate simbolurile cu definițiile necesare, trebuie să ne oprim puțin și să înțelegem ce face sistemul de operare când rulează programul.

Rularea unui program duce la executarea codului mașinii, așa că, evident, trebuie să mutați programul de pe hard disk în memoria de operare, unde procesorul central poate lucra deja cu el. O bucată de memorie pentru un program se numește segment de cod sau segment de text.

Codul nu are valoare fără date, așa că toate variabilele globale trebuie să aibă, de asemenea, spațiu în RAM pentru ele însele. Aici există o diferență între variabilele globale inițializate și neinițializate. Variabilele inițiale au deja propria lor valoare, care este stocată atât în ​​fișierele obiect, cât și în fișierele executabile. Când pornește programul, acestea sunt copiate de pe hard disk în memorie în segmentul de date.

Pentru variabilele neinițializate, sistemul de operare nu va copia valorile din memorie (din moment ce nu există) și va umple totul cu zerouri. O bucată de memorie inițializată la 0 se numește segment bss.

Valoarea inițială a variabilelor inițializate este stocată pe disc, în fișierul executabil; Pentru variabilele neinițializate, dimensiunea lor este stocată.


Vă rugăm să rețineți că în tot acest timp am vorbit doar despre variabile globale și nu am menționat niciodată obiecte locale sau create dinamic.

Aceste date nu au nevoie de linker pentru a funcționa, deoarece durata de viață începe când programul pornește - mult după ce linkerul a terminat de rulat. Cu toate acestea, de dragul completității, vom indica încă o dată

  • Variabilele locale se află pe o bucată de memorie cunoscută sub numele de stivă, care crește și se micșorează pe măsură ce o funcție începe sau se termină executarea.
  • Memoria dinamică este alocată pe o zonă cunoscută sub numele de heap; selecția va fi gestionată de funcția malloc

Acum putem adăuga aceste zone de memorie lipsă la diagrama noastră. Deoarece atât heap-ul, cât și stiva își pot schimba dimensiunea în timp ce programul rulează, pentru a remedia problemele, acestea cresc unul spre celălalt. În acest fel, o întrerupere a memoriei se va întâmpla numai atunci când se întâlnesc (și acest lucru necesită utilizarea multă memorie).


Ce face linkerul? Partea 2

După ce învățăm elementele de bază ale modului în care funcționează linkerul, să începem să trecem mai departe și să studiem capacitățile sale suplimentare, în ordinea în care au apărut istoric și au fost adăugate linkerului.

Prima observație care a condus la dezvoltarea linkerului: aceleași secțiuni de cod sunt adesea reutilizate (intrare/ieșire de date, funcții matematice, citire fișiere etc.). Prin urmare, aș dori să le aloc într-un loc separat și să le folosesc împreună cu multe programe.

În general, este destul de ușor să utilizați același fișier obiect pentru a construi diferite programe, dar este mult mai bine să colectați împreună fișiere obiect similare și să faceți o bibliotecă.

Biblioteci statice

Cea mai simplă formă a unei biblioteci este statică. Secțiunea anterioară spunea că puteți partaja pur și simplu un fișier obiect între programe. În realitate, o bibliotecă statică este puțin mai mult decât un fișier obiect.

Pe sistemele UNIX, o bibliotecă statică este de obicei generată cu comanda ar, iar fișierul bibliotecă în sine are extensia .a. De asemenea, aceste fișiere încep de obicei cu prefixul lib și sunt transmise linker-ului cu indicatorul –l, urmat de numele bibliotecii fără prefixul lib și fără extensie (de exemplu, pentru fișierul libfred.a ați adăuga -lfred).

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

Un exemplu mai complex, să avem trei fișiere

A.c int a_f(int a) ( returnează a + 1; ) b.c int b_f(int a) ( returnează a + 1; ) c.c int c_f(int a) ( returnează a + 1; )

Și fișierul principal

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

Să colectăm a.c, b.c și c.c în biblioteca libabc.a. Mai întâi, să compilam toate fișierele (puteți face acest lucru separat, împreună este mai rapid)

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

Vom primi patru fișiere obiect. După aceea, să colectăm a, b și c într-un singur fișier

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

iar acum putem compila programul

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

Vă rugăm să rețineți că încercați să asamblați astfel

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

va duce la o eroare - linkerul va începe să se plângă de simbolurile nerezolvate.

Pe Windows, bibliotecile statice au de obicei extensia .lib și sunt generate de utilitarul LIB, dar ceea ce face confuză este că aceeași extensie se aplică și bibliotecilor de import, care pur și simplu conțin o listă de lucruri disponibile în biblioteca dinamică (dll). .

Pe măsură ce linkerul traversează colecția de fișiere obiect pentru a le pune împreună, compilează o listă de simboluri care nu au fost încă rezolvate. După procesarea listei tuturor obiectelor declarate explicit, linker-ul are acum încă un loc pentru a căuta simboluri nedeclarate: biblioteca. Dacă un obiect nerezolvat se află în bibliotecă, acesta este adăugat exact ca și cum utilizatorul l-ar fi specificat pe linia de comandă.

Atenție la nivelul de detaliu al obiectelor: dacă este nevoie de un anumit simbol, atunci se adaugă întregul obiect care conține acest simbol. Astfel, linkerul se poate găsi într-o situație în care face un pas înainte și doi pași înapoi, deoarece noul obiect, la rândul său, poate conține simboluri proprii, nerezolvate.

Un alt detaliu important este ordinea evenimentelor. Bibliotecile sunt interogate numai după ce a avut loc legătura normală și sunt procesate în ordine, de la stânga la dreapta. Aceasta înseamnă că, dacă o bibliotecă necesită un simbol care a fost anterior într-o bibliotecă conectată anterioară, linkerul nu îl va putea găsi automat.

Un exemplu ar trebui să vă ajute să înțelegeți mai în detaliu acest lucru. Să avem fișiere obiect a.o, b.o și biblioteci libx.a, liby.b.

Fişier a.o b.o libx.a Libia
Obiect a.o b.o x1.o x2.o x3.o y1.o y2.o y3.o
Definiții a1, a2, a3 b1, b2 x11, x12, x13 x21, x22, x23 x31, x32 y11, y12 y21, y22 y31, y32
Referințe nedefinite b2, x12 a3, y22 x23, y12 y11 y21 x31

După procesarea fișierelor a.o și b.o, linkerul va rezolva legăturile b2 și a3, lăsând x12 și y22 nedefinite. În acest moment, linkerul începe să verifice prima bibliotecă libx.a și află că poate scoate x1.o, care definește simbolul x12; După ce a făcut acest lucru, linkerul primește simbolurile nedefinite x23 și y12 declarate în x1.o (adică lista de simboluri nedefinite include y22, x23 și y23).

Linker-ul încă verifică libx.a, așa că poate rezolva cu ușurință simbolul x23 trăgându-l din biblioteca x2.o a libx.a. Dar acest x2.o adaugă y11 (care acum constă din y11, y22 și y12). Niciuna dintre acestea nu poate fi rezolvată în continuare folosind libx.a, așa că linkerul merge la liby.a.

Aproape același lucru se întâmplă aici, iar linker-ul scoate y1.o și y2.o. Primul adaugă y21, dar se rezolvă ușor deoarece y2.o este deja scos la lumină. Rezultatul întregii lucrări este că linkerul a reușit să rezolve toate simbolurile și a preluat aproape toate fișierele obiect care urmau să fie plasate în executabilul final.

Rețineți că, dacă b.o, de exemplu, ar conține un link către y32, atunci totul ar merge conform unui scenariu diferit. Procesarea libx.a ar fi fost aceeași, dar procesarea liby.a a scos y3.o care conține referința x31 care este definită în libx.a. Deoarece libx.a a terminat deja procesarea, linker-ul ar arunca o eroare.

Acesta este un exemplu de dependență circulară între două biblioteci libx și liby.

Biblioteci partajate

Bibliotecile C standard populare (de obicei libc) au un dezavantaj evident - fiecare fișier executabil va avea propria copie a aceluiași. Dacă fiecare program are o copie a printf, fopen și altele asemenea, atunci o mulțime de spațiu pe disc va fi irosit.

Un alt dezavantaj, mai puțin evident, este că după conectarea statică, codul programului este neschimbat. Dacă cineva găsește și remediază o eroare în printf, atunci toate programele care folosesc această bibliotecă vor trebui reconstruite.

Pentru a rezolva aceste probleme, au fost introduse biblioteci partajate (de obicei au extensia .so sau .dll pe Windows sau .dylib pe Mac OS X). Când lucrați cu astfel de biblioteci, linker-ul nu este necesar să combine toate elementele într-o singură imagine. În schimb, lasă ceva ca un bilet la ordin și amână plata până la lansarea programului.

Pe scurt: dacă linkerul află că un simbol nedefinit se află într-o bibliotecă partajată, nu adaugă o definiție la executabil. În schimb, linkerul scrie în program numele simbolului și biblioteca în care se presupune că este definit.

În timpul execuției programului, sistemul de operare stabilește că acești biți lipsă sunt legați „just la timp”. Înainte de a rula funcția principală, o versiune mai mică a linkerului (adesea ld.so) parcurge listele de debitori și completează partea finală a lucrării - scoate codul din bibliotecă și asamblează puzzle-ul.

Aceasta înseamnă că niciun program executabil nu are o copie a printf. O nouă versiune corectată a libc o poate înlocui pur și simplu pe cea veche și va fi preluată de fiecare dintre programe atunci când este lansată din nou.

Există o altă diferență importantă între o bibliotecă dinamică și una statică, iar aceasta se reflectă în nivelul de detaliu al legăturilor. Dacă un anumit simbol este preluat dintr-o bibliotecă partajată (de exemplu, printf din libc), întregul conținut al acelei biblioteci este mapat în spațiul de adrese. Aceasta este foarte diferită de o bibliotecă statică, din care este extras doar fișierul obiect care conține definiția simbolului declarat.

Cu alte cuvinte, o bibliotecă partajată este rezultatul unui linker (nu doar fișiere obiect asamblate ar) cu referințe rezolvate în obiectele din acel fișier. Încă o dată: nm ilustrează acest lucru cu succes. Pentru o bibliotecă statică, nm va afișa un set de fișiere obiect individuale. Pentru o bibliotecă partajată, liby.so va specifica doar simbolul x31 nedefinit. De asemenea, pentru exemplul nostru cu ordinea aspectului și referința circulară, nu vor fi probleme, deoarece tot conținutul y3.o și x3.o este deja scos.

Există, de asemenea, un alt instrument util numit ldd. Afișează toate bibliotecile partajate de care depinde executabilul sau biblioteca, cu informații despre unde poate fi găsită. Pentru ca programul să ruleze cu succes, trebuie găsite toate aceste biblioteci, împreună cu toate dependențele lor (de obicei, pe sistemele UNIX, încărcătorul caută biblioteci în lista de foldere, care este stocată î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)

Pe Windows, de exemplu

Ldd C:\Windows\System32\rundll32.exe ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x77100000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x763a0000.dll) KERNELBASE0.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x73e10000) apphelp.dll => /c/WINDOWS/system32/apphelp.dll (0x71ec0000) AcLayers.DLL => /c/WINDOWS/AppPatch/AcLayers.DLL (0x788300000) ) msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x74ef0000) USER32.dll => /c/WINDOWS/System32/USER32.dll (0x76fb0000) win32u.dll => /c/WINDOWS/win32u322 .dll (0x74060000) GDI32.dll => /c/WINDOWS/System32/GDI32.dll (0x74b00000) gdi32full.dll => /c/WINDOWS/System32/gdi32full.dll (0x741e0000.dll => /cELL32200.dll) dll /System32/SHELL32.dll (0x74fc0000) cfgmgr32.dll => /c/WINDOWS/System32/cfgmgr32.dll (0x74900000) windows.storage.dll => /c/WINDOWS/System32/windows/windows.xstorage.dll (0x74900000) .dll => /c/WINDOWS/System32/combase.dll (0x76490000) ucrtbase.dll => /c/WINDOWS/System32/ucrtbase.dll (0x74100000) RPCRT4.dll => /c/WINDOWS/System32/RPCRT4.dll (0x76b50000) bcryptPrimitives.dll => /c/WINDOWS/System32/bcryptPrimitives.dll (0x74940000) powrprof.dll => /c/WINDOWS/System32/powrprof.dll (0x73c20000) advapi32.dll => /cy/WINDOWS m32 /advapi32.dll (0x76ad0000) sechost.dll => /c/WINDOWS/System32/sechost.dll (0x76440000) shlwapi.dll => /c/WINDOWS/System32/shlwapi.dll (0x76d30000) =.dllkernel > /c/WINDOWS/System32/kernel.appcore.dll (0x73c10000) shcore.dll => /c/WINDOWS/System32/shcore.dll (0x76c20000) profapi.dll => /c/WINDOWS/System32/profapi.dll ( 0x73c70000 ) OLEAUT32.dll => /c/WINDOWS/System32/OLEAUT32.dll (0x76e20000) msvcp_win.dll => /c/WINDOWS/System32/msvcp_win.dll (0x7408000000000000000) SETUPA P.I. .dll (0x766c0000) MPR.dll => /c/WINDOWS/SYSTEM32/MPR.dll (0x6cac0000) sfc.dll => /c/WINDOWS/SYSTEM32/sfc.dll (0x2380000) WINSPOOL.DRV /WINDOWS /SYSTEM32/WINSPOOL.DRV (0x6f2f0000) bcrypt.dll => /c/WINDOWS/SYSTEM32/bcrypt.dll (0x73b70000) sfc_os.DLL => /c/WINDOWS/SYSTEM32/sfc_os.DLL (0x73b70000) IMM (0x73b70000) => /c/WINDOWS/System32/IMM32.DLL (0x76d90000) imagehlp.dll => /c/WINDOWS/System32/imagehlp.dll (0x749a0000)

Motivul acestei fragmentări mai mari se datorează faptului că sistemul de operare este suficient de inteligent încât să puteți duplica spațiul pe disc cu mai mult decât biblioteci statice. Diferite procese de execuție pot partaja, de asemenea, un segment de cod (dar nu și segmente de date/bss). Pentru a face acest lucru, întreaga bibliotecă trebuie mapată într-o singură trecere, astfel încât toate referințele interne să se alinieze într-un singur rând: dacă un proces a scos a.o și c.o, iar al doilea a scos b.o și c.o, nu va exista nicio potrivire pentru sistem de operare.

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 mi s-a cerut 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 de remarcat faptul că prin definirea unei funcții ca fiind statică, pur și simplu reduce numărul de locuri din care puteți face referire la această 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 „plasare 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:

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

date corespunzătoare definiției variabilelor globale din 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 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 | OBIECTUL|00000004| |.date

y_global_uninit |00000000| b | OBIECTUL|00000004| |.bss

x_global_uninit |00000004| C | OBIECTUL|00000004| |*COM*

y_global_init |00000004| d | OBIECTUL|00000004| |.date

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

Rezultatul poate arăta ușor diferit pe diferite platforme (verificați-l pentru detalii), dar informația cheie este clasa fiecărui personaj și dimensiunea acestuia (dacă este prezentă).

  • 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.



împărțirea unui program în module C++ (7)

Vreau să înțeleg la ce parte a compilatorului programului se uită și la ce se referă linkerul. Așa că am scris următorul cod:

#include folosind namespace std; #include clasa Test ( privat : int i ; public : Test ( int val ) ( i = val ;) void DefinedCorrectFunction ( int val ); void DefinedIncorrectFunction ( int val ); void NonDefinedFunction ( int val ); șablon< class paramType >void FunctionTemplate (paramType val ) ( i = val ) ); void Test :: DefinedCorrectFunction ( int val ) ( i = val ; ) void Test :: DefinedIncorrectFunction ( int val ) ( i = val ) void main () ( Test testObject ( 1 ); //testObject.NonDefinedFunction(2);//testObject.FunctionTemplate (2); }

Am trei functii:

  • DefinedCorrectFunction este o funcție normală, declarată și definită corect.
  • DefinedIncorrectFunction - această funcție este declarată corect, dar implementarea este incorectă (lipsește;)
  • NonDefinedFunction este doar o declarație. Fără definiție.
  • FunctionTemplate - șablon de funcție.

    Acum, dacă compilez acest cod, primesc o eroare de compilator pentru ";" lipsă. în DefinedIncorrectFunction.
    Să presupunem că rezolv acest lucru și apoi comentez testObject.NonDefinedFunction(2). Acum primesc o eroare de linker. Acum comentați testObject.FunctionTemplate(2). Acum primesc o eroare de compilator pentru lipsa „;”.

Pentru șabloanele de funcție, înțeleg că acestea nu sunt atinse de compilator decât dacă sunt apelate în cod. Deci, lipsa „;” nu se plânge de compilator până când nu am apelat testObject.FunctionTemplate(2).

Pentru testObject.NonDefinedFunction(2), compilatorul nu sa plâns, dar linkerul a făcut-o. Din câte am înțeles, întregul compilator ar fi trebuit să știe că a fost declarată o funcție NonDefinedFunction. Nu-i păsa de implementare. Linkerul s-a plâns apoi pentru că nu a găsit o implementare. Până acum, bine.

Deci nu înțeleg exact ce face compilatorul și ce face linkerul. Înțelegerea mea despre componentele generatorului de legături cu apelurile lor. Deci, atunci când NonDefinedFunction este apelată, caută implementarea compilată a NonDefinedFunction și se plânge. Dar compilatorului nu i-a păsat implementarea NonDefinedFunction, dar a făcut-o pentru DefinedIncorrectFunction.

Aș aprecia cu adevărat dacă cineva ar putea explica acest lucru sau oferi o referință.

Ceea ce face compilatorul și ceea ce face linkerul este dependent de implementare: o implementare legală ar putea pur și simplu să stocheze sursa tokenizată în „compilator” și să facă totul în linker. Implementări moderne Toate mai mult este pus deoparte pentru linker, pentru o mai bună optimizare. Și multe implementări timpurii ale șablonului nici măcar nu s-au uitat la codul șablonului până când timpul de referință, în afară de acoladele potrivite, a fost suficient pentru a spune unde s-a terminat șablonul. Din perspectiva unui utilizator, sunteți mai interesat dacă eroarea este un „diagnostic obligatoriu” (care poate fi selectat de compilator sau linker) sau este un comportament nedefinit.

În cazul DefinedIncorrectFunction, aveți textul sursă care este necesar pentru analiză. Acest text conține o eroare care necesită diagnosticare. În cazul NonDefinedFunction: dacă funcția este utilizată, eșecul de a furniza o definiție (sau de a furniza mai mult de o definiție) în programul complet este o încălcare a unei reguli de definiție, care este un comportament nedefinit. Nu este necesară nicio diagnosticare (dar nu îmi pot imagina o implementare care să nu ofere nicio definiție lipsă a funcției utilizate).

În practică, erorile care pot fi detectate cu ușurință prin simpla examinare a textului introdus într-un singur bloc de traducere sunt definite de standardul „require diagnostics” și vor fi detectate de compilator. Erorile care nu pot fi detectate prin verificarea unei singure unități de traducere (de exemplu, o definiție lipsă care poate fi prezentă într-o altă unitate de traducere) sunt un comportament formal nedefinit - în multe cazuri, erorile pot fi detectate de către linker și, în astfel de cazuri, implementarea de fapt aruncă o eroare.

Acest lucru este oarecum modificat în cazuri precum funcțiile încorporate, în care vi se permite să repetați definiția în fiecare unitate de traducere și să modificați enorm șabloanele, deoarece multe erori nu pot fi surprinse până când instanța este instanțiată. În cazul șabloanelor, standardul lasă implementările cu multă libertate: cel puțin, compilatorul trebuie să analizeze suficient șablonul pentru a determina unde se termină șablonul. Cu toate acestea, standardul a adăugat lucruri precum typename , pentru a permite mult mai multă analiză înainte de instanțiere. Totuși, în contexte dependente, este posibil ca unele erori să nu fie detectate până când nu este creată instanțierea, ceea ce poate apărea la momentul compilării sau la un moment de implementare timpurie care favorizează instanțierea; compilarea momentului este dominantă astăzi și este folosită de VC++ și g++.

Cred că aceasta este întrebarea ta:

Unde am fost confuz a fost compilatorul se plângea de DefinedIncorrectFunction. Nu a căutat o implementare a NonDefinedFunction, ci a trecut prin DefinedIncorrectFunction.

Compilatorul a încercat să analizeze DefinedIncorrectFunction (pentru că ați furnizat o definiție în acel fișier sursă) și a apărut o eroare de sintaxă (lipsește punct și virgulă). Pe de altă parte, compilatorul nu a văzut niciodată o definiție pentru NonDefinedFunction, deoarece pur și simplu nu exista cod în acel modul. Este posibil să fi definit NonDefinedFunction într-un alt fișier sursă, dar compilatorul nu știe asta. Compilatorul arată doar unu fișier sursă (și fișierele de antet incluse) la un moment dat.

Un punct și virgulă nevalid este o eroare de sintaxă, deci codul nu ar trebui să fie compilat. Acest lucru se poate întâmpla chiar și în implementarea unui șablon. În esență, există un pas de analiză și, deși pentru un om este evident cum să „repare și să restabilească”, compilatorul nu ar trebui să facă acest lucru. El nu poate doar „pretinde că există un colon pentru că asta ai vrut să spui” și să meargă mai departe.

Compilatorul caută definiții de funcție pe care să le apeleze acolo unde sunt necesare. Acest lucru nu este necesar aici, deci nu există nicio plângere. Nu există nicio eroare în acest fișier deoarece, chiar dacă ar fi necesar, nu ar putea fi implementat în acest bloc de compilare special. Componenta este responsabilă de colectarea diferitelor blocuri de compilare, adică de „legătura” între ele.

Compilatorul verifică dacă codul sursă este conform limbajului și aderă la semantica limbajului. Ieșirea compilatorului este cod obiect.

Linker leagă diverse module obiect împreună pentru a forma un exe. Definițiile funcțiilor sunt localizate în această etapă, iar codul adecvat pentru a le apela este adăugat în această etapă.

Compilatorul compilează codul în unități de traducere. Acesta va compila tot codul care este inclus în fișierul sursă .cpp.
DefinedIncorrectFunction() este definit în fișierul sursă, așa că compilatorul îl verifică pentru a vedea dacă este adevărat.
NonDefinedFunction() are o anumită definiție în fișierul sursă, deci compilatorul nu trebuie să o compileze, dacă definiția este prezentă într-un alt fișier sursă, funcția va fi compilată ca parte a acelei unități de traducere și mai târziu linkerul se va conecta la dacă în etapa de conectare definiția nu va fi găsită de către linker, atunci va arunca o eroare de legătură.

Spune că vrei să mănânci niște supă, așa că mergi la restaurant.

Cauți un meniu de supă. Dacă nu îl găsești în meniu, pleci din restaurant. (un fel ca compilatorul care se plânge că nu a putut găsi o funcție). Daca gasesti unul, ce faci?

Sunați chelnerul să ia supă. Totuși, doar pentru că este în meniu nu înseamnă că sunt și în bucătărie. Poate meniul este depășit, poate cineva a uitat să-i spună bucătarului că ar trebui să facă supă. Deci din nou pleci. (de exemplu, o eroare de la linker că nu a putut găsi simbolul)

Ah, dar puteți avea NonDefinedFunction(int) într-un alt bloc de compilare.

Compilatorul produce câteva date pentru linker, care în principiu spune următoarele (printre altele):

  • Ce simboluri (funcții/variabile/etc.) sunt definite.
  • Ce simboluri sunt specificate, dar nu sunt definite. În acest caz, linkerul trebuie să rezolve referințele căutând prin alte module înrudite. Dacă acest lucru nu este posibil, veți primi o eroare de linker.

Compilatorul trebuie să facă legătura cu codul definit (eventual) în module externe - biblioteci sau fișiere obiect pe care le veți folosi împreună cu acel fișier sursă special pentru a genera executabilul complet. Deci, dacă aveți o declarație, dar nu aveți o definiție, codul dvs. se va compila deoarece compilatorul știe că linkerul poate găsi codul lipsă în altă parte și îl poate face să funcționeze. Deci, în acest caz, veți primi o eroare de la linker și nu de la compilator.

Dacă, pe de altă parte, există o eroare de sintaxă în codul dvs., compilatorul nici măcar nu poate compila și veți primi o eroare în acest moment. Macro-urile și șabloanele se pot comporta diferit fără a provoca erori dacă nu sunt utilizate (șabloanele sunt aproximativ la fel ca macrocomenzile cu o interfață puțin mai frumoasă), dar acest lucru depinde și de gravitatea erorii. Dacă încurcăți atât de mult încât compilatorul nu își poate da seama unde se termină modelul wildcard/macro și începe codul normal, nu se va putea compila.

Când folosește cod obișnuit, compilatorul trebuie să compileze chiar și cod mort (codul nu este specificat în fișierul sursă), deoarece cineva ar putea dori să folosească acel cod dintr-un alt fișier sursă legând fișierul tău .o la codul său. Prin urmare, codul fără șablon/macro trebuie să fie corect din punct de vedere sintactic, chiar dacă nu este utilizat direct în același fișier sursă.