Изучаем c++. часть 1. что такое программа и языки программирования

Содержание

Зачем нужен компилятор?

Процессор — самая важная часть компьютера. Он обрабатывает информацию, выполняет команды пользователя и следит за работой всех подключенных устройств. Но процессор может разобрать только машинный код — набор 0 и 1, которые записаны в определённом порядке.

Почему именно 0 и 1? В процессор поступают электрические сигналы. Сильный сигнал обозначается цифрой 1, а слабый — 0. Набор таких цифр обозначает какую-то команду. Процессор ее распознает и выполняет.

Программы для первых компьютеров выглядели как огромные наборы 0 и 1. Чтобы записать такую программу, инженеры пользовались гибкими картонными карточками — перфокартами. Цифры на перфокарте записывались поочередно, в несколько строк. Чтобы записать 1, программист делал отверстие в карте. Места без отверстия обозначали 0.

Компьютер считывал перфокарту специальным устройством и выполнял записанную команду. Для одной программы составляли сотни перфокарт.

Писать их было долго и сложно, поэтому инженеры стали создавать языки программирования, обозначая команды словами и знаками. Для того, чтобы процессор понимал, какие команды записаны в программе, программисты создали компилятор — программу, которая преобразует программный код в машинный.

Структура компилятора

Процесс компиляции состоит из следующих этапов:

  1. Трансляция программы — трансляция всех или только изменённых модулей исходной программы.
  2. компоновка машинно-ориентированной программы.

В первом случае компилятор представляет собой пакет программ, включающий в себя трансляторы с разных языков программирования и компоновщики. Такой компилятор может компилировать программу, разные части исходно текста которой написаны на разных языках программирования. Нередко такие компиляторы управляются встроенным интерпретатором того или иного командного языка. Яркий пример таких компиляторов — имеющийся во всех UNIX-системах (в частности в Linux) компилятор make.

Во втором случае компилятор де-факто выполняет только трансляцию и далее вызывает компоновщик как внешнюю подпрограмму, который и компонует машинно-ориентированную программу. Этот факт нередко служит поводом считать компилятор разновидностью транслятора, что естественно неверно, — все современные компиляторы такого типа поддерживают организацию импорта программой процедуры (функции) из уже оттранслированого программного модуля, написанного на другом языке программирования. Так в программу на С/С++ можно импортировать функцию написанную например Pascal или Fortran. Аналогично и напротив написанная на С/С++ функция может быть импортирована в Pascal- или Fortran-программу соотвественно. Это как правило было бы невозможно без поддержки многими современными компиляторами организации обработки входных данных в процедуру (функций) в соответствии с соглашениями других языков программирования. Например современные компиляторы с языка Pascal помимо соглашения самого Pascal поддерживает организацию обработки процедурая/функцией входных в соответствии с соглашениями языка С/С++. (Чтобы на уровне машинного кода написанная на Pascal процедура/функция работала с входными параметрами в соответствии с соглашениями языка С/С++, — оператор объявления такой Pascal-процедуры/Pascal-функции должен содержать ключевое слово cdecl.) Примерами таких компиляторов являются компиляторы со всех без исключения языков программирования, используемые непосредственно.

Трансляция программы как неотъемлемая составляющая компиляции включает в себя:

  1. Лексический анализ. На этом этапе последовательность символов исходного файла преобразуется в последовательность лексем.
  2. Синтаксический (грамматический) анализ. Последовательность лексем преобразуется в дерево разбора.
  3. Семантический анализ. Дерево разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их декларациям, типам, проверка совместимости, определение типов выражений и т. д. Результат обычно называется «промежуточным представлением/кодом», и может быть дополненным деревом разбора, новым деревом, абстрактным набором команд или чем-то ещё, удобным для дальнейшей обработки.
  4. Оптимизация. Выполняется удаление излишних конструкций и упрощение кода с сохранением его смысла. Оптимизация может быть на разных уровнях и этапах — например, над промежуточным кодом или над конечным машинным кодом.
  5. Генерация кода. Из промежуточного представления порождается код на целевом машинно-ориентированном языке.

Язык для операционной системы

Для начала стоит абстрагироваться, ведь программирование – это не только вбивание определенных ключей-слов в машину, это еще и тщательно продуманные действия, связанные с компонентами системы. Изначально был двоичный код, потом программисты создали полумашинный язык программирования – ассемблер, но для чего?

Представьте себе, что вам надо считать на калькуляторе программиста каждый бит, потом правильно его связывать и многое другое. В ассемблере все стало чуточку проще, но все еще очень и очень непросто, если сравнивать с современными языками программирования, например, С++, который называют одним из сложнейших, но про него речь пойдет чуть позже.

В языке ассемблер все осуществляется благодаря регистрам процессора: деление, умножение, перемещение значения из точки А в точку Б и т. д. Основная его проблема, что он все еще является полумашинным, но все равно поддается прочтению человеку, в отличии от хаотично раскиданных битов. Еще одним минусом было ограниченное количество этих самых регистров.

В 80-х годах решили придумать язык программирования, благодаря которому можно будет легко и просто написать операционную систему. Так появился С и компилятор С GCC от компании GNU. Если вы пользуетесь Linux, то обязательно должны были видеть продукты данной компании. Кстати, ассемблер используется и поныне, ведь некоторые компиляторы создают объектные файлы с двоичным кодом, а другие исполнительные – с кодом на ассемблере. Все зависит от платформы разработчика.

Современный компилятор имеет следующие программы в себе:

  1. Дебагер – программа, которая отправляет сообщения об ошибке от линковщика, препроцессора, интерпретатора.
  2. Препроцессор – это программа, главной задачей которой является поиск специальных меток, начинающихся со знака #, и выполнение определенного рода команд. Например, добавления сторонней библиотеки для компиляции проекта.
  3. Интерпретатор – программа, которая переводит наш более-менее понятный язык программирования в двоичный код или ассемблер.
  4. Линковщик – программа, благодаря которой недостающие файлы автоматически подключаются.

Также существует 2 типа сборки проекта компилятором: динамическая и статическая. В первой добавляются лишь нужные проекту файлы, несмотря на среду разработки, а во втором случае — все в кучу (подключенные, конечно). Итак, из этого уже можно сказать, что компилятор – это целый список программ для сбора и обработки информации в понятный и логичный для компьютера вид. Дальше мы рассмотрим, с чего все начиналось.

Обновление скомпилированных пакетов

Если вы установили из или из SVN, вам в основном нужно удалить ранее установленные пакеты, загрузить последнюю версию исходного кода, скомпилировать и установить последнюю версию. Если вы установили из Git, это по сути тот же процесс, но немного проще. Перейдите в папку с исходным кодом и введите следующие команды:

Первая команда очищает ранее скомпилированные файлы и синхронизирует их с репозиторием Git и обновляет все файлы, которые изменились с последней версии. Затем перейдите к обычному процессу настройки, создания и установки.

Легко не так ли? Существует гораздо более продвинутые способы компиляции пакетов из исходного кода в Ubuntu, но, надеюсь, это дало вам быстрый пример, которого должно быть достаточно для поддержки типичного домашнего сервера Linux.

Источник записи: https://www.smarthomebeginner.com

Классификация компиляторов в соответствии с исходным кодом и целевым кодом

Для преобразования исходного кода в целевой применяются разные подходы. Некоторые компиляторы преобразуют код на высокоуровневом языке в машинный. Некоторые компиляторы преобразуют с одного языка высокого уровня на другой язык высокого уровня. Таким образом, здесь выделяются следующие типы:

  1. Кросс-компиляторы — такие компиляторы работают на одной платформе и производят код для запуска на другой платформе. Например, компилятор работает на платформе X и создает код для запуска на платформе Y. Такими компиляторами пользуются разработчики встроенных систем.
  2. Традиционные компиляторы — нам лучше всего знаком именно этот тип компиляторов. Такие компиляторы преобразуют исходный код языка высокого уровня в исходный код машинного языка. Набор компиляторов GCC преобразует в низкоуровневые, которые выполняются на .
  3. Транспилеры — они преобразуют исходный код языка высокого уровня в исходный код другого языка высокого уровня. Например, Babel transpiler преобразует ECMAScript 2015+ в JavaScript.
  4. Декомпиляторы — они принимают низкоуровневый исходный код в качестве входных данных и пытаются создать высокоуровневый исходный код, который может быть успешно перекомпилирован.

Архитектура компилятора

Когда компилятор компилирует (переводит) исходный код, он проходит несколько этапов:

  1. Исходный код.
  2. Лексический анализ.
  3. Синтаксический анализ.
  4. Семантический анализ.
  5. Промежуточная генерация кода.
  6. Оптимизация кода.
  7. Генерация кода.
  8. Целевой код.

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

Совет начинающим программистам

Ваш путь будет тернист – это стоит знать прежде всего. Для начала работы с языком, например, если это С, вы обязательно должны будете ознакомиться с компилятором C. А если с ним не подружиться и не понять его логику, то ваши проекты один за одним будут лагать и вылетать.

Попытайтесь как можно больше читать книг не только про основы программирования, но и про историю создания языков, так вы обязательно начнете понимать саму суть процесса. Старайтесь совмещать практику и новые знания, так все куда быстрее запоминается. Кроме того, постарайтесь довести свой английский хотя бы до среднего уровня, иначе вам будет очень сложно в ориентировании по IDE.

Особенность подключения пользовательских библиотек в Си

Подключение пользовательской библиотеки в Си на самом деле не так просто, как кажется.

Рассмотрим пример: есть желание вынести часть кода в отдельный файл — пользовательскую библиотеку.

program.c

#include «mylib.h»const int MAX_DIVISORS_NUMBER = 10000;int main(){    int number = read_number();    int Divisor;    size_t Divisor_top = 0;    factorize(number, Divisor, &Divisor_top);    print_array(Divisor, Divisor_top);    return 0;}

Сама библиотека должна состоять из двух файлов: mylib.h и mylib.c.

mylib.h

#ifndef MY_LIBRARY_H_INCLUDED#define MY_LIBRARY_H_INCLUDED#include <stdlib.h>//считываем числоint read_number();//получаем простые делители числа// сохраняем их в массив, чей адрес нам переданvoid factorize(int number, int *Divisor, int *Divisor_top);//выводим числоvoid print_number(int number);//распечатывает массив размера A_size в одной строке через TABvoid print_array(int A[], size_t A_size);#endif // MY_LIBRARY_H_INCLUDED

mylib.c

#include <stdio.h>#include «my_library.h»//считываем числоint read_number(){    int number;    scanf(«%d», &number);    return number;}//получаем простые делители числа// сохраняем их в массив, чей адрес нам переданvoid factorize(int x, int *Divisor, int *Divisor_top){    for (int d = 2; d <= x; d++) {        while (x%d == 0) {            Divisor = d;            x /= d;        }    }}//выводим числоvoid print_number(int number){    printf(«%d\n», number);}//распечатывает массив размера A_size в одной строке через TABvoid print_array(int A[], size_t A_size){    for(int i = A_size-1; i >= 0; i—)    {        printf(«%d\t», A);    }    printf(«\n»);}

Препроцессор Си, встречая #include «mylib.h», полностью копирует содержимое указанного файла (как текст) в место вызова директивы. Благодаря этому на этапе компиляции не возникает ошибок типа Unknown identifier при использовании функций из библиотеки.

Файл mylib.c компилируется отдельно.

А на этапе компоновки полученный файл mylib.o должен быть включен в исполняемый файл program.exe.

Cреда разработки обычно скрывает весь этот процесс от программиста, но для корректного анализа ошибок сборки важно представлять себе как это делается

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

Перевод

Современные процессоры суперскалярны, то есть способны выполнять несколько инструкций одновременно. Например, некоторые процессоры могут обрабатывать за цикл от четырёх до шести инструкций. Более того, многие такие процессоры способны инициировать команды не по порядку: они могут начать работать с командами, расположенными в коде намного позже.
В то же время, код часто содержит ветвления (операторы ). Такие ветвления часто реализуются как «переходы», при которых процессор или переходит к выполнению инструкции ниже по коду, или продолжает текущий путь.
При суперскалярном выполнении команд вне порядка с ветвлениями справляться сложно. Для этого у процессоров имеются изощрённые блоки предсказания ветвления. То есть процессор пытается предсказать будущее. Когда он видит ветвление, а значит, переход, то пытается догадаться, каким путём пойдёт программа.

Типы в инженерных задачах

Из песочницы

Вот уже 38 лет мне приходится решать разнообразные задачи путем составления соответствующих программ. За все эти годы диапазон таких задач получился весьма большим: от программирования контроллера AT90S2313 до программы автоматического обращения русско-немецкого технического словаря в немецко-русский, и от расчета оптимального сечения стрингера до рисования вида земной поверхности из иллюминатора с орбиты методом обратной трассировки лучей.

Несмотря на разнообразие задач, все их, пожалуй, можно отнести к одному классу, который я условно называю «инженерный», поскольку большей частью они оперируют понятиями научно-технического характера. При решении всех этих задач я не видел пользы в применении таких понятий, как «абстрактный» тип, т.е. назначению некоторого произвольного свойства объектам программы. Обычно эти свойства при трансляции имеют внутреннее представление в виде целых чисел.

Могут возразить, что я не вижу смысла в таких типах потому, что в старых языках (вроде используемого мною PL/1) вообще не было типизации в современном понимании. Нет, понятие типа все-таки было. Например, если в том же PL/1 описать два несовпадающих объекта пусть даже одинакового объема и попытаться присвоить один другому, то при компиляции получится предсказуемое сообщение об ошибке (рис. 1).

Компиляция — это…

Слово образовано от латинского compilatio, и буквально переводится как «ограбление или кража». Но в отрицательном контексте его можно встретить всё же намного реже, чем в положительном.

Компиляция — это сочинительство (не обязательно литературное) на основе чужих работ и исследований. Она представляет пересказ написанного кем-то ранее, и поэтому ничего нового не даёт.

Но на самом деле, даже в таком виде она бывает полезной. Ведь если человек, создающий сие сочинение, хорошо разбирается в теме, и связывает чужие используемые отрывки своими рассуждениями и связками, с помощью которых легче отследить логическую цепочку

И это очень важно и полезно

Такие известные писатели, как С.Цвейг, А.Моруа, были отличными компиляторами, произведениями которых зачитывалось не одно поколение любителей литературы.

О реализации точного представления чисел или «где хранить деньги?»

 «Где хранить деньги?» это шутливое название поста, периодически появляющегося на компьютерных форумах. Имеется в виду вопрос: в переменных какого типа следует хранить результаты вычислений финансово-экономических расчетов. Обычно такой вопрос возникает у программистов, впервые сталкивающихся с подобными расчетами. Интуитивно они понимают, что здесь округления нежелательны или недопустимы. Интересно, что при этом часто вспоминают легенды о хитрых программистах, сумевших разницу между точным и округленным значением финансовых расчетов перечислять на свой собственный счет. Правда, эти истории названы сказками еще в книге , написанной 50 лет назад, причем даже там речь шла лишь о ревизиях банковских систем, основаниями для которых (т.е. для ревизий) могли бы служить подобные истории.

Компьютеры применяются для экономических расчетов уже более 60 лет. Приемы и инструменты таких расчетов хорошо развиты и изучены. Тем не менее, поскольку все-таки возникают вопросы, подобные вынесенному в заголовок, представляется полезным для лучшего понимания проблем экономических расчетов привести как пример одну из самых простых и проверенных временем реализаций в трансляторе аппарата «точных» вычислений и его особенностей. Под «точными» далее будут подразумеваться вычисления без округлений и с исходными данными, представленными в виде десятичных дробей. Разумеется, не все данные можно точно представить в десятичном виде, поэтому понятие «точный» здесь имеет некоторые ограничения, однако для экономических расчетов в большинстве случаев эти ограничения несущественны.

Модификация исполняемого кода как способ реализации массивов с изменяемым границами

В свете все возрастающего потока англоязычных околонаучных терминов в области программирования и следуя за модой, в названии статьи можно было бы вместо некрасивого «модификация исполняемого кода» написать что-нибудь вроде «run-time reflection». Суть от этого не меняется. Речь о реализации в компиляторе такого непростого объекта, как массив с заранее неизвестными границами. Типичный пример использования подобных объектов – это операции (в математическом смысле) с матрицами разных размеров.

В данном случае имеется в виду многомерный массив, который занимает непрерывный участок памяти и у которого границы задаются уже при работе программы, причем нижние границы не обязательно начинаются с нуля. Реализация обращения к таким массивам гораздо сложнее случая границ-констант известных при компиляции.

Обычно текущие значения границ пишутся в специальную структуру («паспорт» массива) и затем используются в командах вычисления адресов элементов массива. Для границ-констант, известных при компиляции, «паспорт» в общем случае в выполняемой программе не требуется.

Из сложности реализации таких массивов следует неприятное следствие: обращение к массивам с «динамическими» границами выполняется существенно медленнее, чем к массивам с границами-константами. Желательно было бы соединить мощь и удобство работы с массивами с задаваемыми при исполнении кода границами с быстротой доступа к массивам с границами-константами, когда часть вычислений адресов элементов может производиться уже на этапе компиляции программы.

Далее все рассуждения приводятся на примере конкретной реализации компилятора PL/1-KT с языка PL/1 для Win64.

На чем написан компилятор?

В 1950-е годы группа разработчиков IBM под руководством Джона Бэкуса разработала первый высокоуровневый язык программирования Fortran, который позволил писать программы на понятном человеку языке. Помимо языка, инженеры работали и над компилятором. Он представлял собой программу с набором исполняемых команд, которая могла компилировать другие программы на Fortran, в том числе и улучшенную версию себя.

В дальнейшем язык Fortran и его компилятор использовали, чтобы написать компиляторы для новых языков программирования. Такой подход используют программисты и в настоящее время.
Писать машинный код долго и неудобно. К тому же, для современных процессоров он может отличаться. Придется писать несколько версий одного и того же компилятора для разных компьютеров. Быстрее и проще написать компилятор на существующем языке программирования. Для этого разработчики выбирают удобный язык и пишут на нем первую версию своего компилятора. Он будет более универсальным для компьютеров и легко скомпилирует улучшенную версию себя.

Семантический анализ

Просто потому, что программа не содержит синтаксических ошибок, код еще не может считаться правильным. Рассмотрим предложение ниже.

Теперь предположим, что все слова в этом предложении — правильные лексемы, идентифицированные на этапе лексического анализа. Как люди, мы знаем, что в предложении на английском языке есть порядок подлежащее -> сказуемое -> дополнение.

Компилятор при анализе синтаксиса может решить, что в этом предложении нет синтаксических ошибок, потому что маркеры (слова) расположены в правильном порядке.

Теперь рассмотрим предложение ниже.

Предположим, что eat — правильный маркер в соответствии с грамматикой. Таким образом, предложение признается правильным на этапе лексического и синтаксического анализа, поскольку слова расположены в правильном порядке. Но в этом предложении нет никакого смысла — никто не может есть компиляторы.

Итак, согласно этапу семантического анализа, эта программа содержит ошибку. Мы называем эту разновидность ошибок семантическими ошибками. Взгляните на этот простой Java-код:

class SemanticAnalysis{  public static void main(String[] args){    int a = 5;    int b = 10;    int total = c + d;  }}

Здесь нет синтаксических ошибок. Все маркеры упорядочены правильно. Но на пятой строке — не имеет никакого значения, так как идентификаторы и не определены. Это и есть семантическая ошибка.

Компиляторы

Компилятор — это программа, превращающая исходный текст, написанный на языке программирования, в машинные инструкции.

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

При компиляции весь исходный программный код (тот, который пишет программист) сразу переводится в машинный. Создается так называемый отдельный исполняемый файл, который никак не связан с исходным кодом. Выполнение исполняемого файла обеспечивается операционной системой. То есть образуется, например,.EXE файл.

Примеры компилируемых языков: C, C++, Pascal, Delphi.

Экстракоды при синтезе программ

Впервые термин «экстракод» я услышал еще применительно к командам БЭСМ-6. Сейчас это слово практически не используется, наиболее близкое понятие — «системный вызов». Из-за особенностей системы команд БЭСМ-6, те экстракоды действительно больше напоминали дополнительные встроенные инструкции, чем, например, вызов функции в MS-DOS с помощью INT 21H.

Смысл термина «экстракод» вполне прозрачен: это расширение системы команд для того, чтобы создать из реальной машины виртуальную машину для заданного языка, например, Паскаль-машину или Лисп-машину. А транслятор после этапа анализа исходного текста программы должен провести синтез программы – т.е. перевести результаты анализа в команды этой виртуальной машины.

Кстати, Лисп-машину когда-то действительно собирались воплотить в аппаратуре. Однако создание таких машин для конкретных языков было бы экономически слишком невыгодно по сравнению с универсальными и поэтому мы используем исключительно универсальные (с точки зрения языков) компьютеры. Правда, по мере развития и удешевления технологий разработки и производства процессоров, экономические преимущества универсальных систем команд над специальными могут стать не столь очевидными.

Генерация кода

Генерация машинного кода

Большинство компиляторов переводит программу с некоторого высокоуровневого языка программирования в машинный код, который может быть непосредственно выполнен физическим процессором. Как правило, этот код также ориентирован на исполнение в среде конкретной операционной системы, поскольку использует предоставляемые ею возможности (системные вызовы, библиотеки функций). Архитектура (набор программно-аппаратных средств), для которой компилируется (собирается) машинно-ориентированная программа, называется целевой машиной.

Результат компиляции — исполнимый программный модуль — обладает максимально возможной производительностью, однако привязан к конкретной операционной системе (семейству или подсемейству ОС) и процессору (семейству процессоров) и не будет работать на других.

Для каждой целевой машины (IBM, Apple, Sun, Эльбрус и т. д.) и каждой операционной системы или семейства операционных систем, работающих на целевой машине, требуется написание своего компилятора. Существуют также так называемые кросс-компиляторы, позволяющие на одной машине и в среде одной ОС генерировать код, предназначенный для выполнения на другой целевой машине и/или в среде другой ОС. Кроме того, компиляторы могут оптимизировать код под разные модели из одного семейства процессоров (путём поддержки специфичных для этих моделей особенностей или расширений наборов команд). Например, код, скомпилированный под процессоры семейства Pentium, может учитывать особенности распараллеливания инструкций и использовать их специфичные расширения — MMX, SSE и т. п.

Некоторые компиляторы переводят программу с языка высокого уровня не прямо в машинный код, а на язык ассемблера. (Пример: PureBasic, транслирующий бейсик-код в ассемблер FASM.) Это делается для упрощения части компилятора, отвечающей за генерацию кода, и повышения его переносимости (задача окончательной генерации кода и привязки его к требуемой целевой платформе перекладывается на ассемблер), либо для возможности контроля и исправления результата компиляции (в т. ч. ручной оптимизации) программистом.

Генерация байт-кода

Результатом работы компилятора может быть программа на специально созданном низкоуровневом языке двоично-кодовых команд, выполняемых виртуальной машиной. Такой язык называется псевдокодом или байт-кодом. Как правило, он не есть машинный код какого-либо компьютера и программы на нём могут исполняться на различных архитектурах, где имеется соответствующая виртуальная машина, но в некоторых случаях создаются аппаратные платформы, напрямую выполняющие псевдокод какого-либо языка. Например, псевдокод языка Java называется байт-кодом Java и выполняется в Java Virtual Machine, для его прямого исполнения была создана спецификация процессора picoJava. Для платформы .NET Framework псевдокод называется Common Intermediate Language (CIL), а среда исполнения — Common Language Runtime (CLR).

Некоторые реализации интерпретируемых языков высокого уровня (например, Perl) используют байт-код для оптимизации исполнения: затратные этапы синтаксического анализа и преобразование текста программы в байт-код выполняются один раз при загрузке, затем соответствующий код может многократно использоваться без перекомляции.

Динамическая компиляция

Из-за необходимости интерпретации байт-код выполняется значительно медленнее машинного кода сравнимой функциональности, однако он более переносим (не зависит от операционной системы и модели процессора). Чтобы ускорить выполнение байт-кода, используется динамическая компиляция, когда виртуальная машина транслирует псевдокод в машинный код непосредственно перед его первым исполнением (и при повторных обращениях к коду исполняется уже скомпилированный вариант).

Наиболее популярной разновидностью динамической компиляции является JIT. Другой разновидностью является .

CIL-код также компилируется в код целевой машины JIT-компилятором, а библиотеки .NET Framework компилируются заранее.

Трасляция байт-кода в машинный код

Трансляция байт-кода в машинный код специальным транслятором байт-кода как указано выше неотъемлемая фаза динамической компиляции. Но трансляция байт-кода применима и для простого преобразования программы на байт-коде в эквивалентную программу на машинном языке. В машинный код может транслироваться как заранее скомпилированный байт-код. Но также трансляция байт-кода в машинный код может выполняться компилятором байт-кода сразу следом за компиляцией байт-кода. Практически всегда в последнем случае трансляция байт-кода выполняется внешним транслятором, вызываемым компилятором байт-кода.

Декомпиляция

Существуют программы, которые решают обратную задачу — перевод программы с низкоуровневого языка на высокоуровневый. Этот процесс называют декомпиляцией, а такие программы — декомпиляторами. Но поскольку компиляция — это процесс с потерями, точно восстановить исходный код, скажем, на C++, в общем случае невозможно. Более эффективно декомпилируются программы в байт-кодах — например, существует довольно надёжный декомпилятор для Flash. Разновидностью декомпиляции является дизассемблирование машинного кода в код на языке ассемблера, который почти всегда благополучно выполняется (при этом сложность может представлять самомодифицирующийся код или код, в котором собственно код и данные не разделены). Связано это с тем, что между кодами машинных команд и командами ассемблера имеется практически взаимно-однозначное соответствие.

Структура компилятора

Процесс компиляции состоит из следующих этапов:

  1. Трансляция программы — трансляция всех или только изменённых модулей исходной программы.
  2. компоновка машинно-ориентированной программы.

В первом случае компилятор представляет собой пакет программ, включающий в себя трансляторы с разных языков программирования и компоновщики. Такой компилятор может компилировать программу, разные части исходно текста которой написаны на разных языках программирования. Нередко такие компиляторы управляются встроенным интерпретатором того или иного командного языка. Яркий пример таких компиляторов — имеющийся во всех UNIX-системах (в частности в Linux) компилятор make.

Во втором случае компилятор де-факто выполняет только трансляцию и далее вызывает компоновщик как внешнюю подпрограмму, который и компонует машинно-ориентированную программу. Этот факт нередко служит поводом считать компилятор разновидностью транслятора, что естественно неверно, — все современные компиляторы такого типа поддерживают организацию импорта программой процедуры (функции) из уже оттранслированого программного модуля, написанного на другом языке программирования. Так в программу на С/С++ можно импортировать функцию написанную например Pascal или Fortran. Аналогично и напротив написанная на С/С++ функция может быть импортирована в Pascal- или Fortran-программу соотвественно. Это как правило было бы невозможно без поддержки многими современными компиляторами организации обработки входных данных в процедуру (функций) в соответствии с соглашениями других языков программирования. Например современные компиляторы с языка Pascal помимо соглашения самого Pascal поддерживает организацию обработки процедурая/функцией входных в соответствии с соглашениями языка С/С++. (Чтобы на уровне машинного кода написанная на Pascal процедура/функция работала с входными параметрами в соответствии с соглашениями языка С/С++, — оператор объявления такой Pascal-процедуры/Pascal-функции должен содержать ключевое слово cdecl.) Примерами таких компиляторов являются компиляторы со всех без исключения языков программирования, используемые непосредственно.

Трансляция программы как неотъемлемая составляющая компиляции включает в себя:

  1. Лексический анализ. На этом этапе последовательность символов исходного файла преобразуется в последовательность лексем.
  2. Синтаксический (грамматический) анализ. Последовательность лексем преобразуется в дерево разбора.
  3. Семантический анализ. Дерево разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их декларациям, типам, проверка совместимости, определение типов выражений и т. д. Результат обычно называется «промежуточным представлением/кодом», и может быть дополненным деревом разбора, новым деревом, абстрактным набором команд или чем-то ещё, удобным для дальнейшей обработки.
  4. Оптимизация. Выполняется удаление излишних конструкций и упрощение кода с сохранением его смысла. Оптимизация может быть на разных уровнях и этапах — например, над промежуточным кодом или над конечным машинным кодом.
  5. Генерация кода. Из промежуточного представления порождается код на целевом машинно-ориентированном языке.

О реализации ввода-вывода с именами

Как-то на одном из компьютерных форумов участники делились соображениями, чего им не хватает в языках, так сказать, в повседневной деятельности. Один заявил, что ему очень не хватает оператора типа put data. Для тех, кто не слышал о таком, поясню, что в языке PL/1 был предусмотрен оператор вывода (или ввода) значений переменных вместе с их именами, т.е. вместе с идентификаторами.

Например, если обычный вывод put list(X,Y); давал что-нибудь вроде:

1280    1024

то оператор put data(X,Y); печатал:

X=   1280  Y=   1024

не заставляя писать для этого вывод в виде оператора:

put list(’X=’,X,’Y=’,Y);

Чаще всего это использовалось в отладочных целях, но не обязательно. Например, используя вывод в текстовый файл с помощью put data, а затем ввод этих же данных как исходных в другой программе с помощью «зеркального» оператора get data можно было получить удобную для чтения и редактирования форму представления исходных данных в текстовом виде.

Можно возразить, что современные «оболочки» систем программирования легко позволяют вывести значения (и имена) любых объектов программы в интерактивном режиме безо всяких put data и поэтому в нем больше нет необходимости.

Я тоже использую встроенный интерактивный отладчик, с помощью которого можно посмотреть любые, даже сложные (составные) данные. Однако потребность в возможностях put data не исчезла. Иногда нужен и анализ данных «на распечатке», (т.е. в логе) с возможностью быстрого и удобного добавления вывода для отладки. К тому же для работы отладочных интерактивных систем используется дополнительная информация, которая обычно в выполняемом exe-файле отсутствует. В этом же случае все имена переменных «зашиты» в самой программе. Таким образом, удобства, предоставляемые языком в виде оператора, подобного put data, не заменяют, а дополняют возможности отладчиков. Кстати, в стародавние времена так называемая «посмертная» выдача на печать после ошибки иногда включала в себя и вывод полного списка всех имен переменных вместе с их текущими значениями. Некоторым программистам так нравился такой вывод, что они специально в конце своей программы ставили деление на ноль.

Оцените статью
Рейтинг автора
5
Материал подготовил
Илья Коршунов
Наш эксперт
Написано статей
134
Добавить комментарий