Като начинаещи в света на езиците за програмиране и компютрите, трябва да сме наясно, че компютрите не могат да „разберат“ нито един от езиците за програмиране, с които работим. Може да интерпретира само машинни езици (нула и единица). Компилаторът е това, което идва на помощ тук. Той „превежда“ езиците за програмиране на машинен език. Или казано по друг начин, той преобразува нашия изходен код в изпълним файл с инструкции за компютри.

GCC означава „GNU Compiler Collection“. GCC е интегрирана дистрибуция на компилатори за няколко основни езика за програмиране. Тези езици в момента включват C, C++, Objective-C, Objective-C++, Java, Fortran и Ada.

Нека преминем през процеса на компилиране на проста C програма и да разберем какво се случва зад кулисите.

Първо, напишете проста C програма, за да отпечатате „Hello World!“. Отварям редактора на vim (който е вграден в Linux), пиша програмата и я записвам като „HelloWorld.c“.

Сега имаме файл с име „HelloWorld.c“, който е написан на език C. Целта е системата да интерпретира C кода и да го преобразува в машинен език, който системата разбира. Има многобройни процеси, през които кодът преминава, за да постигне крайния резултат.

Предварителна обработка

Тези стъпки правят следното: премахване на коментари, разширяване на макроси, разширяване на включените файлове.

Редовете в нашия код, които започват със знака “#”, са директиви на предпроцесора. В нашата програма „HelloWorld.c“ първата директива на препроцесора (#include ‹stdio.h›) изисква стандартен заглавен файл, stdio.h, да бъде включен в нашия изходен файл. Ако използвате макроси във вашата програма, това е етапът, в който той се замества със съответната стойност. В нашата програма #define PRINTTHIS „Hello World\n“ е макросът и всички срещания на PRINTTHIS ще бъдат заменени със съответната стойност, тук от низа „Hello World\n“

Така че в етапа на препроцесора тези включени заглавни файлове и дефинирани макроси се разширяват и обединяват в изходния файл, за да се получи преходен изходен файл.

Чрез използването на флага „-E“ на gcc можем директно да извършим операцията по предварителна обработка.

[bash]$ gcc -E HelloWorld.c -o HelloWorldOutput

Забележете от горния изход на предварително обработения файл, че тъй като нашата програма е поискала заглавката stdio.h да бъде включена в нашия източник, което на свой ред е поискало цял куп други заглавни файлове.

Компилация

Следващата стъпка е да вземете предварително обработения файл като вход, да го компилирате и да създадете междинен компилиран изход. Изходният файл за този етап произвежда асемблиращ код, който зависи от машината.

Чрез използване на флаг “-S” с gcc можем да конвертираме предварително обработения C изходен код в асемблер без да създаваме обектен файл:

[bash]$ gcc -S HelloWorld.i -o HelloWorld.s

Въпреки че не съм много в програмирането на ниво асемблиране, но един бърз поглед заключава, че този изход на ниво асемблиране е под някаква форма на инструкции, които асемблерът може да разбере и да ги преобразува в език на машинно ниво.

Сглобяване

Както всички знаем, машините могат да разбират само двоичен език, така че сега се нуждаем от АСЕМБЛЕР, който преобразува асемблерния код във файла „HelloWorld.c“ в двоичен код.

ASSEMBLER беше един от първите софтуерни инструменти, разработен след изобретяването на цифровия компютър.

Ако има някакви извиквания на външни функции в асемблерния код, асемблерът оставя адресите на външните функции недефинирани, за да бъдат попълнени по-късно от линкера.

Асемблерът може да бъде извикан, както е показано по-долу. Чрез използване на флага “-c” в gcc можем да конвертираме асемблерния код в код на ниво машина:

[bash]$ gcc -c HelloWorld.c -o HelloWorld.o

Единственото нещо, което можем да обясним, като разгледаме файла HelloWorld.o, е за низа ELF в първия ред. ELF означава изпълним и свързваем формат.

Обектният файл и изпълнимият файл се предлагат в няколко формата като ELF (формат за изпълнение и свързване) и COFF (формат за общ обектен файл). Например ELF се използва на Linux системи, докато COFF се използва на Windows системи.

Това е сравнително нов формат за обектни файлове и изпълними файлове на ниво машина, които се произвеждат от gcc. Преди това се използва формат, известен като a.out. Твърди се, че ELF е по-сложен формат от a.out (може да се задълбочим във формата ELF в някоя друга бъдеща статия).

Ако компилирате кода си, без да посочите името на изходния файл, полученият изходен файл има име „a.out“, но форматът вече е променен на ELF. Просто името на изпълнимия файл по подразбиране остава същото.

Свързване

Това е последната фаза, в която се извършват всички свързвания на извиквания на функции с техните дефиниции. Linker знае къде са внедрени всички тези функции (Assembler е оставил адреса на всички външни функции, които да бъдат извикани). До този етап GCC не знае за функция като printf(). Асемблерът би оставил адреса на функциите, които трябва да бъдат извикани, а Linker извършва последния процес на попълване на тези адреси с действителните дефиниции. Линкерът изпълнява и няколко допълнителни задачи вместо нас. Той комбинира нашата програма с някои стандартни процедури, които са необходими, за да може програмата да работи. Така че крайният изпълним размер е много повече от входния файл!

Целият процес на свързване се обработва от gcc и се извиква, както следва:

[bash]$ gcc -o Output HelloWorld.c

Горната команда стартира файла „HelloWorld.c“ и създава крайния изпълним файл „Output“.

Както можете да видите, файлът „Изход“ по подразбиране е изпълним файл с разрешения -rwxrwxr-x, това просто означава, че има изпълнимо разрешение за всички потребители (собственик, група и други). Ако стартирате този изпълним файл, като просто напишете „./Output“, вие получавате крайния изход на нашата програма!

Така че сега знаем как C програма се преобразува в изпълним файл. Ще се потопим малко по-дълбоко в програмирането на C в следващите статии. Дотогава, Приятно учене! :)