Ах. Компилятор. Таинственное устройство, которое берет исходный код и волшебным образом выдает исполняемый код. Компилятор фактически работает в четыре этапа: предварительная обработка, компиляция, сборка и компоновка.

У вас есть исходный файл с именем main.c — давайте посмотрим, что произойдет, когда вы запустите gcc main.c с компилятором GNU Compiler Collection (GCC).

main.c:

/*
 * Prints: "Hello, world" 
 */
#include <stdio.h>
int main(void)
{
   printf("Hello world\n");
   return (0);
}

Что такое ГЦК?

GCC — это система компилятора, созданная проектом GNU, которая компилирует многие языки программирования: C, C++, Fortran, Java и другие. Для каждого из этих языков GCC содержит и использует отдельную программу для каждого из языков, для которых он используется. До сегодняшнего дня GCC все еще совершенствуется и поддерживается многими различными группами программистов по всему миру. GCC также был модифицирован для работы на более чем 60 платформах и принят в качестве основного компилятора для разработки операционных систем, таких как Linux и MacOS.

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

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

Препроцессор C выполняет несколько задач с исходным кодом:

  1. Он ищет файлы заголовков и включает их в наш исходный код.
  2. Если были определены какие-либо макросы, препроцессор заменит макросы их определением во всей программе.
  3. Включать и исключать части программы, например, комментарии, в соответствии с определенными условиями

После этих шагов создается промежуточный файл, который передается на следующий шаг. Если вы хотите получить промежуточный файл, в GCC есть удобная опция для этой цели. Только с опцией -E компилятор выведет содержимое на экран, но мы можем захватить вывод с помощью опции -o и указав имя файла с расширением .i.

Вот несколько последних строк нашего промежуточного файла main.c:

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 943 “/usr/include/stdio.h” 3 4
# 2 “main.c” 2
int main(void)
{
printf(“Hello world\n”);
return (0);
}

Подборка

Затем промежуточный файл (main.i) преобразуется в ассемблерный код, удобочитаемую версию машинного кода. Язык ассемблера — это язык программирования низкого уровня, и каждый оператор программирования близко соответствует реальным инструкциям машинного кода.

Мы можем заставить компилятор остановиться после этапа компиляции для получения файла с ассемблерным кодом, используя опцию -S.

Вот последние несколько строк нашего ассемблерного кода main.c:

call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident “GCC: (Ubuntu 4.8.4–2ubuntu1~14.04.4) 4.8.4”
.section .note.GNU-stack,””,@progbits

Сборка

Наш компьютер может интерпретировать только двоичный код, поэтому задача ассемблера состоит в том, чтобы преобразовать файл main.s в другой файл с объектным кодом. Объектный код — это машинный код, который еще не был связан. Файл, содержащий машинный код, нечитаем, и его могут интерпретировать только машины.

Опять же, мы можем остановить наш компилятор для извлечения нашего объектного кода, используя параметр -c.

Связывание

Полученный файл main.s представляет собой более-менее машинный код, но ассемблеру не удалось правильно расположить код в правильном порядке. Если код использует функции из другой библиотеки (printf), то компоновщик свяжет код с кодом библиотеки (статическая или динамическая компоновка, решает компилятор). Все файлы будут связаны вместе как исполняемый файл с именем a.out.

Когда мы набираем gcc main.c, компилятор выполняет все вышеупомянутые шаги и создает исполняемый файл с именем a.out.

Если бы мы запустили этот файл, он успешно распечатал бы желаемый результат:

Hello world

Это все люди.

Использованная литература: