GNU/Linux >> Znalost Linux >  >> Linux

Cesta programu C do spustitelného Linuxu ve 4 fázích

Napíšete program v C, použijete gcc k jeho kompilaci a získáte spustitelný soubor. Je to docela jednoduché. Správně?

Přemýšleli jste někdy o tom, co se stane během procesu kompilace a jak se program C převede na spustitelný soubor?

Existují čtyři hlavní fáze, kterými prochází zdrojový kód, aby se nakonec stal spustitelným souborem.

Čtyři fáze pro to, aby se program C stal spustitelným souborem, jsou následující:

  1. Předběžné zpracování
  2. Kompilace
  3. Sestavení
  4. Propojení

V části I této série článků probereme kroky, kterými kompilátor gcc prochází, když je zdrojový kód programu C kompilován do spustitelného souboru.

Než půjdeme dále, pojďme se rychle podívat na to, jak zkompilovat a spustit kód „C“ pomocí gcc na jednoduchém příkladu hello world.

$ vi print.c
#include <stdio.h>
#define STRING "Hello World"
int main(void)
{
/* Using a macro to print 'Hello World'*/
printf(STRING);
return 0;
}

Nyní spusťte kompilátor gcc přes tento zdrojový kód a vytvořte spustitelný soubor.

$ gcc -Wall print.c -o print

Ve výše uvedeném příkazu:

  • gcc – Vyvolá kompilátor GNU C
  • -Wall – příznak gcc, který povoluje všechna varování. -W znamená varování a "vše" předáváme -W.
  • print.c – Vstupní program C
  • -o print – Instruuje kompilátor C, aby vytvořil spustitelný soubor C jako tisk. Pokud nezadáte -o, ve výchozím nastavení kompilátor C vytvoří spustitelný soubor s názvem a.out

Nakonec spusťte print, který spustí program C a zobrazí hello world.

$ ./print
Hello World

Poznámka :Když pracujete na velkém projektu, který obsahuje několik programů v jazyce C, použijte nástroj make ke správě kompilace programu v jazyce C, jak jsme probrali dříve.

Nyní, když máme základní představu o tom, jak se gcc používá k převodu zdrojového kódu do binárního kódu, zopakujeme si 4 fáze, kterými musí program C projít, aby se stal spustitelným souborem.

1. PŘEDZPRACOVÁNÍ

Toto je úplně první fáze, kterou prochází zdrojový kód. V této fázi jsou provedeny následující úkoly:

  1. Nahrazení maker
  2. Komentáře jsou odstraněny
  3. Rozšíření zahrnutých souborů

Chcete-li předzpracování lépe porozumět, můžete zkompilovat výše uvedený program „print.c“ pomocí parametru -E, který vytiskne předzpracovaný výstup na stdout.

$ gcc -Wall -E print.c

Ještě lépe můžete použít příznak „-save-temps“, jak je uvedeno níže. Příznak '-save-temps' instruuje kompilátor, aby uložil dočasné přechodné soubory používané kompilátorem gcc do aktuálního adresáře.

$ gcc -Wall -save-temps print.c -o print

Když tedy zkompilujeme program print.c s příznakem -save-temps, získáme následující přechodné soubory v aktuálním adresáři (spolu se spustitelným souborem pro tisk)

$ ls
print.i
print.s
print.o

Předzpracovaný výstup je uložen v dočasném souboru s příponou .i (v tomto příkladu „print.i“)

Nyní otevřete soubor print.i a prohlédněte si obsah.

$ vi print.i
......
......
......
......
# 846 "/usr/include/stdio.h" 3 4
extern FILE *popen (__const char *__command, __const char *__modes) ;
extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__));

# 886 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));

# 916 "/usr/include/stdio.h" 3 4
# 2 "print.c" 2

int main(void)
{
printf("Hello World");
return 0;
}

Ve výše uvedeném výstupu můžete vidět, že zdrojový soubor je nyní naplněn spoustou a spoustou informací, ale stále na jeho konci vidíme řádky kódu, které jsme napsali. Pojďme nejprve analyzovat tyto řádky kódu.

  1. Prvním postřehem je, že argument funkce printf() nyní obsahuje přímo řetězec „Hello World“, nikoli makro. Ve skutečnosti definice a použití makra úplně zmizely. To dokazuje první úkol, že všechna makra jsou rozšířena ve fázi předběžného zpracování.
  2. Druhým postřehem je, že komentář, který jsme napsali v našem původním kódu, tam není. To dokazuje, že všechny komentáře jsou odstraněny.
  3. Třetím postřehem je, že vedle řádku „#include“ chybí a místo toho vidíme na jeho místě spoustu kódu. Je tedy bezpečné dojít k závěru, že stdio.h byl rozšířen a doslova zahrnut do našeho zdrojového souboru. Proto chápeme, jak je kompilátor schopen vidět deklaraci funkce printf().

Když jsem prohledal soubor print.i, našel jsem, Funkce printf je deklarována jako:

extern int printf (__const char *__restrict __format, ...);

Klíčové slovo ‚extern‘ říká, že funkce printf() zde není definována. Je externí k tomuto souboru. Později uvidíme, jak se gcc dostane k definici printf().

Můžete použít gdb k ladění vašich c programů. Nyní, když dobře rozumíme tomu, co se děje během fáze předběžného zpracování. pojďme k další fázi.

2. KOMPILOVÁNÍ

Poté, co je kompilátor dokončen s fází pre-procesoru. Dalším krokem je vzít print.i jako vstup, zkompilovat jej a vytvořit přechodný kompilovaný výstup. Výstupní soubor pro tuto fázi je „print.s“. Výstupem v print.s jsou pokyny na úrovni sestavení.

Otevřete soubor print.s v editoru a zobrazte obsah.

$ vi print.s
.file "print.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
movl $.LC0, %eax
movq %rax, %rdi
movl $0, %eax
call printf
movl $0, %eax
leave
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits

I když se moc nezabývám programováním na úrovni assembleru, letmý pohled dospěje k závěru, že tento výstup na úrovni sestavení je v nějaké formě instrukcí, kterým assembler může porozumět a převést je do jazyka na úrovni stroje.

3. MONTÁŽ

V této fázi se soubor print.s vezme jako vstup a vytvoří se mezisoubor print.o. Tento soubor je také známý jako objektový soubor.

Tento soubor vytváří assembler, který rozumí a převádí soubor „.s“ s pokyny pro sestavení na objektový soubor „.o“, který obsahuje pokyny na úrovni stroje. V této fázi je do strojového jazyka převeden pouze existující kód, volání funkcí jako printf() nejsou vyřešena.

Protože výstupem této fáze je soubor na úrovni stroje (print.o). Nemůžeme tedy zobrazit jeho obsah. Pokud se přesto pokusíte otevřít print.o a zobrazit jej, uvidíte něco, co není vůbec čitelné.

$ vi print.o
^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^A^@>^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@0^
^@UH<89>å¸^@^@^@^@H<89>ǸHello World^@^@GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3^@^
T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^[^L^G^H<90>^A^@^@^\^@^@]^@^@^@^@A^N^PC<86>^B^M^F
^@^@^@^@^@^@^@^@.symtab^@.strtab^@.shstrtab^@.rela.text^@.data^@.bss^@.rodata
^@.comment^@.note.GNU-stack^@.rela.eh_frame^@^@^@^@^@^@^@^@^@^@^@^
...
...
…

Jediná věc, kterou můžeme vysvětlit pohledem na soubor print.o, je o řetězci ELF.

ELF je zkratka pro spustitelný a propojitelný formát.

Toto je relativně nový formát pro soubory objektů a spustitelné soubory na úrovni stroje, které vytváří gcc. Předtím se používal formát známý jako a.out. O ELF se říká, že je propracovanější formát než a.out (formátu ELF bychom se mohli hlouběji věnovat v některém dalším budoucím článku).

Poznámka:Pokud zkompilujete svůj kód bez zadání názvu výstupního souboru, vytvořený výstupní soubor bude mít název „a.out“, ale formát se nyní změnil na ELF. Jde pouze o to, že výchozí název spustitelného souboru zůstává stejný.

4. PROPOJENÍ

Toto je poslední fáze, ve které je provedeno veškeré propojení volání funkcí s jejich definicemi. Jak již bylo zmíněno dříve, až do této fáze gcc neví o definici funkcí, jako je printf(). Dokud kompilátor přesně neví, kde jsou všechny tyto funkce implementovány, jednoduše použije pro volání funkce zástupný symbol. V této fázi je definice printf() vyřešena a skutečná adresa funkce printf() je zapojena.

Linker vstoupí do akce v této fázi a provede tento úkol.

Linker také dělá nějakou práci navíc; kombinuje do našeho programu nějaký extra kód, který je vyžadován při spuštění a ukončení programu. Například existuje kód, který je standardní pro nastavení běžícího prostředí, jako je předávání argumentů příkazového řádku, předávání proměnných prostředí každému programu. Podobně nějaký standardní kód, který je vyžadován pro vrácení návratové hodnoty programu do systému.

Výše uvedené úlohy překladače lze ověřit malým experimentem. Od této chvíle již víme, že linker převádí .o soubor (print.o) na spustitelný soubor (print).

Pokud tedy porovnáme velikosti souborů print.o a print souboru, uvidíme rozdíl.

$ size print.o
   text	   data	    bss	    dec	    hex	filename
     97	      0	      0	     97	     61	print.o 

$ size print
   text	   data	    bss	    dec	    hex	filename
   1181	    520	     16	   1717	    6b5	print

Prostřednictvím příkazu size získáme hrubou představu o tom, jak se zvětší velikost výstupního souboru z objektového souboru na spustitelný soubor. To vše kvůli extra standardnímu kódu, který linker kombinuje s naším programem.

Nyní víte, co se stane s programem C, než se stane spustitelným souborem. Víte o fázích předběžného zpracování, kompilace, sestavení a propojení Fáze propojování je mnohem více, o které se budeme věnovat v našem dalším článku v této sérii.


Linux
  1. Příklady příkazů awk v Linuxu

  2. Vložení ikony do spustitelného souboru Linux

  3. Zdá se, že gdb ignoruje spustitelné funkce

  1. Cheat pro příkazy Linuxu

  2. Příklady příkazů lpr v Linuxu

  3. Můžete získat nějaký program v Linuxu, který by vytiskl trasování zásobníku, pokud selže?

  1. Co používám na linuxu k vytvoření spustitelného programu python

  2. Instalace programu Python na Linux

  3. Rozložení paměti programu v linuxu