Ах. Компилятор. Таинственное устройство, которое берет исходный код и волшебным образом выдает исполняемый код. Компилятор фактически работает в четыре этапа: предварительная обработка, компиляция, сборка и компоновка.
У вас есть исходный файл с именем 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 выполняет несколько задач с исходным кодом:
- Он ищет файлы заголовков и включает их в наш исходный код.
- Если были определены какие-либо макросы, препроцессор заменит макросы их определением во всей программе.
- Включать и исключать части программы, например, комментарии, в соответствии с определенными условиями
После этих шагов создается промежуточный файл, который передается на следующий шаг. Если вы хотите получить промежуточный файл, в 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
Это все люди.
Использованная литература: