16. diel - Kompilácie v jazyku C a C ++ pokračovanie
V minulom diele, Kompilácie v jazyku C a C ++ , sme si povedali o preproccessingu a kompiláciu do objektových súborov. V dnešnom diele sa pozrieme na linkovanie a povieme si, prečo C používa hlavičkové a implementačnej súbory.
Linkovanie
Pri kompilovanie do objektových súborov sme si povedali, že kompilátor nevie, či volaní funkcie existuje a kde je. Použitie správnych adries v pamäti riešia až linker. Vo finálnom spustiteľnom súboru už nefigurujú žiadne mená funkcií. Funkcia začína na nejakom mieste v pamäti (offset), od ktorého sa začnú inštrukcie vykonávať do tej doby, kým procesor nenarazí na inštrukciu peru. Tá vezme posledné záznam v zásobníku a vráti sa na miesto v pamäti určenej vyzdvihnutý hodnotou - tak ako sme to videli v minulom diele. Pre pripomenutie prikladám ešte raz zdrojové súbory z minulej lekcie.
int secti(int a, int b) { return a+b; } int main() { int a = 5; int b = 6; int c = secti(a,b); return 0; }
Vygeneruje nasledujúci kód:
1: _secti: 2: pushl %ebp //uložení registru 3: movl %esp, %ebp //uložení aktuální místo na zásobníku do registru ebp 4: movl 8(%ebp), %edx //načte ze zásobníku číslo 5 5: movl 12(%ebp), %eax //načte ze zásobníku číslo 6 6: addl %edx, %eax //sečte čísla 7: popl %ebp //obnovení registru se začátku funkce 8: ret //návrat z funkce 9: _main: 10: movl $5, 28(%esp) //načtení hodnoty 5 do zásobníku na 28. bajt 11: movl $6, 24(%esp) //načtení hodnoty 6 do zásobníku na 24. bajt 12: movl 24(%esp), %eax //přesun honoty 6 do registru EAX 13: movl %eax, 4(%esp) //přesun hodnoty z registru EAX do zásobníku na 4. bajt 14: movl 28(%esp), %eax //přesun hodnoty 5 do registru EAX 15: movl %eax, (%esp) //přesun hodnoty z registru EAX do zasobníku na 0. bajt 16: call _secti //volání funkce, uloží do zásobníku aktuální místo v programu 17: movl %eax, 20(%esp) //načtení do zásobníku na 20. bajtu hodnotu z registru EAX 18: movl $0, %eax //načtení hodnoty 0 do registru EAX 19: leave //ukončení programu
V zdrojovom kóde teda nie sú žiadne označenie ako _main a _secti, ale iba adresy v pamäti. Úlohou linker je, aby príkaz call _secti nahradil volaním miesta v pamäti. Ak budú čísla (ktoré pôvodne označovali číslo riadku) teraz označovať (iba zjednodušenia, inštrukcie môžu byť rôzne dlhé) miesto v pamäti, kde je funkcia umiestnená, bude kód vyzerať nasledovne:
1: 2: pushl %ebp //uložení registru 3: movl %esp, %ebp //uložení aktuální místo na zásobníku do registru ebp 4: movl 8(%ebp), %edx //načte ze zásobníku číslo 5 5: movl 12(%ebp), %eax //načte ze zásobníku číslo 6 6: addl %edx, %eax //sečte čísla 7: popl %ebp //obnovení registru se začátku funkce 8: ret //návrat z funkce 9: 10: movl $5, 28(%esp) //načtení hodnoty 5 do zásobníku na 28. bajt 11: movl $6, 24(%esp) //načtení hodnoty 6 do zásobníku na 24. bajt 12: movl 24(%esp), %eax //přesun honoty 6 do registru EAX 13: movl %eax, 4(%esp) //přesun hodnoty z registru EAX do zásobníku na 4. bajt 14: movl 28(%esp), %eax //přesun hodnoty 5 do registru EAX 15: movl %eax, (%esp) //přesun hodnoty z registru EAX do zasobníku na 0. bajt 16: call 0x2 //volání funkce na základě její adresy 17: movl %eax, 20(%esp) //načtení do zásobníku na 20. bajtu hodnotu z registru EAX 18: movl $0, %eax //načtení hodnoty 0 do registru EAX 19: leave //ukončení programu
Problém je, že objektové súbory môžeme spájať aj medzi sebou a tým sa bude offset meniť. Úlohou linker je offsety prepočítať tak, aby volaná adresa zodpovedala umiestnenie funkcie.
Operácie linkovanie
Vytvoríme si niekoľko súborov, na ktorých predvedieme operáciu linkovanie (pre jednoduchosť vynechávam include Guard).
//soucet.h int secti(int,int); //soucet.c #include "soucet.h" int secti(int a,int b) { return a+b; } //soucin.h int vynasob(int,int); //soucin.c #include "soucin.h" int vynasob(int a,int b) { return a*b; } //main.c #include "soucet.h" #include "soucin.h" int main() { int a = 5; int b = 6; secti(a,b); vynasob(a,b); }
Teraz všetky súbory skompilujeme do objektových súborov. Potom spustíme linker a spojíme objektové súbory súčinu a súčtu do objektového súboru lib.o. Poslednou fázou je vytvorenie spustiteľného súboru.
gcc -c main.c //objektový soubor main.o gcc -c soucet.c //objektový soubor soucet.o gcc -c soucin.c //objektový soubor soucin.o ld -r -o lib.o soucet.o soucin.o //spuštění linkeru // -r znamená vytvořit opět objektový soubor // -o určuje výstupní soubor gcc -o program.exe main.o lib.o //vytvoření spustitelného souboru //interně se volá opět linker ale s dalšími parametry
A k čomu nám to vlastne všetko je? Kompilácia je operácia veľmi náročná na výpočtový čas. Ak by sme pri každej zmene museli skompilovať celý program, väčšinu času by sme len čakali. Vďaka objektovým súborom máme už niektoré súbory skompilované, a tak je nemusíme kompilovať znova. Napríklad keby sme niečo zmenili v súbore main.c, stačí nám len spustiť prvý a posledný príkaz a získame spustiteľný súbor. Zvyšné dva súbory sa vôbec kompilovať nemusí a ušetríme čas.
Hlavičkové súbory
Po téme linkovanie by už malo byť jasné, prečo sa v C používajú hlavičkové (obsahujú deklaráciu) a implementačné (obsahujú implementáciu) súbory. Z implementačných súborov môžeme vytvoriť súbory objektové a tým ušetriť výkon. Kompilovať hlavičkové súbory do objektových by nedávalo zmysel. Iba hovoria, že niekde existuje určitá funkcia (prípadne štruktúra, trieda a pod.), Ale ďalej o nej nič nehovoria. Po skompilovaniu by súbor neobsahoval žiadny spustiteľný kód. Zároveň ale zvyšné časti aplikácie (ostatné .c / .cpp súbory) musí vedieť, čo je v iných implementačných súboroch obsiahnuté - práve túto informáciu poskytujú hlavičkové súbory.
Na druhú stranu v prípade šablón ešte nie sú známe typy. Ak teda chceme využiť externé šablónu (napríklad z knižnice), musíme mať v čase kompilácie ako deklaráciu, tak implementáciu (aby sa mohla vygenerovať skutočná implementácia pre náš typ). Z toho dôvodu musí byť celé šablóny v hlavičkových súboroch.
Implementácia v hlavičkových súboroch
Ukážme si jednoduchý príklad, prečo by nemala byť implementácia v hlavičkových súboroch:
//superFunkce.h #ifndef __SUPERFUNKCE_H_ #define __SUPERFUNKCE_H_ int funkce(int a,int b,char operace) { switch(operace) { case '*': return a*b; case '+': return a+b; } } #endif //soucin.c #include "superFunkce.h" int vynasob(int a,int b) { return funkce(a,b,'*'); } //soucet.c #include "superFunkce.h" int secti(int a,int b) { return funkce(a,b,'+'); }
Teraz vykonáme postupnou kompiláciu, ako sme si ju ukázali.
$ gcc -c soucet.c $ gcc -c soucin.c $ ld -r -o lib.o soucin.o soucet.o soucet.o:soucet.c:(.text+0x0): multiple definition of `funkce' soucin.o:soucin.c:(.text+0x0): first defined here
Jednotlivé kompilácie do objektových súborov prešli v poriadku, ale ich linkovanie neprešlo. Nepomohli nám ani include guard. Pre prvé dva príkazy sa vždy include nahradilo obsahom súboru, ktorý v tejto chvíli už obsahoval implementáciu. Pri pokuse o spojenie linker zistil, že funkcia je nadefinovaná dvakrát a linkovanie zastavil. Niekoho možno napadlo, že aj pri presune implementácie do implementačných súborov môže nastať chyba. Chyba nastane vo chvíli, keď jeden súbor linkuje dvakrát. Súbor superFunkce.h upravíme nasledovne:
//superFunkce.h #ifndef __SUPERFUNKCE_H_ #define __SUPERFUNKCE_H_ int funkce(int a,int b,char operace); #endif //superFunkce.c #include "superFunkce.h" int funkce(int a,int b,char operace) { switch(operace) { case '*': return a*b; case '+': return a+b; } }
a teraz skúsime najskôr linkovať našu funkciu s operáciami, a až potom ich spojiť:
$ gcc -c superFunkce.c $ gcc -c soucet.c $ gcc -c soucin.c $ ld -r -o soucetLink.o soucet.o superFunkce.o $ ld -r -o soucinLink.o soucin.o superFunkce.o $ ld -r -o lib.o soucinLink.o soucetLink.o soucetLink.o:superFunkce.c:(.text+0x24): multiple definition of `funkce' soucinLink.o:superFunkce.c:(.text+0x24): first defined here
Vidíme, že máme ten rovnaký problém. Našťastie moderné IDE už kompiláciu zvládajú (vrátane objektových súborov) a tak dokážu sami vyhodnotiť, čo je potrebné skompilovať a ako finálny program linkovať. Dôležité je uvedomiť si, že implementácia patrí do implementačných súborov (.c pre C alebo .cpp pre C ++) a deklarácia patrí do súborov hlavičkových (.h pre C a .hpp pre C ++). Tiež je potrebné nezabúdať na include guard.
Ako by teda mala kompilácie správne prebehnúť?
$ gcc -c superFunkce.c $ gcc -c soucet.c $ gcc -c soucin.c $ ld -r -o lib.o superFunkce.o soucet.o soucin.o
Týmto dielom máme dokončenú celú teóriu ohľadom kompilácie programu. Od samotných zdrojových kódov až po spustiteľný súbor. Všetky tieto operácie spravidla obstaráva IDE, ale znalosť týchto postupov pomáha písať správny kód, ktorý nebude problém rozširovať a spravovať. Nabudúce si už vytvoríme vlastnú knižnicu.