2.4. Компиляция при помощи cc

Этот раздел касается только GNU-компилятора для C и C++, так как он поставляется вместе с системой FreeBSD. Он может быть вызван командами cc или gcc. Подробности по генерации программ с помощью интерпретатора зависят от конкретного интерпретатора и обычно хорошо описываются в документации и встроенной системе помощи интерпретатора.

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

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

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

  3. Преобразование исходного кода в код на языке ассемблера--он очень близок к машинному коду, но все еще понятен человеку. Определенно. [1]

  4. Преобразование ассемблерного кода в машинный код--да, мы имеем в виду биты и байты, здесь только нули и единицы.

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

  6. Если вы пытаетесь получить выполнимый файл из нескольких файлов с исходными текстами, работа над тем, как их объединить.

  7. Работа над генерацией того, что загрузчик сможет загрузить в память и запустить.

  8. Наконец, запись выполнимого файла в файловую систему.

Термин компиляция часто используется для обозначения действий на шагах от 1 до 4--остальные шаги называют компоновкой. Иногда шаг 1 называют препроцессорной обработкой, а шаги 3-4 ассемблированием.

К счастью, почти все эти детали скрыты от вас, cc как конечная программа управляет за вас вызовом всех этих программ с правильными аргументами; простой вызов

    % cc foobar.c

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

    % cc foo.c bar.c

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

Имеется огромное количество параметров для cc, все они описаны на справочной странице. Вот несколько из самых важных, с примерами их использования.

-o filename

Имя выходного файла. Если вы не используете этот параметр, cc сгенерирует выполнимый файл с именем a.out. [3]

    % cc foobar.c               выполнимый файл называется a.out
    % cc -o foobar foobar.c     выполнимый файл называется foobar
           
-c

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

    % cc -c foobar.c

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

-g

Создать отладочную версию выполнимого файла. Этот параметр указывает компилятору поместить в выполнимый файл информацию о том, какая строка какого файла с исходным текстом какому вызову функции соответствует. Отладчик может использовать эту информацию для вывода исходного кода при пошаговом выполнении программы, что очень полезно; минусом является то, что вся эта дополнительная информация делает программу гораздо большей. Обычно вы компилируете с параметром -g при работе над программой, а затем, когда убедитесь в работоспособности программы, компилируете "окончательную версию" без параметра -g.

    % cc -g foobar.c

При этом будет сгенерирована отладочная версия программы. [4]

-O

Создать оптимизированную версию выполнимого файла. Компилятор прибегает к различным ухищрениям для того, чтобы сгенерировать выполнимый файл, выполняющийся быстрее, чем обычно. После опции -O вы можете добавить число, указывающее качество оптимизации, но использование этого не защищено от ошибок оптимизации компилятора. Например, известно, что версия компилятора cc, поставляемая с FreeBSD версии 2.1.0, при некоторых условиях генерирует неверный код при использовании опции -O2.

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

    % cc -O -o foobar foobar.c
           

По этой команде будет создана оптимизированная версия программы foobar.

Следующие три флага заставят cc проверять ваш код на соответствие известному международному стандарту, часто называемому стандартом ANSI, хотя, строго говоря, это стандарт ISO.

-Wall

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

-ansi

Выключить большинство, если не все, не-ANSI расширений языка C, имеющиеся в cc. Несмотря на название, этот параметр не гарантирует, что ваш код будет строго соответствовать стандарту.

-pedantic

Выключить все расширения C, имеющиеся в компиляторе cc, не соответствующие стандарту ANSI.

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

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

    % cc -Wall -ansi -pedantic -o foobar foobar.c
         

По этой команде после проверки на соответствие стандарту файла foobar.c будет сгенерирован выполнимый файл foobar.

-llibrary

Задает библиотеку функций, которая будет использоваться на этапе компоновки.

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

При этом если библиотека называется libsomething.a, вы передаете программе cc аргумент -lsomething. Например, математическая библиотека называется libm.a, так что вы передаете программе cc аргумент -lm. Известной "хитростью" при работе с математической библиотекой является то, что в командной строке она должна быть указана в качестве последней библиотеки.

    % cc -o foobar foobar.c -lm
           

По этой команде библиотека математических функций будет скомпонована с foobar.

Если вы компилируете код на языке C++, вам нужно добавить параметр -lg++, или -lstdc++ при использовании FreeBSD 2.2 и выше, к аргументам командной строки для компоновки библиотеки функций C++. Либо вы можете запустить вместо cc компилятор c++, который сделает это за вас. Во FreeBSD c++ может быть вызван как g++.

    % cc -o foobar foobar.cc -lg++     For FreeBSD 2.1.6 and earlier
    % cc -o foobar foobar.cc -lstdc++  For FreeBSD 2.2 and later
    % c++ -o foobar foobar.cc
           

В каждом из этих вариантов из файла foobar.cc с исходным кодом C++ будет создан выполнимый файл foobar. Заметьте, что в Unix-системах файлы с исходным кодом C++ традиционно оканчиваются на .C, .cxx или .cc, в отличие от стиля MS-DOS .cpp (который уже использовался для чего-то еще). gcc использует это для определения того, какой компилятор использовать; однако это ограничение больше не имеет силы, так что вы можете безнаказанно называть ваши файлы с кодом C++ .cpp!

2.4.1. Часто возникающие вопросы и проблемы с cc

2.4.1.1. Я пытаюсь написать программу, которая использует функцию sin() и получаю такую ошибку. Что она означает?
2.4.1.2. Все замечательно, чтобы попрактиковаться в использовании параметра -lm, я написал вот такую простую программу. Все, что она делает, заключается в возведении числа 2.1 в шестую степень.
2.4.1.3. Так как это исправить?
2.4.1.4. Я компилировал файл с именем foobar.c, но не могу найти выполнимый файл с именем foobar. Куда он подевался?
2.4.1.5. Хорошо, у меня получился выполнимый файл с именем foobar, я могу видеть его при запуске команды ls, но когда я набираю в командной строке foobar, выдается сообщение об отсутствии такого файла. Почему он не может быть найден?
2.4.1.6. Я назвал мой выполнимый файл test, но при попытке его запустить ничего не происходит. В чем дело?
2.4.1.7. Я откомпилировал мою программу, и сначала она работала нормально, но потом выдалось сообщение об ошибке с сообщением ``core dumped''. Что это означает?
2.4.1.8. Очаровательно, но что я теперь должен делать?
2.4.1.9. Когда моя программа сбрасывает дамп, она выдает сообщение ``segmentation fault''. Что это такое?
2.4.1.10. Иногда при получении дампа памяти выдается сообщение ``bus error''. В моей книге про Unix утверждается, что это является признаком проблемы с оборудованием, но компьютер работает нормально. Это правда?
2.4.1.11. Эти дампы памяти, если подумать, могут оказаться весьма полезными, если я смогу получать их, когда захочу. Могу ли я это делать, или мне нужно ждать появления ошибки?

2.4.1.1. Я пытаюсь написать программу, которая использует функцию sin() и получаю такую ошибку. Что она означает?

    /var/tmp/cc0143941.o: Undefined symbol `_sin' referenced from text segment
             

При использовании таких математических функций, как sin(), вам нужно указать компилятору cc на компоновку с математической библиотекой, вот так:

    % cc -o foobar foobar.c -lm
             

2.4.1.2. Все замечательно, чтобы попрактиковаться в использовании параметра -lm, я написал вот такую простую программу. Все, что она делает, заключается в возведении числа 2.1 в шестую степень.

    #include <stdio.h>
    
    int main() {
        float f;
    
        f = pow(2.1, 6);
        printf("2.1 ^ 6 = %f\n", f);
        return 0;
    }
             

и я компилирую ее вот так:

    % cc temp.c -lm
             

как вы и сказали, но при ее запуске получаю вот что:

    % ./a.out
    2.1 ^ 6 = 1023.000000
             

Это неправильный результат! Что происходит?

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

2.4.1.3. Так как это исправить?

Объявления математических функций находятся в файле math.h. Если вы включаете этот файл, то компилятор сможет найти объявление и перестанет вытворять странные вещи с вашими расчетами!

    #include <math.h>
    #include <stdio.h>
    
    int main() {
    ...
             

После компиляции таким же образом, как и раньше, запустите программу снова:

    % ./a.out
    2.1 ^ 6 = 85.766121
             

Если вы используете какие-либо математические функции, всегда включайте файл math.h и не забудьте выполнить компоновку с математической библиотекой.

2.4.1.4. Я компилировал файл с именем foobar.c, но не могу найти выполнимый файл с именем foobar. Куда он подевался?

Запомните, что cc будет называть выполнимый файл a.out, если вы не зададите имя явно. Воспользуйтесь опцией -o filename:

    % cc -o foobar foobar.c
             

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

В отличие от MS-DOS, Unix не просматривает текущий каталог, когда пытается найти выполнимый файл, который вы хотите запустить, если вы явно его укажете. Либо введите команду ./foobar, которая означает, что "нужно запустить файл с именем foobar, располагающийся в текущем каталоге", или измените вашу переменную окружения PATH, чтобы она выглядела примерно как

    bin:/usr/bin:/usr/local/bin:.
             

Точка в конце означает "посмотреть в текущем каталоге, если в других ничего не найдено".

2.4.1.6. Я назвал мой выполнимый файл test, но при попытке его запустить ничего не происходит. В чем дело?

В большинстве Unix-систем в каталоге /usr/bin присутствует программа с именем test и до того, как оболочка доберется до текущего каталога, она выбирает именно ее. Наберите:

    % ./test

либо выберите более подходящее для вашей программы имя!

2.4.1.7. Я откомпилировал мою программу, и сначала она работала нормально, но потом выдалось сообщение об ошибке с сообщением ``core dumped''. Что это означает?

Фраза core dump (сброс дампа) своим происхождением обязана ранним дням Unix, когда в качестве устройств для хранения данных в машинах использовалась память на магнитных сердечниках (core). В общем, если в программе при некоторых условиях возникала ошибка, система записывала содержимое памяти на магнитных сердечниках на диск в файл с именем core, в котором затем для обнаружения причины происшедшего мог покопаться программист.

2.4.1.8. Очаровательно, но что я теперь должен делать?

Для анализа дампа воспользуйтесь программой gdb (посмотрите Section 2.6).

2.4.1.9. Когда моя программа сбрасывает дамп, она выдает сообщение ``segmentation fault''. Что это такое?

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

Часто встречаемыми причинами этого являются:

  • Попытка записи в область памяти, на которую ссылается указатель со значением NULL, например

        char *foo = NULL;
        strcpy(foo, "bang!");
               
    
  • Использование указателя, Using a pointer that hasn't been initialised, eg

        char *foo;
        strcpy(foo, "bang!");
               
    

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

  • Попытка выхода за границы массива, к примеру

        int bar[20];
        bar[27] = 6;
               
    
  • Попытка произвести запись в область памяти, доступную только в режиме чтения, например,

        char *foo = "My string";
        strcpy(foo, "bang!");
               
    

    Компиляторы Unix часто размещают строковые выражения типа "My string" в областях памяти, доступных только для чтения.

  • Выполнение сомнительных операций с функциями malloc() и free(), например

        char bar[80];
        free(bar);
               
    

    или

        char *foo = malloc(27);
        free(foo);
        free(foo);
               
    

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

2.4.1.10. Иногда при получении дампа памяти выдается сообщение ``bus error''. В моей книге про Unix утверждается, что это является признаком проблемы с оборудованием, но компьютер работает нормально. Это правда?

Нет, к счастью, нет (если, конечно, у вас действительно нет проблем с оборудованием...). Обычно это является еще одним способом сказать, что вы обращаетесь к памяти неправильным образом.

2.4.1.11. Эти дампы памяти, если подумать, могут оказаться весьма полезными, если я смогу получать их, когда захочу. Могу ли я это делать, или мне нужно ждать появления ошибки?

Да, просто, перейдя на другую консоль или xterm, выдайте команду

    % ps

для определения идентификатора процесса, соответствующего вашей программе, и выполните команду

    % kill -ABRT pid
           

где pid тем самым идентификатором процесса, который вы нашли.

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

Notes

[1]

Строго говоря, cc на этом шаге преобразует исходный код в собственный машинно-независимый p-код, а не в язык ассемблера.

[2]

Если вы не знаете, то двоичная сортировка эффективна, а пузырьковая неэффективна.

[3]

Причины этого следует искать в исторических летописях.

[4]

Заметьте, что мы не использовали флаг -o для указания имени выполнимого файла, поэтому получим выполнимый файл с именем a.out. Получение отладочной версии с именем foobar оставляется читателю в качестве упражнения!