Добавить в избранное | Сделать стартовой страницей

Большая Linux библиотека для пользователей OS Linux и ПО для нее
Есть что сказать? Нужен совет? Посети наш форум.




GCC - корень всего.

Автор : Лорн
Перевод : Алексей Отвагин

GCC - корень всего

Резюме:

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



Правила GCC

Можете ли Вы представить компиляцию свободного программного обеспечения компилятором с закрытыми кодами, находящимся в чьей-либо собственности? Откуда Вы будете знать, что происходит в Вашем исполняемом файле? Здесь может присутствовать черный вход для любого трояна. Ken Thompson, при одном из величайших взломов всех времен, написал компилятор, который оставлял черный ход в программе 'login' и увековечил трояна, когда компилятор, реализующий его, компилировал сам себя. Прочтите описание этой вечной классики здесь: . К счастью, у нас есть gcc. Как только Вы выполняете configure; make; make install , gcc выполняет за кадром массу тяжелой работы. Как нам заставить gcc работать на себя? Мы начнем писать карточную игру, но напишем лишь столько, сколько нам понадобится для демонстрации функциональности компилятора. Поскольку мы начинаем с пустого места, нам потребуется понимание процесса компиляции для того, чтобы знать, что нужно сделать для создания исполняемого файла и в каком порядке. Мы сделаем общий обзор того, как компилируется программа на Си, и обзор опций, которые заставляют gcc делать то, что нам нужно. Этапами (и выполняющими их инструментами являются) Препроцессинг(Pre-compile) (gcc -E), Компиляция(Compile) (gcc), Ассемблирование(Assembly) (as), and Компоновка(Link) (ld).

 

В начале...

Во-первых,однако, мы должны знать, как вызвать компилятор в первый раз. Это действительно просто. Мы начнем с вечной классической первой программы на Си. (Старики должны меня простить).

#include 

int main()
{ printf("Hello World!\n"); }

Сохраните этот файл с именем game.c. Вы можете скомпилировать его из командной строки, запустив:

gcc game.c
По умолчанию, компилятор Си создает исполняемый файл с названием a.out. Вы можете запустить его, набрав:
 a.out
Hello World

Всегда, когда Вы компилируете программу, новый a.out будет замещать предыдущую программу. Вы не можете сказать, какая из программ создала текущий a.out. Мы можем решить эту проблему, сообщив gcc, как мы хотим назвать исполняемый файл, с помощью ключа -o . Мы хотим назвать эту программу game, хотя мы могли бы назвать ее как угодно, поскольку Си не имеет ограничений на имена, которые дает Java.
 gcc -o game game.c 
game
Hello World

В этом месте мы еще очень далеки от получения полезной программы. Если Вы думаете, что это плохо, Вы можете принять во внимание факт, что у нас есть программа, которая компилируется и работает. Как только мы мало-помалу начнем добавлять программе функциональности, мы захотим убедиться в том, что программа осталась работоспособной. Известно, что каждый начинающий программист хочет написать 1,000 строк исходного кода, а затем сразу их исправить. Я думаю, что никто этого не сможет. Вы создаете маленькую работающую программу, Вы изменяете ее и вновь заставляете ее работать. Это ограничит количество ошибок, которые Вам нужно будет исправить за один раз. Плюс к тому, Вы точно знаете, что Вы сделали и это не работает, так что Вы знаете, на чем сосредоточиться. Это сдержит Вас от создания чего-то, что, по Вашему мнению должно работать, и возможно даже компилируется, но никогда не станет исполняемым файлом. Помните - то, что программа компилируется, вовсе не означает, что она верна.

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

 #ifndef DECK_H #define DECK_H

#define DECKSIZE 52

typedef struct deck_t
{
  int card[DECKSIZE];
  /* number of cards used */
  int dealt;
}deck_t;

#endif /* DECK_H */

Сохраните этот файл как deck.h. Так как компилируются только файлы .c, мы должны изменить наш game.c. В строке 2 файла game.c, напишите #include "deck.h". В строке 5, напишите deck_t deck; Чтобы убедиться, что мы ничего не сломали, скомпилируйте его снова.

 gcc -o game game.c

Нет ошибок - нет проблем. Если он не компилируется, поработайте с ним, пока он не заработает.

 

Препроцессинг(Pre-compile)

Откуда компилятор знает, какого типа deck_t? Потому что во время препроцессинга он действительно копирует файл "deck.h" в файл "game.c". Директивы препроцессора в исходном коде предваряются префиксом "#". Вы можете вызвать препроцессор через надстройку gcc с помощью ключа -E.

 gcc -E -o game_precompile.txt game.c wc -l game_precompile.txt 3199
game_precompile.txt 
Почти 3,200 строк вывода! Большинство из них происходят из заголовочного файла stdio.h, но если Вы посмотрите на них, Вы увидите также и наши описания. Если Вы не задали имя выходного файла через ключ -o, он будет выведен на консоль. Препроцессинг дает коду большую гибкость, выполняя три главных цели.
  1. Копирует "включаемые(#include)" файлы в компилируемый исходный файл.
  2. Заменяет "определяемый(#define)" текст действительными значениями.
  3. Заменяет макросы в строках, в которых они вызываются.
Это позволяет Вам иметь именованные константы (например, DECKSIZE, представляющую количество карт в колоде), которые используются сквозным образом во всем коде, определяясь в одном месте и автоматически обновляясь везде, как только значение меняется. На практике, Вы почти никогда не воспользуетесь ключом -E самим по себе, а вместо этого пропустите его вывод к компилятору.

 

Компиляция (Compile)

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

 

Ассемблирование(Assembly)

as переводит код на Ассемблере в объектный код. Объектный код не может реально работать в процессоре, но он очень близок к этому. Опция компилятора -c превращает файл .c в объектный файл с расширением .o. Если мы запускаем

 gcc -c game.c 
, мы автоматически создаем файл с именем game.o.Здесь мы остановимся на важной точке. Мы можем взять любой файл .c и создать из него объектный файл. Как мы увидим ниже, мы можем комбинировать эти объектные файлы в исполняемый файл на этапе компоновки. Давайте вернемся к нашему примеру. Поскольку мы программируем карточную игру и определили колоду карт как deck_t, нам нужно написать функцию, которая тасует колоду. Эта функция получает указатель на тип колоды и заполняет его случайными значениями для карт. Она следит за картами, которые уже использовались, с помощью массива 'drawn'. Этот массив из DECKSIZE элементов предотвратит повторения значения карты.

#include 
#include 
#include 
#include "deck.h"

static time_t seed = 0;

void shuffle(deck_t *pdeck)
{
  /* Keeps track of what numbers have been used */
  int drawn[DECKSIZE] = {0};
  int i;

  /* One time initialization of rand */
  if(0 == seed)
  {
    seed = time(NULL);
    srand(seed);
  }
  for(i = 0; i < DECKSIZE; i++)
  {
    int value = -1;
    do
    {
      value = rand() % DECKSIZE;
    }
    while(drawn[value] != 0);

    /* mark value as used */
    drawn[value] = 1;

    /* debug statement */
    printf("%i\n", value);
    pdeck->card[i] = value;
  }
  pdeck->dealt = 0;
  return;
}

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

Отметьте две вещи.
  1. Мы передали параметр по его адресу, который Вы можете получить с помощью оператора '&' (адрес). Он передает машинный адрес переменной в функцию, так что функция сама может изменить переменную. Это возможно в программах с глобальными переменными, но они должны использоваться лишь в очень редких случаях. Указатели являются важной частью Си и Вы должны хорошо понимать их.
  2. Мы используем вызов функции из нового файла .c. Операционная система всегда ищет функцию, называемую 'main' и начинает выполнение с нее. shuffle.c не содержит функции 'main' и поэтому не может быть самостоятельным исполняемым файлом. Мы должны компилировать его с другой программой, которая содержит 'main' и вызывает функцию 'shuffle'.

Запустите команду

 gcc -c
shuffle.c 
и убедитесь, что она создала новый файл с именем shuffle.o. Отредактируйте файл game.c, и в строке 7, после описания переменной deck_t deck, добавьте строку
shuffle(&deck); 
Теперь, если мы попробуем создать исполняемый файл также, как и раньше, мы получим сообщение об ошибке
gcc -o game game.c

/tmp/ccmiHnJX.o: In function `main':
/tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle'
collect2: ld returned 1 exit status
Компиляция была успешной, поскольку синтаксис был правильным. Этап компоновки завершился с ошибкой, поскольку мы не сказали компилятору, где находится функция 'shuffle'. Что такое link и как нам указать компилятору, где найти эту функцию?

 

Компоновка(Link)

Компоновщик, ld, берет объектный код, созданный ранее as и превращает его в исполняемый командой

 gcc -o game game.o shuffle.o
Это скомбинирует два объекта вместе и создаст исполняемый файл game.

Компоновщик находит функцию shuffle в объектном коде shuffle.o и включает ее в исполняемый файл. Настоящая прелесть объектных файлов в том, что если нам нужно использовать эту функцию вновь, то все, что нам потребуется - это подключить файл "deck.h" и связать объектный файл shuffle.o с новым исполняемым файлом.

Повторное использование кода, как этот пример, удовлетворяет всех. Хотя мы не писали функцию printf, которую мы вызывали выше в отладочной конструкции, компоновщик нашел ее определение в файле, который мы включили с помощью #include и связал с объектным кодом, хранящимся в библиотеке Си (/lib/libc.so.6). Этот способ мы можем использовать где угодно, если известные нам функции работают правильно и заботятся о решении наших проблем. Это является причиной, почему заголовочные файлы обычно содержат только определения данных и функций, а не тела функций. Обычно Вы создаете объектные файлы или библиотеки для компоновщика, чтобы поместить их в исполняемый файл. С Вашим кодом могут возникнуть проблемы, поскольку мы не поместили определений функций в наш заголовочный файл. Что мы можем сделать, чтобы убедиться, что все пройдет гладко?

 

Две очень важные опции

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

 game.c:9: warning:
implicit declaration of function `shuffle' 
Это сообщает нам, что у нас есть еще немного работы. Нам нужно поместить в заголовочный файл строку, в которой мы сообщим компилятору все о нашей функции shuffle, чтобы он мог проверить все, что ему нужно. Это звучит, как ограничение, но разделяет определение от реализации и позволяет нам использовать нашу функцию везде простым включением нашего заголовочного файла и присоединением нашего объектного кода. Мы должны поместить одну строку в файл deck.h.
 void shuffle(deck_t
*pdeck); 
Это избавит нас от предупреждающего сообщения.

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

 

Отладка(Debugging)

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

 game | sort - n | less 
и убедившись, что ничего не пропало. Что нам нужно сделать, если здесь возникнет проблема? Как мы можем посмотреть под крышку и найти ошибку?

Вы можете проверить Ваш код с помощью отладчика. Большинство дистрибутивов предлагают классический отладчик - gdb. Если опции командной строки подавляют Вас, как и меня, KDE предлагает очень хорошую надстройку KDbg. Существуют и другие надстройки, и они очень подобны. Для запуска отладки, Вы выбираете File->Executable, а затем находите Вашу программу game. Когда Вы нажимаете F5 или выбираете Execution->Run из меню, Вы должны увидеть вывод в отдельном окне. Что происходит? Мы ничего не видим в окне. Не волнуйтесь, KDbg не сломался. Проблема состоит в том, что мы не поместили в исполняемый файл отладочную информацию, так что KDbg не может сказать нам, что происходит внутри. Флаг компилятора -g помещает необходимую информацию в объектные файлы. Вы должны компилировать объектные файлы(расширение .o) с этим флагом, так что команда станет такой
gcc -g -c shuffle.c game.c
gcc -g -o game game.o shuffle.o
Это поместит в исполняемый файл метки, которые позволяют gdb и KDbg показать, что же происходит. Отладка является важным навыком, и очень полезно потратить Ваше время, чтобы научиться работать с отладчиком. Способом, которым отладчик помогает программисту, является возможность установки "Точки останова" 'Breakpoint' в исходном коде. Попробуйте поставить ее сейчас нажатием правой кнопки на строке с вызовом функции shuffle. На этой строке должен появиться маленький красный кружок. Теперь, когда Вы нажмете F5, программа остановит выполнение на этой строке. Нажмите F8, чтобы войти в функцию shuffle. Ого, теперь мы смотрим на код из shuffle.c! Мы можем контролировать выполнение шаг за шагом и видеть, что в действительности происходит. Если Вы наведете стрелку на локальную переменную, Вы увидите, что в ней хранится. Приятно. Это гораздо лучше, чем выражения с printf, не так ли?

 

Заключение

Эта статья представляет беглый тур по компиляции и отладке программ Си. Мы обсудили шаги, которые выполняет компилятор и опции, которые ему нужно передать для их выполнения. Мы коснулись компоновки разделяемых библиотек и закончили введением в отладчики. Требуется много труда, чтобы действительно понять, что Вы делаете, но я надеюсь, что это послужит, чтобы Вы начали с той ноги. Вы можете найти больше информации в страницах man и info для gcc, as и ld.

Самостоятельное написание кода научит Вас лучше всего. Для практики, Вы можете взять сырое начало программы карточной игры из этой статьи и написать игру "в очко". Потратьте время, чтобы изучить, как пользоваться отладчиком. Гораздо легче начать с отладчика с графическим интерфейсом, как KDbg. Если Вы сразу добавите немного функциональности, Вам будет приятно, прежде чем Вы поймете это. Помните, программа должна оставаться работающей!

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

  • Определение карточного игрока (например, Вы можете определить deck_t как player_t).
  • Функция, которая раздает нужное количество карт нужному игроку. Помните об увеличении количества 'сданных' в колоде, чтобы отследить, куда сдать следующую карту. Помните о контроле за тем, сколько карт на руках у игроков.
  • Некоторое взаимодействие с пользователем, если игрок хочет взять еще карту.
  • Функция вывода содержимого на руках у игрока. Карта card является значением value % 13 (дающим от 0 до 12), масть suit является значением value / 13 (дающим от 0 до 3).
  • Функция определения значения на руках у игрока. Тузы имеют значение карты 0 и могут считаться за 1 или 11. Короли имеют значение 12 и весят по 10.

 

Ссылки

  • gcc Коллекция компиляторов GNU GCC
  • gdb Отладчик GNU
  • KDbg Отладчик с интерфейсом KDE
  • Award Winning Compiler Hack Ken Thompson's great compiler hack

Обсудить данную тему на нашем форуме "Все о Linux"