Компиляция C файлов с помощью gcc, шаг за шагом
Чтобы объяснить все этапы компиляции, нам нужно предварительно прояснить несколько концепций программирования. В этой статье мы расскажем о том, что такое язык Си, как его скомпилировать с помощью такого инструмента, как gcc, и что происходит при его компиляции.
Язык программирования Си
Все программное обеспечение, программы, веб-сайты и приложения написаны на определенном языке программирования. По сути, все, что мы видим на экране наших компьютеров или смартфонов, - это просто набор кода, написанного на разных языках и собранного определенным образом. Каждый язык программирования используется по-разному, и сегодня мы сосредоточимся на C.
Си - это язык программирования, изобретенный Деннисом Ритчи и впервые появившийся в 1972 году. Это то, что мы называем языком низкого уровня, что означает, что между Си и машинным языком существует лишь небольшая абстракция, поэтому его можно считать более близким к аппаратному обеспечению компьютера. C также является компилируемым языком, в отличие от интерпретируемого, что означает, что исходные файлы, написанные на C, должны быть скомпилированы, чтобы они могли быть исполняемыми.
Инструменты
Прежде всего, давайте поговорим об инструментах, которые мы будем использовать в нашем примере. Мы будем работать с Unix-подобной операционной системой, поэтому примеры могут отличаться от Windows. Нам нужен доступ к командной оболочке, которая представляет собой программу, которая принимает команды с клавиатуры и передает их операционной системе для выполнения, согласно linuxcommand.org. Для этого нам нужен терминал или эмулятор терминала, который представляет собой просто окно, позволяющее нам взаимодействовать с командной оболочкой. Внутри терминала мы должны увидеть приглашение командной строки. Мы можем вводить команды после начального символа в том, что мы называем командной строкой. Нам также нужен текстовый редактор, например vim или любой другой (блокнот, TextEdit, Gedit и другие), для создания исходного файла.
Компиляция
Компиляция - это перевод исходного кода (кода, который мы пишем) в объектный код (последовательность инструкций на машинном языке) компилятором.
Процесс компиляции состоит из четырех различных этапов:
- Предварительная обработка (препроцессор)
- Компиляция (компилятор)
- Сборка
- Линковка
В качестве примера мы будем использовать компилятор gcc, который расшифровывается как GNU Compiler Collection. Проект GNU - это проект свободного программного обеспечения и массового сотрудничества, запущенный Ричардом Столлманом в 1983 году, позволяющий разработчикам бесплатно получать доступ к мощным инструментам.
Gcc поддерживает различные языки программирования, включая C, полностью бесплатен и является универсальным компилятором для большинства Unix-подобных операционных систем. Чтобы использовать его, мы должны убедиться, что устанавливаем его на свой компьютер, если его там еще нет.
Как правило GCC установлен в MacOS и Linux, однако некоторые дистрибутивы его исключают из пакета предустановленных программ, вы можете установить в Linux систему с помощью пакетного менеджера вашего дистрибутива, для Debian систем: sudo apt-get install gcc
Исходный код
Для нашего примера давайте посмотрим на исходный код внутри файла с именем main.c, где .c - это расширение файла, которое обычно означает, что файл написан на C. Исходный код простой программы:
#include <stdio.h>
/*
* main - точка входа в приложение
*
* возвращает 0 если программа завершилась без ошибки
*/
int main() {
printf("Привет мир!\n");
return 0;
}
Директива препроцессора #include, указывает компилятору подключить заголовочный файл библиотеки stdio.h, но мы вернемся к нему позже.
Далее идут комментарии к коду, они полезны для запоминания того, что на самом деле делает ваш код спустя месяцы после его создания. На самом деле они нам не нужны в такой маленькой программе, но это хорошая практика.
Далее у нас есть точка входа, функция main(). Это означает, что программа запустится с выполнения инструкций, которые находятся внутри блока этой функции, заключенного в фигурные скобки. Здесь есть только две инструкции: одна, которая выводит предложение Привет мир! на экран, и другая, которая сообщает программе вернуть 0, если она завершила работу правильно. Итак, как только мы скомпилируем его, если мы запустим эту программу, то увидим только фразу Привет мир!.
Чтобы наш код main.c стал исполняемым, нам нужно ввести команду gcc main.c, и процесс компиляции пройдет все четыре этапа, которые он содержит. Конечно, в gcc есть опции, которые позволяют нам останавливать процесс компиляции после каждого шага. Давайте взглянем на них.
Шаги процесса компиляции
1. Препроцессор
Препроцессор выполняет несколько функций:
- удаляет все комментарии в исходных файлах
- он включает в себя код заголовочных файлов, которые представляют собой файлы с расширением .h, содержащий объявления функций C и определения макросов
- заменяет все макросы (фрагменты кода, которым было присвоено имя) на их значения
Выходные данные этого шага будут сохранены в файле с расширением .i, поэтому здесь они будут в main.i.
Чтобы остановить компиляцию сразу после этого шага, мы можем использовать опцию -E с командой gcc для исходного файла и нажать enter.
gcc -E main.c
Вот как должен выглядеть конец файла main.i:
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, va_list);
# 417 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h" 2 3 4
# 2 "main.c" 2
int main(void) {
printf("Привет мир!\n");
return 0;
}
2. Компилятор
Компилятор возьмет предварительно обработанный файл и сгенерирует IR-код (промежуточное представление), в результате чего будет создан файл .s. При этом другие компиляторы могут создавать ассемблерный код на этом этапе компиляции.
После этого шага мы можем остановиться, выбрав опцию -S в команде gcc, и нажать enter.
gcc -S main.c
Вот как должен выглядеть файл main.s:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 12, 0 sdk_version 13, 1
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Привет мир!\n"
.subsections_via_symbols
3. Сборщик
Ассемблер берет IR-код и преобразует его в объектный код, который является кодом на машинном языке (т.е. двоичным). В результате будет создан файл, оканчивающийся на .o.
Мы можем остановить процесс компиляции после этого шага, используя опцию -c с помощью команды gcc и нажав enter.
gcc -c main.c
Наш файл main.o должен выглядеть следующим образом (да, он не читается человеком):
Ïúíþ^G^@^@^A^C^@^@^@^A^@^@^@^D^@^@^@^H^B^@^@^@ ^@^@^@^@^@^@^Y^
@^@^@<88>^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^
@^@^@^@^@^@^@^@<98>^@^@^@^@^@^@^@(^B^@^@^@^@^@^
@<98>^@^@^@^@^@^@^@^G^@^@^@^G^@^@^@^D^@^@^@^@^@^
@^@__text^@^@^@^@^@^@^@^@^@^@__TEXT^@^@^@^@^@^@^@^
@^@^@^@^@^@^@^@^@^@^@%^@^@^@^@^@^@^@(^B^@^@^D^@
^@^@À^B^@^@^B^@^@^@^@^D^@<80>^@^@^@^@^@^@^@^@^@^
@^@^@__cstring^@^@^@^@^@^@^@__TEXT^@^@^@^@^@^@^@^@^
@^@%^@^@^@^@^@^@^@^N^@^@^@^@^@^@^@M^B^@^@^@^@^
@^@^@^@^@^@^@^@^@^@^B^@^@^@^@^@^@^@^@^@^@^@^@^
@^@^@__compact_unwind__LD^@^@^@^@^@^@^@^@^@^@^@^@8^
@^@^@^@^@^@^@ ^@^@^@^@^@^@^@`^B^@^@^C^@^@^@Ð^B^@^@^A^@^@^@^@^
@^@^B^@^@^@^@^@^@^@^@^@^@^@^@__eh_frame^@^@^@^@^
@^@__TEXT^@^@^@^@^@^@^@^@^@^@X^@^@^@^@^@^@^@@^
@^@^@^@^@^@^@<80>^B^@^@^C^@^@^@^@^@^@^@^@^@^@^
@^K^@^@h^@^@^@^@^@^@^@^@^@^@^@^@2^@^@^@^X^@^@^
@^A^@^@^@^@^@^L^@^@^A^M^@^@^@^@^@^B^@^@^@^X^@^
@^@Ø^B^@^@^B^@^@^@ø^B^@^@^P^@^@^@^K^@^@^@P^@^@^
@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^A^@^@^@^A^@^
@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^
@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^
@^@^@^@^@^@UH<89>åH<83>ì^PÇEü^@^@^@^@H<8d>=^O^@^@^
@°^@è^@^@^@^@1ÀH<83>Ä^P]ÃПривет Мир!
^@^@^@^@^@^@^@^@^@^@^@^@^@^@%^@^@^@^@^@^@^A^
@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^T^@^@^@^@^@^
@^@^AzR^@^Ax^P^A^P^L^G^H<90>^A^@^@$^@^@^@^\^@^@^@
<88>ÿÿÿÿÿÿÿ%^@^@^@^@^@^@^@^@A^N^P<86>^BC^M^F^@^@^@
^@^@^@^@^Y^@^@^@^A^@^@-^R^@^@^@^B^@^@^U^@^@^@^@^A^@^@^F^A^@^@^@^O^A^@
^@^@^@^@^@^@^@^@^@^G^@^@^@^A^@^@^@^@^@^@^@^@
^@^@^@^@_main^@_printf^@^@
4. Компоновщик (linker)
Компоновщик создает конечный исполняемый файл в двоичном формате и может играть две роли:
- связывание всех исходных файлов вместе, то есть всех остальных объектных кодов в проекте. Например, если я хочу скомпилировать main.c с другим файлом, называемым secondary.c, и объединить их в одну программу, на этом этапе объектный код secondary.c (то есть secondary.o) будет связан с объектным кодом main.c (main.o).
- связывание вызовов функций с их определениями. Компоновщик знает, где искать определения функций в статических или динамических библиотеках. Статические библиотеки - это результат того, что компоновщик копирует все используемые библиотечные функции в исполняемый файл, а динамические библиотеки не требуют копирования кода, это делается простым помещением имени библиотеки в двоичный файл. Обратите внимание, что gcc по умолчанию использует динамические библиотеки. В нашем примере это когда компоновщик находит определение нашей функции printf и связывает ее.
По умолчанию, после этого четвертого и последнего шага, то есть когда вы вводите всю команду gcc main.c без каких-либо опций, компилятор создаст исполняемую программу с именем a.out, которую мы можем запустить, набрав ./a.out в командной строке.
Мы также можем выбрать создание исполняемой программы с нужным нам именем, добавив опцию -o в команду gcc, расположенную после имени файла или файлов, которые мы компилируем, и нажав enter:
gcc main.c -o program
Итак, теперь мы можем либо ввести ./a.out, если вы не использовали опцию -o, либо ./program для выполнения скомпилированного кода, на выходе будет Привет мир!, и после этого снова появится запрос командной строки.
Комментарии
Для того чтобы оставить свое мнение, необходимо зарегистрироваться на сайте