1. diel - Kompilácie v jazyku C a C ++
Vitajte u prvej lekcie kurzu pokročilých konštrukcií jazyka C. Z kurzu Základná konštrukcia jazyka C už poznáme tie najzákladnejšie konštrukcie štruktúrovaného programovania az kurzu Dynamická práca s pamäťou v jazyku C vieme používať ukazovatele. Ako asi tušíte, jazyk C má okrem týchto 2 spomínaných kategórií ďalšie konštrukcie, ktoré by mal každý dobrý programátor poznať. Práve tým sa budeme venovať v tomto pokročilom nadväzujúcom kurze.
Kompilácie
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ý ste si inštalovali na začiatku seriálu o programovaní 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:
$ gcc -E main.c # 1 "main.c" # 1 "<built-in>" # 1 "<command-line>" # 1 "main.c" const int SIRKA_INT = 25; int main() { int a = 5; int b = 25; int c = a + b; int d = ((a) + (b)); return 0; }
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.
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úcej lekcii, Kompilácie v jazyku C a C ++ pokračovanie , sa pozrieme na poslednú časť kompilácie - linkovanie.