Сайт содержит тексты редких методических пособий, лабораторных и контрольных работ. Вообщем, то что трудно найти в сети но очень нужно для подготовки к экзаменам, в частности на заочной форме обучения.
- Подписка (RSS)
- Коментарии (RSS)
Основные функции компилятора
Здесь обсуждаются операции, необходимые для компиляции программ типичного языка высокого уровня. В качестве примера мы будем использовать программу на Паскале, изображенную на рис. 1, однако обсуждаемые концепции и подходы приложимы к компиляции программ и с других языков.
Для облегчения построения компиляторов язык высокого уровня обычно описывается в терминах некоторой грамматики. Эта грамматика определяет форму (синтаксис) допустимых предложений языка. Например, оператор присваивания может быть определен в грамматике как имя переменной, за которой следует оператор присваивания (:=), за которым следует выражение. Проблема компиляции может быть сформулирована как проблема поиска соответствия написанных программистом предложений структурам, определенным грамматикой, и генерации соответствующего кода для каждого предложения.
Курс молодого бойца в компилятор GCC (для языков СС++)
Предложения исходной программы удобнее представлять в виде последовательности лексем (tokens), чем просто как строку символов. Лексемы можно понимать как фундаментальные кирпичики, из которых строится язык. Например, лексемой может быть ключевое слово, имя целой переменной, арифметический оператор и т. д. Просмотр исходного текста, распознавание и классификация различных лексем называются лексическим анализом. Часть компилятора, которая выполняет эту функцию, обычно называют сканером.
Как только лексемы выделены, каждое предложение программы может быть распознано как некоторая конструкция языка, как, например, декларативные операторы или оператор присваивания, описанные с помощью грамматики. Процесс, называемый синтаксическим анализом или синтаксическим разбором, осуществляется той частью компилятора, которая обычно называется синтаксическим анализатором (parser). Последним шагом базовой схемы процесса трансляции является генерация объектного кода. Большинство компиляторов генерирует непосредственно программу в машинных кодах, а не программу на ассемблере, предназначенную для последующей трансляции в машинные коды.
Хотя мы указали на три основных шага в процессе компиляции—лексический анализ, синтаксический анализ и генерацию кодов,— важно отметить, что компилятор вовсе не обязан делать три просмотра транслируемой программы. Для некоторых языков возможна компиляция программы за один просмотр. В этом разделе мы обсудим, как может работать однопросмотровый компилятор. Однако компиляторы для других языков и компиляторы, осуществляющие изощренную оптимизацию объектного кода или какой-либо другой анализ программы, обычно являются многопросмотровыми.
Источник: knigechka.blogspot.com
001. Построение компилятора на базе LLVM — Павел Сычев
2. Назовите функции программы-компилятора.
А. Перевод в машинный код всей программы и создание нового файла, готового к исполнению.
В. Перевод в машинный код одного оператора программы.
С. Последовательный перевод в машинный код и исполнение каждого оператора программы.
D. Исполнение каждого оператора программы.
Перечислите основные классы системных программ.
Перечислите основные классы прикладных программ.
Что такое алгоритм?
Что такое машинный код?
Дайте характеристику языку программирования низкого уровня.
- Перечислите несколькоязыков программирования высокого уровня.
Тест № 7 (к теме 3.3. Раздел 3)
- Что такоекомпьютерный вирус?
- Что такоеархивация файлов?
- Назовите наиболее распространенные программыархивации и деархивации.
Источник: studfile.net
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
technosphere / compiler.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cannot retrieve contributors at this time
363 lines (281 sloc) 23.9 KB
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents Copy raw contents
Copy raw contents
Работа с компилятором
Компилятор GCC предоставляет множество опций командной строки. Полезно будет ознакомиться с основными из них. Список всех опций и их описание можно прочитать на сайте GCC.
Заметим что Clang во многом совместим с GCC и описанные здесь основные опции работают и с Clang (если явно не сказано обратное). У Clang также имеется подробная документация.
Функции драйвера компилятора
Программа gcc (а также g++ ) — это так называемый драйвер компилятора, т.е. обёртка, служащая для запуска других программ — компилятора в узком смысле (программы, «переводящей» другие программы с языка высокого уровня на язык ассемблера), ассемблера и компоновщика. По умолчанию g++ запускает все три программы (компилятор, ассемблер и компоновщик) последовательно. Таким образом, исполнение
g++ test.cpp
создаст сразу исполняемый файл с программой, исходный текст которой взят из файла test.cpp . Имя выходного файла по умолчанию — a.out . Для того чтобы задать произвольное имя выходного файла используется ключ -o :
g++ test.cpp -o test
Порядок ключей и имён файлов в большинстве случаев произвольный, т.е. ту же самую команду можно записать как
g++ -o test test.cpp
Для того чтобы получить файл с объектным кодом (т.е. пропустить программу последовательно через компилятор и ассемблер) служит ключ -c :
g++ -c test.cpp -o test.o
Если же требуется получить файл на языке ассемблера, следует использовать ключ -S :
g++ -S test.cpp -o test.s
Скомпоновать несколько объектных файлов в один исполняемый можно следующим образом:
g++ file1.o file2.o -o myprogram
При компоновке важно использовать правильный драйвер: g++ для программ, написанных на языке C++ и gcc для программ на языке C. От выбора драйвера зависит набор стандартных библиотек, который будет по умолчанию подключен к программе.
В операционной системе OS X за файлами gcc и g++ скрывается компилятор Clang, существенная часть которого разработана самой компанией Apple. До того как Clang был создан, в OS X использовался GCC. Чтобы не ломать сборку проектов, в которых явно указано имя файла gcc или g++ , инженеры Apple решили оставить файлы с такими именами.
Выбор языка и стандарта
Язык программы (C либо C++) определяется драйвером исходя из расширения файла и не зависит от того, какой драйвер ( gcc или g++ используется для компиляции). При необходимости язык можно задать явно (с помощью опции -x ), но лучше, разумеется, пользоваться общепринятыми расширениями файлов: .c для программ на языке C и .cpp (или .cc ) для языка C++.
Компилятор GCC поддерживает несколько стандартов языков C и C++ различающихся набором доступных возможностей языка. Версия стандарта задаётся при помощи ключа -std= , например:
g++ -c test.cpp -std=c++14
задаёт стандарт C++14. GCC поддерживает некоторые расширения языков C и C++ (их список весьма внушительный). Чтобы разрешить их использование, используются обозначение стандарта в котором c заменяется на gnu (а c++ на gnu++ ). Например:
gcc -c test.c -std=gnu11
запускает компиляцию с использованием стандарта C11 с включенными расширениями.
Пути к заголовочным файлам и макроопределния
Если вы используете в программе директиву препроцессора вида #include , компилятор должен каким-то образом найти в файловой системе заголовочный файл header.hpp . По умолчанию, GCC ищет файлы в системных путях, заданных ещё в процессе сборки самого компилятора, таких как /usr/include . Часто требуется добавить к ним дополнительные пути к исползуемым библиотекам. Для этого служит опция командной строки -I . Например:
g++ -c file.cpp -Ithird-party/include -Iinclude
добавляет к списку путей директории third-party/include и include (которые должны находиться в текущей директории).
Напомним, что при использовании директивы #include с кавычками, т.е. #include «header.hpp» для поиска также используется директория, в которой находится компилируемый файл.
В некоторых случаях поведением программы удобно урпавлять с помощью макросов. Например, библиотека glibc определяет макрос assert , поведение которого изменяется в зависимости от того, определён ли макрос NDEBUG . А именно, если NDEBUG определён, то assert никак не влияет на поведение программы. Если же он не определён, то assert проверяет, истинно ли значение, переданное ему в качестве аргумента, и если оно ложно, программа аварийно завершается. Такое поведение полезно при отладке, но в дистрибутиве программы, передаваемой пользователю, лишние проверки нежелательны, т.к. отрицательно сказываются на производительности программы. Поэтому при компиляции выпускаемой версии программы определяют макрос NDEBUG:
g++ -c program.cpp -DNDEBUG
Опция -D опредяет макрос NDEBUG так, как если бы в первой строке программы была директива
#define NDEBUG
С помощью опции -D можно также задать значение определяемого макроса:
g++ -c program.cpp -DVERSION=»1.1″
эквивалентно добавлению строки
#define VERSION «1.1»
в начало компилируемого файла.
Опции, влияющие на программы с неопределённым поведением
Отличительной чертой языков С и C++ является т.н. «неопределённое поведение»: стандарты этих языков подразумевают, что программист позаботится о том, чтобы в программе отсутствовали некоторые виды поведения, например, обращение к неинициализированным переменным. Компилятор, в свою очередь, может полагаться на это, поскольку стандарт не предъявляет к компилятору никаких требований относительно того, какой именно код должен сгенерировать компилятор при наличии в программе неопределённого поведения. Такое соглашение между авторами стандарта, авторами компилятора и программистами позволяет во многих случаях генерировать код с максимальным быстродействием. В то же время, обеспечить отсутствие в программе некоторых видов неопределённого поведения трудно, а задача проверки кода на отстуствие неопределённого поведения алгоритмически неразрешима. Поэтому авторы компилятора GCC добавили несколько опций, которые позволяют «доопределить» стандарты языков C и C++, придав некоторым случаям неопределённого поведения вполне конкретную семантику.
В частности, опция -fwrapv (от «wrap oVerflow») определяет, что арифметические операции над целыми числами со знаком должны выполняться с циклическим переполнением. К примеру, согласно стандарту C++, в следующем примере
int x; // . if (x + 1 > x) < // . >
переполнение при вычислении x + 1 является неопределённым поведением, поэтому компилятор может предположить, что этого никогда не произойдёт, а условие x + 1 > x всегда истинно и удалить код, отвечающий за проверку этого условия. Однако, если задать опцию -fwrapv компилятор будет основываться на том, что переполнение циклическое, поэтому x + 1 может быть отрицательным (например, при 32-битном int , 0x7FFFFFFF + 1 равно 0x80000000 или -2147483648 ) и не будет удалять проверку.
Ещё одно проблематичное для многих правила языков C и C++ — это правило о строгом соответствии псевдонимов (strict aliasing) и связанная с ним оптимизация — анализ псевдонимов, основанный на типах (type based alias analysis). Согласно стандартам языков C и C++, через указатель на тип T допускается получать доступ только к значениям типа T . Исключение составляют указатели на символьный тип ( char , signed char и unsigned char ), т.к. через них можно считывать значения других типов. Например, следующий код не вызывает неопределённого поведения:
int x = 5; const char* p = (char *) return *p;
а вот этот пример — вызывает:
double x = 1.0; return *(int *)(
За счёт данного правила компилятор может в следующем фрагменте кода
int foo(int* x, double* y) < y[0] = x[0]; return x[0]; >
считать, что присваивание y[0] = x[0] не меняет значения x[0] (поскольку указатели x и y различаются) и выполнить одну операцию чтения из памяти вместо двух.
Ключ -fno-strict-aliasing отключает данный вид анализа, т.е. заставляет компилятор предполагать, что любые два указателя могут быть псевдонимами друг друга, если обратное не следует, например, из правил адресной арифметики.
Предупреждения компилятора сообщают программисту о найденных в программе потенциальных проблемах — конструкциях, хотя и разрешённых стандартом языка, но могут свидетельствовать о наличии в программе ошибки.
Рассмотрим такой пример:
if (x = 0) < std::cout «x is zero!»; >
Ошибка здесь заключается в том, что вместо оператора сравнения == был использован оператор присваивания = . Условие в if всегда ложно и поэтому строка «x is zero!» никогда не будет выведена. Несмотря на явную ошибку, этот код абсолютно корректен с точки зрения стандарта C++. Если мы попробуем скомпилировать его с включенными предупреждениями
g++ -с -Wall test.cpp
то получим сообщение от компилятора:
test.cc: In function ‘int main()’: test.cc:6:11: warning: suggest parentheses around assignment used as truth value [-Wparentheses] if (x = 0) < ^
GCC сообщает нам, что если мы действительно не имели в виду сравнение, а хотим использовать результат присваивания в качестве булева значения, то следует заключить его в скобки.
Большинство предупреждений имеют отдельный ключ командандной строки, который отвечает только за данное предупреждение. Также существуют ключи, активирующие сразу целую группу предупреждений.
Так, предупреждения, которые с высокой вероятностью вызваны ошибкой в программе активируются опцией -Wall . Предупреждения, активируемые опцией -Wextra , могут в некоторых случаях вызывать ложно-положительные срабатывания, но в большинстве случаях также полезны. Рекомендуется использовать как -Wall , так и -Wextra в своих проектах.
Наконец, предупреждения, не включенные ни в одну из этих групп, могут быть полезны в зависимости от стандартов кодирования, применямых в конкретном проекте. Например, разработчики проекта могут договориться о том, чтобы всегда помечать перекрываемые виртуальные методы с помощью ключевого слова override . В этом случае полезно будет задействовать опцию -Wsuggest-override (данная опция присутствует только в GCC начиная с версии 5), чтобы компилятор выдывал предупреждение о виртуальных методах, в объявлении которых ключевое слово override отсутствует.
Оптимизатор присутствует в большинстве современных компиляторов С и C++, таких как GCC, Clang и MSVC. Функция оптимизатора заключается в том, чтобы преобразовать исходную программу в эквивалентную ей семантически, но работающую быстрее. Оптимизатор GCC организован в виде конвеера, состоящего из т.н. проходов.
Каждый проход выполняет некоторый вид преобразований программы, хранящейся в памяти в виде промежуточного представления. Это представление последовательно подаётся на вход каждого из проходов (этапов конвеера). Как и предпреждения, оптимизации объединены в группы, активируемые различными ключами командной строки.
Так, ключ -O1 (либо -O ) включает наиболее простые и быстро выполняемые оптимизации, -O2 выполняет также и более «дорогие» оптимизации. Наконец, в -O3 входят сложные оптимизации, порой имеющие квадратичное (по количеству кода в отдельной функции программы) время работы. Ключ -Os настраивает оптимизатор так, чтобы генерировать код меньшего размера (иногда в ущерб быстродействию). Такой режим полезен при компиляции программ, предназначенных для встраиваемых систем (например, микроконтроллеров).
Чтобы получить общее представление о том, как работает оптимизатор, рассмотрим в качестве примера один из проходов оптимизатора — встраивание функций (inlining). Встраивание выполняет подстановку тела функции в место её вызова. Например, такой код:
int add2(int x) < return x + x; > int add3(int x) < return x + add2(x); >
Можно преобразовать в эквивалентный:
int add2(int x) < return x + x; > int add3(int x) < return x + x + x; >
тем самым сэкономив в функции add3 время на копировании аргументов функции add2 , её вызове и возврате из функции. Повторимся: оптимизатор работает с промежуточным представлением программы, а не с исходным кодом, поэтому данный пример, разумеется, условный. Кроме того, встраивание на самом деле пытается скопировать код функции без изменений, т.е. результат будет больше похож на
int add3(int x) < int __add2_x = x; int __add2_result = __add2_x + __add2_x; return x + __add2_result; >
От лишних операций копирования помогут избавиться последующие этапы конвеера: проход распространения констант и копий (constant and copy propagation), а также проход устранения мёртвого кода (dead code elimination). Как видно из этого примера, выполнение одной оптимизации может предоставить возможность выполнить другие, поэтому конвеер оптимизатор устроен достаточно сложно: некоторые проходы выполняются несколько раз, а их порядок тщательно выверен.
Оптимизация позволяет значительно увеличить производительность программы: зачастую оптимизированный код выполняется в 1,5-3 раза быстрее неоптимизированного. Разумеется оптимизации не бесплатны: компиляция программы с включённой оптимизацией выполняется медленнее. Ещё один недостаток оптимизированного кода — меньшее удобство в отладке (например, в отладчике может быть недоступен просмотр значений некоторых переменных). Частично этот недостаток устраняется опцией -Og (доступна только в GCC): она выключает те проходы оптимизатора, которые отрицательно сказываются на качестве отладочной информации. Производительность кода с этой опцией примерно соответствует производительности -O1 .
Отладочная информация содержит соответствия между элементами бинарного кода (адреса, регистры процессора) и исходного кода (номера строк, имена функций и переменных) программы. Запись отладочной информации в генерируемые компилятором файлы включается с помощью ключа -g . Ключ -ggdb3 включает вывод дополнительной информаций, специфичной для отладчика GDB. Подробнее об отладке (и отладочной информации) можно прочитать в статье Отладка программ с помощью GDB.
Источник: github.com