IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

15. diel - Kompilácie v jazyku C a C ++

Každý program napísaný v C (alebo v C ++) musí byť pred spustením skompilovaný. Základný priebeh kompilácie sa pre jazyk C vôbec nelíši od kompilácie pre jazyk C ++, preto budem ďalej hovoriť o jazyku C, ale všetky informácie sú všeobecné a týkajú sa aj C ++. Pretože jazyk C nie je jazykom interpretovaným (ako je napríklad Java alebo C #), musí byť pred spustením program skompilovaný priamo do binárneho kódu, ktorý procesor dokáže priamo dekódovať a spustiť. Ukážeme si jednotlivé fázy kompilácie a ako možno s nimi pracovať.

Pre dnešný diel nebudeme pracovať v žiadnom IDE, ale priamo s príkazovým riadkom. Postup bude všeobecne uvedený pre Linux, ale môže byť spustený aj vo Windows pomocou Cygwin, ktorý sa inštaloval na začiatku seriálu o C o programovanie v C. Jednoducho nájdite aplikáciu Cygwin a spustí ju. Predvolený adresár je spravidla C: / Cygwin / home / jmenoUctu / prípadne iné umiestnenie na základe miesta, kam ste Cygwin nainštalovali. Tu budeme vkladať všetky súbory, s ktorými budeme pracovať.

Pozn. autora: Pri testovaní som sa stretol s problémami v Cygwin. Riešením bolo do Cygwin doinštalovať gcc-g ++: GNU Compiller Collection (C ++).

Preproccessor

Najskôr si vytvoríme súbor, s ktorým budeme pracovať. Pomenujeme si ho main.ca bude vyzerať nasledovne:

#define SIRKA 25
#define SECTI(a,b) ((a)+(b))

#ifdef SIRKA
const int SIRKA_INT = SIRKA;
#endif


int main()
{
    int a = 5;
    int b = SIRKA;
    int c = a + b;
    int d = SECTI(a,b);
    return 0;
}

Program je veľmi priamočiary. Hoci nedáva žiadny zmysel, ukážeme si na ňom práve priebeh kompilácie.

Najskôr prebieha fáza preproccessingu. To je fáza, kedy kompilátor nahradí makrá, naincluduje súbory, ktoré sme zvolili, a vôbec vyhodnotí všetky riadky, ktoré začínajú mriežkou (#). Ak chceme spustiť len fázu preprocessing, pridáme prepínač -E do kompilácie. Výsledok si môžete pozrieť na obrázku:

Preproccessing v C - Pokročilé konštrukcia C ++

Vidíme, že na začiatku zostali nejaké riadky s mriežkou na začiatku. To pomáha kompilátora spätne určiť, z kama nasledujúci kód pochádza. Dôležité je, že ďalej žiadny riadok s mriežkou nie sú. Podmienka sa vyhodnotila a vytvorila nám globálnej premennej. Tiež vidíme, že sa makro šírky nahradilo v kóde hodnotou a funkčné makro Spočítajú sa nahradilo operácií, ktorú sme si zadefinovala.

Spomenul som, že preproccessor tiež nahradí všetky include obsahom súboru. Skúsime si na začiatok kódu pridať #include <stdlib.h> a spustiť príkaz znova. Tentoraz bude výpis obsahovať mnohonásobne viac riadkov, pretože preproccessor nahradil include celým obsahom súboru. Ale na tento súbor sa spustil preproccessor tiež. Preproccessor bude pracovať tak dlho, kým neodstráni všetky makrá a include. To tiež znamená, že možno preproccessor ľahko zacykliť. Spustíme Ak preproccessor na nasledujúce súbory, preproccessing skončí až vo chvíli, keď kompilátor vyhodnotí zanorenie ako príliš hlboké.

//first.h
#include "second.h"

//second.h
#include "first.h"

//příkaz
gcc -E first.h

Od vyššie zmienené situácie nám pomáhajú práve include guard, ktorej bol spomenuté v článku o makrách. Nasledujúci program prebehne preproccessingem bez problémov, pritom sa každý súbor includuje práve raz.

//first.h
#ifndef _FIRST_H_
#define _FIRST_H_
#include "second.h"
#endif

//second.h
#ifndef _SECOND_H_
#define _SECOND_H_
#include "first.h"
#endif

Preklad do objektových súborov

Pre fázu preproccessingu prebieha preklad súborov do súborov objektových. To sú súbory, ktoré už obsahujú binárny kód. Tento kód ale nie je spustiteľný, pretože nemá vyriešené závislosti na inej časti programu (napríklad volanie funkcie, ktorá je v inom .c súboru). Nepredpokladám, že by tu niekto rozumel binárnemu kódu, preto si zatiaľ kód preložíme iba do jazyka symbolických adries - to urobíme prepínačom S (gcc -S main.c).

    .file   "main.c"
    .globl  SIRKA_INT
    .section .rdata,"dr"
    .align 4
SIRKA_INT:
    .long   25
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    pushq   %rbp            ;inicializace zasobniku
    .seh_pushreg    %rbp        ;inicializace zasobniku
    movq    %rsp, %rbp      ;inicializace zasobniku
    .seh_setframe   %rbp, 0     ;inicializace zasobniku
    subq    $48, %rsp       ;inicializace zasobniku
    .seh_stackalloc 48      ;inicializace zasobniku
    .seh_endprologue        ;inicializace zasobniku
    call    __main          ;volání __main
    movl    $5, -4(%rbp)        ;začátek naší implementace
    movl    $25, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -12(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -16(%rbp)
    movl    $0, %eax
    addq    $48, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (GNU) 5.4.0"

Pre nás je dôležitá časť od main. Najskôr sa vykonáva inicializácia zásobníka a následne volanie funkcie __main. __main nie je naša funkcia, ale funkcia GCC, ktorá sa stará o přilinkování potrebných knižníc, ktoré program používa. Potom nasleduje už samotné telo našej funkcie main.

Ešte je potrebné poznamenať, že kód vyššie je prepočítaná pre 64 bitovú architektúru. Pre 32 bitovú architektúru by kód vyzeral trochu inak:

    .file   "main.c"
    .globl  _SIRKA_INT
    .section .rdata,"dr"
    .align 4
_SIRKA_INT:
    .long   25
    .def    ___main;    .scl    2;  .type   32; .endef
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $16, %esp
    call    ___main
    movl    $5, 12(%esp)
    movl    $25, 8(%esp)
    movl    12(%esp), %edx
    movl    8(%esp), %eax
    addl    %edx, %eax
    movl    %eax, 4(%esp)
    movl    12(%esp), %edx
    movl    8(%esp), %eax
    addl    %edx, %eax
    movl    %eax, (%esp)
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE0:
    .ident  "GCC: (GNU) 5.4.0"

Zmien nie je veľa, ale vidíme rozdielne názvy registrov a pár drobných zmien. Hoci sa jazyk C považuje za multiplatformový, je multiplatformový práve vďaka tomu, že dokáže preložiť program pre viac platforiem. Výsledný spustiteľný súbor je vždy viazaný na konkrétnu architektúru. Ten rovnaký program nespustíte na procesoroch x86, x64 alebo ARM. Pre každú architektúru sa program musí skompilovať zvlášť.

Stále sme si však nepovedali, ako objektový súbor vytvoriť. Pridáme iba prepínač -c. Kompilátor potom vygeneruje súbor s rovnakým názvom, ale s koncovkou o. V tomto súbore je binárne zapísané presne to, čo sme si ukazovali vyššie v jazyku symbolických adries. Znovu upozorňujem, že tento súbor nie je spustiteľný, iba obsahuje implementáciu jednotlivých funkcií zo zdrojového súboru.

gcc -c main.c -o main.o

Volanie funkcií

Teraz sa pozrieme na to, ako vyzerá volanie funkcie. Náš súbor main.c bude vyzerať nasledovne:

int secti(int a, int b)
{
    return a+b;
}

int main()
{
    int a = 5;
    int b = 6;
    int c = secti(a,b);
    return 0;
}

Následne uvediem kľúčové časti iba pre 32 bitovú architektúru.

0: ; 32-bitova architektura
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

Teraz sa nad tým na chvíľu zamyslíme. Vidíme, že sa parametre najprv uloží do zásobníka a potom sa volá samotná funkcia (tzv. Odovzdanie hodnotou). To tiež znamená, že kompilátor nepotrebuje vedieť, čo funkcia robí. Keď kompilátor vie, aké parametre funkcie prijíma a akú zaberajú časť pamäte (pretože je C jazykom silne typovým, dokáže na zákaldě typu odvodiť jeho veľkosť), potom samotnú implementáciu v tejto časti kódu ani nepotrebuje. Ďalej je (skôr z historických dôvodov) určené, že kompilátor zdrojový kód prejde práve raz a len zhora nadol. Teraz by malo byť jasné, prečo možno rozdeliť funkciu na deklaráciu a definíciu a prečo musí byť deklarácia pred samotným volaním funkcie:

int secti(int,int);

int main()
{
    int a = 5;
    int b = 6;
    int c = secti(a,b);
    return 0;
}

int secti(int a, int b)
{
    return a+b;
}

Kompilátor ide zhora. Najprv zistí, že niekde existuje nejaké funkcie sčítame, ktorá má dva parametre typu int a návratový typ int (vie koľko má vyhradiť miesta). Následne prejde funkciu main a zistí, že sa volá funkcia spočítajú. Zatiaľ ani nevie, či táto funkcia skutočne existuje (či je implementovaná) a kde je. O to sa v skutočnosti nestará kompilátor, ale linker (ten bude prebraný v ďalšom článku). Dôležité je pre neho len to, koľko má vyhradiť miesta pre návratovú hodnotu a parametre. Až potom príde k funkcii sčítame a preloží ju. Tentoraz je výstup v jazyku symbolických adries prevrátený podľa toho, v akom poradí sme funkcie definovali:

    .file   "main.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    ___main
    movl    $5, 28(%esp)
    movl    $6, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    _secti
    movl    %eax, 20(%esp)
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE0:
    .globl  _secti
    .def    _secti; .scl    2;  .type   32; .endef
_secti:
LFB1:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    movl    8(%ebp), %edx
    movl    12(%ebp), %eax
    addl    %edx, %eax
    popl    %ebp
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE1:
    .ident  "GCC: (GNU) 5.4.0"

V budúcom diele, Kompilácie v jazyku C a C ++ pokračovanie , budeme pokračovať s posledná časť kompilácie - linkovanie.


 

Predchádzajúci článok
Šablóny - pokračovanie
Všetky články v sekcii
Pokročilé konštrukcia C ++
Preskočiť článok
(neodporúčame)
Kompilácie v jazyku C a C ++ pokračovanie
Článok pre vás napísal Patrik Valkovič
Avatar
Užívateľské hodnotenie:
1 hlasov
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity