1. díl - Kompilace v jazyce C a C++

C a C++ Céčko Pokročilé konstrukce Kompilace v jazyce C a C++

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

Každý program napsaný v C (nebo v C++) musí být před spuštěním zkompilován. Základní průběh kompilace se pro jazyk C vůbec neliší od kompilace pro jazyk C++, proto budu dále mluvit o jazyce C, ale všechny informace jsou obecné a týkají se i C++. Protože jazyk C není jazykem interpretovaným (jako je například Java nebo C#), musí být před spuštěním program zkompilován přímo do binárního kódu, který procesor dokáže přímo dekódovat a spustit. Ukážeme si jednotlivé fáze kompilace a jak lze s nimi pracovat.

Pro dnešní díl nebudeme pracovat v žádném IDE, ale přímo s příkazovou řádkou. Postup bude obecně uveden pro Linux, ale může být spuštěn i ve Windows pomocí Cygwinu, který jste si instalovali na začátku seriálu o programování v C. Jednoduše najděte aplikaci Cygwin a spustěte ji. Výchozí adresář je zpravidla C:/cygwin/home/jme­noUctu/ popřípadě jiné umístění na základě místa, kam jste Cygwin nainstalovali. Zde budeme vkládat všechny soubory, se kterými budeme pracovat.

Pozn. autora: Při testování jsem se setkal s problémy v Cygwinu. Řešením bylo do cygwinu doinstalovat gcc-g++:GNU Compiller Collection (C++).

Preproccessor

Nejdříve si vytvoříme soubor, se kterým budeme pracovat. Pojmenujeme si ho main.c a bude vypadat následovně:

#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 velice přímočarý. Ačkoliv nedává žádný smysl, ukážeme si na něm právě průběh kompilace.

Nejdříve probíhá fáze preproccessingu. To je fáze, kdy kompilátor nahradí makra, naincluduje soubory, které jsme zvolili, a vůbec vyhodnotí všechny řádky, které začínají mřížkou (#). Pokud chceme spustit pouze fázi preprocessingu, přidáme přepínač -E do kompilace. Výsledek si můžete prohlédnout na obrázku:

Preproccessing v C

Vidíme, že na začátku zůstaly nějaké řádky s mřížkou na začátku. To pomáhá kompilátoru zpětně určit, z kama následující kód pochází. Důležité je, že dále žádný řádek s mřížkou nejsou. Podmínka se vyhodnotila a vytvořila nám globální proměnnou. Také vidíme, že se makro SIRKA nahradilo v kódu hodnotou a funkční makro SECTI se nahradilo operací, kterou jsme si zadefinovali.

Zmínil jsem, že preproccessor také nahradí všechny include obsahem souboru. Zkusíme si na začátek kódu přidat #include <stdlib.h> a spustit příkaz znovu. Tentokrát bude výpis obsahovat mnohonásobně více řádků, protože preproccessor nahradil include celým obsahem souboru. Ale na tento soubor se spustil preproccessor také. Preproccessor bude pracovat tak dlouho, dokud neodstraní všechny makra a includy. To také znamená, že lze preproccessor snadno zacyklit. Spustíme-li preproccessor na následující soubory, preproccessing skončí až ve chvíli, kdy kompilátor vyhodnotí zanoření jako příliš hluboké.

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

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

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

Od výše zmíněné situace nám pomáhají právě include guardy, které byl zmíněny v článku o makrech. Následující program proběhne preproccessingem bez problémů, přitom se každý soubor includuje právě jednou.

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

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

Překlad do objektových souborů

Pro fázi preproccessingu probíhá překlad souborů do souborů objektových. To jsou soubory, které již obsahují binární kód. Tento kód ale není spustitelný, protože nemá vyřešené závislosti na jiné části programu (například volání funkce, která je v jiném .c souboru). Nepředpokládám, že by zde někdo rozuměl binárnímu kódu, proto si zatím kód přeložíme pouze do jazyka symbolických adres - to uděláme přepínačem 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"

Pro nás je důležitá část od main. Nejdříve se provádí inicializace zásobníku a následně volání funkce __main. __main není naše funkce, ale funkce GCC, která se stará o přilinkování potřebných knihoven, které program používá. Poté následuje už samotné tělo naší funkce main.

Ještě je třeba poznamenat, že kód výše je přeložen pro 64 bitovou architekturu. Pro 32 bitovou architekturu by kód vypadal poněkud jinak:

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

Změn není mnoho, ale vidíme rozdílné názvy registrů a pár drobných změn. Ačkoliv se jazyk C považuje za multiplatformní, je multiplatformní právě díky tomu, že dokáže přeložit program pro více platforem. Výsledný spustitelný soubor je vždy vázán na konkrétní architekturu. Ten stejný program nespustíme na procesorech x86, x64 nebo ARM. Pro každou architekturu se program musí zkompilovat zvlášť.

Pořád jsme si ale neřekli, jak objektový soubor vytvořit. Přidáme pouze přepínač -c. Kompilátor poté vygeneruje soubor se stejným názvem, ale s koncovkou o. V tomto souboru je binárně zapsáno přesně to, co jsme si ukazovali výše v jazyku symbolických adres. Znovu upozorňuji, že tento soubor není spustitelný, pouze obsahuje implementaci jednotlivých funkcí ze zdrojového souboru.

Volání funkcí

Nyní se podíváme na to, jak vypadá volání funkce. Náš soubor main.c bude vypadat následovně:

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ásledně uvedu klíčové části pouze pro 32 bitovou architekturu.

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

Nyní se nad tím na chvíli zamyslíme. Vidíme, že se parametry nejprve uloží do zásobníku a poté se volá samotná funkce (tzv. předání hodnotou). To také znamená, že kompilátor nepotřebuje vědět, co funkce dělá. Když kompilátor ví, jaké parametry funkce přijímá a jakou zabírají část paměti (protože je C jazykem silně typovým, dokáže na zákaldě typu odvodit jeho velikost), poté samotnou implementaci v této části kódu ani nepotřebuje. Dále je (spíše z historických důvodů) určeno, že kompilátor zdrojový kód projde právě jednou a pouze shora dolů. Teď by mělo být jasné, proč lze rozdělit funkci na deklaraci a definici a proč musí být deklarace před samotným voláním funkce:

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 jde shora. Nejprve zjistí, že někde existuje nějaké funkce secti, která má dva parametry typu int a návratový typ int (ví kolik má vyhradit místa). Následně projde funkci main a zjistí, že se volá funkce secti. Zatím ani neví, jestli tato funkce skutečně existuje (jestli je implementovaná) a kde je. O to se ve skutečnosti nestará kompilátor, ale linker (ten bude probrán v dalším článku). Důležité je pro něj pouze to, kolik má vyhradit místa pro návratovou hodnotu a parametry. Teprve poté příjde k funkci secti a přeloží ji. Tentokrát je výstup v jazyce symbolických adres převrácený podle toho, v jakém pořadí jsme funkce 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"

Příště se podíváme na poslední část kompilace - linkování.


 

 

Článek pro vás napsal patrik.valkovic
Avatar
Jak se ti líbí článek?
2 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity (3)

 

 

Komentáře
Zobrazit starší komentáře (3)

Avatar
Vlasta Mozny
Člen
Avatar
Vlasta Mozny:22. ledna 17:41

super diky pomohlo

Odpovědět 22. ledna 17:41
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Avatar
Vlasta Mozny
Člen
Avatar
Vlasta Mozny:22. ledna 17:50

jen takova otazka jeste k tomu . to co udavam v 11 radku const int POCET = 10; by me zajimalo jestli je to predefinovany nebo je to nahodne cislo protoze me se zda ze kdyz tam dam jakekoliv cislo tak mi to to ukaze to cislo co tam napisu viz screen

Odpovědět 22. ledna 17:50
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Avatar
Odpovídá na Vlasta Mozny
Marek Chalupa:22. ledna 18:36

Můžeš to sem prosím tě hodit v tom kódu? Ten obrázek je téměř nečitelný.

 
Odpovědět 22. ledna 18:36
Avatar
Vlasta Mozny
Člen
Avatar
Odpovídá na Marek Chalupa
Vlasta Mozny:22. ledna 19:00
#include <iostream>
#include <string>
#include <ctime>
#include <stdlib.h>

using namespace std;

int main()
{
    static int celkem, pocet;
    const int POCET = 10;
    cout << "Hazim kostkou dokud mi nepadne " << POCET << "krat sestka..." <<
    endl;
    srand((unsigned) time(NULL));
    while (pocet < POCET) {
      celkem++;
      if ((rand()% 6 + 1))
      pocet++;
    }
    cout << "A je to!!! Hodu bylo celkem  " << celkem << endl;
    return 0;
}
Odpovědět 22. ledna 19:00
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Avatar
Odpovídá na Vlasta Mozny
Marek Chalupa:22. ledna 19:22

Ono ti to vždy vypíše tu hodnotu, co je uložená v konstantě, protože ten cyklus poběží tolikrát, kolikrát je uloženo v konstantě.

Jestli mě chápeš, nevím jak bych to měl jinak napsat.

 
Odpovědět 22. ledna 19:22
Avatar
Vlasta Mozny
Člen
Avatar
Vlasta Mozny:22. ledna 19:25

ano to mas pravdu v constante napises kolikrat chces aby se hodila sestka a v if ti to ma random spocitat kolikrat jsi hodil kostkou nez jsi dal tu sestku viz konstanta

Odpovědět 22. ledna 19:25
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Avatar
Vlasta Mozny
Člen
Avatar
Odpovídá na Vlasta Mozny
Vlasta Mozny:22. ledna 19:28

pokud teda neni chybny vzorecek

Odpovědět 22. ledna 19:28
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Avatar
Odpovídá na Vlasta Mozny
Marek Chalupa:22. ledna 19:54
#include <iostream>
#include <ctime>
#include  <stdlib.h>

using namespace std;

int main(){
    static int pocet_hodu, pocet_sestek, nahodne_cislo = 0;
    const int KOLIKRAT = 10;

    srand (time(NULL));

    cout << "Hazim, dokud nebude 6 hozena " << KOLIKRAT << " krat" << endl;

    do{
        nahodne_cislo = rand() % 6 + 1;
        if (nahodne_cislo == 6)
            pocet_sestek += 1;
        else
            pocet_hodu += 1;
    } while (pocet_sestek != KOLIKRAT);

    cout << "Sestka byla hozena " << pocet_sestek << " krat" << endl;
    cout << "Zabralo to " << pocet_hodu << " pokusu." << endl;

    return 0;
}

Kód jsem upravil, nyní by již měl fungovat...

 
Odpovědět  +1 22. ledna 19:54
Avatar
Vlasta Mozny
Člen
Avatar
Odpovídá na Marek Chalupa
Vlasta Mozny:22. ledna 20:07

Super tak tohle mi moc pomohlo mockrat dekuji :]

Odpovědět  +1 22. ledna 20:07
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Avatar
Vlasta Mozny
Člen
Avatar
Vlasta Mozny:22. ledna 20:29
#include <iostream>
#include <ctime>
#include  <stdlib.h>

using namespace std;

int main(){
    static int pocet_hodu, pocet_sestek, nahodne_cislo = 0;
    const int KOLIKRAT = 10;

    srand (time(NULL));
    cout << "                         Vitame vas v Hazardnich hrach\n";
    cout << "\n";
    cout << "Hazite, dokud nebude 6 hozena " << KOLIKRAT << " krat. Vyhrava ten kdo bude mit nejmin pokusu.\n" << endl;

    do{
        nahodne_cislo = rand() % 6 + 1;
        if (nahodne_cislo == 6)
            pocet_sestek += 1;
        else
            pocet_hodu += 1;
    } while (pocet_sestek != KOLIKRAT);

    cout << "                           Sestka byla hozena " << pocet_sestek << " krat\n" << endl;
    cout << "                        Zabralo to uctihodnych " << pocet_hodu << " pokusu." << endl;
    cout << "\n";
    cout << "\n";
    cout << "\n";
    cout << "                               Bye (O_O)  (^.^) Bye\n";
    cout << "\n";
    cout << "                     Dekujeme za pomoc s kodem Marku Chalupovy\n";
    return 0;
}
Editováno 22. ledna 20:30
Odpovědět 22. ledna 20:29
„Pokud chcete, aby vás měli za lháře, vždy říkejte pravdu.“
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 13. Zobrazit vše