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

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

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


 

  Aktivity (3)

Článek pro vás napsal patrik.valkovic
Avatar
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu.

Jak se ti líbí článek?
Celkem (2 hlasů) :
55555


 



 

 

Komentáře

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.

Zatím nikdo nevložil komentář - buď první!