Gcc — Си Крашится на операторе return! И GCC на Linux лучше

Содержание

Портирование проекта на GCC+Linux

Начал портировать код OpenGL проектов на Linux. Выбрал Eclipse CDT, по умолчанию под линуксом у меня gcc 4.2.1. О gcc наслышан как о компиляторе который очень хорошо следует стандарту. Но к нему есть большие претензии.

1) memcmp() возвращает -1 при несовпадении блоков и 1 при совпадении. Проверял поля структур в дебаге — всё верно, 1 при совпадении. Поправьте меня, но кажется memcmp() как strcmp() должны в этом случае возвращать 0? Или нужно использовать какой-то dependent token?

Этот код не компилируется. Спасает this->a или если я правильно догадываюсь о стандарте С++, A ::a.
Fucked C++ standart? Мне всю мат. библиотеку придётся корёжить!

Этот код не компилируется. Спасает typedef как подсмотрено в местном STL.
Fucked C++ standart или примочки gcc?

4)
Компилятор просто не видит один из токенов OpenGL. Он есть в хидере, причём компилируется соседний с ним же токен. А этот — не хочет. Правда он длинноват, GL_TRANSFORM_FEEDBACK_ и т. д. Вставил препроцессор и заменил числом из хидера. Таких проблем на MSC никогда не было.

5) Нельзя передавать в макрос меньше параметров, чем объявлено аргументов. MSC это разрешает, за что ему огромное спасибо. gcc ругается.
Ну да это я могу понять, хотя это самое большое разочарование.

6) -xc++ — это правильный способ указания precompiled header? Аналог #pragma hdrstop, или предкомпилированный заголовок в gcc никогда не расширяется?

1) очень сомнительно, честно говоря

2) google two-phase name lookup. Это С++.

Компилируется с -pedantic.

6) -xc++ — это всего лишь явное указание языка (без этого ключа файлы с расширением .c будут компплироваться как С, а не С++)

Zeux
1) Хорошо проверю дополнительно.
2) Это fucked C++. Я понимаю почему MS не следует стандарту а идёт навстречу программистам. Представь что у меня vec2 наследуется от vec . В vec объявлен компонент x, в vec2 — y. Мне придётся захламлять код кучей this-> чтобы эта конструкция заработала. Впрочем, эту проблему обойти можно небольшим хаком.
3) Попробую.
6) Понятно, неправильно разобрал «To create a precompiled header file, simply compile it as you would any other file, if necessary using the -x option to make the driver treat it as a C or C++ header file.» А как тогда указать, что нужно создать precompiled header? Я нашёл такое описание:

A precompiled header file will be searched for when #include is seen in the compilation. As it searches for the included file (see Search Path) the compiler looks for a precompiled header in each directory just before it looks for the include file in that directory. The name searched for is the name specified in the #include with `.gch’ appended. If the precompiled header file can’t be used, it is ignored.

Т. е. для pch.h я должен вручную создать в папке пустой фаил pch.h.gch и тогда компилятор распознает его как precompiled? Как-то размыто описано.

Джо
а g++ -o pch.h.gch pch.h не работает?

$tatic
пишет
g++: pch.h: No such file or directory
я тут погуглил, у многих была такая проблема. значит что-то делается неправильно.
может нужно писать мэйкфаил вручную?

Джо
компилятор gcc 4.3.1 из OpenSuSE 11.0:
2) не компилируется, спасает A ::x, пробовал некоторые ключи совместимости — не помогло;
3) компилируется тупым g++ -c main.cpp безо всяких ключей;
4) не проверял в своей версии, но видимо баг конкретной версии;

6) значит делаешь неправильно. Я написал небольшой пример с файлами main.c, foobar.c, foobar.h и pch.h (инклюдящий stdio.h).
В директории проекта предкомпилировал pch.h командой

При этом gcc по умолчанию создаёт файл pch.h.gch (ключом -o естественно можно переназначить, но не имеет смысла). Затем я для чистоты эксперимента закомментировал инклюд в pch.h и тем не менее зависящие от него main.c и foobar.c компилировались.
Если у тебя не находит файл, то проверь текущую директорию командой pwd или убедись в существовании файла pch.h :)

ЗЫ. Если хочешь — напиши минитесты к пунктам 1 и 4, а я проверю. Просто сейчас голова не тем занята :(

Джо
Дошли руки проверить 1)
Скомпилировал в линуксе пример из MSDN — всё естественно работает как надо: 0 при равенстве, 0 если первый блок больше второго. Как это и следует из стандарта. Так что это ты что-то путаешь.

$tatic
сделал в еклипсе тестовый проект

#ifndef PCH_H_
#define PCH_H_

#warning Pch compilation!

extern void print();

int main()
<
puts( «Hello world.» );
print();
return 0;
>

void print()
<
puts( «Hello world 2» );
>

открыл из папки терминал набрал gcc Pch.h — получил Pch.h.gch
но мне нужно чтобы я в эклипсе смог задать прекомпиляцию т к эти настройки должны сохраняться вместе с распространением проекта.
В итоге всё равно запустил проект на компиляцию и получил три #warning Pch compilation! Т е предкопилированный заголовок реально не используется (хотя в проекте gch файл отображается)

Почему-то все всегда ругают GCC, который всё правильно имплементит, а не MSVC, который имплементит язык, значительно отличающийся от стандартного C++ 2003. Ну а пункт 1 ты просто недоисследовал, и поспешил обвинить рантайм в невозможных грехах.

Джо
с эклипсом мне работать не довелось, предполагаю, что он использует automake. Действительно, gcc в этом случае не захватывает gch-файл. Я не понял пока причину этого.
Если собирать «ручками», то всё выполняется как положено:

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

cppguru
Из-за этого мне пришлось захламить код мат. библиотеки кучей this->x вместо x. А там я считаю код должен быть очень чистым и ясным. Подозреваю MS специально отошла от этого пункта в стандарте чтобы программисты меньше матерились. Спасибо zeux за подсказку про two-phase name lookup я ещё как-нить почитаю подробнее про это и сделаю свои выводы о величине маразма разработчиков стандарта С++. Пункт номер три это тоже ещё тот геморрой. У меня кода такого вида мало, поэтому можно стерпеть. Зато у gсс-вского STL в коде куча локальных тайпдефов для обхода этого маразма. Коды STL это вообще говоря стыд с точки зрения написания читаемых программ, и пару сотен лишних typedef-ов делают его полным уродом окончательно — спасибо С++.

Делать нечего бояре, код желательно должен быть портируемым, и я сейчас этим занимаюсь. Но отношение к C++ у меня подпорчено (MSC С++ до этого очень нравился).

Пункт номер 5. Ну передаю я меньше параметров чем нужно — ну будь человеком, игнорируй остальные аргументы и не вставляй их в код. Нет, в стандарте нужно чётко прописать соотвествие кол-ва аргументов и параметров! У меня был специальный макрос __m() для решения проблем в code style-ом, теперь всё это вылетело в трубу :(

Сравнивалась структура, передаваемая по константной ссылке с локальной структурой метода:

Ну и precompiled headers. Студия тесно интегрирована с собственным компилятором, поэтому все опции к нему можно легко задать в свойствах проекта. IDE второго эшелона используют standalone компиляторы поэтому неудивительно что для того чтобы задать какую-то элементарную вещь надо перечитать тонну мануалов прежде чем найдёшь те три строчки что тебе нужны. Грустно. Я гуглил как мог но нигде не нашёл чёткого объяснения, как непосредственно их эклипса задать pch. Везде у людей No such file or directory хотя pch.h лежит рядом с файлом проекта.

> Я в дебаге проверял поля структур — всё было идентично. Надо попробовать разные варианты. Как доберусь, отпишу.
Равенство всех полей не означает равенство с т.з. memcmp — т.к. заполнение паддинга может быть разное. ну и если не дай бог vfptr и типы разные, то тоже.

Малоизвестные полезные параметры компилятора GCC — часть 2

Оригинал: Uncommon but useful GCC command line options — part 2
Автор: Himanshu Arora
Дата публикации: 1 декабря 2020 г.
Перевод: А.Панин
Дата перевода: 8 февраля 2020 г.

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

В рамках данной серии статей мы рассматриваем некоторые из этих малоизвестных, но полезных параметров командной строки компилятора GCC и уже обсудили пару таких параметров в первой статье.

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

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

Но перед тем, как перейти к рассмотрению обозначенных выше вопросов, следует упомянуть о том, что все примеры, команды и инструкции из данной статьи были протестированы в системе Ubuntu 16.04 LTS с компилятором GCC версии 5.4.0.

Активация вывода предупреждений, не связанных с параметром -Wall

Хотя параметр командной строки -Wall и сообщает компилятору GCC о необходимости вывода подавляющего большинства предупреждений, некоторые предупреждения не выводятся даже при использовании данного параметра. Для их вывода следует использовать параметр -Wextra .

В качестве примера предлагаю рассмотреть следующий код:

Я случайно разместил символ точки с запятой после условной инструкции if . При последующей компиляции кода с помощью приведенной ниже команды компилятор GCC не выведет каких-либо предупреждений:

А теперь рассмотрим случай использования параметра -Wextra :

В этом случае будет выведено аналогичное предупреждение:

Из приведенного выше текста предупреждения очевидно, что использование параметра командной строки -Wextra привело к активации флага компилятора -Wempty-body , в результате чего был выявлен подозрительный фрагмент кода и выведено соответствующее предупреждение. А это полный список флагов предупреждений, активируемых с помощью рассматриваемого параметра командной строки: -Wclobbered , -Wempty-body , -Wignored-qualifiers , -Wmissing-field-initializers , -Wmissing-parameter-type (только для языка C), -Wold-style-declaration (только для языка C), -Woverride-init , -Wsign-compare , -Wtype-limits , -Wuninitialized , -Wunused-parameter (только при использовании с -Wunused или -Wall ) и -Wunused-but-set-parameter (только при использовании с -Wunused или -Wall ).

Если вас интересуют подробные описания упомянутых флагов, вы можете обратиться к странице руководства компилятора GCC .

Кроме того, параметр командной строки -Wextra позволяет компилятору выводить предупреждения в следующих случаях:

  • Указатель сравнивается с целочисленным нулевым значением с помощью оператора или >=.
  • Значения из перечисления и не из перечисления встречаются в одной условной инструкции (только в C++).
  • Отсутствие виртуального наследования от виртуального базового класса (только в C++).
  • Доступ к элементам регистрового массива (только в C++).
  • Получение адреса регистровой переменной (только в C++).
  • Отсутствие инициализации базового класса в рамках конструктора копирования наследуемого класса (только в C++).

Активация предупреждений, связанных со сравнениями значений с плавающей точкой

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

Это код, в котором осуществляется проверка равенства значений переменных с плавающей точкой с помощью оператора == :

А это команда компиляции данного кода с помощью компилятора GCC (содержащая как параметр -Wall , так и параметр -Wextra ):

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

А это команда с данным параметром:

Данная команда позволяет сгенерировать аналогичный вывод:

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

А это выдержка из описания данного параметра на странице руководства компилятора GCC :

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

Оптимальная работа с параметрами командной строки компилятора GCC

Если количество параметров командной строки компилятора GCC в вашем проекте стало настолько большим, что вам неудобно ими управлять, вы можете разместить эти параметры в текстовом файле и передать компилятору имя этого текстового файла в качестве параметра командной строки. Для этой цели должен использоваться специальный параметр командной строки @file .

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

То вы можете разместить три связанных с выводом предупреждений параметра в файле с именем, таким, как gcc-options :

После чего ваша команда компиляции станет более простой и доступной для редактирования:

А это выдержка из описания параметра @file со страницы руководства компилятора GCC:

Чтение параметров командной строки из файла. Параметры читаются и подставляются вместо параметра @file . Если файла с переданным именем не существует или этот файл не может быть прочитан, параметр будет обработан таким же образом, как и все другие параметры, а не удален.

Параметры в файле должны разделяться с помощью символов пробелов. Символ пробела может быть включен в состав параметра путем помещения этого параметра в одинарные или двойные кавычки. Любой символ (включая обратный слэш) может быть включен в состав параметра путем помещения перед этим символом обратного слэша. Сам файл может содержать дополнительные параметры @file ; все эти параметры будут рекурсивно обрабатываться.

Заключение

На данный момент мы рассмотрели пять малоизвестных полезных параметров компилятора GCC: -save-temps , -g , -Wextra , -Wfloat-equal и @file . Потратьте немного своего времени на их испытание и не забудьте прочитать описание каждого из них на странице руководства компилятора GCC.

Вам знакомы другие подобные параметры командной строки компилятора GCC и вы желаете рассказать о них? Вы можете разместить любую информацию об этих параметрах в разделе комментариев.

Gcc — Си: Крашится на операторе return! И GCC на Linux лучше?

Чтобы сразу начать программировать, создадим еще один клон известной программы «Hello World». Что делает эта программа, вы знаете. Откройте свой любимый текстовый редактор и наберите в нем следующий текст:

Я назвал свой файл hello.c. Вы можете назвать как угодно, сохранив суффикс .c. Содержимое файла hello.c — это исходный код программы (‘program source’, ‘source code’ или просто ‘source’). А hello.c — это исходный файл программы (‘source file’). Hello World — очень маленькая программа, исходный код которой помещается в одном файле. В «настоящих» программах, как правило, исходный код разносится по нескольким файлам. В больших программах исходных файлов может быть больше сотни.

Наш исходный код написан на языке программирования C. Языки программирования были придуманы для того, чтобы программист мог объяснить компьютеру, что делать. Но вот беда, компьютер не понимает ни одного языка программирования. У компьютера есть свой язык, который называют машинным кодом или исполняемым кодом (‘executable code’). Написать Hello World в машинном коде можно, но серьезные программы на нем не пишутся. Исполняемый код не только сложный по своей сути, но и очень неудобный для человека. Программа, которую можно написать за один день на языке программирования будет писаться целый год в машинном коде. Потом программист сойдет с ума. Чтобы этого не случилось, был придуман компилятор (‘compiler’), который переводит исходный код программы в исполняемый код. Процесс перевода исходного кода программы в исполняемый код называют компиляцией.

Чтобы откомпилировать наш Hello World достаточно набрать в командной строке следующее заклинание: Если исходный код написан без синтаксических ошибок, то компилятор завершит свою работу без каких-либо сообщений. Молчание — знак повиновения и согласия. Набрав команду ls вы тут же обнаружите новый файл с именем hello. Этот файл содержит исполняемый код программы. Такие файлы называют исполняемыми файлами (‘executable files’) или бинарниками (‘binary files’).

Вы наверняка догадались, что опция -o компилятора gcc указывает на то, каким должно быть имя выходного файла. Как вы позже узнаете, выходным файлом может быть не только бинарник. Если не указать опцию -o, то бинарнику, в нашем случае, будет присвоено имя a.out.

Осталось только запустить полученный бинарник. Для этого набираем в командной строке следующую команду:

Когда мы набираем в командной строке путь к бинарнику, мы, в реальности сообщаем оболочке, что надо выполнить программу. Оболочка «передает» бинарник ядру операционной системе, а ядро системы особым шаманским способом отдает программу на выполнение процессору. Затем, если программа не была запущена в фоновом режиме, то оболочка ждет от ядра сообщения о том, что программа выполнилась. Получив такое сообщение, оболочка выдает приглашение на ввод новой команды. Вы можете еще раз набрать ./hello и процедура повторится. В нашем случае программа выполняется очень быстро, и новое приглашение командной строки «вылетает» практически сразу.

Мы рассмотрели идеальный случай, когда программа написана без синтаксических ошибок. Попробуем намеренно испортить программу таким образом, чтобы она не отвечала канонам языка C. Для этого достаточно убрать точку с запятой в конце вызова функции printf(): Теперь, если попытаться откомпилировать программу, то компилятор выругается, указав нам на то, что он считает неправильным: В первой строке говорится, что в файле hello.c (у нас он единственный) в теле функции main() что-то произошло. Вторая строка сообщает, что именно произошло: седьмая строка файла hello.c вызвала ошибку (error). Далее идет расшифровка: синтаксическая ошибка перед закрывающейся фигурной скобкой.

Заглянув в файл hello.c мы с удивлением обнаружим, что нахулиганили мы не в седьмой, а в шестой строке. Дело в том, что компилятор обнаружил нелады только в седьмой строке, но написал ‘before’ (до), что означает «прокручивай назад».

Естественно, пока мы не исправим ошибку, ни о каком бинарнике не может идти и речи. Если мы удалим старый бинарник hello, доставшийся нам от прошлой компиляции, то увидим, что компиляция испорченного файла не даст никакого результата. Однако иногда компилятор может лишь «заподозрить» что-то неладное, потенциально опасное для нормального существования программы. Тогда вместо ‘error’ пишется ‘warning’ (предупреждение), и бинарник все-таки появляется на свет (если в другом месте нет явных ошибок). Не следует игнорировать предупреждения, за исключением тех случаев, когда вы на 100% знаете, что делаете.

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

2.2. Мультифайловое программирование

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

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

Давайте сначала разберемся, как из исходного файла получается бинарник. Подобно тому как гусеница не сразу превращается в бабочку, так и исходный файл не сразу превращается в бинарник. После компиляции создается объектный код. Это исполняемый код с некоторыми «вкраплениями», из-за которых объектный код еще не способен к выполнению. Сразу в голову приходит стиральная машина: вы ее только что купили и она стоит у вас дома в коробке. В таком состоянии она стирать не будет, но вы все равно рады, потому что осталось только вытащить из коробки и подключить.

Вернемся к объектному коду. Эти самые «вкрапления» (самое главное среди них — таблица символов) позволяют объектному коду «пристыковываться» к другому объектному коду. Такой фокус делает компоновщик (линковщик) — программа, которая объединяет объектный код, полученный из «разных мест», удаляет все лишнее и создает полноценный бинарник. Этот процесс называется компоновкой или линковкой.

Итак, чтобы откомпилировать мультифайловую программу, надо сначала добыть объектный код из каждого исходного файла в отдельности. Каждый такой код будет представлять собой объектный модуль. Каждый объектный модуль записывается в отдельный объектный файл. Затем объектные модули надо скомпоновать в один бинарник.

Цукерберг рекомендует:  Fps - Геймер, ускорение системы, повышение FPS

В Linux в качестве линковщика используется программа ld, обладающая приличным арсеналом опций. К счастью gcc самостоятельно вызывает компоновщик с нужными опциями, избавляя нас от «ручной» линковки.

Попробуем теперь, вооружившись запасом знаний, написать мультифайловый Hello World. Создадим первый файл с именем main.c: Теперь создадим еще один файл hello.c со следующим содержимым:

Здесь функция main() вызывает функцию print_hello(), находящуюся в другом файле. Функция print_hello() выводит на экран заветное приветствие. Теперь нужно получить два объектных файла. Опция -c компилятора gcc заставляет его отказаться от линковки после компиляции. Если не указывать опцию -o, то в имени объектного файла расширение .c будет заменено на .o (обычные объектные файлы имеют расширение .o): Итак, мы получили два объектных файла. Теперь их надо объединить в один бинарник: Компилятор «увидел», что вместо исходных файлов (с расширением .c) ему подбросили объектные файлы (с расширением .o) и отреагировал согласно ситуации: вызвал линковщик с нужными опциями.

Давайте разберемся, что же все-таки произошло. В этом нам поможет утилита nm. Я уже оговорился, что объектные файлы содержат таблицу символов. Утилита nm как раз позволяет посмотреть эту таблицу в читаемом виде. Те, кто пробовал программировать на ассемблере знают, что в исполняемом файле буквально все (функции, переменные) стоит на своей позиции: стоит только вставить или убрать из программы один байт, как программа тут же превратиться в груду мусора из-за смещенных позиций (адресов). У объектных файлов особая роль: они хранят в таблице символов имена некоторых позиций (глобально объявленных функций, например). В процессе линковки происходит стыковка имен и пересчет позиций, что позволяет нескольким объектным файлам объединиться в один бинарник. Если вызвать nm для файла hello.o, то увидим следующую картину: О смысловой нагрузке нулей и литер U,T мы будем говорить при изучении библиотек. Сейчас же важным является то, что в объектном файле сохранилась информация об использованных именах. Своя информация есть и в файле main.o: Таблицы символов объектных файлов содержат общее имя print_hello. В процессе линковки высчитываются и подставляются в нужные места адреса, соответствующие именам из таблицы. Вот и весь секрет.

2.3. Автоматическая сборка

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

Выход из сложившейся ситуации есть. Это утилита make, которая работает со своими собственными сценариями. Сценарий записывается в файле с именем Makefile и помещается в репозиторий (рабочий каталог) проекта. Сценарии утилиты make просты и многофункциональны, а формат Makefile используется повсеместно (и не только на Unix-системах). Дошло до того, что стали создавать программы, генерирующие Makefile’ы. Самый яркий пример — набор утилит GNU Autotools. Самое главное преимущество make — это «интеллектуальный» способ рекомпиляции: в процессе отладки make компилирует только измененные файлы.

То, что выполняет утилита make, называется сборкой проекта, а сама утилита make относится к разряду сборщиков.

Любой Makefile состоит из трех элементов: комментарии, макроопределения и целевые связки (или просто связки). В свою очередь связки состоят тоже из трех элементов: цель, зависимости и правила.

Сценарии make используют однострочные комментарии, начинающиеся с литеры # (решетка). О том, что такое комментарии и зачем они нужны, объяснять не буду.

Макроопределения позволяют назначить имя практически любой строке, а затем подставлять это имя в любое место сценария, где должна использоваться данная строка. Макросы Makefile схожи с макроконстантами языка C.

Связки определяют: 1) что нужно сделать (цель); 2) что для этого нужно (зависимости); 3) как это сделать (правила). В качестве цели выступает имя или макроконстанта. Зависимости — это список файлов и целей, разделенных пробелом. Правила — это команды передаваемые оболочке.

Теперь рассмотрим пример. Попробуем составить сценарий сборки для рассмотренного в предыдущем разделе мультифайлового проекта Hello World. Создайте файл с именем Makefile: Обратите внимание, что в каждой строке перед вызовом gcc, а также в строке перед вызовом rm стоят табуляции. Как вы уже догадались, эти строки являются правилами. Формат Makefile требует, чтобы каждое правило начиналось с табуляции. Теперь рассмотрим все по порядку.

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

Первая строка — комментарий. Здесь можно писать все, что угодно. Комментарий начинается с символа # (решетка) и заканчивается символом новой строки. Далее по порядку следуют четыре связки: 1) связка для компоновки объектных файлов main.o и hello.o; 2) связка для компиляции main.c; 3) связка для компиляции hello.c; 4) связка для очистки проекта.

Первая связка имеет цель hello. Цель отделяется от списка зависимостей двоеточием. Список зависимостей отделяется от правил символом новой строки. А каждое правило начинается на новой строке с символа табуляции. В нашем случае каждая связка содержит по одному правилу. В списке зависимостей перечисляются через пробел вещи, необходимые для выполнения правила. В первом случае, чтобы скомпоновать бинарник, нужно иметь два объектных файла, поэтому они оказываются в списке зависимостей. Изначально объектные файлы отсутствуют, поэтому требуется создать целевые связки для их получения. Итак, чтобы получить main.o, нужно откомпилировать main.c. Таким образом файл main.c появляется в списке зависимостей (он там единственный). Аналогичная ситуация с hello.o. Файлы main.c и hello.c изначально существуют (мы их сами создали), поэтому никаких связок для их создания не требуется.

Особую роль играет целевая связка clean с пустым списком зависимостей. Эта связка очищает проект от всех автоматически созданных файлов. В нашем случае удаляются файлы main.o, hello.o и hello. Очистка проекта бывает нужна в нескольких случаях: 1) для очистки готового проекта от всего лишнего; 2) для пересборки проекта (когда в проект добавляются новые файлы или когда изменяется сам Makefile; 3) в любых других случаях, когда требуется полная пересборка (напрмиер, для измерения времени полной сборки).

Теперь осталось запустить сценарий. Формат запуска утилиты make следующий: Опции make нам пока не нужны. Если вызвать make без указания целей, то будет выполнена первая попавшаяся связка (со всеми зависимостями) и сборка завершится. Нам это и требуется: В процессе сборки утилита make пишет все выполняемые правила. Проект собран, все работает.

Теперь давайте немного модернизируем наш проект. Добавим одну строку в файл hello.c: Теперь повторим сборку: Утилита make «пронюхала», что был изменен только hello.c, то есть компилировать нужно только его. Файл main.o остался без изменений. Теперь давайте очистим проект, оставив одни исходники: В данном случае мы указали цель непосредственно в командной строке. Так как целевая связка clean содержит пустой список зависимостей, то выполняется только одно правило. Не забывайте «чистить» проект каждый раз, когда изменяется список исходных файлов или когда изменяется сам Makefile.

2.4. Модель КИС

Любая программа имеет свой репозиторий — рабочий каталог, в котором находятся исходники, сценарии сборки (Makefile) и прочие файлы, относящиеся к проекту. Репозиторий рассмотренного нами проекта мультифайлового Hello World изначально состоит из файлов main.c, hello.c и, собственно, Makefile. После сборки репозиторий дополняется файлами main.o, hello.o и hello. Практика показывает, что правильная организация исходного кода в репозитории не только упрощает модернизацию и отладку, но и предотвращает возможность появления многих ошибок.

Модель КИС (Клиент-Интерфейс-Сервер) — это элегантная концепция распределения исходного кода в репозитории, в рамках которой все исходники можно поделить на клиенты, интерфейсы и серверы.

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

Часто бывает, что клиент сам становится сервером, точнее начинает играть роль промежуточного сервера. Хороший пример — наш мультифайловый Hello World. Здесь функция print_hello() (клиент) пользуется услугами стандартной библиотеки языка C (сервер), вызывая функцию printf(). Однако в дальнейшем функция print_hello() сама становится сервером, предоставляя свои услуги функции main(). В языке C++ довольно часто клиент создает производный класс, который наследует некоторые механизмы базового класса сервера. Таким образом клиент сам становится сервером, предоставляя услуги своего производного класса.

Клиент с сервером должны «понимать» друг друга, иначе взаимодействие невозможно. Интерфейс (протокол) — это условный набор правил, согласно которым взаимодействуют клиент и сервер. В нашем случае (мультифайловый Hello World) интерфейсом (протоколом) является общее имя в таблице символов двух объектных файлов. Такой способ взаимодействия может привести к неприятным последствиям. Клиент (функция main()) не знает ничего, кроме имени функции print_hello() и, наугад вызывает ее без аргументов и без присваивания. Иначе говоря, клиент не знает до конца правил игры. В нашем случае прототип функции print_hello() неизвестен.

Обычно для организации интерфейсов используются объявления (прототипы), которые помещаются чаще всего в заголовочные файлы. В языке C это файлы с расширением .h; в языке C++ это файлы с раширением .h, .hpp или без расширения. Некоторые «всезнайки» ошибочно называют заголовочные файлы библиотеками и умудряются учить этому других. Забегая вперед скажу, что библиотека — это просто коллекция скомпонованных особым образом объектных файлов, а заголовочный файл — это интерфейс. Основная разница между библиотеками и заголовочными файлами в том, что библиотека — это объектный (почти исполняемый) код, а заголовочный файл — это исходный код. Включая в программу заголовочный файл директивой #include мы соглашаемся работать с сервером (будь то библиотека или простой объектный файл) по его протоколу: если сервер сказал, что функция вызывается без аргументов, то она и будет вызываться без аргументов, иначе компилятор костьми ляжет, но не даст откомпилировать «незаконный вызов».

Незамысловатый блог

03.12.2010

О GCC, компиляции и библиотеках

GCCGNU Compiler Collection — набор компиляторов и сопутствующих утилит, разработанный в рамках движения GNU. GCC один из старейших Open Source проектов, первый релиз состоялся в 1985 году, автор сам Ричард Столлман. В исходном варианте поддерживал только язык C и аббревиатура GCC расшифровывалась как GNU C Compiler. Постепенно набор доступных языков расширялся, были добавлены компиляторы Fortran, C++, Ada. С уверенностью можно сказать, что современный мир Open Source обязан своим рождением GCC (по крайней мере без GCC он был бы другим). В настоящее время проект находиться под крылом Free Software Foundation. GCC выпускается под лицензией GPLv3 и является стандартным компилятором для большинства свободных UNIX-подобных операционных систем. В базовый набор входят компиляторы языков: C, C++, Objective-C, Java, Fortran, Ada. GCC поддерживает все основные процессорные архитектуры. Официальный сайт проекта gcc.gnu.org

Основы

GCC входит в состав любого дистрибутива Linux и, как правило, устанавливается по умолчанию. Интерфейс GCC, это стандартный интерфейс компилятора на UNIX платформе, уходящий своими корнями в конец 60-х, начало 70-х годов прошлого века — интерфейс командной строки. Не стоит пугаться, за прошедшее время механизм взаимодействия с пользователем был отточен до возможного в данном случае совершенства, и работать с GCC (при наличии нескольких дополнительных утилит и путного текстового редактора) проще, чем с любой из современных визуальных IDE. Авторы набора постарались максимально автоматизировать процесс компиляции и сборки приложений. Пользователь вызывает управляющую программу gcc, она интерпретирует переданные аргументы командной строки (опции и имена файлов) и для каждого входного файла, в соответствии с использованным языком программирования, запускает свой компилятор, затем, если это необходимо, gcc автоматически вызывает ассемблер и линковщик (компоновщик).

Любопытно, компиляторы одни из немногих приложений UNIX для которых не безразлично расширение файлов. По расширению GCC определяет что за файл перед ним и, что с ним нужно (можно) сделать. Файлы исходного кода на языке C должны иметь расширение .c , на языке C++, как вариант, .cpp , заголовочные файлы на языке C .h , объектные файлы .o и так далее. Если использовать неправильное расширение, gcc будет работать не корректно (если вообще согласиться, что-либо делать).

Перейдём к практике. Напишем, откомпилируем и исполним какую-нибудь незамысловатую программу. Не будем оригинальничать, в качестве исходного файла примера программы на языке C сотворим файл с вот таким содержимым:

printf( «Hello World \n » );

Теперь в каталоге c hello.c отдадим команду:

Через несколько долей секунды в каталоге появиться файл a.out :

Это и есть готовый исполняемый файл нашей программы. По умолчанию gcc присваивает выходному исполняемому файлу имя a.out (когда-то очень давно это имя означало assembler output).

Запустим получившийся программный продукт:

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

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

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

Почему не рекомендуется вносить . в PATH ? Считается, что в реальной многопользовательской системе всегда найдется какой-нибудь нехороший человек, который разместит в общедоступном каталоге вредоносную программу с именем исполняемого файла, совпадающим с именем какой-нибудь команды, часто вызываемой местным администратором с правами суперпользователя. Заговор удастся если . стоит в начале списка каталогов.

Утилита file выводит информацию о типе (с точки зрения системы) переданного в коммандной строке файла, для некоторых типов файлов выводит всякие дополнительные сведения касающиеся содержимого файла.

$ file hello.c
hello.c: ASCII C program text
$ file annotation.doc
annotation.doc: CDF V2 Document, Little Endian, Os: Windows, Version 5.1, Code page: 1251, Author: MIH, Template: Normal.dot, Last Saved By: MIH, Revision Number: 83, Name of Creating Application: Microsoft Office Word, Total Editing Time: 09:37:00, Last Printed: Thu Jan 22 07:31:00 2009, Create Time/Date: Mon Jan 12 07:36:00 2009, Last Saved Time/Date: Thu Jan 22 07:34:00 2009, Number of Pages: 1, Number of Words: 3094, Number of Characters: 17637, Security: 0

Вот собственно и всё, что требуется от пользователя для успешного применения gcc :)

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

В нашем примере функция main() возвращает казалось бы ни кому не нужное значение 0 . В UNIX-подобных системах, по завершении работы программы, принято возвращать в командную оболочку целое число — в случае успешного завершения ноль, любое другое в противном случае. Интерпретатор оболочки автоматически присвоит полученное значение переменной среды с именем ? . Просмотреть её содержимое можно с помощью команды echo $? :

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

Процесс компиляции можно разбить на 4 основных этапа: обработка препроцессором, собственно компиляция, ассемблирование, линковка (связывание).

Опции gcc позволяют прервать процесс на любом из этих этапов.

Препроцессор осуществляет подготовку исходного файла к компиляции — вырезает комментарии, добавляет содержимое заголовочных файлов (директива препроцессора #include ), реализует раскрытие макросов (символических констант, директива препроцессора #define ).

Воспользовавшись опцией -E дальнейшие действия gcc можно прервать и просмотреть содержимое файла, обработанного препроцессором.

После обработки препроцессором исходный текст нашей программы разбух и приобрел не удобочитаемый вид. Код, который мы когда-то собственноручно набили, свелся к нескольким строчкам в самом конце файла. Причина — подключение заголовочного файла стандартной библиотеки C. Заголовочный файл stdio.h сам по себе содержит много всего разного да ещё требует включения других заголовочных файлов.

Обратите внимание на расширение файла hello.i . По соглашениям gcc расширение .i соответствует файлам с исходным кодом на языке C не требующим обработки препроцессором. Такие файлы компилируются минуя препроцессор:

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

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

Остановить процесс создания исполняемого файла по завершении компиляции позволяет опция -S :

В каталоге появился файл hello.s , содержащий реализацию программы на языке ассемблера. Обратите внимание, задавать имя выходного файла с помощью опции -o в данном случае не потребовалось, gcc автоматически его сгенерировал, заменив в имени исходного файла расширение .c на .s . Для большинства основных операций gcc имя выходного файла формируется путем подобной замены. Расширение .s стандартное для файлов с исходным кодом на языке ассемблера.

Получить исполняемый код разумеется можно и из файла hello.s :

Следующий этап операция ассмеблирования — трансляция кода на языке ассемблера в машинный код. Результат операции — объектный файл. Объектный файл содержит блоки готового к исполнению машинного кода, блоки данных, а также список определенных в файле функций и внешних переменных (таблицу символов), но при этом в нем не заданы абсолютные адреса ссылок на функции и данные. Объектный файл не может быть запущен на исполнение непосредственно, но в дальнейшем (на этапе линковки) может быть объединен с другими объектными файлами (при этом, в соответствии с таблицами символов, будут вычислены и заполнены адреса существующих между файлами перекрестных ссылок). Опция gcc -c , останавливает процесс по завершении этапа ассемблирования:

Для объектных файлов принято стандартное расширение .o .

Если полученный объектный файл hello.o передать линковщику, последний вычислит адреса ссылок, добавит код запуска и завершения программы, код вызова библиотечных функций и в результате мы будем обладать готовым исполняемым файлом программы.

То, что мы сейчас проделали (вернее gcc проделал за нас) и есть содержание последнего этапа — линковки (связывания, компоновки).

Ну вот пожалуй о компиляции и все. Теперь коснемся некоторых, на мой взгляд важных, опций gcc.

Опция -I путь/к/каталогу/с/заголовочными/файлами — добавляет указанный каталог к списку путей поиска заголовочных файлов. Каталог, добавленный опцией -I просматривается первым, затем поиск продолжается в стандартных системных каталогах. Если опций -I несколько, заданные ими каталоги просматриваются слева на право, по мере появления опций.

Опция -Wall — выводит предупреждения, вызванные потенциальными ошибками в коде, не препятствующими компиляции программы, но способными привести, по мнению компилятора, к тем или иным проблемам при её исполнении. Важная и полезная опция, разработчики gcc рекомендуют пользоваться ей всегда. Например масса предупреждений будет выдана при попытке компиляции вот такого файла:

Опция -Werror — превращает все предупреждения в ошибки. В случае появления предупреждения прерывает процесс компиляции. Используется совместно с опцией -Wall .

Опция -g — помещает в объектный или исполняемый файл информацию необходимую для работы отладчика gdb. При сборке какого-либо проекта с целью последующей отладки, опцию -g необходимо включать как на этапе компиляции так и на этапе компоновки.

Опции -O1 , -O2 , -O3 — задают уровень оптимизации кода генерируемого компилятором. С увеличением номера, степень оптимизации возрастает. Действие опций можно увидеть вот на таком примере.

Компиляция с уровнем оптимизации по умолчанию:

Компиляция с максимальным уровнем оптимизации:

Во втором случае в полученном коде даже нет намёка на какой-либо цикл. Действительно, значение i , можно вычислить ещё на этапе компиляции, что и было сделано.

Увы, для реальных проектов разница в производительности при различных уровнях оптимизации практически не заметна.

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

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

Опция -march= architecture — задает целевую архитектуру процессора. Список поддерживаемых архитектур обширен, например, для процессоров семейства Intel/AMD можно задать i386 , pentium , prescott , opteron-sse3 и т.д. Пользователи бинарных дистрибутивов должны иметь в виду, что для корректной работы программ с указанной опцией желательно, что бы и все подключаемые библиотеки были откомпилированы с той же опцией.

Об опциях передаваемых линковщику будет сказано ниже.

Собственно о компиляции все. Далее поговорим о раздельной компиляции и создании библиотек.

Выше было сказано, что gcc определяет тип (язык программирования) переданных файлов по их расширению и, в соответствии с угаданным типом (языком), производит действия над ними. Пользователь обязан следить за расширениями создаваемых файлов, выбирая их так, как того требуют соглашения gcc. В действительности gcc можно подсовывать файлы с произвольными именами. Опция gcc -x позволяет явно указать язык программирования компилируемых файлов. Действие опции распространяется на все последующие перечисленные в команде файлы (вплоть до появления следующей опции -x ). Возможные аргументы опции:

c c-header c-cpp-output

objective-c objective-c-header objective-c-cpp-output

objective-c++ objective-c++-header objective-c++-cpp-output

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

$ mv hello.c hello.txt
$ gcc -Wall -x c -o hello hello.txt
$ ./hello
Hello World

Раздельная компиляция

Сильной стороной языков C/C++ является возможность разделять исходный код программы по нескольким файлам. Даже можно сказать больше — возможность раздельной компиляции это основа языка, без неё эффективное использование C не мыслимо. Именно мультифайловое программирование позволяет реализовать на C крупные проекты, например такие как Linux (здесь под словом Linux подразумевается как ядро, так и система в целом). Что даёт раздельная компиляция программисту?

Цукерберг рекомендует:  Новостной блок своими руками

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

2. Позволяет сократить время повторной компиляции проекта. Если изменения внесены в один файл нет смысла перекомпилировать весь проект, достаточно заново откомпилировать только этот изменённый файл.

3. Позволяет распределить работу над проектом между несколькими разработчиками. Каждый программист творит и отлаживает свою часть проекта, но в любой момент можно будет собрать (пересобрать) все получающиеся наработки в конечный продукт.

4. Без раздельной компиляции не существовало бы библиотек. Посредством библиотек реализовано повторное использование и распространение кода на C/C++, причем кода бинарного, что позволяет с одной стороны предоставить разработчикам простой механизм включения его в свои программы, с другой стороны скрыть от них конкретные детали реализации. Работая над проектом, всегда стоит задумываться над тем, а не понадобиться что-либо из уже сделанного когда-нибудь в будущем? Может стоит заранее выделить и оформить часть кода как библиотеку? По моему, такой подход, существенно упрощает жизнь и экономит массу времени.

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

Вот практический пример (правда весьма и весьма условный).

Набор файлов исходного кода:

#include «first.h»
#include «second.h»

printf( «Main function. \n » );

printf( «First function. \n » );

printf( «Second function. \n » );

В общем имеем вот что:

Все это хозяйство можно скомпилировать в одну команду:

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

Что мы сделали? Из каждого исходного файла (компилируя с опцией -c ) получили объектный файл. Затем объектные файлы слинковали в итоговый исполняемый. Разумеется команд gcc стало больше, но в ручную ни кто проекты не собирает, для этого есть утилиты сборщики (самая популярная make). При использовании утилит сборщиков и проявятся все из перечисленных выше преимуществ раздельной компиляции.

Возникает вопрос: как линковщик ухитряется собирать вместе объектные файлы, правильно вычисляя адресацию вызовов? Откуда он вообще узнаёт, что в файле second.o содержится код функции second() , а в коде файла main.o присутствует её вызов? Оказывается всё просто — в объектном файле присутствует так называемая таблица символов, включающая имена некоторых позиций кода (функций и внешних переменных). Линковщик просматривает таблицу символов каждого объектного файла, ищет общие (с совпадающими именами) позиции, на основании чего делает выводы о фактическом местоположении кода используемых функций (или блоков данных) и, соответственно, производит перерасчёт адресов вызовов в исполняемом файле.

Просмотреть таблицу символов можно с помощью утилиты nm.

Появление вызова puts объясняется использованием функции стандартной библиотеки printf() , превратившейся в puts() на этапе компиляции.

Таблица символов прописывается не только в объектный, но и в исполняемый файл:

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

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

Библиотеки

Библиотека — в языке C, файл содержащий объектный код, который может быть присоединен к использующей библиотеку программе на этапе линковки. Фактически библиотека это набор особым образом скомпонованных объектных файлов.

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

С точки зрения операционной системы и прикладного программного обеспечения библиотеки бывают статическими и разделяемыми (динамическими).

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

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

Парадигма разделяемых библиотек предоставляет три существенных преимущества:

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

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

3. Отпадает необходимость пересобирать каждый исполняемый файл в случае внесения изменений в код общей для них библиотеки. Изменения и исправления кода динамической библиотеки автоматически отразятся на каждой из использующих её программ.

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

Теперь немного практики.

Для иллюстрации воспользуемся набором исходных файлов из предыдущего примера. В нашу самодельную библиотеку поместим код (реализацию) функций first() и second() .

В Linux принята следующая схема именования файлов библиотек (хотя соблюдается она не всегда) — имя файла библиотеки начинается с префикса lib , за ним следует собственно имя библиотеки, в конце расширение .a (archive) — для статической библиотеки, .so (shared object) — для разделяемой (динамической), после расширения через точку перечисляются цифры номера версии (только для динамической библиотеки). Имя, соответствующего библиотеке заголовочного файла (опять же как правило), состоит из имени библиотеки (без префикса и версии) и расширения .h . Например: libogg.a , libogg.so.0.7.0 , ogg.h .

В начале создадим и используем статическую библиотеку.

Функции first() и second() составят содержимое нашей библиотеки libhello . Имя файла библиотеки, соответственно, будет libhello.a . Библиотеке сопоставим заголовочный файл hello.h .

в файлах main.c , first.c и second.c необходимо заменить на:

Ну а теперь, введем следующую последовательность команд:

Как уже было сказано — библиотека это набор объектных файлов. Первыми двумя командами мы и создали эти объектные файлы.

Далее необходимо объектные файлы скомпоновать в набор. Для этого используется архиватор ar — утилита «склеивает» несколько файлов в один, в полученный архив включает информацию требуемую для восстановления (извлечения) каждого индивидуального файла (включая его атрибуты принадлежности, доступа, времени). Какого-либо «сжатия» содержимого архива или иного преобразования хранимых данных при этом не производится.

Опция c arname — создать архив, если архив с именем arname не существует он будет создан, в противном случае файлы будут добавлены к имеющемуся архиву.

Опция r — задает режим обновления архива, если в архиве файл с указанным именем уже существует, он будет удален, а новый файл дописан в конец архива.

Опция s — добавляет (обновляет) индекс архива. В данном случае индекс архива это таблица, в которой для каждого определенного в архивируемых файлах символического имени (имени функции или блока данных) сопоставлено соответствующее ему имя объектного файла. Индекс архива необходим для ускорения работы с библиотекой — для того чтобы найти нужное определение, отпадает необходимость просматривать таблицы символов всех файлов архива, можно сразу перейти к файлу, содержащему искомое имя. Просмотреть индекс архива можно с помощью уже знакомой утилиты nm воспользовавшись её опцией -s (так же будут показаны таблицы символов всех объектных файлов архива):

$ nm -s libhello.a
Archive index:
first in first.o
second in second.o

Для создания индекса архива существует специальная утилита ranlib. Библиотеку libhello.a можно было сотворить и так:

Впрочем библиотека будет прекрасно работать и без индекса архива.

Теперь воспользуемся нашей библиотекой:

Ну теперь комментарии. Появились две новые опции gcc:

Опция -l name — передаётся линковщику, указывает на необходимость подключить к исполняемому файлу библиотеку libname . Подключить значит указать, что такие-то и такие-то функции (внешние переменные) определены в такой-то библиотеке. В нашем примере библиотека статическая, все символьные имена будут ссылаться на код находящийся непосредственно в исполняемом файле. Обратите внимание в опции -l имя библиотеки задается как name без приставки lib .

Опция -L /путь/к/каталогу/с/библиотеками — передаётся линковщику, указывает путь к каталогу содержащему подключаемые библиотеки. В нашем случае задана точка . , линковщик сначала будет искать библиотеки в текущем каталоге, затем в каталогах определённых в системе.

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

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

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

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

Теперь создадим и используем библиотеку динамическую.

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

Что получили в результате?

Файл libhello.so.2.4.0.5 , это и есть наша разделяемая библиотека. Как её использовать поговорим чуть ниже.

Опция -fPIC — требует от компилятора, при создании объектных файлов, порождать позиционно-независимый код (PIC — Position Independent Code), его основное отличие в способе представления адресов. Вместо указания фиксированных (статических) позиций, все адреса вычисляются исходя из смещений заданных в глобальной таблицы смещений (global offset table — GOT). Формат позиционно-независимого кода позволяет подключать исполняемые модули к коду основной программы в момент её загрузки. Соответственно, основное назначение позиционно-независимого кода — создание динамических (разделяемых) библиотек.

Опция -shared — указывает gcc, что в результате должен быть собран не исполняемый файл, а разделяемый объект — динамическая библиотека.

Опция -Wl,-soname,libhello.so.2 — задает soname библиотеки. О soname подробно поговорим в следующем абзаце. Сейчас обсудим формат опции. Сея странная, на первый взгляд, конструкция с запятыми предназначена для непосредственного взаимодействия пользователя с линковщиком. По ходу компиляции gcc вызывает линковщик автоматически, автоматически же, по собственному усмотрению, gcc передает ему необходимые для успешного завершения задания опции. Если у пользователя возникает потребность самому вмешаться в процесс линковки он может воспользоваться специальной опцией gcc -Wl, -option , value1 , value2 . . Что означает передать линковщику ( -Wl ) опцию -option с аргументами value1 , value2 и так далее. В нашем случае линковщику была передана опция -soname с аргументом libhello.so.2 .

Теперь о soname. При создании и распространении библиотек встает проблема совместимости и контроля версий. Для того чтобы система, конкретно загрузчик динамических библиотек, имели представление о том библиотека какой версии была использована при компиляции приложения и, соответственно, необходима для его успешного функционирования, был предусмотрен специальный идентификатор — soname, помещаемый как в файл самой библиотеки, так и в исполняемый файл приложения. Идентификатор soname это строка, включающая имя библиотеки с префиксом lib , точку, расширение so , снова точку и оду или две (разделенные точкой) цифры версии библиотеки — lib name .so. x . y . То есть soname совпадает с именем файла библиотеки вплоть до первой или второй цифры номера версии. Пусть имя исполняемого файла нашей библиотеки libhello.so.2.4.0.5 , тогда soname библиотеки может быть libhello.so.2 . При изменении интерфейса библиотеки её soname необходимо изменять! Любая модификация кода, приводящая к несовместимости с предыдущими релизами должна сопровождаться появлением нового soname.

Как же это все работает? Пусть для успешного исполнения некоторого приложения необходима библиотека с именем hello , пусть в системе таковая имеется, при этом имя файла библиотеки libhello.so.2.4.0.5 , а прописанное в нем soname библиотеки libhello.so.2 . На этапе компиляции приложения, линковщик, в соответствии с опцией -l hello , будет искать в системе файл с именем libhello.so . В реальной системе libhello.so это символическая ссылка на файл libhello.so.2.4.0.5 . Получив доступ к файлу библиотеки, линковщик считает прописанное в нем значение soname и наряду с прочим поместит его в исполняемый файл приложения. Когда приложение будет запущено, загрузчик динамических библиотек получит запрос на подключение библиотеки с soname, считанным из исполняемого файла, и попытается найти в системе библиотеку, имя файла которой совпадает с soname. То есть загрузчик попытается отыскать файл libhello.so.2 . Если система настроена корректно, в ней должна присутствовать символическая ссылка libhello.so.2 на файл libhello.so.2.4.0.5 , загрузчик получит доступ к требуемой библиотеки и далее не задумываясь (и ни чего более не проверяя) подключит её к приложению. Теперь представим, что мы перенесли откомпилированное таким образом приложение в другую систему, где развернута только предыдущая версия библиотеки с soname libhello.so.1 . Попытка запустить программу приведет к ошибке, так как в этой системе файла с именем libhello.so.2 нет.

Таким образом, на этапе компиляции линковщику необходимо предоставить файл библиотеки (или символическую ссылку на файл библиотеки) с именем lib name .so , на этапе исполнения загрузчику потребуется файл (или символическая ссылка) с именем lib name .so. x . y . При чем имя lib name .so. x . y должно совпадать со строкой soname использованной библиотеки.

В бинарных дистрибутивах, как правило, файл библиотеки libhello.so.2.4.0.5 и ссылка на него libhello.so.2 будут помещены в пакет libhello , а необходимая только для компиляции ссылка libhello.so , вместе с заголовочным файлом библиотеки hello.h будет упакована в пакет libhello-devel (в devel пакете окажется и файл статической версии библиотеки libhello.a , статическая библиотека может быть использована, также только на этапе компиляции). При распаковке пакета все перечисленные файлы и ссылки (кроме hello.h ) окажутся в одном каталоге.

Убедимся, что заданная строка soname действительно прописана в файле нашей библиотеки. Воспользуемся мега утилитой objdump с опцией -p :

Утилита objdump — мощный инструмент, позволяющий получить исчерпывающую информацию о внутреннем содержании (и устройстве) объектного или исполняемого файла. В man странице утилиты сказано, что objdump прежде всего будет полезен программистам, создающими средства отладки и компиляции, а не просто пишущих какие-нибудь прикладные программы :) В частности с опцией -d это дизассемблер. Мы воспользовались опцией -p — вывести различную метаинформацию о объектном файле.

В приведенном примере создания библиотеки мы неотступно следовали принципам раздельной компиляции. Разумеется скомпилировать библиотеку можно было бы и вот так, одним вызовом gcc:

Теперь попытаемся воспользоваться получившейся библиотекой:

Линковщик ругается. Вспоминаем, что было сказано выше о символических ссылках. Создаем libhello.so и повторяем попытку:

Теперь все довольны. Запускаем созданный бинарник:

Ошибка. Ругается загрузчик, не может найти библиотеку libhello.so.2 . Убедимся, что в исполняемом файле действительно прописана ссылка на libhello.so.2 :

Создаем соответствующую ссылку и повторно запускаем приложение:

Заработало. Теперь комментарии по новым опциям gcc.

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

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

Узнать какие разделяемые библиотеки необходимы приложению можно и с помощью утилиты ldd:

В выводе ldd для каждой требуемой библиотеки указывается её soname и полный путь к файлу библиотеки, определённый в соответствии с настройками системы.

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

В соответствии с соглашениями FHS (Filesystem Hierarchy Standard) в системе должны быть два (как минимум) каталога для хранения файлов библиотек:

/lib — здесь собраны основные библиотеки дистрибутива, необходимые для работы программ из /bin и /sbin ;

/usr/lib — здесь хранятся библиотеки необходимые прикладным программам из /usr/bin и /usr/sbin ;

Соответствующие библиотекам заголовочные файлы должны находиться в каталоге /usr/include .

Загрузчик по умолчанию будет искать файлы библиотек в этих каталогах.

Кроме перечисленных выше, в системе должен присутствовать каталог /usr/local/lib — здесь должны находиться библиотеки, развернутые пользователем самостоятельно, минуя систему управления пакетами (не входящие в состав дистрибутива). Например в этом каталоге по умолчанию окажутся библиотеки скомпилированные из исходников (программы установленные из исходников будут размещены в /usr/local/bin и /usr/local/sbin , разумеется речь идет о бинарных дистрибутивах). Заголовочные файлы библиотек в этом случае будут помещены в /usr/local/include .

В ряде дистрибутивов (в Ubuntu) загрузчик не настроен просматривать каталог /usr/local/lib , соответственно, если пользователь установит библиотеку из исходников, система её не увидит. Сиё авторами дистрибутива сделано специально, что бы приучить пользователя устанавливать программное обеспечение только через систему управления пакетами. Как поступить в данном случае будет рассказано ниже.

В действительности, для упрощения и ускорения процесса поиска файлов библиотек, загрузчик не просматривает при каждом обращении указанные выше каталоги, а пользуется базой данных, хранящейся в файле /etc/ld.so.cache (кэшем библиотек). Здесь собрана информация о том, где в системе находится соответствующий данному soname файл библиотеки. Загрузчик, получив список необходимых конкретному приложению библиотек (список soname библиотек, заданных в исполняемом файле программы), посредством /etc/ld.so.cache определяет путь к файлу каждой требуемой библиотеки и загружает её в память. Дополнительно, загрузчик может просмотреть каталоги перечисленные в системных переменных LD_LIBRARY_PATH , LIBRARY_PATH и в поле RPATH исполняемого файла (смотри выше).

Для управления и поддержания в актуальном состоянии кэша библиотек используется утилита ldconfig. Если запустить ldconfig без каких-либо опций, программа просмотрит каталоги заданные в командной строке, доверенные каталоги /lib и /usr/lib , каталоги перечисленные в файле /etc/ld.so.conf . Для каждого файла библиотеки, оказавшегося в указанных каталогах, будет считано soname, создана основанная на soname символическая ссылка, обновлена информация в /etc/ld.so.cache .

Убедимся в сказанном:

Первым вызовом ldconfig мы внесли в кэш нашу библиотеку, вторым вызовом исключили. Обратите внимание, что при компиляции main была опущена опция -Wl,-rpath,. , в результате загрузчик проводил поиск требуемых библиотек только в кэше.

Теперь должно быть понятно как поступить если после установки библиотеки из исходников система её не видит. Прежде всего необходимо внести в файл /etc/ld.so.conf полный путь к каталогу с файлами библиотеки (по умолчанию /usr/local/lib ). Формат /etc/ld.so.conf — файл содержит список разделённых двоеточием, пробелом, табуляцией или символом новой строки, каталогов, в которых производится поиск библиотек. После чего вызвать ldconfig без каких-либо опций, но с правами суперпользователя. Всё должно заработать.

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

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

Существует опция gcc -static — указание линковщику использовать только статические версии всех необходимых приложению библиотек:

Размер исполняемого файла в 60 раз больше, чем в предыдущем примере — в файл включены стандартные библиотеки языка C. Теперь наше приложение можно смело переносить из каталога в каталог и даже на другие машины, код библиотеки hello внутри файла, программа полностью автономна.

Как же быть если необходимо осуществить статическую линковку только части использованных библиотек? Возможный вариант решения — сделать имя статической версии библиотеки отличным от имени разделяемой, а при компиляции приложения указывать какую версию мы хотим использовать на этот раз:

Так как размер кода библиотеки libhello ничтожен,

размер получившегося исполняемого файла практически не отличается от размера файла созданного с использованием динамической линковки.

Ну вот пожалуй и все. Большое спасибо всем, кто закончил чтение на этом месте.

Установка GNU GCC Компилятора и среды разработки на Ubuntu Linux

Как установить компилятор GNU / GCC (C и C ++) и связанные с ним инструменты (например, make, debugger, man pages) в операционной системе Ubuntu Linux с использованием параметров командной строки?

Вам необходимо установить следующие пакеты на Debian и Ubuntu Linux:

Build-essential package – Устанавливает следующую коллекцию для компиляции c / c ++-программ на Ubuntu Linux, включая:

  1. libc6-dev – стандартная C библиотека.
  2. gcc – C компилятор.
  3. g++ – C++ компилятор.
    1. make – GNU делает утилиту для поддержки групп программ.
    2. dpkg-dev – Инструменты разработки пакетов Debian.

В принципе, build-essential пакет содержит информационный список пакетов, которые считаются необходимыми для создания пакетов Ubuntu, включая gcc-компилятор, make и другие необходимые инструменты. Этот пакет также зависит от пакетов в этом списке, чтобы упростить установку build-essential пакетов. В этом руководстве вы узнаете об установке компилятора GNU C и компилятора GNU C ++ на Ubuntu Linux.

Установка компиляторов с помощью команды apt

Откройте приложение терминала и введите следующее apt command /apt-get command :

Пример вывода данных:

Рис.01: Как я устанавливаю инструменты разработки на Ubuntu Linux?

Цукерберг рекомендует:  Taskbar - Показать текст в Win10 на taskbar

Проверка установки

Введите следующую команду:

Рис.02: Поиск установленной версии make и gcc

Установка страниц dev man на Ubuntu Linux

Введите следующую команду:

Чтобы посмотреть запросы библиотеки (функция в библиотеках программы) введите:

Применение специальных возможностей GCC в ядре Linux

Откройте для себя расширения GCC для языка C

GCC и Linux — это великолепная пара. Хотя это независимые друг от друга программные продукты, Linux полностью зависит от GCC при развертывании на новых архитектурах. Кроме того, возможности GCC, известные как расширения, используются в Linux для получения большей функциональности и оптимизации. В этой статье исследуются многие из этих важных расширений и рассказывается об их использовании в ядре Linux.

Текущая стабильная версия GCC (версия 4.3.2) поддерживает 3 версии стандарта C:

  • оригинальный стандарт Международной организации по стандартизации (ISO) языка C (ISO C89 или C90)
  • ISO C90 с поправкой 1
  • Текущий стандарт ISO C99 (стандарт, используемый GCC по умолчанию, в статье предполагается использование именно его)

Замечание: В статье предполагается, что вы используете стандарт ISO C99. Если вы укажете использовать более раннюю версию стандарта, чем ISO C99, некоторые из расширений, описанных в этой статье, могут быть выключены. Указать GCC используемую версию стандарта, можно с помощью опции командной строки -std . Вы можете справится в руководстве пользователя GCC какие расширения поддерживаются в каждой конкретной версии стандарта (см. ссылку в разделе Ресурсы).

О версиях

В этой статье предполагается использование расширений GCC в ядре Linux 2.6.27.1 и версии GCC 4.3.2. Для каждого расширения C указан файл исходного кода ядра, в котором есть пример его использования.

Имеющиеся расширения C можно классифицировать несколькими способами. В этой статье мы их разделяем на две большие группы:

  • Функциональные расширения, дающие вам благодаря GCC новые возможности.
  • Оптимизационные расширения, — помогающие генерировать более эффективный код.

Функциональные расширения

Начнем с изучения некоторых приемов GCC, расширяющих стандартный язык C.

Распознавание типа

GCC позволяет идентифицировать тип переменной по ссылке на нее. Такой подход создает возможности для реализации того, что часто называют обобщенным программированием (generic programming). Подобная функциональность присутствует во многих современных языках, таких как: C++, Ada, и Java™. В Linux для построения зависимых от типа операций, таких как min и max используется команда typeof . В листинге 1 показано, как можно использовать typeof для создания обобщенных макросов (из ./linux/include/linux/kernel.h).

Листинг 1. Использование typeof для создания обобщенных макросов

Интервалы

GCC включает в себя поддержку интервалов, которые можно использовать во многих областях языка C. Одним из таких мест являются инструкции case в блоках switch / case . В сложных структурах условий обычно приходится использовать каскады инструкций if для получения того же самого результата, что представлен в более элегантной форме в листинге 2 (из ./linux/drivers/scsi/sd.c). Кроме того, при использовании switch / case в компиляторе включается оптимизация, использующая реализацию таблиц перехода.

Листинг 2. Использование интервалов внутри инструкций case

Интервалы также можно использовать для инициализации данных, как показано ниже (из ./linux/arch/cris/arch-v32/kernel/smp.c). В этом примере создается массив spinlock_t размера LOCK_COUNT . Каждый элемент массива инициализируется значением SPIN_LOCK_UNLOCKED .

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

Массивы нулевой длины

Согласно стандарту С, для массива необходимо определить как минимум один элемент. Как правило, это требование усложняет проектирование кода. Однако GCC поддерживает концепцию массивов нулевой длины, которые могут быть особенно полезны при определении структур данных. Эта концепция похожа на гибкие элементы массива в ISO C99, но использует другой синтаксис.

В следующем примере в конце структуры объявляется массив нулевой длины (из ./linux/drivers/ieee1394/raw1394-private.h). Это позволяет экземпляру этой структуры ссылатся на память, следующую непосредственно за ней. Это может быть полезно, когда вам необходимо иметь переменное количество элементов в массиве.

Определение адреса вызова

Вом многих случаях, может быть полезно или даже необходимо определить место, откуда была вызвана функция. GCC для этого предоставляет встроенную функцию __builtin_return_address . Эта функция часто используется при отладке, но также имеет множество других применений в ядре Linux.

Как показано в коде ниже, __builtin_return_address имеет аргумент, называемый level . Этот аргумент определяет уровень в стеке вызовов, для которого вы хотите получить адрес. Например, если вы зададите level равным 0 , вы получите адрес текущей функции. Если вы зададите level равным 1 , вы получите адрес вызывающей функции и так далее.

Функция local_bh_disable в следующем примере (из ./linux/kernel/softirq.c) выключает механизмы отложенных прерываний (softirq), тасклетов и механизм нижних половин на локальном процессоре. Адрес возврата узнается с помощью __builtin_return_address для дальнейшего использования при трассировке.

Выявление констант

GCC предоставляет встроенную функцию, которую можно использовать чтобы определить, является ли некоторое значение константой времени компиляции или нет. Это полезная информация, зная которую вы можете составлять выражения, которые могут быть оптимизированы с помощью свертки констант. Для такой проверки используется функция __builtin_constant_p .

Прототип для функции __builtin_constant_p показан ниже. Заметьте, что __builtin_constant_p определяет не все константы, так как некоторые из них не так просто выявить средствами GCC.

Выявление констант довольно часто используется в Linux. В примере, показанном в листинге 3 (из ./linux/include/linux/log2.h), выявление констант используется для оптимизации макроса roundup_pow_of_two . Если выражение распознается как константа, то для оптимизации используется специальное константное выражение. Если же выражение не является константой, вызывается другая макрофункция для округления значения до степени двойки.

Листинг 3. Использование выявления констант для оптимизации макрофункций

Атрибуты функций

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

Как показано в листинге 4 (из ./linux/include/linux/compiler-gcc3.h), атрибутам функций даются символьные обозначения (алиасы). Вы можете использовать этот листинг как руководство при чтении следующих примеров кода, демонстрирующих использование атрибутов функций.

Листинг 4. Определение атрибутов функций

Определения в листинге 4 отражают некоторые атрибуты функций, доступные в GCC. Также это одни из наиболее полезных атрибутов функций в ядре Linux. В дальнейшем мы покажем, как наилучшим образом использовать эти атрибуты:

  • always_inline — указывает GCC всегда подставлять функции, независимо от того включена оптимизация или нет.
  • deprecated — сигнализирует вам, что функция устарела и ее больше не следует использовать. Если вы попытаетесь использовать устаревшую функцию, компилятор выдаст предупреждение. Этот атрибут также можно применять к типам и переменным.
  • __used__ — сообщает компилятору, что эта функция используется, независимо от того найдет ли GCC экземпляры вызова этой функции. Это может быть полезно в тех случаях, когда функции С вызываются из ассемблера.
  • __const__ — сообщает компилятору, что эта функция не имеет состояния (т.е. использует для генерации возвращаемого результата только переданные ей аргументы).
  • warn_unused_result — принуждает компилятор всегда проверять, что возвращаемое значение функции проверяется в месте вызова. Этим гарантируется, что везде, откуда вызывается функция результат будет проверяться, что позволяет обработать потенциальные ошибки.

Далее показаны примеры таких функций, используемые в ядре Linux. Пример deprecated взят из независимого от архитектуры ядра (./linux/kernel/resource.c), а пример const из кода ядра для архитектуры IA64 (./linux/arch/ia64/kernel/unwind.c).

Расширения оптимизации

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

Подсказывание наиболее вероятной ветви

Одна из самых широко используемых в ядре Linux техник оптимизации — это __builtin_expect . Работая с условиями в коде, вы часто знаете какая ветвь наиболее вероятна, а какая — нет. Если компилятор знает эту прогнозную информацию, он может сгенерировать наиболее оптимальный код обхода ветвей.

Как показано ниже, использование __builtin_expect основано на двух макросах, называемых likely и unlikely (из ./linux/include/linux/compiler.h).

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

Например, если условие помечено «likely», то компилятор может поместить порцию кода True непосредственно после ветвления. Код для варианта False в этом случае будет доступен через инструкцию ветвления, что не так оптимально, но и менее вероятно. При таком способе код оптимизируется для наиболее вероятного варианта.

В листинге 5 показана функция, в которой используются как макрос likely , так и unlikely (из ./linux/net/core/datagram.c). Функция ожидает, что переменная sum будет равна нулю (контрольная сумма для пакета верна) и что переменная ip_summed не равна CHECKSUM_HW .

Листинг 5. Пример использования макросов likely и unlikely

Предварительная выборка

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

  • Кэш 1-го уровня — как правило поддерживает доступ к данным в течение одного такта
  • Кэш 2-го уровня поддерживает доступ к данным в течение двух тактов
  • Системная память — поддерживает более продолжительное время доступа

Чтобы минимизировать задержки доступа к данным и таким образом улучшить производительность, лучше всего держать данные в ближайшей к процессору памяти. Выполнение этой задачи вручную называется предварительной выборкой. GCC поддерживает предварительную выборку данных вручную с помощью встроенной функции, называемой __builtin_prefetch . Эта функция используется для помещения данных в кэш незадолго до того как они понадобятся. Как показано ниже, функция __builtin_prefetch принимает три аргумента:

  • адрес данных
  • параметр rw — используется для индикации того, подготавливаются ваши данные для чтения (операция Read) или для записи (операция Write)
  • параметр locality , определяющий что следует сделать с данными после использования, — оставить в кэше или удалить их оттуда

Предварительная выборка данных интенсивно используется ядром Linux. Наиболее часто она реализуется с помощью макросов и оберточных функций. Листинг 6 содержит пример вспомогательной функции, в которой используется такая функция-обертка (из ./linux/include/linux/prefetch.h). В функции реализуется механизм упреждающего просмотра вперед для потоковых операций. Использование этой функции, как правило, дает улучшение производительности за счет минимизации неудачных обращений к кэшу и простаивания данных в кэше.

Листинг 6. Оберточная функция для предварительной выборки блока данных

Атрибуты переменных

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

Следующий пример используется для программной приостановки выполнения (из ./linux/arch/i386/mm/init.c). Объект PAGE_SIZE представляет собой требуемое выравнивание страницы.

Пример в листинге 7 иллюстрирует пару моментов, связанных с оптимизацией:

  • Атрибут packed упаковывает элементы структуры таким образом, чтобы она занимала как можно меньше места. Это значит, что если определена переменная типа char , она будет занимать не больше чем байт (8 бит). Битовые поля сжимаются до одного бита, вместо того чтобы занимать больше места.
  • В этом коде оптимизация осуществляется с помощью одной спецификации __attribute__ , которая определяет несколько разделенных запятой атрибутов.
Листинг 7. Упаковка структур и задание множественных атрибутов

Двигаясь дальше

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

Ресурсы для скачивания

Похожие темы

  • Оригинал статьи: GCC hacks in the Linux kernel (EN).
  • Коллекция компиляторов GNU (EN) — это источник всей информации о GCC. Здесь вы найдете новости GCC и самый свежий исходный код (включая как старые, так и новейшие дитрибутивы). Также здесь вы найдете детальную историю для каждого релиза и онлайновую документацию (EN), в которой детально освещается GCC и все его расширения.
  • Экспериментальные расширения GCC (EN) — это поддерживаемый GNU список расширений, еще не входящих в стандартный дистрибутив. Это замечательное место для знакомства с грядущими изменениями в расширениях GCC.
  • В статье «Знакомимся с GCC 4» developerWorks, октябрь 2008) более подробно рассказывается о различных изменениях, произошедших в четвертом большом релизе GCC, называемом GCC4. Эта статья знакомит вас с GCC4 и его развитием на протяжении последних четырех малых релизов.
  • В основном разделе Linux и в разделе Linux для новичков вы найдете множество ресурсов для Linux-разработчиков; также просмотрите наши самые популярные статьи и руководства (EN).
  • Ознакомьтесь с другими советами и руководствами по Linux на сайте developerWorks (EN).

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

Разработка на C/С++ под Linux: как правильно подобрать компилятор и >

Не так давно перешел с Windows на Linux-based системы и вместе с тем приобщаюсь к программированию на C/C++. Программа университета на начальной стадии рассчитана на использование Borland Pascal/C, далее работа организовывается вокруг использование Visual Studio — разумеется всё под Windows. С Pascal альтернатива в Linux очевидна, с вот C/С++ многие моменты пока что непонятны. Преподаватели отмахиваются от каких-либо объяснений в отношении Clang, уверенно заявляют «кривости и старости» GCC, агитируя, опять же, использование Visual Studio.

Каковы наиболее подходящие варианты компилятора и IDE под Linux?

Gcc — Си: Крашится на операторе return! И GCC на Linux лучше?

Есть всего три популярных, высококачественных, широко принятых в индустрии компиляторов C/C++:

  • GCC (Gnu Compiler Collections и GNU C Compiler), кроссплатформенный и Open-Source, используется в Linux как основной, на Windows известен как MinGW
  • MSVC (Microsoft Visual C/C++), низкая кроссплатформенность и закрытый код, используется в Windows как основной
  • LLVM/Clang, кроссплатформенный и Open-Source, используется в Mac OSX как основной, на Windows умеет быть совместимым и с MinGW, и с MSVC, доступен в Visual Studio 2015 и выше в модификации Clang/C2

Принципы работы GCC и Clang можно детально исследовать благодаря открытому исходному коду и отладочным средствам.

GCC (компиляция и вывод ассемблера)

Разберёмся, как использовать GCC из командной строки. На UNIX-платформах GCC доступен по команде gcc , а для Windows есть порт GCC — MinGW. Воспользуемся примером кода, складывающего два числа:

Компиляция файла из командной строки с опциями по умолчанию (отладочная сборка без оптимизаций):

Вывод программы после запуска:

Получение ассемблерного кода для отладочного режима без оптимизаций возможно с опцией -S . По умолчанию создаваемый ассемблер использует синтаксис AT&T, который заметно отличается от синтаксиса Intel.

Можно получить ассемблерный код в режиме с оптимизациями, используя флаг -O2 , где “O” в верхнем регистре. Если сравнить отладочный и оптимизированный код с помощью утилиты diff, будут видны сильные отличия в цепочках инструкций.

Вы можете скомпилировать ассемблер с помощью того же gcc, который сам передаст нужные параметры утилите “gas” (GNU Assembler).

Clang (компиляция, вывод ассемблера и LLVM-IR)

Clang разрабатывался как прозрачная замена компилятору GCC для Linux и Mac OSX. Поэтому большая часть опций, касающихся компиляции C/C++, у этих двух компиляторов совпадает. Компиляция примера на языке C выглядит точно так же:

Генерация ассемблера с синтаксисом Intel:

Бекенды GCC и Clang

GCC и Clang оба используют гибкие фреймворки для построения бекендов компилятора. В GNU Compiler Collections используется собственный промежуточный язык и бекенд GIMPLE, который сильно упрощает написание компиляторов для новых языков в составе GNU Compiler Collections, но плохо подходит для изучения новичком. Проект LLVM гораздо дружественнее к новичкам и студентам, и именно его использует компилятор Clang.

Вы можете изучать промежуточный код проекта LLVM, называемый LLVM-IR, с помощью clang, исследуя преобразование кода из C в LLVM-IR:

Упражнения

  • Напишите 3-4 простейших программы в 10-20 строк на C (сложение двух чисел, вывод текущего времени с начала эпохи UNIX, вывод версии операционной системы, переворачивание строки т.п.). Сгенерируйте из этих программ листинги в машинном ассемблере либо в LLVM-IR, и сравните листинги от разных программ с помощью diff. Попробуйте собрать минимальный шаблон ассемблерного кода, который можно было бы разворачивать в полноценную программу путём подстановки цепочки инструкций вместо переменной .

PS-Group

Материалы для курсов в Институте Программных Систем и в Волгатехе

Установка GCC в Ubuntu 16.04

Большинство программ в Linux написаны на Си или С++, и если вы хотите собирать их из исходников, то вам обязательно понадобиться компилятор, также он понадобиться, если захотите начать писать свои программы на одном из этих языков.

Существует два основных компилятора в Linux — это GCC и Clang, они похожи по своим возможностям, но так сложилось, что первый считается стандартом для Ubuntu. GCC расшифровывается как GNU Compiler Collection. В этой статье мы рассмотрим как выполняется установка GCC Ubuntu 16.04, а также рассмотрим базовые приемы работы с этим набором программ в терминале.

Набор компиляторов GCC

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

В базовую поставку компилятора входят такие программы:

  • libc6-dev — заголовочные файлы стандартной библиотеки Си;
  • libstdc++6-dev — заголовочные файлы стандартной библиотеки С++;
  • gcc — компилятор языка программирования Си;
  • g++ — компилятор языка программирования C++;
  • make — утилита для организации сборки нескольких файлов;
  • dpkg-dev — инструменты сборки пакетов deb.

Все эти пакеты являются зависимостями пакета build-essential, поэтому для установки всего необходимого достаточно установить этот пакет.

Установка GCC в Ubuntu 16.04

Если вас устраивает текущая версия GCC, которая есть в официальных репозиториях дистрибутива, то вам достаточно установить пакет build-essential. Для этого выполните команду:

sudo apt install build-essential

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

whereis gcc make

Но если вы хотите более новую версию компилятора, например, на данный момент последняя версия — 6.2, то можно использовать PPA разработчиков с тестовыми сборками. Для добавления PPA в систему выполните:

sudo add-apt-repository ppa:ubuntu-toolchain-r/test
$ sudo apt update

Затем установите сами компиляторы:

sudo apt install gcc-snapshot
$ sudo apt install gcc-6 g++-6

Это не заменит ваш текущий компилятор на новый. В системе просто появятся компиляторы gcc-6 и g++-6, которые вы можете использовать для своих программ. Это лучший вариант на данный момент, но если вы хотите все же сделать gcc-6 компилятором по умолчанию, выполните:

sudo update-alternatives —install /usr/bin/gcc gcc /usr/bin/gcc-6 60 —slave /usr/bin/g++ g++ /usr/bin/g++-6

Готово, теперь вы можете проверить версию gcc-6:

Установка GCC Ubuntu 16.04 завершена, и можно переходить к сборке программ. Для удаления компилятора достаточно удалить пакет build-essential:

sudo apt purge build-essential
$ sudo apt autoremove

А чтобы заменить новую версию обратно, установите и замените настройки программ по умолчанию:

sudo apt-get install gcc-4.8 g++-4.8
$ sudo update-alternatives —install /usr/bin/gcc gcc /usr/bin/gcc-4.8 60 —slave /usr/bin/g++ g++ /usr/bin/g++-4.8;

Использование GCC в Ubuntu 16.04

Давайте рассмотрим пример компиляции минимальной программы hello.c для освоения работы с gcc. Вот код программы, сохраните его в файле hello.c:

int main(void)
<
printf(«Hello, world!\n»);
return 0;
>

Теперь запустим сборку программы:

Когда сборка программы будет завершена, вы можете ее запустить:

Готово, компилятор прекрасно работает в системе, и теперь вы можете писать свои программы или собирать чужие.

Выводы

В этой статье мы рассмотрели как установить gcc Ubuntu 16.04, это один из самых популярных компиляторов для этой операционной системы. И устанавливается он очень просто, если у вас остались вопросы, спрашивайте в комментариях!

На завершение видео с демонстрацией самого процесса:

SLUSAR.SU

Логово Программиста

Программируем на C. Установка и настройка компилятора СИ и С++ на Linux

О том, как установить полноценный компилятор СИ и С++ на Windows я уже писал в статье: Программируем на C. Установка и настройка компилятора СИ и С++ на windows по взрослому.

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

Итак, линукс установлен, интернет подключен. Открываем консоль и прописываем команду установки компиляторов.
$sudo apt-get install gcc g++

Вот и все, компиляторы установлены. Теперь осталось проверить.
Заходим в домашнюю папку и создаем файл hello.c, открываем его и пишем простую программку:

Затем открываем консоль и компилируем скрипт в программу:
$gcc hello.c -o hello

Все, в папке должна появиться программа hello. Теперь запустим её.
$./hello

Но программировать в простом блокноте и компилировать в консоли — это особое извращение. Нам же нужна мало-мальски нормальная IDE для программирования на С и С++ со встроенным компилятором.

Если вы читали мою статью Geany — среда разработки для C, C++, HTML, python, php и т.д. , то вы поняли о чем идет речь. Да, мы установим Geany. Это отличная среда разработки для многих языков. Писать консольные программы в ней само удовольствие.
Открываем консоль и пишем:
$sudo apt-get install geany

Соглашаемся с установкой и ждем ее окончания. После чего запускаем программу.

Открываем в ней тот самый файл hello.c и немного модифицируем его, затем жмем на кнопку похожую на кирпич «Собрать текущий файл» и запускаем красной кнопкой «Посмотреть или запустить текущий файл». И у нас появиться консольное окно с результатом:

Если у вас возникнет ошибка сигментации в коде «scanf(«%d»,a)» то поступите так «scanf(«%d»,&a)«.

С++ тоже отлично работает:

На этом все. Задавайте вопросы в комментариях.

6 идей о “ Программируем на C. Установка и настройка компилятора СИ и С++ на Linux ”

Описан лютый пиздец. Всё делал по инструкции, не компилируется.
2.c:4:1: warning: incompatible implicit declaration of built-in function ‘printf’
2.c:4:1: note: include ‘’ or provide a declaration of ‘printf’
2.c:5:1: warning: ‘return’ with no value, in function returning non-void
return;
Сборка завершилась с ошибкой.
Автор, гори в аду.

С такими нервишками вам будет тяжело. Вы хоть сами читали текст ошибок?

Виталий, вы до сих пор не видите какую хню вы написали? С таким зрением вам тяжело будет. include main птить…

АХАХАХАХ
это не комментарий, это ШЕДЕВР :DDD

Добавить комментарий Отменить ответ

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.

Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих