Программирование под Linux

Опубликовано admin в Ср, 27/10/2010 - 16:29

Программирование под Linux

Шаг 1 - Компиляция программ на языке C/C++ (30.01.2002 - 2 Kb)
Шаг 2 - Отладочная информация (30.01.2002 - 4.2 Kb)
Шаг 3 - Компиляция нескольких файлов (15.02.2002 - 3.7 Kb)
Шаг 4 - Библиотеки объектных файлов (20.02.2002 - 3.0 Kb)
Шаг 5 - Создание статической библиотеки (20.02.2002 - 3.7 Kb)
Шаг 6 - Создание динамической библиотеки (20.02.2002 - 5.2 Kb)
Шаг 7 - Использование динамических библиотек (23.02.2002 - 4.9 Kb)
Шаг 8 - Функции работы с динамическими библиотеками (23.02.2002 - 5.0 Kb)
Шаг 9 - Инициализация динамических библиотек (27.02.2002 - 3.5 Kb)
Шаг 10 - Передача опций в программу - getopt (15.10.2002 - 6.6 Kb)
Шаг 11 - Передача длинных опций в программу - getopt_long (19.12.2002 - 11.2 Kb)
Шаг 12 - Вывод сообщений об ошибках программы (22.12.2002 - 7.5 Kb)
Шаг 13 - Получение информации о пользователе (22.12.2002 - 10.2 Kb)
Шаг 14 - Получение данных из shadow password (22.12.2002 - 5.5 Kb)
Шаг 15 - Работа с паролями системы с помощью функции crypt() (24.12.2002 - 10.0 Kb)
Шаг 16 - Получение информации о группах пользователей (18.02.2002 - 3.1 Kb)
Шаг 17 - Работа с переменными окружения - setenv и getenv (15.11.2005 - 6.6 Kb)
Шаг 18 - Генерация хешей MD2, MD4, MD5 с помощью OpenSSL (15.09.2006 - 5.6 Kb)
Шаг 19 - Чтение ZIP файла в Linux с помощью библиотеки libzip (22.03.2009 - 8.2 Kb)
Шаг 20 - Чтение содержимого директорий diropen, readdir, closedir (25.03.2009 - 4.2 Kb)


Шаг 1 - Компиляция программ на языке C/C++

Компилятор превращает код программы на "человеческом" языке в объектный код понятный компьютеру. Компиляторов под Linux существует много, практически для каждого распространенного языка. Большинство самых востребованных компиляторов входит в набор GNU Compiler Collection, известных под названием GCC (http://gcc.gnu.org).

Изначально аббревиатура GCC имела смысл GNU C Compiler, но в апреле 1999 года сообщество GNU решило взять на себя более сложную миссию и начать создание компиляторов для новых языков с новыми методами оптимизации, поддержкой новых платформ, улучшенных runtime-библиотек и других изменений (http://gcc.gnu.org/gccmission.html). Поэтому сегодня коллекция содержит в себе компиляторы для языков C, C++, Objective C, Chill, Fortran, Ada и Java, как библиотеки для этих языков (libstdc++, libgcj, ...).

Компиляция программ производится командой:

gcc <имя_файла>

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

./a.out

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

#include <stdio.h>

int main(){
	printf("[http://linux.firststeps.ru]\n");
	printf("Our first program for Linux.\n");
	return 0;
};

И запустим ее:

1_1.gif (957 b)

Загрузить проект

Шаг 2 - Отладочная информация

Любой компилятор по умолчанию снабжает объектный файл отладочной информацией. Компилятор gcc также снабжает файл такой информацией и на результат вы можете посмотреть сами. При компиляции проекта из предыдущего шага у нас появился файл a.out размером 11817 байт (возможно у вас он может быть другого размера).

Вся эта отладочная информация предназначается для отладки программы отладчиком GNU Debugger. Запустить его вы можете командой:

gdb a.out

При этом запустится отладчик и наша скомпилированная программа. Об отладке мы с вами поговорим отдельно и более подробно.

Компилятор gcc может создавать отладочную информацию в различных объемах и форматах, контролировать которые можно специальными ключами. Посмотреть их подробное описание можно командой man gcc:

Debugging Options
	-a -dletters -fpretend-float -g -glevel -gcoff
	-gxcoff -gxcoff+ -gdwarf -gdwarf+ -gstabs -gstabs+
	-ggdb -p -pg -save-temps -print-file-name=library
	-print-libgcc-file-name -print-prog-name=program

Ключ -g создает отладочню информацию в родном для операционной системы виде, он выбирает между несколькими форматами: stabs, COFF, XCOFF или DWARF. На многих системах данный ключ позволяет использовать специальную информацию, которую умеет использовать только отладчик gdb. Другие ключи позволяют более тонко контролировать процесс встраивания отладочной информации.

Ключ -ggdb включает в исполняемый файл отладочную информацию в родном для ОС виде и дополняет ее специализированной информацией для отладчика gdb.

Ключ -gstabs создает отладочную информацию в формате stabs без дополнительных расширений gdb. Данный формат используется отладчиком DBX на большинстве BSD систем. Ключ -gstabs+ дополняет отладочную информацию расширенниями понятными отладчику gdb.

Ключ -gcoff создает отладочную информацию в формате COFF, которая используется отладчиком SDB на большинстве систем System V до версии System V R4.

Ключ -gxcoff снабжает файл информацией в формате XCOFF, который используется отладчиком DBX на системах IBM RS/6000. Использование -gxcoff+ влкючает использование дополнительной информации для gdb.

Ключ -gdwarf добавляет инфомацию в формате DWARF приняотм в системе System V Release 4. Соответственно ключ -gdwarf+ прибавляет возможностей отладчику gdb.

Добавление к этим ключам в конце цифры позволяет увеличить или уменьшить уровень отладки, т.е. управлять размером требуемой отладочной информации. Например ключ:

gcc -g3 ...

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

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

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

strip -s a.out

После обработки файла этой командой его размер уменьшился практически в три раза и стал 3156 байт. По сравнению с 11 Кб до этого это очень даже хорошо.

Загрузить проект

Шаг 3 - Компиляция нескольких файлов

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

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

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

Для того, чтобы вынести функцию или переменную в отдельный файл надо перед ней поставить зарезервированное слово extern. Давайте для примера создадим программу из нескольких файлов. Сначала создадим главную программу, в которой будут две внешние процедуры. Назовем этот файл main.c:

#include <stdio.h>

// описываем функцию f1() как внешнюю
extern int f1();

// описываем функцию f2() как внешнюю
extern int f2();

int main()
{
	int n1, n2;

	n1 = f1();
	n2 = f2();

	printf("f1() = %d\n",n1);
	printf("f2() = %d\n",n2);

	return 0;
}

Теперь создаем два файла, каждый из которых будет содержать полное определение внешней функции из главной программы. Файлы назовем f1.c и f2.c:

// файл f1.c
int f1()
{
	return 2;
}

// файл f2.c
int f2()
{
	return 10;
}

После этого процесс компиляции программы с помощью gcc будет выглядеть несколько иначе от описанного в "Шаг 1 - Компиляция программ на языке C/C++".

Компилировать можно все файлы одновременно одной командой, перечисляя составные файлы через пробел после ключа -c:

gcc -c main.c f1.c f2.c

Или каждый файл в отдельности:

gcc -c f1.c
gcc -c f2.c
gcc -c main.c

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

main.o
f1.o
f2.o

Чтобы их собрать в один файл с помощью gcc надо использовать ключ -o, при этом линкер соберет все файлы в один:

gcc main.o f1.o f2.o -o rezult

В результате вызова полученной программы rezult командой:

./rezult

На экране появится результат работы:

dron:~# ./rezult
f1() = 2
f2() = 10
dron:~#

Теперь, если мы изменим какую-то из процедур, например f1():

int f1()
{
	return 25;
}

То компилировать заново все файлы не придется, а понадобится лишь скомпилировать измененный файл и собрать результирующий файл из кусков:

dron:~# gcc -c f1.c
dron:~# gcc main.o f1.o f2.o -o rezult2
dron:~# ./rezult2
f1() = 25
f2() = 10
dron:~#

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



Шаг 4 - Библиотеки объектных файлов

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

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

Объектные библиотеки по способу использования разделяются на два вида:

  • Статические библиотеки
  • Динамические библиотеки

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

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

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

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



Шаг 5 - Создание статической библиотеки

Для создания статических библиотек существует специальная простая программа называемая ar (сокр. от archiver - архиватор). Она используется для создания, модификации и просмотра объектных файлов в статических библиотеках, которые в действительности представляют из себя простые архивы.

Давайте вернемся к проекту из шага "Шаг 3 - Компиляция нескольких файлов" и создадим из файлов f1.c и f2.c отдельную библиотеку. Для начала компилируем эти файлы:

dron:~# gcc -c f1.c f2.c

В результате получим, как обычно, два файла - f1.o и f2.o. Для того, чтобы создать библиотеку из объектых файлов надо вызвать программу ar со следующими параметрами:

ar rc libимя_библиотеки.a [список_*.o_файлов]

Допустим наша библиотека будет называться fs, тогда команда запишется в виде:

dron:~# ar rc libfs.a f1.o f2.o

В результате получим файл libfs.a, в котором будут лежать копии объектых файлов f1.o и f2.o. Если файл библиотеки уже существует, то архиватор будет анализировать содержимое архива, он добавит новые объектные файлы и заменит старые обновленными версиями. Опция c заставляет создавать (от create) библиотеку, если ее нет, а опция r (от replace) заменяет старые объектные файлы новыми версиями.

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

ranlib libимя_библиотеки.a

Программа ranlib добавит индекс к архиву и получится полноценная статическая библиотека объектных файлов. Стоит отметить, что на некоторых системах программа ar автоматически создает индекс, и использование ranlib не имеет никакого эффекта. Но тут надо быть осторожным при атоматической компиляции библиотеки с помощью файлов makefile, если вы не будете использовать утилиту ranlib, то возможно на каких-то системах библиотеки будут создаваться не верно и потеряется независимость от платформы. Так что возьмем за правило тот факт, что утилиту ranlib надо запускать в любом случае, даже если он нее нет никакого эффекта.

Для компиляции нашего основного файла main.c надо сообщить компилятору, что надо использовать библиотеки. Чтобы компилятор знал где искать библиотеки ему надо сообщить каталог, в котором они содержатся и список этих билиотек. Каталог с библиотеками указывается ключом -L, в нашем случае библиотека находится в текущем каталоге, значит путь до нее будет в виде точки (-L.). Используемые библиотеки перечисляются через ключ -l, после которого указывается название библиотеки без префикса lib и окончания .a. В нашем случае этот ключ будет выглядеть, как -lfs. Теперь все одной командой:

dron:~# gcc -c main.c
dron:~# gcc main.o -L. -lfs -o rezult

Или можно чуть короче:

dron:~# gcc main.c -L. -lfs -o rezult

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

dron:~# gcc -c main.c -L. -lfs
gcc: -lfs: linker input file unused since linking not done

Что означает, что файлы библиотек не нужны, до процесса линковки. Данная команда создаст лишь файл main.o, который в итоге потом придется собирать отдельно.



Шаг 6 - Создание динамической библиотеки

Как мы уже говорили в шаге "Шаг 4 - Библиотеки объектных файлов" динамические библиотеки немного лучше статических, но их использование более сложное. И не из-за того, что процесс загрузки программы замедляется. Проблемы начинаются уже на этапе компиляции :)

Для начала стоит сказать, что объектный файл создаваемый нашим проверенным способом вовсе не подходит для динамических библиотек. Связано это с тем, что все объектные файлы создаваемые обычным образом не имеют представления о том в какие адреса памяти будет загружена использующая их программа. Несколько различных программ могут использовать одну библиотеку, и каждая из них располагается в различном адресном пространстве. Поэтому требуется, чтобы переходы в функциях библиотеки (операции goto на ассемблере) использовали не абсолютную адресацию, а относительную. То есть генерируемый компилятором код должен быть независимым от адресов, такая технология получила название PIC - Position Independent Code. В компиляторе gcc данная возможность включается ключом -fPIC.

Теперь компилирование наших файлов будет иметь вид:

dron:~# gcc -fPIC -c f1.c
dron:~# gcc -fPIC -c f2.c

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

dron:~# gcc -shared -o libfsdyn.so f1.o f2.o

В результате получим динамическую библиотеку libfsdyn.so, которая по моей задумке будет динамической версией библиотеки libfs.a, что видно из названия :) Теперь, чтобы компилировать результирующий файл с использованием динамической библиотеки нам надо собрать файл командой:

dron:~# gcc -с main.с
dron:~# gcc main.o -L. -lfsdyn -o rezultdyn

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

На этом фокусы не кончаются, если Вы сейчас попробуете запустить файл rezultdyn, то получите ошибку:

dron:~# ./rezultdyn
./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open 
shared object file: No such file or directory
dron:~#

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

А сейчас стоит поговорить еще об одном моменте использования библиотек. Я специально создал динамическую библиотеку с названием fsdyn, чтобы она отличалась от названия статической библиотеки fs. Дело в том, что если у Вас две библиотеки статическая и динамическая с одинаковыми названиями, то есть libfs.a и libfs.so, то компилятор всегда будет использовать динамическую библиотеку.

Связано это с тем, что в ключе -l задается часть имени библиотеки, а префикс lib и окончание .a или .so приставляет сам компилятор. Так вот алгоритм работы компилятора таков, что если есть динамическая библиотека, то она используется по умолчанию. Статическая же библиотека используется когда компилятор не может обнаружить файл .so этой библиотеки. Во всей имеющейся у меня документации пишется, что если использовать ключ -static, то можно насильно заставить компилятор использовать статическую библиотеку. Отлично, попробуем...

dron:~# gcc -staticmain.o -L. -lfs  -o rez1

Как бы я не пробовал играть с позицией ключа -static, результирующий файл rez1 получается размером в 900 Кб. После применения программы strip размер ее уменьшается до 200 Кб, но это же не сравнить с тем, что наша первая статическая компиляция давала программу размером 10 Кб. А связано это с тем, что любая программа написанная на C/C++ в Linux использует стандартную библиотеку "C" library, которая содержит в себе определения таких функций, как printf(), write() и всех остальных. Эта библиотека линкуется к файлу как динамическая, чтобы все программы написанные на C++ могли использовать единожды загруженные функции. Ну, а при указании ключа -static компилятор делает линковку libc статической, поэтому размер кода увеличивается на все 200 Кб.

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



Шаг 7 - Использование динамических библиотек

В прошлый раз мы с Вами обнаружили, что запуск программ, скомпилированных вместе с динамическими библиотеками, вызывает ошибку:

dron:~# ./rezultdyn
./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open 
shared object file: No such file or directorydron:/#

Это сообщение выдает загрузчик динамических библиотек(динамический линковщик - dynamic linker), который в нашем случае не может обнаружить библиотеку libfsdyn.so. Для настройки динамического линковщика существует ряд программ.

Первая программа называется ldd. Она выдает на экран список динамических библиотек используемых в программе и их местоположение. В качестве параметра ей сообщается название обследуемой программы. Давайте попробуем использовать ее для нашей программы rezultdyn:

dron:~# ldd rezultdyn
        libfsdyn.so => not found
        libc.so.6 => /lib/libc.so.6 (0x40016000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
dron:~#

Как видите все правильно. Программа использует три библиотеки:

  • libc.so.6 - стандартную библиотеку функций языка C++.
  • ld-linux.so.2 - библиотеку динамической линковки программ ELF формата.
  • libfsdyn.so - нашу динамическую библиотеку функций.

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

Для того, чтобы добавить нашу директорию с библиотекой в список известных директорий надо подредактировать файл /etc/ld.so.conf. Например, у меня этот файл состоит из таких строк:

dron:~# cat /etc/ld.so.conf
/usr/X11R6/lib
/usr/i386-slackware-linux/lib
/usr/i386-slackware-linux-gnulibc1/lib
/usr/i386-slackware-linux-gnuaout/lib
dron:~#

Во всех этих директории хранятся всеми используемые библиотеки. В этом списке нет лишь одной директории - /lib, которая сама по себе не нуждается в описании, так как она является главной. Получается, что наша библиотека станет "заметной", если поместить ее в один их этих каталогов, либо отдельно описать в отдельном каталоге. Давайте для теста опишем, добавим строку в конец файла ld.so.conf:

/root

У меня этот файл валяется в домашнем каталога пользователя root, у Вас он может быть в другом месте. Теперь после этого динамический линковщик будет знать где можно найти наш файл, но после изменения конфигурационного файла ld.so.conf необходимо, чтобы система перечитала настройки заново. Это делает программа ldconfig. Пробуем запустить нашу программу:

dron:~# ldconfig
dron:~# ./rezultdyn
f1() = 25
f2() = 10
dron:~#

Как видите все заработало :) Если теперь Вы удалите добавленную нами строку и снова запустите ldconfig, то данные о расположении нашей библиотеки исчезнут и будет появляться таже самая ошибка.

Но описанный метод влияет на всю систему в целом и требует доступа администратора системы, т.е. root. А если Вы простой пользователь без сверх возможностей ?!

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

dron:~# echo $LD_LIBRARY_PATH

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

dron:~# LD_LIBRARY_PATH=/root
dron:~# export LD_LIBRARY_PATH

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

dron:~# LD_LIBRARY_PATH=/root:${LD_LIBRARY_PATH}
dron:~# export LD_LIBRARY_PATH

Если Вы обнулите эту переменную, то снова библиотека перестанет работать:

dron:~# LD_LIBRARY_PATH=""
dron:~# export LD_LIBRARY_PATH
dron:~# ./rezultdyn
./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open 
shared object file: No such file or directory
dron:~#

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



Шаг 8 - Функции работы с динамическими библиотеками

Если Вы подумали, что фокусы с динамическими библиотеками кончились, то Вы очень сильно ошиблись. До того были цветочки, а ягодки будут сейчас :)

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

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

#include <dlfcn.h>

Чтобы вызывать какие-то функции из динамической библиотеки сначала надо открыть эту библиотеку (можно сказать "загрузить"). Открывается она функцией:

void *dlopen (const char *filename, int flag);

Параметр filename содержит путь до требуемой библиотеки, а параметр flag задает некоторые специфические флаги для работы. Функция возвращает указатель на загруженную библиотеку. В случае любой ошибки возвращается указатель NULL. В таком случае тест ошибки понятный человеку можно получить с помощью функции dlerror(). Пока мы не будем задумываться над этим, и я приведу стандартный код для открытия библиотеки:

void *library_handler;

//......

//загрузка библиотеки

library_handler = dlopen("/path/to/the/library.so",RTLD_LAZY);
if (!library_handler){
	//если ошибка, то вывести ее на экран
	fprintf(stderr,"dlopen() error: %s\n", dlerror());
	exit(1); // в случае ошибки можно, например, закончить работу программы
};

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

void *dlsym(void *handle, char *symbol);

Для этой функции требуется адрес загруженной библиотеки handle, полученный при открытии функцией dlopen(). Требуемая функция или переменная задается своим именем в переменной symbol.

Закрывается библиотека функцией:

dlclose(void *handle);

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

Для примера создадим программу, которая в качестве параметра получает название функции, которую она будет использовать в работе. Например, это будут математические функции возведения в степень. Создадим сначала динамическую библиотеку. Пишем ее код:

double power2(double x){
	return x*x;
};

double power3(double x){
	return x*x*x;
};

double power4(double x){
	return power2(x)*power2(x);
};

//......

Сохраняем его в файл lib.c и создаем динамическую библиотеку libpowers.so следующими командами:

dron:~# gcc -fPIC -c lib.c
dron:~# gcc -shared lib.o -o libpowers.so

Теперь создаем основную программу в файле main.c:

#include <stdio.h>

/* заголовочный файл для работы с динамическими библиотеками */
#include <dlfcn.h>

int main(int argc, char* argv[]){

	void *ext_library;	// хандлер внешней библиотеки
	double value=0;		// значение для теста
	double (*powerfunc)(double x);	// переменная для хранения адреса функции

	//загрузка библиотеки
	ext_library = dlopen("/root/libpowers.so",RTLD_LAZY);
	if (!ext_library){
		//если ошибка, то вывести ее на экран
		fprintf(stderr,"dlopen() error: %s\n", dlerror());
		return 1;
	};

	//загружаем из библиотеки требуемую процедуру
	powerfunc = dlsym(ext_library, argv[1]);	
	value=3.0;

	//выводим результат работы процедуры
	printf("%s(%f) = %f\n",argv[1],value,(*powerfunc)(value));

	//закрываем библиотеку
	dlclose(ext_library);
};

Код главной программы готов. Требуется его откомпилировать с использованием библиотеки dl:

dron:~# gcc main.c -o main -ldl

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

dron:~# ./main power2
power2(3.000000) = 9.000000
dron:~# ./main power3
power3(3.000000) = 27.000000
dron:~# ./main power4
power4(3.000000) = 81.000000
dron:~#

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

Загрузить проект

Шаг 9 - Инициализация динамических библиотек

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

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

  • void _init() - инициализация
  • void _fini() - деинициализация

Чтобы понять, что к чему, введем в нашей библиотеке lib.c переменную test и возвращающую ее функцию:

char *test;

char *ret_test(){
	return test;
};

Пишем основную программу main.c. Она очень похожа на предыдущий наш проект, поэтому можете его модифицировать:

#include <stdio.h>
#include <dlfcn.h>

int main(){

	void *ext_library;
	double value=0;
	char * (*ret_test)();

	ext_library = dlopen("libtest.so",RTLD_LAZY);
	if (!ext_library){
		fprintf(stderr,"dlopen() error: %s\n", dlerror());
		return 1;
	};

	ret_test = dlsym(ext_library,"ret_test");	

	printf("Return of ret_test: \"%s\" [%p]\n",(*ret_test)(),(*ret_test)());

	dlclose(ext_library);
};

После компиляции всего этого хозяйства мы получим результат:

dron:~# gcc -c lib.c -fPIC
dron:~# gcc -shared lib.o -o libtest.so
dron:~# gcc -o main main.c -ldl
dron:~# ./main
Return of ret_test: "(null)" [(nil)]
dron:~#

Как видите переменная test оказалась равной NULL, а нам бы хотелось нечто другое. Ну, так давайте посмотрим как работают функции _init() и _fini(). Создадим вторую библиотеку lib1.c:

#include <stdlib.h>

char *test;

char *ret_test(){
	return test;
};

void _init(){
	test=(char *)malloc(6);
	if (test!=NULL){
		*(test+0)='d';
		*(test+1)='r';
		*(test+2)='o';
		*(test+3)='n';
		*(test+4)='!';
		*(test+5)=0;
	};
	printf("_init() executed...\n");
};

void _fini(){
	if (test!=NULL) free(test);	
	printf("_fini() executed...\n");
};

Теперь пробуем компилировать:

dron:~# gcc -c lib1.c -fPIC
dron:~# gcc -shared lib1.o -o libtest.so
lib1.o: In function `_init':
lib1.o(.text+0x24): multiple definition of `_init'
/usr/lib/crti.o(.init+0x0): first defined here
lib1.o: In function `_fini':
lib1.o(.text+0xc0): multiple definition of `_fini'
/usr/lib/crti.o(.fini+0x0): first defined here
collect2: ld returned 1 exit status
dron:~#

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

dron:~# gcc -shared -nostdlib lib1.o -o libtest.so
dron:~#

Смотрите-ка, все прекрасно скомилировалось. Теперь попробуем запустить main:

dron:~# ./main
_init() executed...
Return of ret_test: "dron!" [0x8049c20]
_fini() executed...
dron:~#

Ну как ? Помоему классно. Теперь можно спокойно создавать для работы библиотеки правильно инициализированные переменные. Однако классные эти штуки - динамические библиотеки ! А Вы что хотели ? Тот же Windows только на своих DLL и живет. А Linux ничем не хуже...

Загрузить проект

Шаг 10 - Передача опций в программу - getopt

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

Вы наверняка знаете, что передача параметров в программу на C/C++ осуществляется через массив функции main(). Так повелось, что он называется argv (от arguments values - значения аргументов), но в принципе его можно назвать и по другому. Количество этих параметров передается через переменную argc (от arguments counter - счетчик аргументов).

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

int main(int argc, char *argv[]{
};

int main(int argc, char **argv){
};

Давайте напишем маленькую программку, которая выводит значения переданных параметров:

// программа test.c

#include <stdio.h>
int main(int argc, char *argv[]){
	int i=0;
	for (i=0;i<argc;i++){
		printf("Argument %d: %s\n",i,argv[i]);
	};
};

Сохраняем в файл test.c и компилируем:

dron:~# gcc test.c -o test

После этого попробуем запустить программу:

dron:~# ./test
Argument 0: ./test

Передадим несколько параметров:

dron:~# ./test qwe sdf fgh hjk kl 123 --help
Argument 0: ./test
Argument 1: qwe
Argument 2: sdf
Argument 3: fgh
Argument 4: hjk
Argument 5: kl
Argument 6: 123
Argument 7: --help

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

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

-h       - короткий параметр
--help   - длинный параметр

-s 10    - параметры со значениями
--size 10
--size=10

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

  • int getopt(...) - Обрабатывает короткие параметры
  • int getopt_long(...) - Обрабатывает короткие и длинные параметры
  • int getopt_long_only(...) - Обрабатывает параметры только как длинные

Давайте разберемся с работой первой функции - getopt(...). Ее определение выглядит следующим образом:

#include <unistd.h>

int getopt(int argc, char * const argv[],
	const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

Эта функция последовательно перебирает переданные параметры в программу. Для работы в функцию передается количество параметров argc, массив параметров argv[] и специальная строка optstring, в которой перечисляются названия коротких параметров и признаки того, что параметры должны иметь значение. Например, если программа должна воспринимать три параметра a, b, F , то такая строка бы выглядела как "abF". Если параметр должен иметь значение, то после буквы параметра ставится двоеточие, например параметр F и d имеют значения, а параметры e, a и b не имеют, тогда эта строка могла бы выглядеть как "eF:ad:b". Если параметр может иметь (т.е. может и не иметь) значение, то тогда ставится два знака двоеточия, например "a::" (это специальное расширение GNU). Если optstring содержит "W:", то тогда параметр -W opt переданный в программу, будет восприниматься как длинный параметр --opt. Это связано с тем, что параметр W зарезервирован в POSIX.2 для расширения возможностей.

Для перебора параметров функцию getopt() надо вызывать в цикле. В качестве результата возвращется буква названия параметра, если же параметры кончились, то функция возвращает -1. Индекс текущего параметра хранится в optind, а значение параметра помещается в optarg (указатель просто указывает на элемент массива argv[]). Если функция находит параметр не перечисленный в списке, то выводится сообщение об ошибке в stderr и код ошибки сохраняется в opterr, при этом в качестве значения возврящается "?". Вывод ошибки можно запретить, если установить opterr в 0.

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    int rez=0;

//	opterr=0;
	while ( (rez = getopt(argc,argv,"ab:C::d")) != -1){
		switch (rez){
		case 'a': printf("found argument \"a\".\n"); break;
		case 'b': printf("found argument \"b = %s\".\n",optarg); break;
		case 'C': printf("found argument \"C = %s\".\n",optarg); break;
		case 'd': printf("found argument \"d\"\n"); break;
		case '?': printf("Error found !\n");break;
        };
	};

};

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

dron:~# gcc test.c -o test

dron:~# ./test -a -b -d -C
found argument "a".
found argument "b = -d".
found argument "C = (null)".

dron:~# ./test -a -b -C -d
found argument "a".
found argument "b = -C".
found argument "d"

dron:~# ./test -a -b1 -C -d
found argument "a".
found argument "b = 1".
found argument "C = (null)".
found argument "d"

dron:~# ./test -b1 -b2 -b 15
found argument "b = 1".
found argument "b = 2".
found argument "b = 15".

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

dron:~# ./test -h -a
./test: invalid option -- h
Error found !
found argument "a".

Как я и говорил, функция вывела сообщение об ошибке в stderr. Давайте выключим вывод сообщений, для этого надо где-то в программе перед вызовом функции вставить opterr=0;. Компилируем и запускаем:

dron:~# ./test -h -a
Error found !
found argument "a".

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



Шаг 11 - Передача длинных опций в программу - getopt_long

Парсинг длинных параметров командной строки достаточно сложный процесс, поэтому бибилиотека GNU C Library имеет специальную функцию getopt_long(), которая может работать одновременно и с длинными и с короткими параметрами. Для работы только с длинными именами параметров существует функция getopt_long_only.

Для того, чтобы работать с этими функциями Вам потребуется подключить файл getopt.h. Выглядят эти функции следующим образом:

#define _GNU_SOURCE
#include <getopt.h>

int getopt_long(int argc, char * const argv[],
	const char *optstring,
	const struct option *longopts, int *longindex);

int getopt_long_only(int argc, char * const argv[],
	const char *optstring,
	const struct option *longopts, int *longindex);

Для работы функции getopt_long ей нужны следующие данные:

  • argc - счетчик количества параметров командой строки argc.
  • argv - значения парамеров командной строки argv[].
  • optstring - список коротких названий параметров, которые мы изучали в "Шаг 10 - Передача опций в программу - getopt".
  • longopts - специальный массив с названиями длинных параметров.
  • longindex - указатель на переменную, в которую будет помещен индекс текущего параметра из массива longopts.

Основным отличием этих фукнций от getopt является потребность в специальном массиве. О нем и поговорим. Массив longopts состоит из записей struct option имеющих следующий вид:

struct option {
	const char *name;
	int has_arg;
	int *flag;
	int val;
};

В первом поле name задается название длинного параметра.

Поле has_arg определяет нужно ли для этого параметра значение. Для этого в getopt.h определены специальные значения:

#define no_argument            0
#define required_argument      1
#define optional_argument      2

Как видите, если значение has_arg равно 0 (no_argument), то параметр не должен иметь значение, если 1 (required_argument), то параметр должен иметь значение. Если же значение для параметра опционально, то has_arg равен 3 (optional_argument).

Поле flag задает указатель на флаг, в который помещается значение val, если найден данный параметр (сама функция при этом возвращает 0). Если указатель равен NULL, то функция возвращает значение val в качестве результата работы.

Поле var содержит значение, которое помещается в flag или возвращается в качестве результата работы функции.

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

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

Давайте посмотрим пример longopt1.c:

#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>


int main (int argc, char *argv[]){

    int flag_a = 0;
    int flag_b = 0;
    int flag_c = 0;

    const char* short_options = "abc";

    const struct option long_options[] = {
        {"opta",no_argument,&flag_a,1},
        {"optb",no_argument,&flag_b,10},
        {"optc",no_argument,&flag_c,-121},
        {NULL,0,NULL,0}
    };

    while (getopt_long(argc,argv,short_options,
        long_options,NULL)!=-1);

    printf("flag_a = %d\n",flag_a);
    printf("flag_b = %d\n",flag_b);
    printf("flag_c = %d\n",flag_c);
	printf("\n");
};

После компиляции gcc longopt1.c -o longopt1 получим программу. Вот некоторые результаты работы:

dron~# ./longopt1
flag_a = 0
flag_b = 0
flag_c = 0

dron~# ./longopt1 --opta
flag_a = 1
flag_b = 0
flag_c = 0

dron~# ./longopt1 --optb --optc
flag_a = 0
flag_b = 10
flag_c = -121

dron~# ./longopt1 -a -b -c
flag_a = 0
flag_b = 0
flag_c = 0

Как видите, когда функция увидела параметры --opta, --optb или --optc она сразу же установила переменные flag_a, flag_b и flag_с значениями, которые были указаны в массиве long_options. Но посмотрите на короткие параметры -a, -b и -c. Они не были задействованы. А все от того, что в качестве результата работы функция возвращает:

  • если короткий параметр, то его название (т.е. символ)
  • если длинный параметр, то значение val при flag=NULL, иначе 0 и флагу flag присваивается значение val.

Мы с вами обработку коротких параметров не предусмотрели, если Вы сейчас модифицируете код, то сможете увидеть и их:

// добавьте переменную
int rez;

// новый цикл обработки параметров
while ((rez=getopt_long(argc,argv,short_options,
	long_options,NULL))!=-1)
{
	printf("rez: %d = \'%c\'\n",rez,rez);
};

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

dron~# ./longopt1 -abc
rez: 97 = 'a'
rez: 98 = 'b'
rez: 99 = 'c'
flag_a = 0
flag_b = 0
flag_c = 0

dron~# ./longopt1 -a -c -g
rez: 97 = 'a'
rez: 99 = 'c'
./a.out: invalid option -- g
rez: 63 = '?'
flag_a = 0
flag_b = 0
flag_c = 0

Теперь можно обрабатывать и короткие параметры, а чтобы это делать все сразу, существует как раз второй метод использования этой функции. Это когда указатели flag устанавливают в NULL, а значения val устанавливают в названия коротких параметров. При этом вся обработка результатов происходит в switch структуре. Давайте попробуем создать файл longopt2.c:

#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>


int main (int argc, char *argv[]){

	const char* short_options = "hs::f:";

	const struct option long_options[] = {
		{"help",no_argument,NULL,'h'},
		{"size",optional_argument,NULL,'s'},
		{"file",required_argument,NULL,'f'},
		{NULL,0,NULL,0}
	};

	int rez;
	int option_index;

	while ((rez=getopt_long(argc,argv,short_options,
		long_options,&option_index))!=-1){

		switch(rez){
			case 'h': {
				printf("This is demo help. Try -h or --help.\n");
				printf("option_index = %d (\"%s\",%d,%c)\n",
					option_index,
					long_options[option_index].name,
					long_options[option_index].has_arg,
					long_options[option_index].val
				);
				break;
			};
			case 's': {
				if (optarg!=NULL)
					printf("found size with value %s\n",optarg);
				else
					printf("found size without value\n");
				break;
			};
	
			case 'f': {
				printf("file = %s\n",optarg);
				break;
			};
			case '?': default: {
				printf("found unknown option\n");
				break;
			};
		};
	};
	return 0;
};

Теперь посмотрите на работу программы. Попробуем параметр --help и -h.

dron~# ./longopt2 --help
This is demo help. Try -h or --help.
option_index = 0 ("help",0,h)

dron~# ./longopt2 -h
This is demo help. Try -h or --help.
Segmentation fault

В первом случае все удачно, вывелась помощь и значение option_index. Во втором случае программа "упала" с сообщением об ошибке. Почему ? Ошибка Segmentation fault выдается когда программа пытается работать с неверными указателями. А в нашем случае мы пытаемся получить по option_index название параметра. В случае когда найден короткий параметр значение option_index не определено. Что же делать ?! А все просто. Модифицируем код чуток:

int option_index=-1; //обнулим в начале (установим признак ошибки)

while (...){
	switch(...){
	};
	option_index = -1; // снова делаем ошибку
};

При такой работе option_index можно применять для определения типа переданного параметра. Если он был длинным, то это значение будет больше нуля и равно порядковому номеру параметра в массиве. Если же -1, то это значит, что параметр короткий:

if (option_index<0)
	printf("short help option\n");
else
	printf("option_index = %d (\"%s\",%d,%c)\n",
		option_index,
		long_options[option_index].name,
		long_options[option_index].has_arg,
		long_options[option_index].val
	);

Теперь все работает:

dron~# ./longopt2 --help
This is demo help. Try -h or --help.
option_index = 0 ("help",0,h)

dron~# ./longopt2 -h
This is demo help. Try -h or --help.
short help option

Но это еще не все фокусы :) Попробуйте поиграть с параметрами size и file:

dron~# ./longopt2 -s 10
found size without value

dron~# ./longopt2 -s10
found size with value 10

dron~# ./longopt2 --size 10
found size without value

dron~# ./longopt2 --size=10
found size with value 10

dron~# ./longopt2 -f asd
file = asd

dron~# ./longopt2 -fasd
file = asd

dron~# ./longopt2 --file asd
file = asd

dron~# ./longopt2 --file=asd
file = asd

Как видите не все так просто. У нас size задан в массиве как optional_argument, т.е. параметр с опциональным аргументом. И получается, что когда нет явного признака присвоения, т.е. когда в коротком виде значение не стоит рядом с названием, а в длинном виде нет знака "=", наш параметр не получает никакого значения.

А вот параметр file заданный как required_argument получает свое значение в любом случае. И в этом заключается отличие типов аргументов. Теперь при разработке программ надо всегда учитывать, что если вы используете опциональный аргумент, то пользователь может ошибочно ввести командную строку, и вместо значения введенного пользователем Ваша программа будет использовать значение, которое вы используете по умолчанию. Незнаю, помоему достаточно значительная проблема, о которой следует помнить.

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

dron~# ./longopt3 -f 10
file = 10

dron~# ./longopt3 -f10
file = 10

dron~# ./longopt3 --file=10
file = 10

dron~# ./longopt3 -file 10
file = 10

dron~# ./longopt3 -fil 100
file = 100

Вот так вот. Данная возможность может являться и плюсом и минусом. Полезной она оказывается, когда пользователь ошибочно набирает строку и указывает вместо --opt параметр -opt. Но эта функция может сыграть злую шутку, если вдруг из коротких названий параметров получится название длинного параметра и вместо перечисления '-size' = '-s -i -z -e' у Вас получится название длинного параметра --size от -s. Вообще работа данной функции несколько загадочна и неоднозначна, поэтому сами попробуйте ее в действии. Я же думаю, что в программах со сложными входными параметрами лучше воздержаться от ее использования. Зато в программах с небольшим количеством параметров эта функция может позволить игнорировать ошибки пользователя.

Будьте бдительны и Ваши программы будут работать без ошибок !!!



Шаг 12 - Вывод сообщений об ошибках программы

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

Вывод сообщений обычно делается через стандартный поток stderr, который перенаправляет все данные на консоль. Можно делать это и через stdout, но он может быть перенаправлен в файл или куда-либо еще, к тому же он обладает буфером, который ему не позволяет выводить данные моментально и если программа резко "обрушится", то Вы можете не увидеть нужных сообщений вообще. В потоке stderr буфер отключен, поэтому вызов функции fflush(stderr) не обязателен.

Все это конечно хорошо, если Вы разрабытаваете пользовательское консольное приложение, но как быть, если Ваша программа является сетевым приложением, например POP3 или каким-нибудь другим сервером. В этом случае работа с stderr не возможна, и отладка мягко выражаясь может сильно усложниться. Но не все так печально. Для этих случаев существует демон сообщений syslogd, который все сообщения от ядра и программ записывает в файлы хранящиеся в папке /var/log.

Для того, чтобы начать работу с этим демоном надо подключить файл syslog.h и вам станут доступными три процедуры:

#include <syslog.h>

void openlog( char *ident, int option, int  facility)

void syslog( int priority, char *format, ...)

void closelog( void )

Предназначение функции closelog() думаю ясно, она закрывает дескриптор, который использовался для передачи сообщений в system logger. Ее использование опционально и, если вы забудете ее вызвать, то ничего катастрофичного не произойдет.

Для начала вывода сообщений Вам придется передать в функцию openlog() несколько необходимых параметров:

  • ident - текстовый идентификатор программы, обычно ее название. Он добавляется к началу каждого сообщения для того, чтобы было видно от какой программы поступают сообщения.
  • option - установки открываемого соединения, которые посредством операции OR могут складываться из следующих:
    • LOG_CONS - вывод напрямую в системную консоль, если вдруг происходит ошибка во время отправления сообщения
    • LOG_NDELAY - открывать соединение сразу, обычно соединение открывается после появления первого сообщения
    • LOG_PERROR - выводить сообщения в stderr
    • LOG_PID - добавлять PID программы в каждое сообщение. Полезно когда может работать одновременно несколько одинаковых программ, в этом случае их можно различить по идентификатору процесса.
  • facility - позволяет задать тип программы, которая выводит сообщение. Это полезно для того, чтобы разделять сообщения от различных программ и записывать их в разные файлы. Все это настраивается для syslogd файлом конфигурации /etc/syslog.conf. А значения этого параметра могут быть следующими:
    • LOG_AUTH - сообщения безопасности/авторизации (рекомендуется использовать LOG_AUTHPRIV)
    • LOG_AUTHPRIV - приватные сообщения безопасности/авторизации
    • LOG_CRON - сообщения от демонов времени (например, cron или at)
    • LOG_DAEMON - сообщения от других демонов системы
    • LOG_KERN - сообщения ядра системы
    • LOG_LOCAL0...LOG_LOCAL7 - зарезервированы для локального использования
    • LOG_LPR - подсистема принтера
    • LOG_MAIL - почтовая подсистема
    • LOG_NEWS - подсистема новостей USENET
    • LOG_SYSLOG - внутренние сообщения сгенерированные syslogd
    • LOG_USER (по умолчанию) - сообщения пользовательского уровня
    • LOG_UUCP - сообщения системы UUCP

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

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

  • LOG_EMERG - система не работает, грубо говоря в обмороке и требует госпитализации :)
  • LOG_ALERT - необходимо немедленно принять меры
  • LOG_CRIT - критическое состояние
  • LOG_ERR - ошибочное состояние
  • LOG_WARNING - состояние предупреждения
  • LOG_NOTICE - нормальное, но значимое, состояние
  • LOG_INFO - информационное сообщение
  • LOG_DEBUG - сообщение отладки, то что как раз нужно при разработке

А теперь попробуем написать программу test.c, использующую syslog:

#include <stdlib.h>
#include <stdio.h>
#include <syslog.h>

#define DEBUG

int main(){

    int i=0;
    openlog("test",LOG_PID,LOG_USER);

#ifdef DEBUG
    syslog(LOG_DEBUG,"try to sending 10 messages");
#endif

    for (i=0;i<10;i++){
        syslog(LOG_INFO,"info message [i = %d] ",i);
    };

#ifdef DEBUG
    syslog(LOG_DEBUG,"try log to stderr");
#endif
    closelog();

    openlog("test_stderr",LOG_PERROR | LOG_PID,LOG_USER);
    syslog(LOG_INFO,"this is attempt to use stderr for syslog");
    closelog();

    return 0;
};

Компилируем программу и попробуем запустить:

dron~# ./test
test_stderr[6222]: this is attempt to use stderr for syslog

Теперь можем зайти в файл /var/log/messages и посмотреть, что там получилось. А получилось вот что:

Dec 20 11:25:04 dron-linux test[6222]: info message [i = 0]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 1]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 2]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 3]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 4]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 5]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 6]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 7]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 8]
Dec 20 11:25:04 dron-linux test[6222]: info message [i = 9]
Dec 20 11:25:04 dron-linux test_stderr[6222]: this is attempt to use stderr for syslog

Помоему классно, но почему-то не хватает некоторых сообщений. Посмотрим /var/log/debug и увидим, что все на месте :)

Dec 20 11:25:04 dron-linux test[6222]: try to sending 10 messages
Dec 20 11:25:04 dron-linux test[6222]: try log to stderr

То есть тут мы можем увидеть, что сообщения разных типов выводятся в разные файлы. Настроить это можно с помощью файла /etc/syslog.conf. К примеру в данном случае он выглядит вот так:

# /etc/syslog.conf
# For info about the format of this file, see "man syslog.conf"
*.=info;*.=notice	/var/log/messages
*.=debug		/var/log/debug
*.err			/var/log/syslog

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



Шаг 13 - Получение информации о пользователе

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

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

  • USER - имя пользователя
  • HOME - путь до пользовательской домашней директории
  • PATH - пути для поиска запускаемых программ (разделены двоеточием)

Получить переменные среды можно с помощью функции getenv():

#include <stdlib.h>

char *getenv(const char *name);

Данная функция ищет переменную среды с именем name и возвращает на нее указатель в случае удачи, иначе возвращает NULL.

#include <stdlib.h>

int main(){
    printf("USER = \'%s\'\n",getenv("USER"));
    printf("HOME = \'%s\'\n",getenv("HOME"));
    printf("PATH = \'%s\'\n",getenv("PATH"));
	return 0;
};

Для примера результат работы данной программы:

dron~# ./a.out
USER = 'root'
HOME = '/root'
PATH = '/usr/local/sbin:/usr/sbin:/usr/bin'

Но почему нельзя полагаться на эти переменные ? Да потому, что их можно легко изменить одной командой (или вызовом фукнции в программе):

dron~# export USER=""
dron~# export HOME="/yoyoyoy"
dron~# ./a.out
USER = ''
HOME = '/yoyoyoy'
PATH = '/usr/local/sbin:/usr/sbin:/usr/bin'

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

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

#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void);
uid_t geteuid(void);

Функция getuid() (get user id) возвращает реальный идентификатор пользователя для текущего процесса, который установлен в соответствие идентификатору вызывающего процесса.

Функция geteuid() (get effective user id) возвращает эффективный идентификатор пользователя, который устанавливается в соответствии с битом set ID на запускаемом файле.

Давайте посмотрим как работают эти функции, для этого напишем простую программку test.c:

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(){

    printf ("Real User ID = %d\n",getuid());
    printf ("Effective User ID = %d\n\n",geteuid());

    return 0;
};

Теперь скомпилируем командой gcc test.c -o test и запустим. Я сейчас сижу под пользователем root. Посмотрим сначала что вышло в каталоге:

dron~# ls -l
total 20
-rwxr-xr-x    1 root     root        13500 Dec 22 04:45 test
-rw-r--r--    1 root     root          197 Dec 22 04:39 test.c

Как видите владелец обоих файлов является root. Теперь запускаем программу test:

dron~# ./test
Real User ID = 0
Effective User ID = 0

Так как программа была запущена из под root идентификатор пользователя равен 0. А как же насчет geteuid(), сейчас ее результат аналогичен работе getuid(). Давайте попробуем добиться того, чтобы этот идентификатор был другим. Как было написано выше эта функция возвращает идентификатор пользователя установленного на файле, да к томе же если на нем установлен бит set ID. Давайте сначала поменяем на файле пользователя и посмотрим, что выйдет из этого.

dron~# chown dron:users test
dron~# ls -l
total 20
-rwxr-xr-x    1 dron     users       13500 Dec 22 04:45 test
-rw-r--r--    1 root     root          197 Dec 22 04:39 test.c
dron~# ./test
Real User ID = 0
Effective User ID = 0

Интересно. Мы поменяли пользователя на dron, что вы можете увидеть из результата команды ls, однако как и следовало ожидать результат работы функции geteuid() остался таким же.

Теперь установим бит set ID на файл test:

dron~# chmod +s test
dron~# ls -l
total 20
-rwsr-sr-x    1 dron     users       13500 Dec 22 04:45 test
-rw-r--r--    1 root     root          197 Dec 22 04:39 test.c

Теперь, если вы сравните старые биты привилегий -rwxr-xr-x с новыми -rwsr-sr-x, то увидите вместо x букву s, а это означает, что бит set ID установлен. Теперь если мы запустим программу снова, то увидим другой результат:

dron~# ./test
Real User ID = 0
Effective User ID = 1000

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

if (getuid()!=geteuid()){
	printf("Вы не можете использовать чужую программу.\n");
	exit();
};

Тогда эту программу не сможет запустить никто кроме ее законного владельца. Незнаю насколько это "эффективно", может быть у Вас есть какие-то мысли по этому поводу. Я даже уверен, что Вы когда-нибудь ее примените в своей разработке :)

Ну, а теперь надо учиться что-то делать с этими идентификаторами. Наша начальная задача была получить настройки для пользователя, давайте этим и займемся. Откуда Вы можете достоверно получить информацию о пользователе ? Вы знаете ответ на этот вопрос ? Ну, конечно же из файла /etc/passwd.

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

#include <pwd.h>
#include <sys/types.h>

struct passwd *getpwnam(const char * name);
struct passwd *getpwuid(uid_t uid);

Первая функция getpwnam() возвращает информацию о пользователе по его имени name, вторая функция getpwuid() получает информацию о пользователе по идентификатору, который получать мы уже умеем :)

Обе данные функции возвращают информацию в виде заполненной структуры struct passwd:

struct passwd {
	char    *pw_name;       /* user name */
	char    *pw_passwd;     /* user password */
	uid_t   pw_uid;         /* user id */
	gid_t   pw_gid;         /* group id */
	char    *pw_gecos;      /* real name */
	char    *pw_dir;        /* home directory */
	char    *pw_shell;      /* shell program */
};

Как видите тут можно получить даже больше данных, чем через переменные среды. А, что мы хотели получить ? Ну, давайте к примеру выведем домашний каталог и командный интерпретатор (ака shell).

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>

int main(){

    struct passwd *userinfo;
    uid_t userid;

    printf ("Real User ID = %d\n",userid = getuid());

    userinfo = getpwuid(userid);
    if (userinfo!=NULL){
        printf("user name = '%s'\n",userinfo->pw_name);
        printf("user home dir = '%s'\n",userinfo->pw_dir);
        printf("user shell = '%s'\n",userinfo->pw_shell);
    };

    return 0;
};

Результат работы программы будет выглядеть так:

dron~# ./a.out
Real User ID = 0
user name = 'root'
user home dir = '/root'
user shell = '/bin/bash'

У данной функции есть несомненный плюс - это, конечно же, способность возвратить самые точные системные настройки для требующегося пользователя. НО ! Посмотрите на возвращаемую структуру, вы видите что-нибудь интересное ? Ну, как же... Это поле pw_passwd, в котором содержится пароль пользователя, пусть и в зашифрованном виде. Получается, что любая программа запущенная под любым пользователем может получить пароль другого пользователя. В дальнейшем к примеру, этот пароль можно попробовать расшифровать и уже получить доступ под другим пользователем. А что может случиться, если этот атакуемый пользователь был root ?! Тогда возможна угроза полного взлома системы. Все разработчики системы Linux понимали это, и именно поэтому придумали технологию shadow passwords. В соответствии с данной технологией создается второй файл /etc/shadow с полной копией информации из /etc/passwd, а затем из файла passwd удаляются пароли(заменяются на символ x), а на shadow ставится доступ на чтение только пользователю root. Таким образом любая программа может получить всю информацию о пользователях, кроме их паролей. Сами же пароли может получить лишь программа запущенная в привилегированном режиме с администраторскими правами.

Вот такие вот пироги... :) В принципе это все, но только пришла мне тут кое-какая мысль. Что будет, если на /etc/passwd поставить доступ только пользователю root.

dron~# chmod 600 /etc/passwd

Заходим под другим пользователем в систему и пробуем запустить нашу программ (не забудьте ее записать в домашний каталог этого пользователя):

dron~$ ./a.out
Real User ID = 1000

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

dron~# ./a1.out
USER = 'dron'
HOME = '/home/dron'
PATH = '/usr/bin:/usr/local/bin'

Вот, что происходит. Если вдруг "неопытный" администратор системы отключит по соображениям "безопасности" доступ к файлу /etc/passwd, то многие программы недальновидных разработчиков могут просто перестать работать. Что нам остается ?! Ну, я думаю первым делом вы все вызовите команду chmod 644 /etc/passwd. А, во-вторых, перепишите код таким образом, чтобы если что-то не так с функцией getpwuid, то программа пользовалась переменными среды, как единственно доступной в данном случае информацией.

Собственно говоря, на этом все. Вы уже начинаете чувствовать, что не все просто в нашем мире linux ?



Шаг 14 - Получение данных из shadow password

В прошлый раз мы с Вами разобрались с тем, как получать информацию о пользователе из файла /etc/passwd, но как оказалось получить пароль нам не удастся, потому что его там нет. А как быть, если он Вам нужен ? Ну, например, Вы решили написать собственный сервер POP3, который для авторизации требует наличие пароля.

Для работы со скрытыми паролями надо подключить файл shadow.h и вам станут доступны аналогичные процедуры:

#include <shadow.h>

struct spwd *getspnam (const char *name);

Обратите внимание, что в файле shadow.h нет определения функции получения информации о пользователе по его UID. Т.е. для работы нужно знать имя пользователя, соответственно сначала нужно воспользоваться функцией getpwuid(), чтобы по UID получить имя.

Функция getspnam() возвращает структуру struct spwd, либо NULL в случае неудачи. Данная структура определена следующим образом:

struct spwd
{
	char *sp_namp;              /* Login name.  */
	char *sp_pwdp;              /* Encrypted password. */
	long int sp_lstchg;         /* Date of last change. */
	long int sp_min;            /* Minimum number of days between changes. */
	long int sp_max;            /* Maximum number of days between changes. */
	long int sp_warn;           /* Number of days to warn user to change
                                   the password. */
	long int sp_inact;          /* Number of days the account may be
                                   inactive. */
	long int sp_expire;         /* Number of days since 1970-01-01 until
                                   account expires. */
	unsigned long int sp_flag;  /* Reserved.  */
};

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

И наверняка Вы заметили, что я немного соврал Вам в прошлый раз, когда сказал, что /etc/shadow содержит информацию аналогичную /etc/passwd. Получается, что кроме логина и пароля Вы тут не найдете домашней директории и шелла. В принципе верно, зачем хранить одно и тоже в нескольких местах ?! Выходит, что нам не удастся "отбиться от коллектива" и придется пользоваться обоими файлами.

Давайте посмотрим, как работает эта функция. Напишем маленькую программку shadowtest.c:

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <shadow.h>

int main(){
	struct passwd *userinfo;
	struct spwd *passw;
	uid_t userid;     	

	userid = getuid();
	userinfo = getpwuid(userid);

	if (userinfo != NULL){

		passw = getspnam(userinfo->pw_name);

		if (passw != NULL){
			printf("user login: %s\n",userinfo->pw_name);
			printf("user home: %s\n",userinfo->pw_dir);
			printf("user shell: %s\n",userinfo->pw_shell);
			printf("user password: %s\n",userinfo->pw_passwd);
			printf("user shadow password: %s\n",passw->sp_pwdp);
			printf("user last change: %ld\n",passw->sp_lstchg);
		};

	};

	return 0;
};

Компилируем и запускаем:

dron~# ./gcc shadowtest.c -o shadowtest
dron~# ./shadowtest
user login: root
user home: /root
user shell: /bin/bash
user password: x
user shadow password: $1$02p9xyDo$gnkh4vts/rArhJselceTV1
user last change: 12028

Как видите пароль нам получить удалось только из структуры struct spwd. Но он зашифрованный алгоритмом MD5, в данном случае настоящий пароль 12345678 (можете не мучаться над взломом :). Тут кстати следует поговорить о том, как хранятся пароли. Понятное дело, что если пароли будут храниться в виде plain text, т.е. в виде текста "как есть", то можно будет узнать пароль для любого пользователя совершенно спокойно. Современные правила безопасности вообще не разрешают хранить пароль в таком виде. Вместо этого пароль хранится в виде хеша от настояшего пароля. Функция вырабатывающая хеш берет настоящий пароль и вырабатывает на его основе уникальную последовательность чисел, которую не возможно обратно преобразовать в пароль, потому что математические функции работающие над выработкой пароля специально создаются однонаправленными. Создание таких процедур является сложной криптографической задачей и порой под силу только крупным научно-исследовательским институтам. К примеру у нас в России существуют засекреченные алгоритмы, некоторые из которых разрабатывались в течение 10 лет, так вот представьте каких трудов это стоит и представьте какие эти алгоритмы совершенные. Взять к примеру наш алгоритм шифрования ГОСТ 28147-89, который существует с 89 года и до сих пор остается одним из самых защищенных (он может иметь длину ключа 256 бит, в то время как DES имеет всего 56 бит и при нынешнем развитии компьютеров является чрезвычайно устаревшим). Однако для выработки хеша в системах Linux используются в основном алгоритмы DES и MD5, хотя первый уже используется крайне редко.

Так вот, сравнение правильности пароля происходит следующим образом. Программа получает настоящий пароль от пользователя, потом вырабатывает на его основе хеш и сравнивает его с тем, который она получила из файла /etc/shadow. Если хеши не совпадают, то значит пароли разные.



Шаг 15 - Работа с паролями системы с помощью функции crypt()

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

За генерацию паролей отвечает функция crypt(). Подключить ее к программе можно так:

#define _XOPEN_SOURCE
#include <unistd.h>

char *crypt(const char *key, const char *salt);

Вместо заголовочного файла unistd.h для подключения функций шифрования можно использовать другой файл - crypt.h. В принципе без разницы, они оба имеют одинаковые определения функции crypt().

Для работы функции требуется два параметра:

  • key - это секретный пароль пользователя, который требуется зашифровать
  • salt - это так называемый "открытый ключ", или же в понятиях алгоритмов шифрования инициализационные данные. Данный параметр должен состоять из символов a-zA-Z0-9./, т.е. из любых латинских букв, цифр или некоторых символов.

Если же с паролем все ясно, то salt может заставить задуматься. Давайте посмотрим на что влияет этот параметр. Напишем тестовую программку crypt.c:

#include <stdlib.h>
#include <crypt.h>

int main(){
    printf("crypt(\"password\",\"ab\") = \"%s\"\n",crypt("password","ab"));
    printf("crypt(\"password\",\"ab12\") = \"%s\"\n",crypt("password","ab12"));
    printf("crypt(\"password\",\"ac\") = \"%s\"\n",crypt("password","ac"));
    printf("crypt(\"password\",\"ac123\") = \"%s\"\n",crypt("password","ac123"));
    printf("crypt(\"password\",\"1a\") = \"%s\"\n",crypt("password","1a"));
    printf("crypt(\"password\",\"1a.\") = \"%s\"\n",crypt("password","1a."));
    return 0;
};

Данная программа пытается выработать хеш с разным сальтом для пароля "password". Компилируем программу:

dron~# gcc crypt.c -o crypt
/tmp/ccBXde1R.o: In function `main':
/tmp/ccBXde1R.o(.text+0x17): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0x3f): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0x67): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0x8f): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0xb7): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0xdf): more undefined references to `crypt' follow
collect2: ld returned 1 exit status

Не так быстро. Просто так откомпилировать программу нельзя, надо обязательно подключить библиотеку libcrypt.a, делается это такой командой:

dron~# gcc crypt.c -o crypt -lcrypt
dron~# ./crypt
crypt("password","ab") = "abJnggxhB/yWI"
crypt("password","ab12") = "abJnggxhB/yWI"
crypt("password","ac") = "acBxUIBkuWIZE"
crypt("password","ac123") = "acBxUIBkuWIZE"
crypt("password","1a") = "1abtv8E0hkEd6"
crypt("password","1a.") = "1abtv8E0hkEd6"

Посмотрите на результат работы функции. Хоть я и задавал разные salt некоторые хеши получились совершенно одинаковыми. Из этого можно сделать вывод и том, что в нем играют роль только первые два символа. Значения этих символов используются для шифрования пароля алгоритмом DES. Заметьте также, что salt входит в хеш как его начальная часть. И это правильно, ведь если его удалить, то неизвестно, что надо использовать для проверки достоверности пароля. На самом деле, как мы узнаем дальше, это сделано специально для того, чтобы можно было без особых мучений с паролем из /etc/shadow сразу же отправлять его в функцию crypt().

Хорошо. Нам теперь, думаю, стало понятно как работает алгоритм DES, а как же MD5 ? Ведь, как я уже говорил, пароли сегодня чаще всего хранятся в виде MD5. Хороший вопрос. Ответ на него я искал некоторое время. Исходя из того, что совершенно точно функция crypt() должна работать с MD5 я начал рыскать в исходнике gnu-pop3d, который совсем недавно чуть ли не переписал заново :) Пролистав все и не обнаружив ни #define ни хитрых #include я попробовал залезть в файл библиотеки /usr/lib/libcrypt.a. Получить список функций из нее можно командой nm:

dron~# nm /usr/lib/libcrypt.a

crypt-entry.o:
0000000000000117 t Letext
0000000000000000 T __crypt_r
                 U __md5_crypt
                 U __md5_crypt_r
                 U _ufc_dofinalperm_r
                 U _ufc_doit_r
                 U _ufc_foobar
                 U _ufc_mk_keytab_r
                 U _ufc_output_conversion_r
                 U _ufc_setup_salt_r
00000000000000d0 T crypt
0000000000000000 W crypt_r
00000000000000d0 W fcrypt
0000000000000000 r md5_salt_prefix
                 U strncmp
                 U strncpy

md5-crypt.o:
000000000000070a t Letext
                 U __assert_fail
                 U __errno_location
0000000000000690 T __md5_crypt
0000000000000000 T __md5_crypt_r
                 U __md5_finish_ctx
                 U __md5_init_ctx
...............

Посмотрите, список функций очень большой, поэтому не привожу весь, но видно определенно, что libcrypt.a содержит функции для работы с MD5. Но тогда как ?! Ведь нет никаких параметров дополнительных. А все оказалось куда проще %) Посмотрите на листинг, видите имя md5_salt_prefix. Не правда ли очень говорящее название ?! А теперь посмотрите типичный пароль закодированный с помощью MD5:

$1$/DrNy/Cv$ZBydbOBsEvdI5u5sib2X/0
$1$02p9xyDo$gnkh4vts/rArhJselceTV1

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

$1$..salt..$.........hash.........

Именно по этой структуре функция crypt() определяет каким методом ей шифровать пароль. Не поленимся однако и посмотрим исходники crypt() в библиотеке libc. Вот к примеру строки из файла crypt-entry.c:

/* Define our magic string to mark salt for MD5 encryption
replacement.  This is meant to be the same as for other MD5 based
encryption implementations.  */

static const char md5_salt_prefix[] = "$1$";

.......
  
/* Try to find out whether we have to use MD5 encryption replacement.*/
if (strncmp (md5_salt_prefix, salt, sizeof (md5_salt_prefix) - 1) == 0)
	return __md5_crypt_r (key, salt, (char *) data, sizeof (struct crypt_data));

Помоему классно :) Именно "магическая строчка" $1$ и является тем методом переключения между различными алгоритмами. Тут еще интересен вопрос о том, какой длины должен быть этот salt, изучая исходниках дальше Вы сможете найти в файле md5-crypt.c строчки:

/* Find beginning of salt string.  The prefix should normally always
be present.  Just in case it is not.  */
if (strncmp (md5_salt_prefix, salt, sizeof (md5_salt_prefix) - 1) == 0)
	/* Skip salt prefix.  */
	salt += sizeof (md5_salt_prefix) - 1;

salt_len = MIN (strcspn (salt, "$"), 8);
key_len = strlen (key);

Тут не вооруженным глазом видно, что после $1$ ищется второй символ $ и берется длина строки ограниченная этими признаками. Далее выбирается минимум между длиной строки и 8, т.е. получается что salt в алгоритме MD5 может быть любой длины не больше 8-ми. Это и требовалось доказать, теперь давайте попробуем :)

#include <stdlib.h>
#include <crypt.h>

int main(){
    printf("crypt(\"12345678\",\"$1$abasdlkasl123$\") = \"%s\"\n",
    	crypt("password","$1$abasdlkasl123$"));
    printf("crypt(\"12345678\",\"$1$dfg$\") = \"%s\"\n",
    	crypt("password","$1$dfg$"));
    return 0;
};

Снова компилируем, и не забываем про библиотеку crypt:

dron~#  gcc crypt1.c -o crypt1 -lcrypt
dron~# ./crypt1
crypt("12345678","$1$abasdlkasl123$") = "$1$abasdlka$z9aVWR2l14E3WngLCABSt1"
crypt("12345678","$1$dfg$") = "$1$dfg$fF0Vo9cC5CyBY827ltEdn0"

Все получилось :) А Вы как думали ?! И обратите внимание на то, что длинный salt в первом случае обрезался до 8-ми символов. Кстати, помоему длина 8 символов куда лучше, чем два. Это еще раз говорит о том, что метод MD5 лучше DES. И раз вообще заговорили про размер, то сравните длину получающихся хешей от работы этих алгоритмов.

Теперь, собственно говоря, сам процесс проверки пароля. Как Вы уже наверно поняли он сводится к простому сравнению, смотрим код:

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <shadow.h>

int main(int argc,int *argv){
	struct passwd *userinfo;
	struct spwd *passw;
	uid_t userid;

	if (argc<2) {
		printf("Try to use: %s uin password\n",argv[0]);
		return 1;
	};

	userid = (uid_t)atoi(argv[1]);
	userinfo = getpwuid(userid);

	if (userinfo != NULL){
		passw = getspnam(userinfo->pw_name);
		if (passw != NULL){
			printf("Try to test password for \"%s\": ",userinfo->pw_name);
			if (strcmp(passw->sp_pwdp,crypt(argv[2],passw->sp_pwdp))==0)
				printf ("Ok...\n");
			else
				printf ("Failed...\n");
		} else
			printf("Can't find password for user with UIN = %s\n",argv[1]);
	} else
		printf("Can't find user with UIN = %s\n",argv[1]);

	return 0;
};

Теперь компилируем и запускаем:

dron~# ./testpasswd
Try to use: ./testpasswd uin password

dron~# ./testpasswd 1000 12345678
Try to test password for "dron": Ok...

dron~# ./testpasswd 1000 1234
Try to test password for "dron": Failed...

Помоему мы научились проверять правильность паролей для пользователей :) Только не забывайте про то, что пароли из /etc/shadow доступны только из под root, но об этом мы не однократно говорили раньше.

И еще я все время пытаюсь Вам привить то, что исходники не только для того, чтобы их компилировать. Они нужны для того, чтобы изучать программирование и мы будем продолжать их просматривать :) Мало ли может глюки найдем %)



Шаг 16 - Получение информации о группах пользователей

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

Для работы с группами требуется подключить файл grp.h и Вам станут доступны следующие функции:

#include <grp.h>
#include <sys/types.h>

struct group *getgrnam(const char *name);
struct group *getgrgid(gid_t gid);

Функция getgrnam() ищет группу в файле /etc/group с именем name и возвращает указатель на структуру struct group, либо NULL в случае неудачи.

Функция getgrgid() ищет группу по идентификатору и также возвращает стуктуру struct group, либо NULL в случае неудачи. Данная структура имеет следующий вид:

struct group {
	char *gr_name;		/* имя группы */
	char *gr_passwd;		/* групповой пароль */
	gid_t gr_gid;		/* идентификатор группы */
	char **gr_mem;		/* члены группы */
};

Назначение полей думаю не требуют объяснения. Один лишь момент в том, что поле members это массив указателей на строки, в котором для обозначения конца последний элемент равен NULL.

Давайте попробуем написать программку, которая будет выводить информацию о группе:

#include <stdlib.h>
#include <grp.h>
#include <sys/types.h>

int main(){
	struct group *g= NULL;
	char **p = NULL;

	g = getgrnam("webmasters");
	if (g != NULL){
		printf ("gr_name = \"%s\"\n",g->gr_name);
		printf ("gr_passwd = \"%s\"\n",g->gr_passwd);
		printf ("gr_gid = \"%d\"\n",g->gr_gid);
		printf ("gr_members = ");
		p = g->gr_mem;
		while (*p != NULL){
			printf ("\"%s\" ", *p);
			p++;
		};
		printf ("\n");
	};
	return 0;
};

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

dron~# ./a.out
gr_name = "webmasters"
gr_passwd = "x"
gr_gid = "102"
gr_members = "dron" "kost" "aneta"

Теперь, если посмотреть содержимое файла /etc/group можно увидеть следующую строку:

webmasters:x:102:dron,kost,aneta

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



Шаг 17 - Работа с переменными окружения - setenv и getenv

Раньше мы познакомились с тем, как получать параметры в программе с помощью семейства функций getopt (подробнее читайте "Шаг 10 - Передача опций в программу - getopt" и "Шаг 11 - Передача длинных опций в программу - getopt_long"). Но существует еще один метод - переменные окружения или переменные среды. С ними мы успели уже познакомиться раньше в шаге "Шаг 13 - Получение информации о пользователе". С помощью данных переменных можно передавать в программу дополнительные специфические параметры, которые могут использоваться несколькими программами одновременно и они характеризуют "среду", в которой выполняется программа.

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

#!/bin/bash

/yoyosystem/cheduler --time=60 --work-dir=/yoyosystem/data --temp-dir=/tmp/yoyosystem
/yoyosystem/counter -c 25 -w 31 --work-dir=/yoyosystem/data --temp-dir=/tmp/yoyosystem
/yoyosystem/pusher --stack-size=512 -m 12 --work-dir=/yoyosystem/data --temp-dir=/tmp/yoyosystem

Ну, как ?! Громодко ! А вот как более красиво:

#!/bin/bash

export YOYO_WORK_DIR="/yoyosystem/data"
export YOYO_TEMP_DIR="/tmp/yoyosystem"

/yoyosystem/cheduler --time=60
/yoyosystem/counter -c 25 -w 31
/yoyosystem/pusher --stack-size=512 -m 12

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

А теперь собственно давайте познакомимся с этими переменными среды. Библиотека glibc предоставляет весь спектр функций для работы с ними в заголовочном файле stdlib.h.

Сначала познакомимся с самим массивом переменных окружения, который определен в заголовочном файле unistd.h в таком виде:

/* NULL-terminated array of "NAME=VALUE" environment variables.  */
extern char **__environ;
#ifdef __USE_GNU
extern char **environ;
#endif

Представлет собой массив указателей на строки формата "имя=значение" и заканчивается нулевым указателем NULL. Давайте попробуем написать программу вывода всех переменных:

#include <stdlib.h>
#include <unistd.h>

int main(int argc,char **argv) {
    int i=0;

    while (__environ[i] != NULL) {
        printf("%s\n",__environ[i]);
        i++;
    };
    return 0;
};

Данная программа выведет весь список переменных среды окружения, которые доступны программе:

root@darkstar:/@@@@@@# ./a.out
MANPATH=/usr/local/man:/usr/man:/usr/X11R6/man:/usr/lib/java/man
HOSTNAME=darkstar.example.net
SHELL=/bin/bash
TERM=xterm
USER=root
MC_TMPDIR=/tmp/mc-root
T1LIB_CONFIG=/usr/share/t1lib/t1lib.config
MINICOM=-c on
PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:
MAIL=/var/mail/root
LC_COLLATE=C
PWD=/@@@@@@
INPUTRC=/etc/inputrc
JAVA_HOME=/usr/lib/java
LANG=en_US
PS1=\u@\h:\w\$
HISTCONTROL=ignorespace
PS2=>
HOME=/root
SHLVL=2
LOGNAME=root
LESS=-M
LESSOPEN=|lesspipe.sh %s
OLDPWD=/usr/include
_=./a.out

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

Для доступа к переменным окружения, есть несколько функций:

  • char * getenv (const char *name) - возвращает указатель на значение переменной с именем name, если переменная среды не найдена, то возвращается NULL. Не рекомендуется модифицировать значение полученной строки, так как она указывает непосредственно в массив переменных среды, и можно испортить их значения. В связи с этим также не требуется выделение дополнительной памяти для сохранения результата.
  • int setenv (const char *name, const char *value, int replace) - добавляет новое или заменяет старое значение в массиве переменных среды с именем name и значением value (даже если value пустая строка). Под новую переменную выделяется память и заносится строка вида "имя=значение". Если в переменных среды уже есть переменная с именем name, то процесс замены контролируется параметром replace, если он равен нулю, то никаких действий не производится.
  • int putenv (char *string) - добавляет или удаляет переменную окружения. Для добавления или изменения переменной используйте строку формата "имя=значение", для удаления просто "имя". В отличие от setenv() функция putenv() не выделяет память для параметра, а использует указатель на значение string, поэтому после вызова этой функции любое изменение строки string автоматически приведет к изменению переменной среды. Также перед удалением строки из памяти, следует сначала удалить эту переменную из массива переменных окружения.
  • int unsetenv (const char *name) - удаляет полностью из массива переменных окружения переменную с именем name.
  • int clearenv(void) - полностью очищает массив переменных окружения.

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

#include <stdlib.h>

int main(int argc,char **argv) {
    char *HOME = getenv("HOME");
    char *USER = getenv("USER");

    printf("Home directory: \"%s\"\n",HOME);
    printf("User name: \"%s\"\n",USER);

    return 0;
};

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

root@darkstar:/@@@@@@# ./a.out
Home directory: "/root"
User name: "root"

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



Шаг 18 - Генерация хешей MD2, MD4, MD5 с помощью OpenSSL

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

Набор хеш-функций MD2, MD4 и MD5 в библиотеке OpenSSL используется для вычисления хешей данных. Для тех, кто не знает, что такое хеш, возможно будет более понятным термин контрольная сумма или чексумма (от англ. checksum). Это число генерируется на основе содержимого данных и используется в основном для проверки целостности данных. Все алгоритмы вычисления хешей разрабатывались таким образом, чтобы даже при изменении одного бита данных результирующий хеш менялся до неузнаваемости, по сравнению с первоначальным. Причем по хешу данных (по понятным причинам необратимости работы алгоритма) предсказать содержимое данных невозможно.

 
root@localhost:/home/dron# echo -n "Arnold Schwarzenegger" | md5sum
c8d96c6f9a91ba729ca1688f90f6d2b7  -
root@localhost:/home/dron# echo -n "Brnold Schwarzenegger" | md5sum
c0707d286aaadc1a8c1f5e77cb5a4961  -

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

Реализация APOP в POP3 позволяет произвести проверку пароля между сервером и клиентом без передачи пароля в открытом виде. Идея проста и заключается в том, что обе стороны (сервер и клиент) изначально пароль знают, да в общем-то другого и быть не может :) Сервер во время подключения клиента к своему приглашению добавляет слово-сальт (словом salt в терминах Юникса обозначается префикс используемый для шифрования паролей DES). Сальт меняется сервером периодически, для этого можно использовать текущее время в секундах, идентификатор процесса или просто случайное число. Это слово-сальт клиент добавляет к паролю и потом передает хеш-функцию MD5 от полученной строки серверу. Сервер производит такую же операцию и сравнивает результат работы MD5 с переданным хешем от клиента. Таким образом получается, что пароль в открытом виде не передается, а по передаваемому хешу практически не возможно подобрать пароль.

Библиотека OpenSSL для работы с хешами предоставляет множество различных по скорости работы и криптостойкости алгоритмов. Я приведу пример работы с семеством Message-Digest (MD) алгоритмов, в которое входят алгоритмы MD2, MD4 и MD5. Набор функций для работы с ними одинаковы и отличаются соответственно названиями:

  • MD*_Init() - инициализация контекста для хеширования.
  • MD*_Update() - обновление контекста новыми данными.
  • MD*_Final() - выработка конечного хеша.
  • MD*() - обобщенная функция для хеширования.

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

 
#include <openssl/md5.h>

Выработка хеша делается следующим образом (для примера возьмем последовательность 0123456789):

 
/* start main.c */
#include <stdio.h>
#include <stdlib.h>
#include <openssl/md5.h>
 
int main() {
    int i;
    MD5_CTX md5handler;
    unsigned char md5digest[MD5_DIGEST_LENGTH];
	    
    MD5_Init(&md5handler);
    MD5_Update(&md5handler, "01234", 5);
    MD5_Update(&md5handler, "56789", 5);
    MD5_Final(md5digest,&md5handler);
			    
    for (i=0;i<MD5_DIGEST_LENGTH;i++) {
        printf("%02x",md5digest[i]);
    };
					    
    printf("\n");
    return 0;
};
/* end of main.c */

Компилируем:

 
gcc main.c -lcrypto

Пробуем:

 
root@localhost:/home/dron# ./a.out
781e5e245d69b566979b86e28d23f2c7

Для проверки результата работы можно использовать команду md5sum:

 
root@localhost:/home/dron# echo -n "0123456789" | md5sum
781e5e245d69b566979b86e28d23f2c7  -

Как видите совпадает. Этим набором функций можно хешировать большие объемы данных последовательно вызывая MD5_Update:

 
void MD5_Update(MD5_CTX *c, const void *data,
		    unsigned long len);

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

 
unsigned char *MD5(const unsigned char *d, unsigned long n,
		    unsigned char *md);

Пример:

 
#include <stdio.h>
#include <stdlib.h>
#include <openssl/md5.h>
 
int main() {
    int i;
    MD5_CTX md5handler;
    unsigned char md5digest[MD5_DIGEST_LENGTH];
 
    MD5("0123456789",10, md5digest);
 
    for (i=0;i<MD5_DIGEST_LENGTH;i++) {
	printf("%02x",md5digest[i]);
    };
 
    printf("\n");
    return 0;
};

Получается более компактное и красивое решение.

Использовать алгоритмы MD2 и MD4 можно аналогично, хотя сегодня они практически нигде не применяются, так как являются более старыми версиями.



Шаг 19 - Чтение ZIP файла в Linux с помощью библиотеки libzip

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

В данной ситуации очень помогают библиотеки созданные кем-то ранее и начинаешь понимать силу Open Source. Так вот с помощью исследования просторов интернета и нескольких программ различной направленности я случайно нашел для себя библиотеку libzip (http://nih.at/libzip) для работы с ZIP архивами.

К сожалению в поставке Slackware 12.2 я такой библиотеки не нашел. Поэтому скачиваем ее:

 
wget http://nih.at/libzip/libzip-0.9.tar.bz2 

Распаковываем архив:

 
tar xvjf libzip-0.9.tar.bz2

Переходим внутрь директории с библиотекой и компилируем ее:

 
cd libzip-0.9
./configure --prefix=/usr
make && make install

Теперь библиотека установлена.

Давайте для теста создадим специальный ZIP архив, а для этого создадим структуру каталогов с файлами:

 
mkdir /testzip
cd /testzip
mkdir dir1
mkdir dir2
echo "file 1 in dir 1" > dir1/file1.txt
echo "file 2 in dir 1" > dir1/file2.txt
echo "file 1 in dir 2" > dir2/file1.txt
echo "file 2 in dir 2" > dir2/file2.txt
echo "file 1 in root" > file1.txt
echo "file 2 in root" > file2.txt

Теперь архивируем это все:

 
root@darkstar:/testzip# zip test.zip -R *
  adding: dir1/ (stored 0%)
  adding: dir1/file1.txt (stored 0%)
  adding: dir1/file2.txt (stored 0%)
  adding: dir2/ (stored 0%)
  adding: dir2/file1.txt (stored 0%)
  adding: dir2/file2.txt (stored 0%)
  adding: file1.txt (stored 0%)
  adding: file2.txt (stored 0%)
 
root@darkstar:/testzip# unzip -l test.zip
Archive:  test.zip
  Length     Date   Time    Name
 --------    ----   ----    ----
       16  03-22-09 09:40   dir1/file1.txt
       16  03-22-09 09:40   dir1/file2.txt
       16  03-22-09 09:40   dir2/file1.txt
       16  03-22-09 09:40   dir2/file2.txt
       15  03-22-09 09:40   file1.txt
       15  03-22-09 09:40   file2.txt
 --------                   -------
       94                   6 files

Теперь давайте напишем простейшую программу для работы с ZIP файлом:

 
// файл zip1.c 
#include <stdio.h>
#include <stdlib.h>
#include <zip.h>
 
int main(int argc, char **argv) {
	struct zip *zip_file; // дескриптор zip файла 
	int err; // переменая для возврата кодов ошибок 
	int files_total; // количество файлов в архиве 
 
	if (argc < 2) {
	    fprintf(stderr,"usage: %s <zipfile>\n",argv[0]);
		return -1;
	};
 
	// открываем файл zip с именем переданным в качестве параметра 
	zip_file = zip_open(argv[1], 0, &err);
	if (!zip_file) {
		fprintf(stderr,"Error: can't open file %s\n",argv[1]);
		return -1;
	};
 
	files_total = zip_get_num_files(zip_file); // количество файлов в архиве 
	printf("Files in ZIP: %d\n",files_total);
	
	zip_close(zip_file);
 
	return 0;
};

Компилируем и пробуем запускать:

 
root@darkstar:/testzip# gcc -lzip zip1.c -o zip1
 
root@darkstar:/testzip# ./zip1
usage: ./zip1 <zipfile>
 
root@darkstar:/testzip# ./zip1 1.zip
Error: can't open file 1.zip
 
root@darkstar:/testzip# ./zip1 test.zip
Files in ZIP: 6

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

Для получения списка имен файлов в архиве давайте создадим файл zip2.c:

 
// файл zip2.c 
#include <stdio.h>
#include <stdlib.h>
#include <zip.h>
 
int main(int argc, char **argv) {
	struct zip *zip_file; // дескриптор zip файла 
	struct zip_stat file_info; // информация о файле 
	struct tm file_time;
	int err; // переменая для возврата кодов ошибок 
	int files_total; // количество файлов в архиве 
	int i;
 
	if (argc < 2) {
		fprintf(stderr,"usage: %s <zipfile>\n",argv[0]);
		return -1;
	};
 
	// открываем файл zip с именем переданным в качестве параметра 
	zip_file = zip_open(argv[1], 0, &err);
	if (!zip_file) {
		fprintf(stderr,"Error: can't open file %s\n",argv[1]);
		return -1;
	};
 
	files_total = zip_get_num_files(zip_file); // количество файлов в архиве 
	printf("Files in ZIP: %d\n",files_total);
 
	for (i = 0; i < files_total; i++) {
		// получаем информацию о файле с номером i в структуру file_info 
		zip_stat_index(zip_file, i, 0, &file_info);
 
		printf("index=%d ",file_info.index); // номер файла 
 
		printf("name=\"%s\" ",file_info.name); // имя файла 
 
		printf("size=%u ",file_info.size); // размер файла 
 
		localtime_r(&file_info.mtime, &file_time); // дата модификации файла 
		printf("date=\"%02d-%02d-%04d %02d:%02d\"",
			file_time.tm_mday,file_time.tm_mon+1,
			file_time.tm_year+1900, file_time.tm_hour,
			file_time.tm_min);
 
		printf("\n");
	};	
 
	zip_close(zip_file);
 
	return 0;
};

Компилируем и тестируем:

 
root@darkstar:/testzip# gcc -lzip zip2.c -o zip2
root@darkstar:/testzip# ./zip1 test.zip
Files in ZIP: 6
index=0 name="dir1/file1.txt" size=16 date="22-03-2009 09:40"
index=1 name="dir1/file2.txt" size=16 date="22-03-2009 09:40"
index=2 name="dir2/file1.txt" size=16 date="22-03-2009 09:40"
index=3 name="dir2/file2.txt" size=16 date="22-03-2009 09:40"
index=4 name="file1.txt" size=15 date="22-03-2009 09:40"
index=5 name="file2.txt" size=15 date="22-03-2009 09:40"

Заметьте, что у нас легко и непринужденно получился аналог команды unzip -l test.zip со своим форматом вывода данных.

Модифицируем немного исходник для того, чтобы извлекать из него данные:

 
// файл zip3.c 
#include <stdio.h>
#include <stdlib.h>
#include <zip.h>
 
int main(int argc, char **argv) {
	struct zip *zip_file; // дескриптор zip файла 
	struct zip_file *file_in_zip; // дексриптор файла внутри архива 
	int err; // переменая для возврата кодов ошибок 
	int files_total; // количество файлов в архиве 
	int file_number;
	int r;
	char buffer[10000];
 
	if (argc < 3) {
	    fprintf(stderr,"usage: %s <zipfile> <fileindex>\n",argv[0]);
		return -1;
	};
 
	// открываем файл zip с именем переданным в качестве параметра 
	zip_file = zip_open(argv[1], 0, &err);
	if (!zip_file) {
		fprintf(stderr,"Error: can't open file %s\n",argv[1]);
		return -1;
	};
 
	file_number = atoi(argv[2]); // номер файла берем из 3 аргумента 
	files_total = zip_get_num_files(zip_file); // количество файлов в архиве 
	if (file_number > files_total) {
		printf("Error: we have only %d files in ZIP\n",files_total);
		return -1;
	};
 
	// открываем файл внутри архива по номеру file_number 
	file_in_zip = zip_fopen_index(zip_file, file_number, 0);
	if (file_in_zip) {
		// читаем в цикле содержимое файла и выводим 
		while ( (r = zip_fread(file_in_zip, buffer, sizeof(buffer))) > 0) {
			printf("%s",buffer);
		};
		// закрываем файл внутри архива 
		zip_fclose(file_in_zip);
	} else {
		fprintf(stderr,"Error: can't open file %d in zip\n",file_number);
	};
 
	zip_close(zip_file);
 
	return 0;
};

Компилируем и пробуем запустить:

 
root@darkstar:/testzip# gcc -lzip zip3.c -o zip3
 
root@darkstar:/testzip# ./zip3
usage: ./zip3 <zipfile> <fileindex>
 
root@darkstar:/testzip# ./zip3 test.zip 10
Error: we have only 6 files in ZIP
 
root@darkstar:/testzip# ./zip3 test.zip 5
file 2 in root
 
root@darkstar:/testzip# ./zip3 test.zip 2
file 1 in dir 2

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

Библиотека libzip естественно имеет функции и для добавления файлов в архив, но это можно будет рассмотреть потом или же те, кому это понадобится, самостоятельно найдут решение.

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



Шаг 20 - Чтение содержимого директорий diropen, readdir, closedir

На одном из почтовых серверов у меня есть директория в которую складываются пришедшие письма. За некоторое время количество файлов в этой директории начинает превышать несколько тысяч и стандартной командой удаления rm *.eml удалить ничего не получается:

 
root@mailserver:/mail/queue# rm *.eml
bash: /bin/rm: Argument list too long

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

Итак, приступим. Для работы с директориями необходимо подключить файлы:

 
#include <sys/types.h>
#include <dirent.h>

Директория сама по себе представляет файл состоящий из специальных записей dirent, которые содержат данные о файлах в директории:

 
struct dirent {
  ino_t          d_ino;       /* inode number */
  off_t          d_off;       /* offset to the next dirent */
  unsigned short d_reclen;    /* length of this record */
  unsigned char  d_type;      /* type of file; not supported
                                 by all file system types */
  char           d_name[256]; /* filename */
};

Данная структура содержит имя файла d_name, порядковый номер файла d_ino в файловой системе и несколько других. Разберемся.

Для работы с директориями необходимо определить переменную типа DIR (по смыслу она похожа на тип FILE). Для открытия/закрытия директорий существует две функции:

 
DIR *opendir(const char *name);
int closedir(DIR *dirp);

Функция opendir() открывает директорию для чтения с именем name и возвращает указатель на directory stream (иногда сложно перевести со смыслом, директорный поток или поток директории ?), при ошибке возвращает NULL и соответствующим образом устанавливает код ошибки errno. Функция closedir() без комментариев.

Чтение из этого файла-директории осуществляется функциями со схожими названиями:

 
struct dirent *readdir(DIR *dirp);
int readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result);

Первая функция readdir() возвращает следующую структуру dirent считанную из файла-директории. При достижении конца списка файлов в директории или возникновении ошибки возвращает NULL. Вторая функция должна использоваться для чтения содержимого директорий при разработке программ работающих в мультипоточном режиме.

Давайте попробуем прочитать какую-нибудь директорию:

 
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
 
int main() {
    DIR *dir;
    struct dirent *entry;
 
    dir = opendir("/");
    if (!dir) {
        perror("diropen");
        exit(1);
    };
 
    while ( (entry = readdir(dir)) != NULL) {
        printf("%d - %s [%d] %d\n",
            entry->d_ino, entry->d_name, entry->d_type, entry->d_reclen);
    };
 
    closedir(dir);
};

Компилируем и запускаем:

 
root@home:/root/dirent# gcc dirread.c
root@home:/root/dirent# ./a.out
2 - . [0] 16
1 - .. [0] 16
114 - bin [0] 16
19 - dev [0] 16
109 - etc [0] 16
89 - lib [0] 16
117 - mnt [0] 16
127 - opt [0] 16
144 - srv [0] 16
112 - tmp [0] 16
108 - sys [0] 16
4 - var [0] 16
37 - usr [0] 16
113 - boot [0] 16
116 - home [0] 16
145 - proc [0] 16
115 - sbin [0] 16
149 - root [0] 16
691340 - readme.txt [0] 32
128 - media [0] 20
575756 - netio [0] 20

Из всего этого думаю полезностей извлечь можно не много. Думаю кроме имени файла d_name ничего особо полезного нет, но собственно больше ничего и не надо. Как и было написано в man 3 readdir поле d_type многие файловые системы не устанавливают (в примере используется reiserfs), а жаль. Судя по описанию уже по этому полю можно было бы определить тип записи: файл, директория или символьная ссылка. Ну ничего страшного.







Статья взята с http://www.firststeps.ru (_http://www.firststeps.ru/linux/general1.html)