Трансляция, компиляция и интерпретация
Программа это последовательность инструкций, предназначенных для выполнения компьютером. В настоящее время программы оформляются в виде текста, который записывается в файлы. Этот текст является результатом деятельности программиста и, несмотря на специфику формального языка, остаётся программой для программиста.
Процесс создания программы предполагает несколько этапов. За этапом разработки проекта программы следует этап программирования. На этом этапе пишется программа. Программистами этот текст воспринимается легче двоичного кода, поскольку различные мнемонические сокращения и имена заключают дополнительную информацию.
Файл с исходным текстом программы (его также называют исходным модулем) обрабатывается транслятором, который осуществляет перевод программы с языка программирования в понятную машине последовательность кодов.
Транслятор программаили техническое средство, выполняющеетрансляцию программы. Машинная программа, которая транслирует с одного языкана другой и, в частности, с одногоязыка программированияна другой. Обрабатывающая программа, предназначенная для преобразования исходной программы вобъектный модуль.
Транслятор обычно выполняет также диагностику ошибок, формирует словари идентификаторов, выдаёт для печати тексты программы и т.д.
Трансляция программы преобразование программы, представленной на одном из языков программирования, в программу на другом языке и, в определённом смысле, равносильную первой.
Язык, на котором представлена входная программа, называется исходным языком, а сама программа исходным кодом. Выходной язык называется целевым языком или объектным кодом.
Виды трансляторов
- Адресный. Функциональное устройство, преобразующее виртуальный адрес (англ. Virtual address) в реальный адрес (англ. Memory address).
- Диалоговый. Обеспечивает использование языка программирования в режиме разделения времени.
- Многопроходной. Формирует объектный модуль за несколько просмотров исходной программы.
- Обратный. То же, что детранслятор. См. также: декомпилятор, дизассемблер.
- Однопроходной. Формирует объектный модуль за один последовательный просмотр исходной программы.
- Оптимизирующий. Выполняет оптимизацию кода в создаваемом объектном модуле.
- Синтаксически-ориентированный (синтаксически-управляемый). Получает на вход описание синтаксиса и семантики языка и текст на описанном языке, который и транслируется в соответствии с заданным описанием.
- Тестовый. Набор макрокоманд языка ассемблера, позволяющих задавать различные отладочные процедуры в программах, составленных на языке ассемблера.
- Программа, предназначенная для трансляции высокоуровневого языка в абсолютный кодили, иногда, в язык ассемблера. Входной информацией для компилятора (исходный код) является описание алгоритма или программа напроблемно-ориентированном языке, а на выходе компилятора — эквивалентное описание алгоритма намашинно-ориентированном языке(объектный код).
Процесс компиляции программ на C++
В данной статье я хочу рассказать о том, как происходит компиляция программ, написанных на языке C++, и описать каждый этап компиляции. Я не преследую цель рассказать обо всем подробно в деталях, а только дать общее видение. Также данная статья — это необходимое введение перед следующей статьей про статические и динамические библиотеки, так как процесс компиляции крайне важен для понимания перед дальнейшим повествованием о библиотеках.
Все действия будут производиться на Ubuntu версии 16.04.
Используя компилятор g++ версии:
$ g++ --version g++ (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
Состав компилятора g++
Мы не будем вызывать данные компоненты напрямую, так как для того, чтобы работать с C++ кодом, требуются дополнительные библиотеки, позволив все необходимые подгрузки делать основному компоненту компилятора — g++.
Зачем нужно компилировать исходные файлы?
Исходный C++ файл — это всего лишь код, но его невозможно запустить как программу или использовать как библиотеку. Поэтому каждый исходный файл требуется скомпилировать в исполняемый файл, динамическую или статическую библиотеки (данные библиотеки будут рассмотрены в следующей статье).
Этапы компиляции:
Перед тем, как приступать, давайте создадим исходный .cpp файл, с которым и будем работать в дальнейшем.
driver.cpp:
#include using namespace std; #define RETURN return 0 int main()
1) Препроцессинг
Самая первая стадия компиляции программы.
Препроцессор — это макро процессор, который преобразовывает вашу программу для дальнейшего компилирования. На данной стадии происходит происходит работа с препроцессорными директивами. Например, препроцессор добавляет хэдеры в код (#include), убирает комментирования, заменяет макросы (#define) их значениями, выбирает нужные куски кода в соответствии с условиями #if, #ifdef и #ifndef.
Хэдеры, включенные в программу с помощью директивы #include, рекурсивно проходят стадию препроцессинга и включаются в выпускаемый файл. Однако, каждый хэдер может быть открыт во время препроцессинга несколько раз, поэтому, обычно, используются специальные препроцессорные директивы, предохраняющие от циклической зависимости.
Получим препроцессированный код в выходной файл driver.ii (прошедшие через стадию препроцессинга C++ файлы имеют расширение .ii), используя флаг -E, который сообщает компилятору, что компилировать (об этом далее) файл не нужно, а только провести его препроцессинг:
g++ -E driver.cpp -o driver.ii
Взглянув на тело функции main в новом сгенерированном файле, можно заметить, что макрос RETURN был заменен:
int main()
В новом сгенерированном файле также можно увидеть огромное количество новых строк, это различные библиотеки и хэдер iostream.
2) Компиляция
На данном шаге g++ выполняет свою главную задачу — компилирует, то есть преобразует полученный на прошлом шаге код без директив в ассемблерный код. Это промежуточный шаг между высокоуровневым языком и машинным (бинарным) кодом.
Ассемблерный код — это доступное для понимания человеком представление машинного кода.
Используя флаг -S, который сообщает компилятору остановиться после стадии компиляции, получим ассемблерный код в выходном файле driver.s:
$ g++ -S driver.ii -o driver.s
driver.s
.file "driver.cpp" .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .section .rodata .LC0: .string "Hello, world!" .text .globl main .type main, @function main: .LFB1021: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %esi movl $_ZSt4cout, %edi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi movq %rax, %rdi call _ZNSolsEPFRSoS_E movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1021: .size main, .-main .type _Z41__static_initialization_and_destruction_0ii, @function _Z41__static_initialization_and_destruction_0ii: .LFB1030: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl %edi, -4(%rbp) movl %esi, -8(%rbp) cmpl $1, -4(%rbp) jne .L5 cmpl $65535, -8(%rbp) jne .L5 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi call __cxa_atexit .L5: nop leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1030: .size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii .type _GLOBAL__sub_I_main, @function _GLOBAL__sub_I_main: .LFB1031: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $65535, %esi movl $1, %edi call _Z41__static_initialization_and_destruction_0ii popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1031: .size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I_main .hidden __dso_handle .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits
Мы можем все также посмотреть и прочесть полученный результат. Но для того, чтобы машина поняла наш код, требуется преобразовать его в машинный код, который мы и получим на следующем шаге.
3) Ассемблирование
Так как x86 процессоры исполняют команды на бинарном коде, необходимо перевести ассемблерный код в машинный с помощью ассемблера.
Ассемблер преобразовывает ассемблерный код в машинный код, сохраняя его в объектном файле.
Объектный файл — это созданный ассемблером промежуточный файл, хранящий кусок машинного кода. Этот кусок машинного кода, который еще не был связан вместе с другими кусками машинного кода в конечную выполняемую программу, называется объектным кодом.
Далее возможно сохранение данного объектного кода в статические библиотеки для того, чтобы не компилировать данный код снова.
Получим машинный код с помощью ассемблера (as) в выходной объектный файл driver.o:
$ as driver.s -o driver.o
Но на данном шаге еще ничего не закончено, ведь объектных файлов может быть много и нужно их всех соединить в единый исполняемый файл с помощью компоновщика (линкера). Поэтому мы переходим к следующей стадии.
4) Компоновка
Компоновщик (линкер) связывает все объектные файлы и статические библиотеки в единый исполняемый файл, который мы и сможем запустить в дальнейшем. Для того, чтобы понять как происходит связка, следует рассказать о таблице символов.
Таблица символов — это структура данных, создаваемая самим компилятором и хранящаяся в самих объектных файлах. Таблица символов хранит имена переменных, функций, классов, объектов и т.д., где каждому идентификатору (символу) соотносится его тип, область видимости. Также таблица символов хранит адреса ссылок на данные и процедуры в других объектных файлах.
Именно с помощью таблицы символов и хранящихся в них ссылок линкер будет способен в дальнейшем построить связи между данными среди множества других объектных файлов и создать единый исполняемый файл из них.
Получим исполняемый файл driver:
$ g++ driver.o -o driver // также тут можно добавить и другие объектные файлы и библиотеки
5) Загрузка
Последний этап, который предстоит пройти нашей программе — вызвать загрузчик для загрузки нашей программы в память. На данной стадии также возможна подгрузка динамических библиотек.
Запустим нашу программу:
$ ./driver // Hello, world!
Заключение
В данной статье были рассмотрены основы процесса компиляции, понимание которых будет довольно полезно каждому начинающему программисту. В скором времени будет опубликована вторая статья про статические и динамические библиотеки.
Основные принципы программирования: компилируемые и интерпретируемые языки
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Как и в предыдущей статье этого цикла, я хочу обратить ваше внимание на ключевые принципы программирования, которые влияют на всё то, что мы делаем, но с которыми мы редко сталкиваемся напрямую и поэтому не до конца их понимаем. Тема сегодняшней статьи — компилируемые и интерпретируемые языки.
Будучи разработчиками, мы часто сталкиваемся с такими понятиями, как компилятор и интерпретатор, но я считаю, что многие не совсем понимают, что они означают. Между тем, компиляция и интерпретация — это основы работы всех языков программирования. Давайте взглянем на то, как на самом деле устроены эти понятия.
Вступление
Мы полагаемся на такие инструменты, как компиляция и интерпретация, чтобы преобразовать наш код в форму, понятную компьютеру. Код может быть исполнен нативно, в операционной системе после конвертации в машинный (путём компиляции) или же исполняться построчно другой программой, которая делает это вместо ОС (интерпретатор).
Компилируемый язык — это такой язык, что программа, будучи скомпилированной, содержит инструкции целевой машины; этот машинный код непонятен людям. Интерпретируемый же язык — это такой, в котором инструкции не исполняются целевой машиной, а считываются и исполняются другой программой (которая обычно написана на языке целевой машины). Как у компиляции, так и у интерпретации есть свои плюсы и минусы, и именно это мы и обсудим.
Прежде чем мы продолжим, стоит отметить, что многие языки программирования имеют как компилируемую, так и интерпретируемую версии, поэтому классифицировать их затруднительно. Тем не менее, чтобы не усложнять, в дальнейшем я буду разделять компилируемые и интерпретируемые языки.
Компилируемые языки
Главное преимущество компилируемых языков — это скорость исполнения. Поскольку они конвертируются в машинный код, они работают гораздо быстрее и эффективнее, нежели интерпретируемые, особенно если учесть сложность утверждений некоторых современных скриптовых интерпретируемых языков.
Низкоуровневые языки как правило являются компилируемыми, поскольку эффективность обычно ставится выше кроссплатформенности. Кроме того, компилируемые языки дают разработчику гораздо больше возможностей в плане контроля аппаратного обеспечения, например, управления памятью и использованием процессора. Примерами компилируемых языков являются C, C++, Erlang, Haskell и более современные языки, такие как Rust и Go.
Проблемы компилируемых языков, в общем-то, очевидны. Для запуска программы, написаной на компилируемом языке, её сперва нужно скомпилировать. Это не только лишний шаг, но и значительное усложнение отладки, ведь для тестирования любого изменения программу нужно компилировать заново. Кроме того, компилируемые языки являются платформо-зависимыми, поскольку машинный код зависит от машины, на которой компилируется и исполняется программа.
Интерпретируемые языки
В отличие от компилируемых языков, интерпретируемым для исполнения программы не нужен машинный код; вместо этого программу построчно исполнят интерпретаторы. Раньше процесс интерпретации занимал очень много времени, но с приходом таких технологий, как JIT-компиляция, разрыв между компилируемыми и интерпретируемыми языками сокращается. Примерами интерпретируемых языков являются PHP, Perl, Ruby и Python. Вот некоторые из концептов, которые стали проще благодаря интерпретируемым языкам:
- Независимость от платформы;
- Рефлексия;
- Динамическая типизация;
- Меньший размер исполняемых файлов:
- Динамические области видимости.
Основным недостатком интерпретируемых языком является их невысокая скорость исполнения. Тем не менее, JIT-компиляция позволяет ускорить процесс благодаря переводу часто используемых последовательностей инструкции в машинный код.
Бонус: байткод-языки
Байткод-языки — это такие языки, которые используют для исполнения кода как компиляцию, так и интерпретацию. Java и фреймворк .NET — это типичные примеры байткод-языков. На самом деле, Java Virtual Machine (JVM) — это настолько популярная виртуальная машина для интерпретации байткода, что на ней работают реализации нескольких языков. Кстати, недавно стало известно, что в новой версии Java будет также поддерживаться и статическая компиляция.
В байткод-языке сперва происходит компиляция программы из человекочитаемого языка в байткод. Байткод — это набор инструкций, созданный для эффективного исполнения интерпретатором и состоящий из компактных числовых кодов, констант и ссылок на память. С этого момента байткод передаётся в виртуальную машину, которая затем интерпретирует код также, как и обычный интерпретатор.
При компиляции кода в байткод происходит задержка, но дальнейшая скорость исполнения значительно возрастает в силу оптимизации байткода. Кроме того, байткод-языки являются платформо-независимыми, превосходя при этом по скорости интерпретируемые. Для них также доступна JIT-компиляция.
Заключение
Многие языки в наши дни имеют как компилируемые, так и интерпретируемые реализации, сводя разницу между ними на нет. У каждого вида исполнения кода есть преимущества и недостатки.
Вкратце, компилируемые языки являются самыми эффективными, поскольку они исполняются как машинный код и позволяют использовать аппаратное обеспечение системы. Однако это вводит дополнительные ограничение на написание кода и делает его платформо-зависимым. Интерпретируемые же языки не зависят от платформы и позволяют использовать такие техники динамического программирования, как метапрограммирование. Тем не менее, в скорости исполнения они значительно уступают компилируемым языкам.
Байткод-языки, в свою очередь, пытаются использовать сильные стороны обоих видов языков, и у них это неплохо получается.
При подготовке материала использовались источники:
https://studfile.net/preview/3582383/
https://habr.com/ru/articles/478124/
https://tproger.ru/translations/programming-concepts-compilation-vs-interpretation