Учебная деятельность    

Разработка и компиляция программ под Linux

GNU Compiler Collection

Наиболее популярным средством для транслирования текстов программ в исполняемый файл является GNU Compiler Collection (gcc) - это коллекция компиляторов различных языков программирования (C, C++, Objective-C, Java, Fortran, Ada) и сопутствующие средства: ассемблер, препроцессор, редактор связей и т.п.

Схематично процесс трансляции можно разбить на следующие стадии:
program.c + stdio.h
  
препроцессор
компилятор
оптимизатор
ассемблер
редактор связей
a.out
  program.i program.s program.o

  1. Исходный текст программы на Си (суффикс '.c') или на Си++ (суффикс '.C' или '.cpp'), а также заголовочные файлы (суффикс '.h') обрабатываются препроцессором. На этой стадии в тексте программы интерпретируются такие директивы, как #define, #include, #if и т.п.) Полученный результат (текст программы, не требующий препроцессорной обработки) обычно имеет суффикс '.i' и без явного указания не создаётся.
  2. Компилятор Си/Си++ производит синтаксический разбор текста и формирует соответствующий ему ассемблерный код. После чего возможна обработка текста оптимизатором, позволяющим сократить размер получаемого кода и/или увеличить скорость его работы. Полученный результат (текст программы на языке ассемблера) обычно имеет суффикс '.s' и без явного указания не создаётся. Gcc генерирует ассемблерные файлы не в семантике Intel (принятой, например, в DOS или Windows), а в семантике AT&T.
  3. Ассемблер транслирует программу в машинные инструкции. При этом создаётся так называемый объектный модуль (суффикс '.o'). Объектный файл представляет собой блоки машинного кода и данных с неопределёнными (импортируемыми) адресами ссылок на данные и процедуры в других объектных модулях и содержит список своих (определённых, экспортируемых) процедур и данных.
  4. Редактор связей (компоновщик) связывает один или несколько объектных модулей, полученных в результате компиляции, в исполняемый файл или динамически загружаемую библиотеку (суффикс '.so'). Для каждого импортируемого имени находится его определение в других модулях, упоминание имени заменяется на его адрес (этот процесс называется "разрешение" - resolve). При этом в процессе компоновки происходит связывание программы с динамическими или статическими библиотеками (последние являются архивами объектных файлов) и добавляется код, обеспечивающий инициализацию экземпляра программы при её запуске. Если не указано иное, результирующий файл создаётся с именем a.out.

Описанная последовательность может быть начата или прервана на любой стадии, что задаётся опциями в командной строке. Например, опция -E указывает завершить обработку файла после стадии препроцессора (результат выдаётся на стандартный вывод); опция -S указывает завершить обработку файла после стадии компилятора (перед ассемблером); опция -c указывает завершить обработку файла перед компоновкой редактором связей.

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

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

В командной строке gcc можно указывать несколько исходных файлов (не обязательно от одной и той же стадии), тогда в результате они будут собраны в один исполняемый файл. Это бывает удобно при разработке многомодульных приложений, когда отдельный модуль описан в отдельном исходном файле.

Задание. Создайте простую программу типа "hello world" и посмотрите, что получается в результате каждой из описанных стадий. При помощи программы nm просмотрите списки ссылок в объектном модуле и в готовом исполняемом файле.

 

GNU Debugger

Для отладки программ в Linux предназначена программа GNU Debugger (gdb), которая предоставляет средства выполнения программы по шагам, просмотра переменных программы, регистров процессора, стека и т.п. Программа имеет командный интерфейс - при запуске выдаётся приглашение, после которого вводятся команды gdb и их параметры.

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

#include <stdio.h>

char buf[256];

int main() {
    float f;
    scanf("%g", f);
    scanf("%s", buf);
    printf("f=%g buf=[%s]\n", f, buf);
    return 0;
}

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

$ ./a.out
23
Segmentation fault

Надпись "Segmentation fault" означает, что программа попыталась выполнить недопустимую операцию (скорее всего, попыталась обратиться к области памяти, которая ей не принадлежит). В таком случае весьма полезно воспользоваться отладчиком, чтобы идентифицировать место возникновения проблемы в программе. (Жирным шрифтом выделен текст, набираемый оператором).

$ gdb ./a.out
GNU gdb Red Hat Linux (6.1post-1.20040607.43rh)
Copyright ...
(no debugging symbols found)...

(gdb) run
Starting program: ./a.out
23

Program received signal SIGSEGV, Segmentation fault.
0x20000000000f1e00 in _IO_vfscanf_internal () from /lib/tls/libc.so.6.1
(gdb) backtrace
#0  0x20000000000f1e00 in _IO_vfscanf_internal () from /lib/tls/libc.so.6.1
#1  0x20000000000fb190 in scanf () from /lib/tls/libc.so.6.1
#2  0x40000000000007b0 in main ()
(gdb) quit
The program is running.  Exit anyway? (y or n) y

При старте отладчик сообщает свою версию, правовую информацию и некоторые особенности загружаемой программы. Имя исполняемого файла можно указать в командной строке отладчика или позднее при помощи команды отладчика file. Команды отладчика вводятся после текста приглашения "(gdb)". Для запуска используется команда run. Для завершения работы с отладчиком - quit.

Программа была запущена, оператор ввёл число, после чего отладчик перехватил сигнал системы о недопустимой операции и приостановил выполнение программы. Можно изучить состояние программы в этот момент. Например, команда backtrace позволяет просмотреть стек вызовов процедур, т.е. внутри какой процедуры в данный момент выполнялся код. На вершине стека оказалась функция _IO_vfscanf_internal() из стандарной библиотеки языка Си (libc). Эта функция была вызвана из функции scanf() также из libc. Вызов scanf() был произведён из функции main().

Очевидно, проблему вызвал какой-то из параметров функции scanf(). Но о каком вызове scanf() идёт речь? Ведь в программе их два. В данной конфигурации gdb это достаточно сложно выяснить, т.к. при загрузке gdb сообщил, что в исполняемом файле a.out отсутствует отладочная информация: "no debugging symbols found".

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

Если перекомпилировать исходный текст программы с отладочной информацией и снова запустить в отладчике, получим следующий результат:

$ gcc -g example.c
$ gdb ./a.out
GNU gdb Red Hat Linux (6.1post-1.20040607.43rh)
Copyright ...

(gdb) run
Starting program: ./a.out
34

Program received signal SIGSEGV, Segmentation fault.
0x20000000000f1e00 in _IO_vfscanf_internal () from /lib/tls/libc.so.6.1
(gdb) backtrace
#0  0x20000000000f1e00 in _IO_vfscanf_internal () from /lib/tls/libc.so.6.1
#1  0x20000000000fb190 in scanf () from /lib/tls/libc.so.6.1
#2  0x40000000000007b0 in main () at example.c:7
(gdb)

Обратите внимание - отладчик вывел имя исходного файла и номер строки, где выполнение было приостановлено (седьмая строка). Посмотреть на листинг программы можно при помощи команды list.

(gdb) list
7           scanf("%g", f);
8           scanf("%s", buf);
9           printf("f=%g buf=[%s]\n", f, buf);
10          return 0;
11      }

Очевидно, проблема с первым вызовом scanf(). Изучение справочного руководства по функции scanf() должно подсказать неопытному программисту, что вторым параметром в данном случае должно быть не значение переменной f, а её адрес, т.е. правильной строкой будет:

	scanf("%g", &f);

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

$ gcc -g -Wall example.c
example.c: In function `main':
example.c:7: warning: format argument is not a pointer (arg 2)

Начинающим программистам следует постоянно использовать эту опцию.

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

$ ./a.out
34
gjhgjhg
f=34 buf=[gjhgjhg]

В процессе поиска ошибок в программе может потребоваться выполнение по шагам. Чтобы начать пошаговое выполнение программы, следует использовать команду start. Очередной шаг выполняется командой step:

$ gdb a.out
GNU gdb Red Hat Linux (6.1post-1.20040607.43rh)
Copyright ...

(gdb) start
Breakpoint 1 at 0x4000000000000772: file example.c, line 7.
Starting program: ./a.out
main () at example.c:7
7           scanf("%g", &f);
(gdb) step
45
8           scanf("%s", buf);

Не обязательно каждый раз набирать step. В gdb имеется история команд, прокрутка по которой осуществляется при помощи [ ↑ ] и [ ↓ ]. Кроме того, нажатие [Enter] в пустой строке приводит к повтору последней команды.

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

(gdb) print f
$1 = 45
(gdb) print buf
$2 = "fgfgfg", '\0' <repeats 249 times>

Выполнение программы по шагам - достаточно трудоёмкая процедура. Чаще всего бывает необходимо пошаговое наблюдение за программой лишь на небольшом её кусочке. В таком случае в программе устанавливают контрольные точки и запускают её обычным образом (run). Выполнение программы будет приостановлено при достижении контрольной точки, после чего программа может выполняться по шагам (step) или до следующей контрольной точки (continue). Установка контрольной точки в определённой строке программы - команда break.

(gdb) break 9
Breakpoint 2 at 0x40000000000007c1: file example.c, line 9.
(gdb) run
Starting program: ./a.out
45fgfg

Breakpoint 2, main () at example.c:9
9           printf("f=%g buf=[%s]\n", f, buf);
(gdb)

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

(gdb) start
Breakpoint 1 at 0x4000000000000772: file example.c, line 7.
Starting program: ./a.out
main () at example.c:7
7           scanf("%g", &f);
(gdb) watch buf[0]
Hardware watchpoint 2: buf[0]
(gdb) continue
Continuing.
35 qwerty
Hardware watchpoint 2: buf[0]

Old value = 0 '\0'
New value = 113 'q'
0x20000000000edd81 in _IO_vfscanf_internal () from /lib/tls/libc.so.6.1
(gdb) backtrace
#0  0x20000000000edd81 in _IO_vfscanf_internal () from /lib/tls/libc.so.6.1
#1  0x20000000000fb190 in scanf () from /lib/tls/libc.so.6.1
#2  0x40000000000007c0 in main () at example.c:8

В данном примере была установлена контрольная точка по данным на нулевой элемент массива buf (первый символ строки). Программа была приостановлена в 8-й строке внутри функции scanf() в служебной функции _IO_vfscanf_internal(), когда та попыталась изменить старое значение (символ с кодом 0) на введённое пользователем значение 'q'.

Контрольные точки удаляются при помощи команды delete.

Задание. Создайте программу с циклом (например, суммирующую числа от 1 до 10), скомпилируйте её с отладочной информацией, загрузите в отладчике. Выполните программу тремя способами, выводя перед началом каждой итерации цикла значения счётчика цикла и сумму:

 

GNU make

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

Сценарий для программы make обычно хранится в файле с именем Makefile. Имя сценария можно передать программе make при помощи ключа -f:

make -f my_makefile

Рассмотрим пример. Предположим, что вы собираете проект baseline, который состоит из четырех основных модулей: main.c, create.c, update.c и delete.c. Предположим также, что файл update.c использует файл определений change.h. Взаимосвязи файлов можно схематично представить следующим образом:

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

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

# Makefile для построения программы baseline
CFLAGS=-g -Wall

baseline: main.o create.o update.o delete.o
	gcc -o baseline main.o create.o update.o delete.o
main.o: main.c

create.o: create.c

update.o: update.c change.h

delete.o: delete.c

Строки, начинающиеся с решетки (#), являются комментариями и игнорируются. Далее следуют определения некоторых переменных окружения. В данном примере объявлена переменная CFLAGS, которая хранит опции компилятора по умолчанию. Сценарий содержит несколько блоков вида:

<цель>: <зависимости>
	<команда>
	<команда>
	...

В данном примере результат компилирования проекта (первая цель в сценарии Makefile) - файл baseline. Файл зависит от объектных модулей main.o, create.o, update.o и delete.o. Чтобы получить этот файл, должна быть выполнена команда, указанная в этом блоке. Команды в блоке записываются не в начале блока, а после символа табуляции (важно, чтобы это был именно символ табуляции, а не пробелы). Остальные цели описывают зависимости этих объектных модулей. Например, файл main.o зависит от файла main.c. Если для какой-то цели не указаны команды, как получить целевой файл из его зависимостей, срабатывает механизм умолчаний. Так, файлы с суффиксом '.c' обрабатываются Си-компилятором; файлы с суффиксом '.cpp' обрабатываются Си++-компилятором и т. п. Таким образом, программа make определяет, что для получения перечисленных объектных модулей надо скомпилировать соответствующие файлы.

$ make
gcc -g -Wall -с main.c
gcc -g -Wall -с create.c
gcc -g -Wall -с update.c
gcc -g -Wall -с delete.c
gcc -о baseline main.o create.o update.o delete.o

Если теперь внести изменения в файл change.h, то при запуске программа make по времени модификации определит, что он новее, чем зависящий от него файл update.o и перекомпилирует этот объектный модуль. После этого в соответствии со сценарием потребуется перекомпиляция основной цели - файла baseline, тогда как остальные объектные модули останутся неизменными.

$ make
gcc -g -Wall -с update.c
gcc -о baseline main.o create.o update.o delete.o

Если выполнить make в то время, когда не было никаких изменений, то программа вас предупредит:

$ make
'baseline' is up to date

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

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

Например: http://kompot.petrsu.ru/ftp/pub/integral.tgz.

 

Сборка готового программного продукта

Набор утилит GNU содержит средства для автоматической генерации сценариев компиляции с учётом особенностей платформы. Утилиты autoconf и automake используются для генерации специального сценария командного интерпретатора (обычно называется configure). Этот сценарий распространяеися вместе с исходным кодом проекта и при запуске определяет тип платформы и необходимые для компиляции проекта средства. В результате своей работы такой сценарий создаёт Makefile с необходимыми опциями компилятора, редактора связей и т.п., а также заголовочный файл (обычно config.h), в котором указываются директивы препроцессора, управляющие генерацией кода, специфичного для данной платформы.

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

./configure --help
./configure [необходимые параметры]
make
make install

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

Задание. С указанного преподавателем ресурса (например, отсюда) загрузите два каких-либо архива с исходными текстами (bc-*.tar.bz2, file-*.tar.bz2, links-*.tar.bz2, mp3info-*.tar.bz2, units-*.tar.bz2, wget-*.tar.bz2). Распакуйте архивы в своём домашнем каталоге (воспользуйтесь командой tar). Изучите файлы README или INSTALL, находящиеся внутри архивов, на предмет порядка конфигурирования, инсталляции и установки выбранного программного обеспечения. Сконфигурируйте сценарии компиляции таким образом, чтобы выбранные программые продукты устанавливались в ваш домашний каталог в подкаталог soft (~/soft). Скомпилируйте и установите выбранные программные продукты. Убедитесь в работоспособности. (Что делают выбранные вами программы? Для чего они нужны? В отчёте отразите примеры успешных запусков.)