Вступление
В настоящее время большинство программ написано на языках высокого уровня, таких как C, Java или Python. Эти языки предназначены больше для людей, чем для машин, поскольку они скрывают от программиста некоторые аппаратные детали конкретного компьютера.
Проще говоря, языки высокого уровня упрощают задание компьютеру, что ему делать. Однако, поскольку компьютеры понимают инструкции только в машинном коде (в виде единиц и нулей), мы не можем правильно общаться с ними без какого-либо переводчика.
Вот почему существуют языковые процессоры.
Языковой процессор - это специальная система транслятора, используемая для превращения программы, написанной на языке высокого уровня, который мы называем «исходным кодом», в машинный код, который мы называем «объектной программой» или «объектным кодом».
Для разработки языкового процессора необходимо очень точное описание лексики и синтаксиса, а также семантики языка высокого уровня.
Есть три типа языковых процессоров:
- Ассемблер
- Устный переводчик
- Компилятор
В следующих нескольких разделах мы рассмотрим каждый из этих типов процессоров и обсудим их назначение, различия и т. Д.
Языки ассемблера и ассемблер
Большинство языков ассемблера очень похожи на машинный код (поэтому они специфичны для компьютерной архитектуры или операционной системы), но вместо использования двоичных чисел для описания инструкции в нем используются мнемонические символы .
Каждый мнемонический символ представляет собой код операции или
инструкцию, и нам обычно требуется несколько из них вместе, чтобы
сделать что-нибудь полезное. Эти инструкции могут использоваться для
перемещения значений между регистрами (в архитектуре Intel86-64 эта
команда будет MOV
), для выполнения основных арифметических операций
со значениями, таких как сложение, вычитание, умножение и деление (ADD,
SUB, MUL, DIV), а также основные логические операции, такие как сдвиг
числа влево или вправо или отрицание ( SHL
, SHR
, NEG
). Он также
может использовать безусловные и условные переходы, что полезно для
реализации цикла «for», цикла «while» или оператора «if» ( JMP
, JE
, JLE
...).
Например, если процессор интерпретирует двоичную команду 10110
как
«перемещение из одного регистра в другой регистр», язык ассемблера
заменит ее командой, такой как MOV
.
Каждый регистр также имеет двоичный идентификатор, например 000
. Его
также можно заменить более "человеческим" именем, например EAX
,
который является одним из основных регистров в x86.
Если бы мы, скажем, захотели переместить значение в регистр, машинный код выглядел бы примерно так:
00001 000 00001010
- 00001 : Это команда перемещения
- 000 : Идентификатор регистра
- 00001010 : значение, которое мы хотим переместить
На ассемблере это можно записать примерно так:
MOV EAX, A
MOV
- это команда перемещенияEAX
- идентификатор регистраA
- шестнадцатеричное значение, которое мы хотим переместить (10 в десятичном формате)
Если бы мы хотели записать EAX = 7 + 4 - 2
, оно выглядело бы примерно
так:
00001 000 00000111
00001 001 00000100
00010 000 001
00001 001 00000010
00011 000 001
- 00001 - это команда "переместить"
- 00010 - это команда "сложения"
- 00011 - команда "вычитания"
- 000, 001 - идентификаторы регистров
- 00000111, 00000100, 00000010 - это целые числа, которые мы используем в этих выражениях.
В сборке этот набор двоичных чисел будет записан как:
MOV EAX, 7
MOV R8, 4
ADD EAX, R8
MOV R9, 2
SUB EAX, R9
MOV
- это команда перемещенияADD
- это команда сложенияSUB
- это команда вычитанияEAX
,R8
,R9
- идентификаторы регистров- 7, 4, 2 : целые числа, которые мы используем в этих выражениях.
Хотя он все еще не так удобочитаем, как язык высокого уровня, он все же намного удобнее для чтения человеком, чем двоичная команда. Аппаратные компоненты процессора и регистров гораздо более абстрактны.
Это облегчает программисту написание исходного кода, избавляя от необходимости манипулировать числами для программирования. Перевод в объектный код на машинном языке прост и понятен, выполняется ассемблером .
Поскольку исходный код уже очень похож на машинный код, нет необходимости компилировать или интерпретировать код - он собран как есть.
Устные языки и переводчик
Каждая программа имеет этап перевода и этап выполнения . В интерпретируемых языках эти две фазы взаимосвязаны - инструкции, написанные на языке программирования высокого уровня, выполняются напрямую без предварительного преобразования в объектный код или машинный код.
Обе фазы выполняются интерпретатором - языковым процессором, который переводит один оператор (строку кода), немедленно выполняет его и затем переходит к следующей строке. При возникновении ошибки интерпретатор завершает процесс перевода на этой строке и отображает ошибку. Он не может перейти к следующей строке и выполнить ее, если предыдущая ошибка не будет удалена.
Интерпретаторы использовались с 1952 года, и их задача заключалась в том, чтобы упростить программирование с учетом ограничений компьютеров в то время (например, в компьютерах первого поколения было значительно меньше места для хранения, чем сейчас). Первым интерпретируемым языком высокого уровня был Lisp , впервые реализованный в 1958 году на компьютере IBM704.
В настоящее время наиболее распространенными интерпретируемыми языками программирования являются Python , Perl и Ruby .
Скомпилированные языки и компилятор
В отличие от интерпретируемых языков программирования, этап перевода и этап выполнения в компилируемых языках программирования полностью разделены, и перевод выполняется компилятором .
Компилятор - это языковой процессор, который считывает весь исходный код, написанный на языке высокого уровня, и переводит его в эквивалентный объектный код в целом. Обычно этот объектный код хранится в файле. Если в исходном коде есть ошибки, компилятор указывает их в конце компиляции вместе со строками, в которых были обнаружены ошибки. После их удаления исходный код можно перекомпилировать.
Языки низкого уровня обычно компилируются, потому что, будучи напрямую переведены в машинный код, они позволяют программисту гораздо больше контролировать аппаратные компоненты, такие как память или ЦП.
Первым компилируемым языком программирования высокого уровня был FORTRAN , созданный в 1957 году командой под руководством Джона Бэкуса из IBM.
В настоящее время наиболее распространенными компилируемыми языками являются C ++ , Rust и Haskell .
Языки байт-кода
Языки байт-кода, также называемые языками « переносимого кода » или « p-кода », представляют собой тип языков программирования, которые подпадают под категории как интерпретируемых, так и компилируемых языков, поскольку они используют как компиляцию, так и интерпретацию при переводе и выполнении кода.
Проще говоря, байт-код - это программный код, который был скомпилирован из исходного кода в низкоуровневый код, разработанный для программного интерпретатора. После компиляции (из исходного кода в байт-код) его можно скомпилировать в машинный код, который распознается ЦП, или он может быть выполнен виртуальной машиной, которая затем действует как интерпретатор.
Байт-код универсален и может быть передан в скомпилированном состоянии на другие устройства (со всеми преимуществами скомпилированного кода). Затем ЦП преобразует его в специальный машинный код для устройства. При этом вы можете скомпилировать исходный код один раз и запускать его везде - при условии, что на устройстве есть другой уровень, который используется для преобразования байт-кода в машинный код.
Самая известная виртуальная машина для интерпретации байт-кода - это виртуальная машина Java (JVM), которая настолько распространена, что на нескольких языках есть реализации, созданные для ее работы.
{.ezlazyload}
[Предоставлено: ViralPatel.]{.small}
Когда программа впервые запускается на языке байт-кода, происходит задержка, пока код компилируется в байт-код, но скорость выполнения значительно увеличивается по сравнению со стандартными интерпретирующими языками (поскольку исходный код оптимизирован для интерпретатора).
Одним из самых больших преимуществ языков байт-кода является независимость от платформы, которая раньше была типичной только для интерпретируемых языков, в то время как программы намного быстрее, чем обычные интерпретируемые языки, когда дело доходит до выполнения.
Еще одна вещь, о которой стоит упомянуть, - это JIT-компиляция. В отличие от компиляции с опережением времени (AOT), код компилируется по мере выполнения . Это существенно увеличивает скорость компиляции и использует преимущества компиляции с гибкостью интерпретации.
Опять же, динамическая компиляция не всегда должна быть лучше / быстрее статической - это в основном зависит от того, над каким проектом вы работаете.
Флагманскими языками, которые компилируются в байт-код, являются Java и C #, а также такие языки, как Clojure , Groovy , Kotlin и Scala .
Преимущества и недостатки: скомпилировано или интерпретировано
Представление
Поскольку компилятор переводит весь исходный код языка программирования в исполняемый машинный код для ЦП, анализ исходного кода занимает много времени, но после завершения анализа и компиляции общее выполнение происходит намного быстрее.
С другой стороны, интерпретатор переводит исходный код строка за строкой, каждая из которых выполняется по мере перевода, что приводит к более быстрому анализу исходного кода, но выполнение значительно медленнее.
Отладка
Когда дело доходит до интерпретируемых языков программирования, отладка намного проще, потому что код транслируется до тех пор, пока не будет обнаружена ошибка, поэтому мы точно знаем, где она находится, и ее легче исправить.
Напротив, отладка на скомпилированном языке намного утомительнее. Если программа написана на скомпилированном языке, ее нужно скомпилировать вручную, что является дополнительным шагом для запуска программы. Это может показаться не проблемой - и это не относится к небольшим программам.
Помните, что для компиляции массивных проектов могут потребоваться десятки минут, а некоторые даже часы.
Более того, компилятор генерирует сообщение об ошибке после сканирования исходного кода в целом, поэтому ошибка может быть где угодно в программе. Даже если указана строка с ошибкой, после изменения исходного кода и исправления его нужно перекомпилировать, и только тогда можно будет выполнить улучшенную версию. Это может показаться не проблемой - и это не относится к небольшим программам.
Помните, что для компиляции массивных проектов могут потребоваться десятки минут, а некоторые даже часы. К счастью, перед компиляцией с помощью IDE можно заметить множество ошибок, но не все.
Исходный код против объектного кода
Для интерпретируемых языков программирования исходный код необходим для выполнения. Это означает, что исходный код приложения доступен пользователю - как JavaScript отображается в браузере.
Разрешение пользователям полностью читать исходный код может позволить злоумышленникам манипулировать и находить лазейки в логике. Это может быть до некоторой степени ограничено с помощью обфускации кода , но это все еще намного более доступно, чем скомпилированный код.
С другой стороны, как только программа, написанная на скомпилированном языке программирования, скомпилирована в объектный код, она может выполняться бесконечное количество раз, и исходный код больше не нужен.
Вот почему при передаче программы пользователю достаточно просто
отправить ему объектный код, а не исходный код, обычно в виде .exe
в
Windows.
Интерпретируемый код более восприимчив к атакам путем внедрения кода, и тот факт, что он не проверяется по типу, знакомит нас с совершенно новым набором исключений и ошибок программирования.
Заключение
Не существует «лучшего» способа перевода исходного кода, и как скомпилированные, так и интерпретируемые языки программирования имеют свои преимущества и недостатки, как упоминалось выше.
Во многих случаях граница между «скомпилированным» и «интерпретируемым» четко не определена, когда речь идет о более современном языке программирования, на самом деле, ничто не мешает вам, например, написать компилятор для интерпретируемого языка.