Второй пример: решение квадратных уравнений




Компилятор

Сейчас существует много компиляторов языка C, более-менее совместимых с различными стандартами. Тем не менее, пока что в среде GNU/Linux наиболее применимым остаётся компилятор C, входящий в комплект GNU Compilers Collection (GCC). Этот компилятор, кроме стандарта C, поддерживает некоторое количество расширений стандарта. Эти расширения, в частности, широко используются в исходных текстах ядра Linux. В последнее время появляются компиляторы, способные скомпилировать ядро Linux (например, llvm-clang, или EKO).

Компилятор GCC запускается из командной оболочки командой вида

gcc [OPTIONS] program.c

где program.c — имя входного файла. Кроме того, по стандарту POSIX, компилятор может быть запущен командой cc program.c (cc — от "C compiler").

При обычном запуске компилятор пытается создать исполняемый файл. По умолчанию, выходной файл называется a.out (такое название осталось от древних версий UNIX). Другое название можно задать с помощью опции компилятора -o, например,

gcc -o program program.c

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

gcc -o program main.c module1.o module2.o …

Чтобы только скомпилировать один исходный файл в объектный код (не пытаясь собрать исполняемый файл), нужно дать команду вида

gcc -c module.c

(имя выходного файла по умолчанию будет module.o).

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

Статическая библиотека в UNIX-подобных системах представляет собой архив (старинного формата ar), включающий набор объектных файлов. Такой архив создаётся командой вида

ar r libsomething.a module1.o module2.o …

Имена файлов библиотек традиционно начинаются с префикса lib.

Динамически загружаемая библиотека представляет собой объектный файл специального формата (расчитанного на динамическую загрузку). Такая библиотека создаётся командой вида

gcc -shared -o libsomething.so module1.c module2.c …

Для использования библиотеки при сборке программы её нужно указать компилятору при помощи опции -l, например

gcc -o program -lm program.c

(здесь будет использоваться файл библиотеки libm.so, префикс lib компилятор подставляет по умолчанию). По умолчанию компилятор собирает программу, использующую динамические библиотеки. Если нужно использовать статические версии библиотек, компилятору нужно указать опцию -static.

Подробную информацию об опциях gcc см. в man gcc.

Hello, world!

Считается, что традиция начинать изучение языка программирования с написания программы, выводящей строку "Hello, world!", пошла с книги Кернигана и Ричи "Язык C" [kernigan_richie]. В случае с языком C эта программа выглядит следующим образом:

#include <stdio.h>

int main (int argc, char* argv[]) {
printf ("Hello world!\n");
return 0;
}

Чтобы запустить эту программу, этот текст нужно записать в файл с именем, скажем, hello.c, и из директории, в которой расположен этот файл, дать команду вида

gcc -o hello hello.c

Впрочем, в случае такой простой программы достаточно дать команду

make hello

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

./hello

Порядок сборки

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


На входе компилятор имеет в общем случае набор файлов с исходными текстами. Перед началом собственно компиляции эти файлы обрабатываются т.н. препроцессором (программа cpp). Главная функция этой программы — выполнение директив вида #include. Встретив такую директиву, препроцессор вставляет содержимое указанного файла (в данном случае, stdio.h) на место этой директивы. Препроцессор понимает ещё некоторые директивы, но сейчас на них останавливаться я не буду.

После препроцессора выполняется собственно компиляция. Из исходных файлов на этом этапе получаются т.н. объектные файлы. Это файлы, содержащие исполняемый машинный код, но ещё не готовые для запуска. Главное, чего в них недостаёт — это адреса вызываемых библиотечных функций. Например, код функции printf() содержится в библиотеке libc. А в объектном файле содержится только имя этой функции. Кроме того, объектный файл содержит имена всех объявленных в нём функций.

Объектные файлы, а также используемые библиотеки подаются на вход компоновщику (программа ld). Компоновщик ищет все вызываемые из различных объектных файлов функции (по именам) в объектных файлах и в библиотеках. Если все функции найдены, то компоновщик собирает собственно исполняемый файл. При этом имена вызываемых функций заменяются на конкретные адреса памяти. В случае использования динамической библиотеки имя используемой функции остаётся, и к нему добавляется имя файла динамической библиотеки, в которой при запуске программы нужно будет искать эту функцию.

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

Второй пример: решение квадратных уравнений

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

#include <stdio.h>
#include <math.h>

/* solve: calculate roots of square equation.
* a, b, c are coefficients in equation.
* Roots would be stored at x1, x2.
* Return value: count of real roots.
*/
int solve(double a, double b, double c,
double* x1, double* x2) {
double D = b*b - 4*a*c;
double sqrtD;

if (D > 0) {
sqrtD = sqrt(D);
*x1 = (-b - sqrtD)/(2.0 * a);
*x2 = (-b + sqrtD)/(2.0 * a);
return 2;
} else if (D < 0)
return 0;
else {
*x1 = -b/(2.0*a);
return 1;
}
}

int main (int argc, char* argv[]) {
double a,b,c;
double x1, x2;
int roots_count;

// Input coefficients
printf("A: ");
scanf("%lf", &a);
printf("B: ");
scanf("%lf", &b);
printf("C: ");
scanf("%lf", &c);

// Solve the equation
roots_count = solve(a,b,c, &x1, &x2);

// Output results
switch (roots_count) {
case 0:
printf("No (real) roots.\n");
break;
case 1:
printf("One root: %0.4lf\n", x1);
break;
case 2:
printf("Two roots: %0.4lf and %0.4lf\n",
x1, x2);
break;
}

return 0;
}


По аналогии с предыдущим примером, запишем этот текст в файл square.c и попытаемся скомпилировать его командой

gcc -o square square.c

Но на этот раз мы получим ошибку примерно такого вида:

/tmp/cc6RNFIi.o: In function `solve': square.c:(.text+0x6d): undefined reference to `sqrt' collect2: ld returned 1 exit status

В чём здесь дело? Ясно, что компилятору почему-то не понравился вызов функции sqrt(). Причём, он жалуется уже не на файл исходного кода, а на объектный файл (вот этот cc6RNFIi.o). Это означает, что исходный файл благополучно скомпилировался, а проблемы возникли на стадии компоновки (что можно видеть и по упоминанию в тексте ошибки программы ld — это стандартный в GNU/Linux компоновщик). Компоновщик не смог найти функцию sqrt(). В данном случае, это произошло из-за того, что эта функция содержится в библиотеке libm, а мы не просили компилятор использовать её. Чтобы избавиться от этой ошибки, нам нужно изменить команду компиляции на следующую:

gcc -o square -lm square.c

Такая команда должна отработать без ошибок и создать исполняемый файл square.

При сборке любой достаточно сложной программы нам придётся использовать несколько библиотек, и, возможно, понадобится указывать ещё какие-то опции компилятору. Команда может получиться довольно длинная. Что же, каждый раз набирать её вручную? Нет. Один из принципов философии UNIX гласит: «Всё, что может быть автоматизировано, должно быть автоматизировано». Здесь нам пригодится одна из древнейших UNIX-утилит — программа make. Чтобы воспользоваться ею, нужно написать файл с именем Makefile (в той же директории, что и наш исходный файл) со следующим содержимым:

square: square.c $(CC) -o $@ -lm $<

Теперь собрать исполняемый файл можно просто дав команду make. Как это работает?

Make

Утилита make предназначена для сборки программ (хотя может использоваться для автоматизации многих других похожих задач). Она читает файл с именем Makefile и видит в нём набор правил. Каждое правило определяет три вещи: цель (goal, т.е. то, что нужно собрать), список исходных файлов и набор команд, которые нужно выполнить, чтобы собрать цель из исходных файлов. В примере выше, square — это имя цели, square.c — единственный в данном случае исходный файл (если их несколько, они перечисляются через пробел), а вторая строчка — команда. В команде могут использоваться переменные. Некоторые из переменных имеют специальное значение. В частности, в любом правиле $@ обозначает имя цели, а $< — первый исходный файл. Переменная $(CC) указывает на компилятор C, используемый в системе по умолчанию (в большинстве случаев это gcc, но бывает и что-нибудь другое).

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

%.o: %.c $(CC) -c $<

обозначает, что файлы с именем, заканчивающимся на.o, нужно собирать из соответствующих файлов с суффиксом.c.

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

%: %.c $(CC) -o $@ $<

Благодаря этому правилу, в примере с «Hello, world!» просто команда make hello запускала cc -o hello hello.c.

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

Более подробную информацию об этой утилите см., например, в man make.

Отладка

Для отладки в Linux используется отладчик gdb. Но сначала, для того, чтобы программу было удобно отлаживать, её нужно скомпилировать с опцией -g. Сейчас нам достаточно изменить Makefile, приведя его к виду

square: square.c $(CC) -o $@ -lm -g $<

и пересобрать программу.

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

Отладка запускается командой вида

gdb./square

Если отлаживаемой программе нужно передать какие-то опции командной строки, их можно указать здесь же:

gdb./some-program -a -b

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

GNU gdb (GDB) 7.2-ubuntu Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see:... Reading symbols from /home/portnov/LUG/src/square...done. (gdb)

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

К наиболее часто используемым командам относятся:

list

Напечатать очередной кусок исходника (печатается 10 строк). Можно указать конкретные номера строк после имени команды, например l 10,15.

run

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

break

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

next

Выполнить одну строку программы.

print

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

display

Добавить выражение к списку постоянно отображаемых. Значения этих выражений будут показываться после исполнения каждой команды. Рядом с каждым выражением печатается его номер в списке. Удалить выражение из списка можно командой undisplay с номером выражения.

quit

Выход из отладчика.

Более подробную информацию по GDB см. в man gdb.

 



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2019-05-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: