IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

2. diel - Kompilácie v jazyku C a C ++ pokračovanie

V minulej lekcii, Kompilácie v jazyku C a C ++ , sme si povedali o preproccessingu a kompiláciu do objektových súborov. V dnešnom C tutoriálu 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.

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.h
#include "superFunkce.h"
int vynasob(int a,int b)
{
    return funkce(a,b,'*');
}

//soucet.h
#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.

Touto lekcií 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ť. V budúcej lekcii, Pokročilé cykly v jazyku C , prejdeme späť na niečo praktického.


 

Predchádzajúci článok
Kompilácie v jazyku C a C ++
Všetky články v sekcii
Pokročilé konštrukcia jazyka C
Preskočiť článok
(neodporúčame)
Pokročilé cykly v jazyku C
Článok pre vás napísal Patrik Valkovič
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity