Разработка приложений в среде Linux. Второе издание

Джонсон Майкл К.

Троан Эрик В.

Часть II

Инструментальные средства и среда разработки

 

 

Глава 4

Инструментальные средства разработки

 

Для работы в Linux доступно потрясающее разнообразие средств разработки. Любому программисту, работающему в Linux, нужно ознакомиться с некоторыми наиболее важными из них.

Дистрибутивы Linux включают в себя множество серьезных и проверенных средств разработки; большинство из этих средств на протяжении нескольких лет входили в системы разработки под Unix. Средства разработки Linux не отличаются ненужными излишествами и броскостью; большинство из них представляют собой инструменты командной строки без графического интерфейса пользователя. За все годы их применения эти средства зарекомендовали себя с самой лучшей стороны, и их изучение лишним не будет.

Если вы уже знакомы с Emacs, vi, make, gdb, strace и ltrace, в этой главе ничего нового вы для себя не найдете. Тем не менее, в оставшейся части книги предполагаются хорошие знания какого-нибудь текстового редактора. Практически весь свободный исходный код Unix и Linux собирается при помощи make, a gdb — один из самых распространенных отладчиков, доступных для Linux и Unix. Утилита strace (или подобная утилита под названием trace либо truss) доступна в большинстве систем Unix; утилита ltrace была изначально написана для Linux и в большинстве систем недоступна (на момент написания книги).

Однако не стоит думать, что для Linux нет графических средств разработки; на самом деле, все как раз наоборот. Этих средств огромное количество.

На момент написания этой книги привлекали внимание две интегрированных среды разработки (Integrated Development Environment — IDE), которые могут входить в используемый вами дистрибутив: KDevelop (http://kdevelop.org/), часть среды рабочего стола KDE, и Eclipse (http://eclipse.org/), межплатформенная среда, основанная на Java, которая первоначально, была разработана IBM, а теперь поддерживается крупным консорциумом. Однако в этой книге мы не будем останавливаться на рассмотрении упомянутых сред, поскольку они сопровождаются детальной документацией.

Даже несмотря на то, что для работы в Linux доступны многочисленные IDE, они пользуются не такой популярностью, как на других платформах. Даже если среда IDE применяется, все же более практичным считается написание программного обеспечения с открытым исходным кодом без ее задействования. Все это делается для того, чтобы другие программисты, которые захотят сделать свой вклад в ваш проект, не были стеснены вашим выбором IDE. Среда KDevelop поможет собрать проект, который будет использовать стандартные инструменты Automake, Autoconf и Libtool, используемые в многочисленных проектах с открытым исходным кодом.

Сами по себе стандартные средства Automake, Autoconf и Libtool играют важную роль в процессе разработки. Они были созданы для помощи в построений приложений таким образом, чтоб эти приложения могли быть почти автоматически перенесены в другие операционные системы. Ввиду того, что эти средства сложны, в настоящей книге мы рассматривать их не будем. Кроме того, эти средства регулярно изменяются; электронные версии GNU Autoconf, Automake и Libtool [41] доступны по адресу http://sources.redhat.com/autobook/.

 

4.1. Редакторы

 

Разработчики Unix традиционно придерживались строгих и разнотипных предпочтений особенно в выборе редакторов.

Доступно множество редакторов, которые легко изучить самостоятельно; наиболее распространенными можно считать vi и Emacs. Оба редактора являются мощными представителями своего типа, чего не скажешь на первый взгляд. У обоих редакторов сравнительно крутая кривая обучения, и они радикально отличаются друг от друга. Emacs является достаточно крупным; он сам себе операционная среда, vi не занимает много места и разработан специально для внедрения в среду Unix. Было написано множество клонов и альтернативных версий каждого редактора, и у самих версий также имеются свои клоны.

В этой книге мы не будем углубляться в изучение vi и Emacs, поскольку материал занял бы слишком много места. В [32] каждому редактору посвящены отдельные главы, кроме того, рекомендуем обратиться к [5] и [17]. В нашей книге мы только сравним Emacs и vi и расскажем, как получить оперативную справку по каждому из них.

Emacs включает исчерпывающий набор руководств, в которых объясняется не только использование Emacs как редактора, но и показывается, как применять Emacs для чтения и отправки электронной почты и новостей в Usenet, для игр (игра гомоку очень даже неплоха) и для ввода команд оболочки. В Emacs, написав полное имя внутренней команды, всегда можно ее выполнить, даже если она не привязана к клавишам.

В отличие от Emacs, документация по vi менее развернутая и менее известна. vi является только редактором, и многие важные команды можно выполнить путем нажатия одной клавиши. Здесь можно переключаться между режимом, в котором при нажатии стандартных букв алфавита они помещаются в текст, и режимом, в котором эти буквы являются командами. Например, можно использовать клавиши h, j, k и l в качестве клавиш управления курсором для навигации по документу.

Оба редактора позволяют создавать макросы для упрощения работы, но их макроязыки очень сильно отличаются. Emacs включает в себя целый язык программирования под названием elisp (Emacs Lisp), который очень тесно связан с языком программирования Common Lisp. В первоначальном варианте vi встроен более спартанский язык, ориентированный на стек. Большинство пользователей просто связывают с клавишами простые однострочные команды vi, но эти команды зачастую запускают программы за пределами vi, чтобы управлять данными внутри vi. По Emacs Lisp написано огромное руководство, включающее пособие по использованию; по языку, встроенному в vi, документация сравнительно скупа.

Некоторые редакторы позволяют смешивать и совмещать функциональности. Так, существуют редакторы, в которых можно использовать Emacs в режиме vi (viper), позволяющем использование стандартных команд vi; в другом клоне vi под названием vile ("vi like Emacs") можно использовать vi в режиме Emacs.

 

4.1.1. Emacs

Emacs встречается в нескольких вариациях. Первоначальный редактор Emacs был написан Ричардом Столлманом (Richard Stallman), одним из лидеров Фонда свободного ПО (Free Software Foundation — FSF). В течение многих лет его GNU Emacs был самым популярным редактором. С недавних пор популярностью начал пользоваться другой вариант GNU Emacs — XEmacs, в котором больше места уделяется поддержке графического интерфейса. XEmacs начал свою жизнь в качестве Lucid Emacs, набора расширений GNU Emacs, разработанного теперь уже распавшейся компанией Lucid Technologies. В намерения этой компании входило официально включить XEmacs в GNU Emacs. Но из-за технических различий команды не смогли слить свои коды. Несмотря ни на что, эти два редактора отлично совмещаются, а программисты обоих команд заимствуют коды друг у друга. Ввиду того, что обе эти версии очень похожи, в дальнейшем мы будем ссылаться на них как на Emacs.

Лучший способ для удобной работы с редактором Emacs — изучить пособие по работе с ним. Запустите emacs и наберите ^ht. Наберите ^x^c для выхода из Emacs. С помощью обучающей программы можно узнать, где получить дополнительную информацию по Emacs. Здесь вы не узнаете, как получить руководство по Emacs, распространяемое вместе с самим редактором. Для вызова этого руководства наберите ^hi.

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

 

4.1.2.

vi

Если вы быстро набираете текст и хотите, чтоб ваши пальцы находились в правильном положении, vi вам наверняка понравится, поскольку его набор команд был разработан таким образом, чтобы движений пальцев печатающего было как можно меньше. Этот редактор также ориентирован на пользователей Unix. Если вы знакомы с sed или awk либо другими программами Unix, использующими стандартные регулярные выражения с ^ для перехода к началу строки и $ для перехода к ее концу, работа с vi покажется вам простой и естественной.

К сожалению, освоение vi может оказаться более сложным, нежели Emacs. Дело в том, что хоть пособия по vi подобны учебникам по Emacs, ни в одной версии vi нет стандартного способа запуска учебного пособия. Тем не менее, многие версии, включая версию, поставляемую с обычными дистрибутивами Linux, поддерживают команду :help.

В наиболее общей версии vi, vim ("Vi IMproved"), есть множество интегрированных средств из набора разработки Emacs, включая выделение синтаксиса, автоматическое расположение текста, язык написания сценариев и разбор ошибок компилятора.

 

4.2.

make

 

Основой программирования под Unix является make — средство, которое существенно упрощает описание компиляции программ. Даже притом, что небольшим программам порой достаточно одной команды для компиляции их исходного кода в исполняемый файл, все же намного легче написать make, чем строку вроде gcc -02 -ggdb -DSOME DEFINE -о foo foo.c. Более того, если имеется множество файлов для компиляции, а код был изменен лишь в некоторых из них, make создаст новые объектные файлы только для тех файлов, на которые повлияли изменения. Чтобы make совершила это "чудо", потребуется описать все файлы в make-файле (Makefile), пример которого показан ниже.

 1: # Makefile

 2:

 3: OBJS = foo.о bar.о baz.o

 4: LDLIBS = -L/usr/local/lib/ -lbar

 5:

 6: foo: $(OBJS)

 7:       gcc -o foo $ (OBJS) $ (LDLIBS)

 8:

 9: install: foo

10:       install -m 644 foo /usr/bin

11: .PHONY: install

• Строка 1 — это комментарий; make следует обычной традиции Unix определения комментариев с помощью символа #.

• В строке 3 определяется переменная по имени OBJS как foo.о bar.о baz.о.

• В строке 4 определяется другая переменная — LDLIBS.

• В строке 6 начинается определение правила, которое указывает на то, что файл foo зависит от (в этом случае, собран из) файлов, имена которых содержатся в переменной OBJS. foo называется целевым объектом, а $(OBJS) — списком зависимостей. Обратите внимание на синтаксис расширения переменной: имя переменной помещается в $(...).

Строка 7 — это командная строка, указывающая на то, как построить целевой объект из списка зависимостей. Командных строк может быть много, и первым символом в командной строке должна быть табуляция.

• Строка 9 — довольно интересный целевой объект. Фактически тут не предпринимается попытка создать файл по имени install; вместо этого (как видно в строке 10) foo инсталлируется в /usr/bin с помощью стандартной программы install. Эта строка вызывает неоднозначность в make: что, если файл install уже существует и является более новым, нежели foo? В этом случае запуск команды make install приведет к выдаче сообщения 'install' is up to date (install является новее) и завершению работы.

• Строка 11 указывает make на то, что install не является файлом, и что необходимо игнорировать любой файл по имени install при вычислении зависимости install. Таким образом, если зависимость install была вызвана (как это сделать мы рассмотрим далее), команда в строке 10 всегда будет выполняться. .PHONY — это директива, которая изменяет операцию make; в этом случае она указывает make на то, что целевой объект install не является именем файла. Целевые объекты .PHONY часто используются для совершения действий вроде инсталляции или создания одиночного имени целевого объекта, которое основывается на нескольких других уже существующих целевых объектов, например:

all: foo bar baz

.PHONY: all

К сожалению, .PHONY не поддерживается некоторыми версиями make. Менее очевидный, не такой эффективный, но более переносимый способ для этого показан ниже.

all: foo bar baz FORCE

FORCE:

Это срабатывает только тогда, когда нет файла по имени FORCE.

Элементы в списках зависимостей могут быть именами файлов, но, поскольку это касается make, они являются целевыми объектами. Элемент foo в списке зависимости install — это целевой объект. При попытке make вычислить зависимость install становится ясно, что в первую очередь необходимо вычислить зависимость foo. А для этого make потребуется вычислить зависимости foo.о, bar.о и baz.о.

Обратите внимание на отсутствие строк, явно указывающих make, как строить foo.о, bar.о или baz.о. Вы не будете определять эти строки непосредственно в редакторе. make обеспечивает предполагаемые зависимости, которые записывать не нужно. Если в файле есть зависимость, заканчивающаяся на .о, и есть файл с таким же именем, но он заканчивается на .с, make предполагает, что этот объектный файл зависит от исходного файла. Встроенные суффиксные правила, которые поддерживаются make, позволяют значительно упростить многие make-файлы и, если встроенное правило не соответствует требованиям, можно создать свои собственные суффиксные правила (речь об этом пойдет ниже).

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

Аргумент -k заставляет make создавать максимально возможное количество файлов без останова, даже если какая-то команда вернула ошибку. Это полезно, например, при переносе программы; можно построить столько объектных файлов, сколько нужно, а потом перенести файлы, которые вызвали ошибку, без нежелательных перерывов в работе.

Если известно, что какая-то одна команда будет всегда возвращать ошибку, но вы хотите проигнорировать ее, можно воспользоваться "магией" командной оболочки. Команда /bin/false всегда возвращает ошибку, таким образом, /bin/false всегда будет вызывать прекращение работы, если только не указана опция -k. С другой стороны, конструкция любая_команда || /bin/true никогда не вызовет прекращение работы; даже если любая_команда возвращает false, оболочка затем запускает /bin/true, которая вернет успешный код завершения.

make интерпретирует нераспознанные аргументы командной строки, которые не начинаются с минуса (-), как целевые объекты для сборки. Таким образом, make install приводит к сборке целевого объекта install. Если целевой объект foo устарел, make сначала соберет зависимости foo, а затем инсталлирует его. Если требуется собрать целевой объект, начинающийся со знака минус, этот знак перед именем целевого объекта должен быть продублирован (--).

 

4.2.1 Сложные командные строки

Каждая командная строка выполняется в своей собственной подоболочке, таким образом, команды cd в командной строке влияют только на строку, в которой они записаны. Любую строку в make-файле можно расширить на множество строк, указывая в конце каждой обратный слэш. Ниже показан пример того, как иногда могут выглядеть командные строки.

 1: cd первый_ каталог ; \

 2:   сделать что-то с файлом $ (FOO) ; \

 3:  сделать еще что-то

 4: cd второй_каталог ; \

 5:  if [ -f некоторый_файл ] ; then\

 6:   сделать что-то другое ; \

 7:  done; \

 8:  for i in * ; do \

 9:   echo $$i >> некоторый__файл ; \

10:  done

make находит в этом фрагменте кода только две строки. Первая командная строка начинается в строке 1 и продолжается до строки 3, а вторая начинается в строке 4 и заканчивается в строке 10. Здесь следует отметить несколько моментов.

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

• Строки, образующие каждую командную строку, передаются оболочке в виде одной строки. Таким образом, все символы ;, которые нужны оболочке, должны присутствовать, включая даже те, которые обычно в сценариях оболочки опускаются, поскольку их наличие подразумевается благодаря символам новой строки. Более детально о программировании программной оболочки рассказывается в [22].

• Если требуется разыменовывать переменную make, это делается обычным образом (то есть $( переменная ) ), но если нужно разыменовывать переменную оболочки, необходимо применять двойной символ $: $$i.

 

4.2.2. Переменные

Часто бывает необходимо определить только один компонент переменной за раз. Можно написать, например, такой код:

OBJS = foo.о

OBJS = $(OBJS) bar.о

OBJS = $(OBJS) baz.о

Ожидается, что здесь OBJS будет определен как foo.о bar.о baz.о, но в действительности он определен как $(OBJS) baz.о, поскольку make не разворачивает переменные до момента их использования. При ссылке в правиле на OBJS make войдет в бесконечный цикл. Во избежание этого во многих make-файлах разделы организуются следующим образом:

OBJS1 = foo.о

OBJS2 = bar.о

OBJS3 = baz.о

OBJS = $(OBJS1) $(OBJS2) $(OBJS3)

Объявления переменных вроде предыдущего встречаются, когда объявление одной переменной оказывается слишком длинным и потому не удобным.

Развертывание переменной вызывает типичный вопрос, который программист в Linux должен решить. Инструменты GNU, распространяемые с Linux, обычно более функциональны, чем версии инструментов, включенных в другие системы, и GNU make — не исключение. Авторы GNU make предусмотрели альтернативный способ присваивания переменных, но не все версии make понимают эти альтернативные формы. К счастью, GNU make можно собрать для любой системы, в которую можно перенести исходный код, написанных под Linux. Существует форма простого присваивания переменных, которая показана ниже.

OBJS := foo.о

OBJS := $(OBJS) bar.о

OBJS := $(OBJS) baz.о

Операция := заставляет GNU make вычислить выражение переменной при присваивании, а не ждать вычисления выражения при его использовании в правиле. В результате выполнения этого кода OBJS действительно получит foo.о bar.о baz.о.

Простое присваивание переменной используется очень часто, но в GNU make есть еще и другой синтаксис присваивания, который позаимствован из языка С:

OBJS := foo.о

OBJS += bar.о

OBJS += baz.о

 

4.2.3. Суффиксные правила

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

.c.o:

$(CC) -с $ (CFLAGS) $ (CPPFLAGS) -о $<

.SUFFIXES: .с .о

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

Это суффиксное правило демонстрирует другую возможность make — автоматические переменные. Понятно, что нужно найти способ подставить зависимость и целевой объект в командную строку. Автоматическая переменная $@ выступает в качестве целевого объекта, $< выступает в качестве первой зависимости, а $^ представляет все зависимости.

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

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

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

% .о: % .с

$(CC) -с $(CFLAGS) $(CPPFLAGS) -о $<

Дополнительные сведения о make можно получить в [26]. GNU make также включает замечательное и удобное в обращении руководство в формате Texinfo, которое можно почитать на сайте FSF, распечатать или заказать у них в форме книги.

Большинство крупных проектов с открытым исходным кодом используют инструменты Automake, Autoconf и Libtool. Эти инструменты представляют собой коллекцию знаний об особенностях различных систем и стандартах сообщества, которая может помочь в построении проектов. Таким образом, потребуется писать лишь немного кода, специфического для проекта. Например, Automake пишет целевые объекты install и uninstall, Autoconf автоматически определяет возможности системы и настраивает программное обеспечение для его соответствия системе, a Libtool отслеживает различия в управлении совместно используемыми библиотеками на разных системах.

По этим трем инструментам написана целая книга — [41]; здесь мы даем лишь основу, которая понадобится для работы с GNU Autoconf, Automake и Libtool.

 

4.3. Отладчик GNU

gdb — это отладчик, рекомендуемый Free Software Foundation, gdb представляет собой хороший отладчик командной строки, на котором строятся некоторые инструменты, включая режим gdb в Emacs, графический отладчик Data Display Debugger (http://www.gnu.org/software/ddd/) и встроенные отладчики в некоторых графических интерфейсах IDE. В этом разделе рассматривается только gdb.

Запустите gdb с помощью команды gdb имя_программы . gdb не будет просматривать значение PATH в поисках исполняемого файла. Отладчик загрузит символьную информацию для исполняемого файла и запросит дальнейших действий.

Существует три способа проверить процесс с помощью gdb.

• Используя команду run для обычного выполнения программы.

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

• Исследуя существующий файл ядра для определения состояния процесса при его завершении. Для исследования файла ядра gdb потребуется запустить с помощью команды имя_программы файл_ядра .

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

gdb не требует написания полного имени команды; указание r достаточно для run, n — для next, s — для step. Более того, для повторения наиболее часто употребляемой команды, нужно просто нажать клавишу . Таким образом, пошаговое выполнение программы становится проще.

Ниже предложен небольшой набор полезных команд gdb; gdb включает полное онлайновое руководство в формате GNU info (запустите info gdb), в котором детально объясняются все опции gdb. В [19] содержится неплохое подробное руководство по работе с gdb. gdb также поддерживает оперативную справку, ссылки на которую можно найти внутри gdb; доступ к справочным файлам можно получить, введя команду help. Можно также получить справку по каждой определенной команде, набрав help команда или help тема .

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

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

Идентификаторы формата отделены от команд символом / и состоят из трех элементов: цифра, буква формата и буква, отражающая размер. Цифра и буква размера не обязательны; по умолчанию в качестве цифры устанавливается 1, а размер получает подходящее значение по умолчанию, основанное на букве формата.

Буквы формата следующие: о для обозначения восьмеричного числа, x для шестнадцатеричного числа, d для десятичного числа, и для беззнакового десятичного числа, t для двоичных данных, f для числа с плавающей запятой, а для адреса, i для инструкций, с для символа, s для строки.

Символы, отображающие размер, таковы: b — байт, h — полуслово (2 байта), w — слово (4 байта), g — слово-гигант (8 байт).

attach, at

Подключает отладчик к уже запущенному процессу. Единственным аргументом является идентификатор процесса (pid), к которому осуществляется подключение. Процессы, с которыми установлено подключение, останавливаются, прерывая любые ожидающие или текущие системные вызовы, которые разрешено прерывать. См. detach.

backtrace, bt, where, w

Выводит трассировку стека.

break, b

Устанавливает точку прерывания. Можно указать имя функции, номер строки текущего файла (файл, содержащий выполняемый в данный момент код), пару имя_файла:номер_строки или даже произвольный адрес с помощью * адрес .gdb назначает и выводит уникальный номер для каждой точки прерывания. См. condition, clear и delete.

clear

Удаляет точку прерывания. Принимает такой же аргумент, как break. См. delete.

condition

Изменяет точку прерывания, определенную номером (см. break), для прерывания, только если выражение истинно. Допускаются произвольные выражения.

(gdb) b664

Breakpoint 3 at 0х804а5с0: file ladsh4.c, line664.

(gdb) condition 3 status==0

delete

Удаляет точку прерывания, определенную номером.

detach

Отключается от текущего подключенного процесса.

display

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

Help

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

jump

Переходит на произвольный адрес и продолжает выполнение процесса с этой точки. Адрес — единственный аргумент; его можно определить в форме номера строки или адреса, указанного как *адрес.

list, l

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

next, n

Переходит на следующую строку исходного кода в текущей функции, без захода внутрь функций. См. step.

nexti

Переходит на следующую инструкцию машинного языка без захода внутрь функций. См. stepi.

print, p

Выводит значение выражения в понятной форме. Если есть переменная char* с, команда print с выведет адрес строки, a print *с выведет саму строку. Для структур выводятся их члены. Можно использовать приведения типов, которые gdb будет учитывать. Если код скомпилирован с опцией -ggdb, в выражениях станут доступны перечислимые значения и определения препроцессора. См. display. Команда print принимает идентификаторы формата, несмотря на то, что при указании и преобразовании типов идентификаторы формата зачастую не нужны. См. x.

run, r

Запускает текущую программу с начала. Аргументы команды run передаются в командную строку для запуска программы. В gdb, подобно оболочке, можно универсализировать имена файлов с помощью * и [], а также осуществлять переадресацию посредством <, > и >>, но нельзя создавать каналы или внутренние документы. Без аргументов run использует аргументы, которые были определены в самой последней команде run или set args. Для запуска без аргументов после их задействования используется команда set args без дополнительных аргументов.

set

gdb позволяет менять значения переменных, например:

(gdb) set а = argv[5]

Также каждый раз при выводе выражения с помощью print создается сокращенная переменная вроде $1, на которую впоследствии можно ссылаться. Таким образом, если ранее был выведен argv[5] и gdb указал на то, что результат сохранен в $6, можно переписать предыдущее присваивание так:

(gdb) set а = $6

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

step, s

Выполняет инструкции программы до достижения новой строки исходного кода. См. next.

stepi

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

undisplay

Если выдана без аргумента, отменяет все отображения. В противном случае отменяет отображения указанные номерами. См. display.

whatis

Выводит тип данных выражения, переданного в качестве аргумента команды.

where, w

См. backtrace.

x

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

 

4.4. Действия при трассировке программы

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

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

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

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

Обе утилиты поддерживают похожий набор опций.

-С или --demangle Только для ltrace . Декодирует (или расшифровывает) имена библиотечных символов в читабельные имена. В результате убираются начальные символы подчеркивания (многие функции glibc имеют внутренние имена с начальными символами подчеркивания) и функции библиотеки С++ становятся более читабельными (С++ шифрует информацию о типе в символьные имена).
Только для strace . Указывает подмножество вызовов, которые нужно вывести. Существует множество возможных спецификаций, описанных на man-странице strace ; самой распространенной спецификацией является -е trace=file , которая трассирует только системные вызовы, связанные с файловым вводом-выводом и обработкой файлов.
-f Пытается «следовать вызову fork() », по возможности трассируя дочерние процессы. Обратите внимание, что дочерний процесс может некоторое время работать без трассировки до тех пор, пока strace или ltrace сможет подключиться к нему и трассировать его действия.
-о имя_файла Вместо вывода на стандартное устройство вывода выводит в файл имя файла.
-р pid Вместо запуска нового экземпляра программы подключается к процессу с идентификатором pid .
-S Только для ltrace . Отслеживает системные и библиотечные вызовы.
-v Только для strace . Не сокращает большие структуры в системных вызовах вроде семейства вызовов stat() , termios и так далее.

На man-страницах утилит можно найти описание этих и других опций, здесь не упомянутых.

 

Глава 5

Опции и расширения

gcc

 

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

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

Стремление соблюсти ISO-стандарт С весьма полезно, но в связи с тем, что С является низкоуровневым языком, встречаются ситуации, когда стандартные средства недостаточно выразительны. Существуют две области, в которых широко применяются расширения gcc: взаимодействие с ассемблерным кодом (эти вопросы раскрываются по адресу http://www.delorie.com/djgpp/doc/brennan/) и сборка совместно используемых библиотек (см. главу 8). Поскольку заголовочные файлы являются частью совместно используемых библиотек, некоторые расширения проявляются также в системных заголовочных файлах.

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

 

5.1. Опции

gcc

gcc принимает множество командных опций. К счастью, набор опций, о которых действительно нужно иметь представление, не так велик, и в этой главе мы его рассмотрим.

Большинство опций совпадают или подобны опциям других компиляторов, gcc включает в себя огромную документацию по своим опциям, доступную через info gcc (man gcc также выдает эту информацию, однако man-страницы не обновляются настолько часто, как документация в формате Texinfo).

-о имя_файла Указывает имя выходного файла. Обычно в этом нет необходимости, если осуществляется компиляция в объектный файл, то есть по умолчанию происходит подстановка имя_файла .с на имя_файла .о . Однако если вы создаете исполняемый файл, по умолчанию (по историческим причинам) он создается под именем а.out . Это также полезно в случае, когда требуется поместить выходной файл в другой каталог.
Компилирует без компоновки исходный файл, указанный для командной строки. В результате для каждого исходного файла создается объектный файл. При использовании make компилятор gcc обычно вызывается для каждого объектного файла; таким образом, в случае возникновения ошибки легче обнаружить, какой файл не смог скомпилироваться. Однако если вы вручную набираете команды, часто в одном вызове gcc указывается множество файлов. В случае, если при задании множества файлов в командной строке может возникнуть неоднозначность, лучше указать только один файл. Например, вместо gcc -с -о а.о а.с b.с имеет смысл применить gcc -с -o a.o b.c .
-D foo Определяет препроцессорные макросы в командной строке. Возможно, потребуется отменить символы, трактуемые оболочкой как специальные. Например, при определении строки следует избегать употребления ограничивающих строки символов " . Вот два наиболее употребляемых способа: '-Dfoo="bar"' и -Dfoo=\"bar\" . Первый способ работает намного лучше, если в строке присутствуют пробелы, поскольку оболочка рассматривает пробелы особым образом.
-I каталог Добавляет каталог в список каталогов, в которых производится поиск включаемых файлов.
-L каталог Добавляет каталог в список каталогов, в которых производится поиск библиотек, gcc будет отдавать предпочтение совместно используемым библиотекам, а не статическим, если только не задано обратное.
-l foo Выполняет компоновку с библиотекой lib foo . Если не указано обратное, gcc отдает предпочтение компоновке с совместно используемыми библиотеками ( lib foo .so ), а не статическими ( lib foo .a ). Компоновщик производит поиск функций во всех перечисленных библиотеках в том порядке, в котором они перечислены. Поиск завершается тогда, когда будут найдены все искомые функции.
-static Выполняет компоновку с только статическими библиотеками. См. главу 8.
-g , -ggdb Включает отладочную информацию. Опция -g заставляет gcc включить стандартную отладочную информацию. Опция -ggdb указывает на необходимость включения огромного количества информации, которую в силах понять лишь отладчик gdb .
Если дисковое пространство ограничено или вы хотите пожертвовать некоторой функциональностью ради скорости компоновки, следует использовать -g . В этом случае, возможно, придется воспользоваться другим отладчиком, а не gdb . Для максимально полной отладки необходимо указывать -ggdb . В этом случае gcc подготовит максимально подробную информацию для gdb . Следует отметить, что в отличие от большинства компиляторов, gcc помещает некоторую отладочную информацию в оптимизированный код. Однако трассировка в отладчике оптимизированного кода может быть сопряжена со сложностями, так как во время выполнения могут происходить прыжки и пропуски фрагментов кода, которые, как ожидалось, должны были выполняться. Тем не менее, при этом можно получить хорошее представление о том, как оптимизирующие компиляторы изменяют способ выполнения кода.
-O , -O n Заставляет gcc оптимизировать код. По умолчанию, gcc выполняет небольшой объем оптимизации; при указании числа ( n ) осуществляется оптимизация на определенном уровне. Наиболее распространенный уровень оптимизации — 2; в настоящее время в стандартной версии gcc самым высоким уровнем оптимизации является 3. Мы рекомендуем использовать -O2 или -O3 ; -O3 может увеличить размер приложения, так что если это имеет значение, попробуйте оба варианта. Если для вашего приложения важна память и дисковое пространство, можно также использовать опцию -Os , которая делает размер кода минимальным за счет увеличения времени выполнения. gcc включает встроенные функции только тогда, когда применяется хотя бы минимальная оптимизация ( -O ).
-ansi Поддержка в программах на языке С всех стандартов ANSI (X3.159-1989) или их эквивалента ISO (ISO/IEC 9899:1990) (обычное называемого С89 или реже С90). Следует отметить, что это не обеспечивает полное соответствие стандарту ANSI/ISO.
Опция -ansi отключает расширения gcc , которые обычно конфликтуют со стандартами ANSI/ISO. (Вследствие того, что эти расширения поддерживаются многими другими компиляторами С, на практике это не является проблемой.) Это также определяет макрос __STRICT_ANSI__ (как описано далее в этой книге), который заголовочные файлы используют для поддержки среды, соответствующей ANSI/ISO.
-pedantic Выводит все предупреждения и сообщения об ошибках, требуемые для ANSI/ISO-стандарта языка С. Это не обеспечивает полное соответствие стандарту ANSI/ISO.
-Wall Включает генерацию всех предупреждений gcc , что обычно является полезным. Но таким образом не включаются опции, которые могут пригодиться в специфических случаях. Аналогичный уровень детализации будет установлен и для программы синтаксического контроля lint в отношении вашего исходного кода, gcc позволяет вручную включать и отключать каждое предупреждение компилятора. В руководстве по gcc подробно описаны все предупреждения.

 

5.2. Заголовочные файлы

 

Время от времени вы можете застать себя на том, что просматриваете заголовочные файлы Linux. Скорее всего, вы найдете рад конструкций, не совместимых со стандартом ANSI/ISO. Некоторые из них стоят того, чтобы в них разобраться. Все конструкции, рассматриваемые в этой книге, более подробно изложены в документации по gcc.

 

5.2.1. long long

Тип long long указывает на то, что блок памяти, по крайней мере, такой же большой, как long. На Intel i86 и других 32-разрядных платформах long занимает 32 бита, а long long — 64 бита. На 64-разрядных платформах указатели и long long занимают 64 бита, a long может занимать 32 или 64 бита в зависимости от платформы. Тип long long поддерживается в стандарте С99 (ISO/IEC 9899:1999) и является давним расширением С, которое обеспечивается gcc.

 

5.2.2. Встроенные функции

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

 

5.2.3. Альтернативные расширенные ключевые слова

В gcc у каждого расширенного ключевого слова (ключевые слова, не описанные стандартом ANSI/ISO) есть две версии: само ключевое слово и ключевое слово, окруженное с двух сторон двумя символами подчеркивания. Когда компилятор применяется в стандартном режиме (обычно тогда, когда задействована опция -ansi), обычные расширенные ключевые слова не распознаются. Так, например, ключевое слово attribute в заголовочном файле должно быть записано как __attribute__.

 

5.2.4. Атрибуты

Расширенное ключевое слово attribute используется для передачи gcc большего объема информации о функции, переменной или объявленном типе, чем это позволяет код С, соответствующий стандарту ANSI/ISO. Например, атрибут aligned дает указание gcc о том, как именно выравнивать переменную или тип; атрибут packed указывает на то, что заполнение использоваться не будет; noreturn определяет то, что возврат из функции никогда не произойдет, что позволяет gcc лучше оптимизироваться и избегать фиктивных предупреждений.

Атрибуты функции объявляются путем их добавления в объявление функции, например:

void die_die_die(int, char*) __attribute__ ((__noreturn__));

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

int printm(char*, ...)

__attribute__((const,

 format(printf, 1, 2)));

В этом примере видно, что printm не рассматривает никаких значений, кроме указанных, и не имеет побочных эффектов, относящихся к генерации кода (const), printm указывает на то, что gcc должен проверять аргументы функции так же, как и аргументы printf(). Первый аргумент является форматирующей строкой, а второй — первым параметром замены (format).

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

 

Глава 6

Библиотека GNU C

 

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

 

6.1. Выбор возможностей

glibc

В glibc существует набор макросов для выбора возможностей. Эти макросы используются для выбора стандарта, которому будет подчиняться glibc. Иногда стандарты конфликтуют между собой, a glibc позволяет выбирать именно тот набор стандартов (формальный, де-юре, и неформальный, де-факто), которым нужно соответствовать полностью либо частично. Технически такие макросы называются макросами проверки возможностей.

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

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

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

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

Набор макросов по умолчанию содержит _SVID_SOURCE=1, _BSD_SOURCE=1, _POSIX_SOURCE=1 и _POSIX_C_SOURCE=199506L. Описание каждой из этих опций можно найти ниже, но все это, по сути, транслируется в поддержку возможностей стандарта 1995 POSIX (этот стандарт использовался до объединения стандартов POSIX и Single Unix), всех стандартных функций System V и всех функций BSD, которые не конфликтуют с функциями System V. Данного набора макросов достаточно для большинства программ.

При определении в gcc опции -ansi, как было описано ранее, автоматически определяется внутренний макрос __STRICT_ANSI__, который отключает все макросы, определенные по умолчанию.

За исключением __STRICT_ANSI__, который представляет собой специальный макрос (и должен настраиваться только компилятором в контексте опции командной строки -ansi), эти макросы имеют накопительный характер, то есть можно определять любые их комбинации. Точное определение изменений _BSD_SOURCE зависит от настройки других макросов (более детально об этом — ниже); все остальные макросы — исключительно накопительные.

Некоторые макросы определяются различными версиями POSIX или других стандартов, другие являются общими, а третьи могут использоваться только в glibc.

_POSIX_SOURCE Если указан этот макрос, становятся доступными все интерфейсы, определенные как часть оригинальной спецификации POSIX.1. Данный макрос был определен в первоначальном стандарте POSIX.1-1990.
_POSIX_C_SOURCE Этот макрос может заменять _POSIX_SOURCE . Если установлен в 1, то эквивалентен _POSIX_SOURCE . Если его значение больше либо равно 2, макрос включает интерфейсы С, соответствующие POSIX.2, и задействует регулярные выражения. Если значение больше либо равно 199309L, макрос включает в себя дополнительные интерфейсы С, соответствующие пересмотренному в 1993 году стандарту POSIX, в частности, включая функциональность реального времени. Если его значение больше либо равно 199506L (по умолчанию), макрос включает дополнительные интерфейсы С, соответствующие пересмотренному в 1995 году стандарту POSIX, в частности, включая потоки POSIX. Этот макрос был описан версией POSIX, выпущенной после 1990 года для разграничения поддержки различных версий стандартов POSIX (а теперь также и Single Unix). Во многих случаях полностью замещается _XOPEN_SOURCE .
_XOPEN_SOURCE Макрос _XOPEN_SOURCE определен XSI-частью стандарта Single Unix и описывает логическое надмножество интерфейсов, включенных с помощью _POSIX_C_SOURCE . Этот макрос также был определен XPG. Если макрос определен, указываются функциональные возможности из начального стандарта XPG4 (Unix95). Если макрос определен со значением 500 , включаются функциональные возможности из стандарта XPG5 (Unix98, SuS версии 2). Если установлено значение 600 , включаются функциональные возможности из начального стандарта IEEE Std 1003.1-2003 (комбинированный документ по POSIX и SuS).
_ISOC99_SOURCE Этот макрос проверки возможностей экспортирует интерфейсы, определенные в новых стандартах ISO/IEC С99.
_SVID_SOURCE При указании данного макроса для выбора возможностей становится доступным стандарт SVID (System V Interface Definition). Это не значит, что glibc обеспечивает полную реализацию стандарта SVID; она всего лишь открывает указанную функциональность SVID, существующую в glibc .
_BSD_SOURCE Функции BSD могут конфликтовать с другими функциями, и эти конфликты всегда разрешаются в пользу поведения, соответствующего стандарту System V, или, если определен или подразумевается любой макрос функций POSIX, X/Open или System V, единственным макросом, который включает поведение BSD является _ISOC99_SOURCE . (Точное определение этого макроса временами изменялось и может меняться в дальнейшем, поскольку он не регламентируется каким-либо стандартом.)
_GNU_SOURCE В случаях конфликта _GNU_SOURCE включает все, что возможно, отдавая предпочтение интерфейсам System V, а не BSD. Этот макрос также добавляет некоторые специальные для GNU и Linux интерфейсы, например, владение файлами.

Когда стандартного набора макросов недостаточно, обычно определяют макрос _GNU_SOURCE (включает все — самое простое решение), _XOPEN_SOURCE=600 (наиболее вероятно, что пригодится поднабор _GNU_SOURCE) или _ISOC99_SOURCE (использование функций наиболее позднего стандарта С, поднабор _XOPEN_SOURCE=600).

 

6.2. Интерфейсы POSIX

 

6.2.1. Обязательные типы POSIX

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

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

dev_t Арифметический тип данных, содержащий старшие (major) и младшие (minor) числа, соответствующие специальным файлам устройств, обычно расположенным в подкаталоге /dev . В Linux dev_t можно манипулировать с помощью макросов major() , minor() и makedev() , которые определены в <sys/sysmacros.h> . Обычно dev_t используется только в системном программировании, описанном в главе 11.
uid_t , gid_t Целочисленные типы, содержащие уникальные идентификаторы, соответственно, пользователя и группы. Удостоверения идентификаторов пользователя и группы рассматриваются в главе 10.
pid_t Целочисленный тип, обеспечивающий уникальное значение для системного процесса (описан в главе 10).
id_t Целочисленный тип, способный хранить без усечения любой тип pid_t , uid_t или gid_t .
off_t Целочисленный тип со знаком для измерения размера файла в байтах.
size_t Целочисленный тип без знака для измерения размеров объектов в памяти, например, символьных строк, массивов или буферов.
ssize_t Целочисленный тип со знаком для подсчета байтов (положительные значения) или хранения кода возврата ошибки (отрицательные значения).
time_t Целочисленный тип (во всех обычных системах) или тип с плавающей точкой (позволяет рассматривать VMS как операционную систему POSIX), выдающий время в секундах, как описано в главе 18.

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

Кроме того, в будущих версиях Linux представленные типы могут изменяться в рамках, установленных стандартом POSIX.

 

6.2.2. Раскрытие возможностей времени выполнения

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

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

#include

long sysconf (int);

Целочисленный аргумент в sysconf() — это один из наборов макросов с префиксом _SC_. Ниже перечислены макросы, которые используются чаще всего.

_SC_CLK_TCK Возвращает количество тактов в секунду внутренних часов ядра, различаемое программами. Следует отметить, что ядро может содержать одни или больше часов, работающих на более высокой частоте. _SC_CLK_TCK обеспечивает подсчет тактов, которые используются для получения информации из ядра, и этот макрос не является индикатором времени ожидания системы.
_SC_STREAM_MAX Возвращает максимальное количество стандартных потоков ввода-вывода С, которые могут быть одновременно открыты в системе.
_SC_ARG_MAX Возвращает максимальную длину аргумента командной строки и переменных окружения в байтах, которые используются любой из функций exec() . Если это ограничение превышено, exec() вернет ошибку Е2ВIG .
_SC_OPEN_MAX Возвращает максимальное количество файлов, которые одновременно могут быть открыты процессом; это то же самое, что и программное ограничение RLIMIT_NOFILE , которое может быть запрошено функцией getrlimit() и установлено функцией setrlimit() . Это единственное значение sysconf() , которое может изменяться во время выполнения программы; при вызове setrlimit() для изменения ограничения RLIMIT_NOFILE . _SC_OPEN_MAX также подчиняется новому программному ограничению.
_SC_PAGESIZE или _SC_PAGE_SIZE Возвращает размер одной страницы в байтах. В системах, которые могут поддерживать разные размеры страниц, возвращается размер одной обычной страницы, для которой выделено определенное количество памяти и которая считается естественным размером страниц для конкретной системы.
_SC_LINE_MAX Возвращает максимальную длину в байтах входной строки, обрабатываемой текстовыми утилитами, включая завершающий символ новой строки. Следует отметить, что во многих утилитах GNU, используемых в Linux-системах, фактически нет жестко закодированной максимальной длины строки, потому могут применяться входные строки произвольной длины. Однако переносимая программа не должна вызывать текстовые утилиты для строк, длина которых превышает _SC_LINE_MAX ; во многих Unix-системах утилиты работают с фиксированным максимальным размером строки, и его превышение может привести к неопределенным результатам.
_SC_NGROUPS_MAX Возвращает количество дополнительных групп (см. главу 10), которые может иметь процесс.

 

6.2.3. Поиск и настройка базовой системной информации

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

Системный вызов uname() позволяет программе обнаружить информацию времени ее выполнения.

#include

int uname(struct utsname* unameBuf);

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

Таблица 6.1. Члены структуры utsname

Член Описание
sysname Название операционной системы (в данном случае Linux ).
release Номер версии выполняющегося ядра. Это полная версия вроде 2.6.2 . Номер может быть легко изменен тем, кто выполнял сборку ядра, и вполне возможно, что цифр будет больше трех. Во многих версиях можно встретить дополнительную цифру для описания примененных исправлений, например, 2.4.17-23 .
version Под Linux здесь содержится временная метка, описывающая время, когда собиралось ядро.
machine Короткая строка, указывающая тип микропроцессора, на котором работает операционная система. Для Pentium Pro или более мощных она может быть i686 , для процессоров класса Alpha — alpha , а для 64-разрядных процессоров PowerPC — ррс64 .
nodename Имя хоста машины, которое обычно является первичным именем хоста в Internet.
domainname Домен NIS (или YP), которому принадлежит машина.

Член nodename (имя узла) часто называется системным именем хоста (то, что отображает команда hostname), однако его не следует путать с именем Internet-хоста. Несмотря на то что во многих системах эти члены не различаются, путать их не стоит. В системе с множеством Internet-адресов есть множество имен Internet-хостов, но только одно имя узла, поэтому эти имена не являются эквивалентными.

Более распространенная ситуация связана с домашними компьютерами, которые используют Internet-каналы широкополосной связи. Обычно их имя хоста в Internet выглядит вроде host127-56.raleigh.myisp.com, а имена Internet-хостов меняются каждый раз при отключении на длительное время от модема. Владельцы этих машин дают своим компьютерам имя узла, которое им больше нравится, например, loren или eleanor, что совершенно не относится к адресам Internet. При наличии множества машин, работающих на одном домашнем шлюзе, все они будут разделять один Internet-адрес (и одно имя Internet-хоста), но могут иметь имена вроде Linux.mynetwork.org и freebsd.mynetwork.org, которые все еще не являются именами Internet-хоста. В связи со всеми вышеперечисленными причинами, предполагать, что имя системного узла является допустимым именем Internet-хоста для машины не верно.

Имя узла системы устанавливается с помощью системного вызова sethostname(), и имя домена NIS (YP) — посредством системного вызова setdomainname().

#include

int sethostname(const char * name, size_t len);

int setdomainname(const char * name, size_t len);

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

 

6.3. Совместимость

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

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

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

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

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

 

Глава 7

Средства отладки использования памяти

 

Несмотря на то что С бесспорно является стандартным языком программирования в системах Linux, он имеет ряд особенностей, не дающих программистам возможности писать код, не содержащий тонких ошибок, которые впоследствии очень сложно отладить. Утечки памяти (когда память, выделенная с помощью malloc(), никогда не освобождается посредством free()) и переполнение буфера (например, запись за пределы массива) — наиболее распространенные и трудные для обнаружения программные ошибки. Недогрузка буфера (вроде записи перед началом массива) — менее распространенное, но обычно еще более тяжелое для отслеживания явление. В этой главе представлены несколько средств отладки, которые могут значительно упростить обнаружение и изоляцию упомянутых проблем.

 

7.1. Код, содержащий ошибки

 1: / * broken.с* /

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: char global[5];

 8:

 9: int broken(void){

10:  char *dyn;

11:  char local[5];

12:

13:  /* Для начала немного перезаписать буфер */

14:  dyn = malloc(5);

15:  strcpy(dyn, "12345");

16:  printf ("1: %s\n", dyn);

17:  free(dyn);

18:

19:  /* Теперь перезаписать буфер изрядно */

20:  dyn = malloc(5);

21:  strcpy(dyn, "12345678");

22:  printf("2: %s\n", dyn);

23:

24:  /* Пройти перед началом выделенного с помощью malloc локального буфера */

25:  * (dyn-1) ='\0';

26:  printf ("3: %s\n", dyn);

27:  /* обратите внимание, что указатель не освобожден! */

28:

29:  /* Теперь двинуться после переменной local */

30:  strcpy(local, "12345");

31:  printf ("4: %s\n", local);

32:  local[-1] = '\0';

33:  printf("5: %s\n", local);

34:

35:  /* Наконец, атаковать пространство данных global */

36:  strcpy(global, "12345");

37:  printf ("6: %s\n", global);

38:

39:  /* И записать поверх пространства перед буфером global */

40:  global[-1] = '\0';

41:  printf("7: %s\n", global);

42:

43:  return 0;

44: }

45:

46: int main (void) {

47:  return broken();

48: }

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

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

Ниже показан пример выполнения программы.

$ gcc -Wall -о broken broken.с

$ ./broken

1: 12345

2: 12345678

3: 12345678

4: 12345

5: 12345

6: 12345

7: 12345

 

7.2. Средства проверки памяти, входящие в состав

glibc

 

Библиотека GNU С (glibc) предлагает три простых средства проверки памяти. Первые два — mcheck() и MALLOC_CHECK_ — вызывают проверку на непротиворечивость структуры данных кучи, а третье средство — mtrace() — выдает трассировку распределения и освобождения памяти для дальнейшей обработки.

 

7.2.1. Поиск повреждений кучи

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

Если вы установили переменную окружения MALLOC_CHECK_, выбирается другой, несколько более медленный набор функций управления памятью. Этот набор более устойчив к ошибкам и может обнаруживать ситуации, когда free() вызывается более одного раза для одного и того же указателя, а также когда происходят однобайтные переполнения буфера. Если MALLOC_CHECK_ установлена в 0, функции управления памятью просто более устойчивы к ошибкам, но не выдают никаких предупреждений. Если MALLOC_CHECK_ установлена в 1, функции управления памятью выводят предупреждения о стандартных ошибках при замеченной проблеме. Если MALLOC_CHECK_ установлена в 2, функции управления памятью вызывают abort(), когда замечают проблемы.

Установка MALLOC_CHECK_ в 0 может оказаться полезной, если вам мешает найти ошибку в памяти другая ошибка, которую в этот момент исправить нет возможности; эта установка позволяет работать с другими средствами отслеживания ошибок памяти.

Установка MALLOC_CHECK_ в 1 полезна в случае, когда никаких проблем не видно, поэтому определенные уведомления могут помочь.

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

$ MALLOC_CHECK_=1 ./broken

malloc: using debugging hooks

malloc: используются отладочные функции

1: 12345

free(): invalid pointer 0x80ac008!

free(): недопустимый указатель 0x80ac008!

2: 12345678

3: 12345678

4: 12345

5: 12345

6: 12345

7: 12345

$ MALLOC_CHECK_=2 gdb ./broken

...

(gdb) run

Starting program: /usr/src/lad/code/broken

Запуск программы: /usr/src/lad/code/broken

1: 12345

Program received signal SIGABRT, Aborted.

Программа получила сигнал SIGABRT, прервана.

0x00 с 64 с 32 in _dl_sysinfo_int80() from/lib/ld-linux.so.2

(gdb) where

#0 0x00c64c32 in _dl_sysinfo_int80() from /lib/ld-linux.so.2

#1 0x00322969 in raise() from /lib/tls/libc.so.6

#2 0x00324322 in abort() from /lib/tls/libc.so.6

#3 0x0036d9af in free_check() from /lib/tls/libc.so.6

#4 0x0036afa5 in free() from /lib/tls/libc.so.6

#5 0x0804842b in broken() at broken.c:17

#6 0x08048520 in main() at broken.с:47

Другой способ заставить glibc проверить кучу на непротиворечивость — воспользоваться функцией mcheck():

typedef void(*mcheck Callback)(enummcheck_status status);

void mcheck(mcheck Callback cb) ;

В случае вызова функции mcheck(), функция malloc() размещает известные последовательности байтов перед и после возвращенной области памяти, чтобы можно было обнаружить переполнение или недогрузку буфера, free() ищет эти сигнатуры и, если они были повреждены, вызывает функцию, указанную аргументом cb. Если cb равен NULL, выполняется выход. Запуск программы, связанной с mcheck(), в gdb может показать, какие именно области памяти были повреждены, если только они явно освобождаются с помощью free(). Однако метод mcheck() не может точно определить место ошибки; лишь программист может вычислить это, основываясь на понимании логики работы программы.

Компоновка нашей тестовой программы с mcheck дает следующие результаты:

$ gcc -ggdb -о broken broken.с -lmcheck

$ ./broken

1: 12345

memory clobbered past end of allocated block

память разрушена после конца распределенного блока

Вследствие того, что mcheck всего лишь выдает сообщения об ошибках и завершает работу, найти ошибку невозможно. Для точного обнаружения ошибки потребуется запустить программу внутри gdb и заставить mcheck вызывать abort() при обнаружении ошибки. Можно просто вызвать mcheck() внутри gdb или поместить mcheck(1) в первой строке вашей программы (веред вызовом malloc()). (Следует отметить, что mcheck() можно вызвать в gdb без необходимости компоновки программы с библиотекой mcheck.)

$ rm -f broken; make broken

$ gdb broken

...

(gdb) break main

Breakpoint 1 at 0x80483f4: file broken.c, line 14.

Точка прерывания 1 по адресу 0x80483f4: файл broken, с, строка 14.

(gdb) command 1

Type commands for when Breakpoint 1 is hit, one per line.

End with a line saying just "end".

Наберите команды, которые выполнятся при достижении точки прерывания 1, по одной в строке.

Завершите строкой, содержащей только "end".

> call mcheck(&abort)

> continue

> end (gdb) run

Starting program: /usr/src/lad/code/broken

Запуск программы: /usr/src/lad/code/broken

Breakpoint 1, main () at broken.с: 14

47 return broken();

$1 = 0

1: 12345

Program received signal SIGABRT, Aborted.

Программа получила сигнал SIGABRT, прервана.

0x00e12c32 in _dl_sysinfo_int80() from /lib/ld-linux.so.2

(gdb) where

#00x00el2c32 in _dl_sysinfo_int80() from /lib/ld-linux.so.2

#1 0x0072c969 in raise() from /lib/tls/libc.so.6

#2 0x0072e322 in abort() from /lib/tls/libc.so.6

#3 0x007792c4 in freehook() from /lib/tls/libc.so.6

#4 0x00774fa5 in free() from /lib/tls/libc.so.6

#5 0x0804842b in broken() at broken.c:17

#6 0x08048520 in main() at broken.с:47

Важной частью этого кода является обнаруженная ошибка в строке 17 файла broken.с. Видно, что ошибка была обнаружена во время первого вызова free(), который указал на наличие проблемы в области памяти dyn. (freehook() представляет собой ловушку, с помощью которой mcheck выполняет проверки.)

Библиотека mcheck не может помочь в обнаружении переполнения или недогрузки буфера в локальных или глобальных переменных, а только в областях памяти, распределенных с помощью malloc().

 

7.2.2. Использование

mtrace()

для отслеживания распределений памяти

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

В отличие от mcheck(), в mtrace() нет соответствующей библиотеки для компоновки. Это не проблема, поскольку трассировку можно осуществлять в gdb. Однако для включения трассировки с помощью mtrace() должна быть установлена переменная окружения MALLOC_TRACE в допустимое имя файла; это может быть либо имя существующего файла, в который процесс может вести запись, либо имя нового файла, который процесс создаст и будет в него записывать.

$ MALLOC_TRACE=mtrace.log gdb broken

...

(gdb) breakmain

Breakpoint 1 at 0x80483f4: filebroken.c, line 14.

(gdb) command 1

Type commands for when Breakpoint 1 is hit, one per line.

End with a line saying just "end".

>call mtrace()

>continue

>end

(gdb) run

Starting program: /usr/src/lad/code/broken

Breakpoint 1, main() at broken.с:47

47 return broken();

$1 = 0

1: 12345

2: 12345678

3: 12345678

4: 12345

5: 12345

6: 12345

7: 12345

Program exited normally.

Программа завершена нормально.

 (gdb) quit

$ ls -l mtrace.log

-rw-rw-r-- 1 ewt ewt 220 Dec 27 23:41 mtrace.log

$ mtrace ./broken mtrace.log

Memory not freed:

He освобождена память:

----------------------

   Address   Size Caller

     Адрес Размер Место вызова

0x09211378    0x5 at /usr/src/lad/code/broken.с:20

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

 

7.3. Поиск утечек памяти с помощью

mpr

Возможности mtrace() в glibc достаточно неплохие, но профилировщик распределения памяти mpr (http://www3.telus.net/taj_khattra/mpr.html) в некоторых аспектах более прост в использовании и содержит более совершенные сценарии для обработки выходных журнальных файлов.

Первый шаг в применении mpr (после сборки кода с включенной отладочной информацией) состоит в установке переменной окружения MPRFI, которая указывает mpr, с какой командой связывать журнал (если переменная не установлена, журнал не генерируется). Для небольших программ MPRFI устанавливается подобно cat >mpr.log. Для программ покрупнее MPRFI можно существенно сэкономить пространство за счет сжатия журнального файла во время его записи, установив MPRFI в gzip -1 >mpr.log.gz.

Самый легкий способ — воспользоваться сценарием mpr для запуска программы; если MPRFI еще не установлена, она получит значение gzip -1 >log.%p.gz, что приведет к созданию журнального файла с идентификатором процесса отлаживаемой программы и загрузке библиотеки mpr. В результате сборка программы не понадобится. Ниже показан пример создания журнального файла для исправленной версии нашей тестовой программы.

$ MPRFI="cat >mpr.log" mpr ./broken

1: 12345

2: 12345678

3: 12345678

4: 12345

5: 12345

6: 12345

7: 12345

$ ls -l mpr.log

-rw-rw-r-- 1 ewt ewt 142 May 17 16:22 mpr.log

После создания журнального файла доступны многие средства для его анализа. Все эти программы получают журнальный файл mpr в качестве стандартного ввода. Если вывод из этих средств содержит числа в тех местах, где ожидаются имена функций (возможно, с предупреждением вроде "cannot map pc to name" ("невозможно отобразить программный счетчик на имя")), проблема может быть связана с версией утилиты awk, которую использует mpr. В документации mpr для достижения лучших результатов рекомендуется экспортировать переменную окружения MPRAWK для выбора mawk в качестве версии awk: export MPRAWK='mawk -W sprintf=4096'. Кроме того, сбить с толку mpr может еще и рандомизация стека, которая обеспечивается функциональностью ядра "Exec-shield"; исправить положение можно за счет использования команды setarch, отключающей Exec-shield во время работы исследуемой программы и во время работы фильтров mpr: setarch i386 mpr программа и setarch i386 mprmap ...

В конечном итоге для некоторых стековых фреймов mpr может не найти символическое имя; в этом случае просто проигнорируйте их.

mprmap программа Преобразует адреса программы в журнале mpr в имена функций и местоположения в исходном коде. В аргументе указывается имя исполняемого файла, для которого должен генерироваться журнал. Чтобы увидеть все распределения в программе вместе с цепочкой вызовов функций, которые осуществили эти распределения, можно использовать mprmap программа < mpr.log . По умолчанию отображаются имена функций. При указании флажка -f отображаются также имена файлов, а при указании -l — еще и номера строк внутри файлов. Флажок -l подразумевает наличие -f . Вывод этой программы является допустимым журнальным файлом mpr , который может быть связан каналом с любой другой утилитой mpr .
mprchain Преобразует журнал в вывод, сгруппированный по цепочкам вызовов. Цепочка вызовов функций — это список всех функций, активных в программе на определенный момент. Например, если main() вызывает getargs() , которая впоследствии вызывает parsearg() , активная цепочка вызовов во время работы parsearg() отображается как main:getargs:parsearg . Для каждой отдельной цепочки вызовов, в которой распределялась память во время выполнения программы, mprchain отображает количество распределений и общее количество распределенных байт.
mprleak Этот фильтр просматривает журнальный файл на предмет наличия всех неосвобожденных фрагментов памяти. В качестве стандартного вывода генерируется новый журнальный файл, содержащий только те распределения, которые могут привести к утечкам памяти. Вывод этой программы является допустимым журнальным файлом mpr , который может быть связан каналом с любой другой утилитой mpr .
mprsize Этот фильтр сортирует распределения памяти по размеру. Чтобы просмотреть утечки памяти по размеру, нужно передать вывод mprleak на вход mprsize .
mprhisto Отображает гистограмму распределений памяти.

Теперь, когда известно об анализаторах журнальных файлов, очень просто найти утечки памяти в нашей тестовой программе. Для этого достаточно воспользоваться командой mprleak mpr.log | mprmap -l ./broken (что эквивалентно mprmap -l ./broken mpr.log | mprleak) и в результате обнаружить утечку памяти в строке 20.

$ mprleak mpr.log | mpr map -l ./broken

m:broken(broken.c,20): main(broken.c,47):5:134518624

 

7.4. Обнаружение ошибок памяти с помощью Valgrind

Valgrind (http://valgrind.kde.org/) представляет собой специфический для Intel х86 инструмент, эмулирующий центральный процессор класса х86 для непосредственного наблюдения над всеми обращениями к памяти и анализа потока данных (он может, например, выявлять чтения неинициализированной памяти, тем не менее перенос содержимого неинициализированной ячейки в другую ячейку, которая никогда для читается, как неинициализированное чтение он не трактует). Valgrind обладает множеством других возможностей, включая просмотр использования кэша и поиск состязаний в многопоточных программах. В действительности, в Valgrind имеется универсальное средство для добавления большего количества возможностей, основанных на его эмуляторе центрального процессора. Однако для наших целей мы лишь кратко рассмотрим выполнение с его помощью агрессивного поиска ошибок памяти, что представляет его стандартное поведение.

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

$ valgrind ./broken

==30882== Memcheck, a.k.a. Valgrind, a Memory ERROR detector for x86-linux.

==30882== Copyright (C) 2002-2003, and GNU GPL'd, by Julian Seward.

==30882== Using valgrind-2.0.0, a program super vision framewok for x86-linux.

==30882== Copyright (C) 2000-2003, and GNU GPL'd, by Julian Seward.

==30882== Estimated CPU clock rate is 1547 MHz

==30882== For more details, rerun with: -v

==30882==

==30882== Invalid write of size 1

==30882== Недопустимая запись размером 1

==30882== at 0xC030DB: strcpy (mac_replace_strmem.с:174)

==30882== by 0x8048409: broken (broken.с:15)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: libc_start_main (in /lib/libc-2.3.2.so)

==30882== Address 0x650F029 is 0 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.с:153)

==30882== by 0x80483F3: broken (broken.с:14)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Conditional jump or move depends on uninitialised value(s)

==30882== Условный переход или перемещение зависит от

          неинициализироваиного значения(й)

==30882== at 0x863D8E: __GI_strlen (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf (in /lib/libc-2.3.2.so)

==30882== by 0x804841C: broken (broken.с:16)

==30882== by 0x804851F: main (broken.с:47)

1: 12345

==30882==

==30882== Invalid write of size 1

==30882== at 0xC030D0: strcpy (mac_replace_.с: 173)

==30882== by 0x804844D: broken (broken.с:21)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: _libc_start_main (in /lib/libc-2.3.2.so)

==30882== Address 0x650F061 is 0 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_ malloc.с:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid write of size 1

==30882== at 0xC030DB: strcpy (mac_replace_strmem.с:174)

==30882== by 0x804844D: broken (broken.с:21)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: libc_start_main (in /lib/libc-2.3.2.so)

==30882== Address 0x650F064 is 3 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.с:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid read of size 4

==30882== Недопустимое чтение размером 4

==30882== at 0x863D50: __GI_strlen (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf (in /lib/libc-2.3.2.so)

==30882== by 0x8048460: broken (broken.с:22)

==30882== by 0x804851F: main (broken.с:47)

==30882== Address 0x650F064 is 3 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.с:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid read of size 1

==30882== at 0x857A21: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.2.so)

==30882== by 0x835309: _IO_vfprintf_internal (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf(in /lib/libc-2.3.2.so)

==30882== by 0x8048460: broken (broken.с:22)

==30882== Address 0x650F063 is 2 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.с:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.c:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid read of size 1

==30882== at 0x857910: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.2.so)

==30882== by 0x835309: _IO_vfprintf_internal (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf (in /lib/libc-2.3.2.so)

==30882== by 0x8048460: broken (broken.с:22)

==30882== Address 0x650F061 is 0 bytes after a block of size 5'alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.с:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

2: 12345678

==30882==

==30882== Invalid write of size 1

==30882== at 0x8048468: broken (broken.с:25)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882== by 0x8048354: (within /usr/src/d/lad2/code/broken)

==30882== Address 0x650F05B is 1 bytes before a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.c:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid read of size 4

==30882== at 0x863D50: __GI_strlen (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf (in /lib/libc-2.3.2.so)

==30882== by 0x804847A: broken (broken.c:2 6)

==30882== by 0x804851F: main (broken.c:47)

==30882== Address 0x650F064 is 3 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.c:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.c:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid read of size 1

==30882== at 0x857A21: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.2.so)

==30882== by 0x835309: _IO_vfprintf_internal (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf (in /lib/libc-2.3.2.so)

==30882== by 0x804847A: broken (broken.c:2 6)

==30882== Address 0x650F063 is 2 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.c:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882==

==30882== Invalid read of size 1

==30882== at 0x857910: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.2.so)

==30882== by 0x835309: _IO_vfprintf_internal (in /lib/libc-2.3.2.so)

==30882== by 0x83BC31: _IO_printf (in /lib/libc-2.3.2.so) ==30882== by 0x804847A: broken (broken.c:2 6)

==30882== Address 0x650F061 is 0 bytes after a block of size 5 alloc'd

==30882== at 0xC0C28B: malloc (vg_replace_malloc.с:153)

==30882== by 0x8048437: broken (broken.с:20)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

3: 12345678

4: 12345

==30882==

==30882== Invalid write of size 1

==30882== at 0x80484A6; broken (broken.c:3 2)

==30882== by 0x804851F: main (broken.с:47)

==30882== by 0x802BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==30882== by 0x8048354: (within /usr/src/d/lad2/code/broken)

==30882== Address 0xBFF2D0FF is just below %esp. Possibly a bug in GCC/G++

==30882== v 2.96 or 3.0.X. To suppress, use: --workaround-gcc 296-bugs = yes

5: 12345

6: 12345

7: 12345

==30882==

==30882== ERROR SUMMARY: 22 ERRORS from 12 contexts (suppressed: 0 from 0)

==30882== malloc/free: in use at exit: 5 bytes in 1 blocks.

==30882== malloc/free: 2 allocs, 1 frees, 10 bytes allocated.

==30882== For a detailed leak analysis, rerun with: --leak-check=yes

==30882== For counts of detected ERRORS, rerun with: -v

==30882== ИТОГИ ПО ОШИБКАМ: 22 ошибки в 12 контекстах (подавлено: 0 из 0)

==30882== malloc/free: используются после завершения: 5 байт в 1 блоке.

==30882== malloc/free: 2 распределения, 1 освобождение, 10 байт распределено.

==30882== Для детального анализа утечек памяти запустите с: --leak-check=yes

==30882== Для подсчета обнаруженных ошибок запустите с: -v

Обратите внимание, что Valgrind нашел все, кроме глобального переполнения и недогрузки, и указал на ошибки более точно, нежели любое другое ранее описанное средство.

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

$ valgrind --leak-check=yes ./broken

...

==2292== searching for pointers to 1 not-freed blocks.

==2292== checked 5318724 bytes.

==2292== поиск указателей на 1 неосвобожденный блок.

==2292== проверено 5318724 байт.

==2292==

==2292== 5 bytes in 1 blocks are definitely lost in loss record 1 of 1

==2292== 5 байт в 1 блоке определенно потеряны в потерянной записи 1 из 1

==2292== at 0хЕС528В: malloc (vg_replace_malloc.с:153)

==2292== by 0x8048437: broken (broken.с:20)

==2292== by 0x804851F: main (broken.с:47)

==2292== by 0x126BAE: __libc_start_main (in /lib/libc-2.3.2.so)

==2292==

==2292== LEAK SUMMARY:

==2292== definitely lost: 5 bytes in 1 blocks.

==2292== possibly lost: 0 bytes in 0 blocks.

==2292== still reachable: 0 bytes in 0 blocks.

==2292== suppressed: 0 bytes in 0 blocks.

==2292== Reachable blocks (those to which a pointer was found) are not shown.

==2292== To see them, rerun with: --show-reachable=yes

==2292== ИТОГИ ПО УТЕЧКАМ:

==2292== определенно потеряно: 5 байт в 1 блоке.

==2292== возможно потеряно: 0 байт в 0 блоке.

==2292== пока достижимы: 0 байт в 0 блоке.

==2292== подавлено: 0 байт в 0 блоке.

==2292== Достижимые блоки (на которые найдены указатели) не показаны.

==2292== Чтобы увидеть их, запустите с: --show-reachable=yes

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

Valgrind могут слегка ввести в заблуждение программы, скомпилированные с высокими уровнями оптимизации. Если вы получаете отчет об ошибке памяти, которая не выглядит осмысленно, попробуйте перекомпилировать программу с -О вместо -O2 (или выше) и сгенерировать новый отчет.

 

7.5. Electric Fence

 

Следующее средство, которое мы рассмотрим — это Electric Fence (доступен на ftp://sunsite.unc.edu/pub/Linux/devel/lang/c и во многих дистрибутивах). Несмотря на то что Electric Fence не обнаруживает утечки памяти, он очень помогает в изоляции переполнений буфера. Каждый современный компьютер (включая все машины, работающие под Linux) обеспечивают аппаратную защиту памяти. Linux этим пользуется для изоляции программ друг от друга (например, сеанс vi не имеет доступа к памяти gcc) и для безопасного разделения кода между процессами, делая его только для чтения. Системный вызов mmap() в Linux (см. главу 13) позволяет процессу воспользоваться также аппаратной защитой памяти.

Electric Fence заменяет обычную функцию malloc() библиотеки С версией, которая распределяет запрошенную память и (обычно) непосредственно после запрошенной выделяет фрагмент памяти, доступ к которой процессу не разрешен. Если процесс попытается получить доступ к этой памяти, ядро немедленно остановит его с выдачей ошибки сегментации. За счет такого распределения памяти Electric Fence обеспечивает уничтожение программы, если та предпримет попытку чтения или записи за границей буфера, распределенного malloc(). Детальную информацию по использованию Electric Fence можно прочитать на его man-странице (man libefence).

 

7.5.1. Использование Electric Fence

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

$ ./broken

Electric Fence 2.2.0 Copyright (C) 1987 - 1999 Bruce Perens.

1: 12345

Segmentation fault (core dumped)

Ошибка сегментации (дамп ядра сброшен)

Хотя Electric Fence непосредственно не указывает на место, где произошла ошибка, проблема становится намного очевидней. Можно легко и точно определить проблемный участок, запустив программу под управлением отладчика, например gdb. Для того чтоб gdb смог точно указать на проблему, соберите программу с отладочной информацией, указав для gcc флажок -g, затем запустите gdb и зададите имя исполняемого файла, который нужно отладить. Когда программы уничтожается, gdb точно показывает, в какой строке произошел сбой.

Вот как выглядит описанная процедура.

$ gcc -ggdb -Wall -о broken broken.с -lefence

$ gdb broken

...

(gdb) run

Starting program: /usr/src/lad/code/broken

Electric Fence 2.2.0 Copyright (C) 1987 - 1999 Bruce Perens.

1: 12345

Program received signal SIGSEGV, Segmentation fault.

Программа получила сигнал SIGSEGV, ошибка сегментации.

0х007948с6 in strcpy() from /lib/tls/libc.so.6

(gdb) where

#0 0x007948c6 in strcpy() from /lib/tls/libc.so.6

#1 0x08048566 in broken() at broken.c:21

#2 0x08048638 in main() at broken.c:47

(gdb)

Благодаря Electric Fence и gdb, становится понятно, что в строке 21 файла broken.с имеется ошибка, связанная со вторым вызовом strcpy().

 

7.5.2. Выравнивание памяти

Хотя инструмент Electric Fence очень помог в обнаружении второй проблемы в коде, а именно — вызова strcpy(), переполнившего буфер, первое переполнение буфера найдено не было.

Проблему в этом случае нужно решать с помощью выравнивания памяти. Большинство современных компьютеров требуют, чтобы многобайтные объекты начинались с определенных смещений в оперативной памяти. Например, процессоры Alpha требуют, чтобы 8-байтовый тип — длинное целое (long) — начинался с адреса, кратного 8. Это значит, что длинное целое может располагаться по адресу 0x1000 или 0x1008, но не 0x1005.

На основе этих соглашений реализации malloc() обычно возвращают память, первый байт которой выровнен в соответствии с размером слова процессора (4 байта для 32-разрядных и 8 байтов на 64-разрядных процессоров). По умолчанию Electric Fence пытается эмулировать такое поведение, предлагая функцию malloc(), возвращающую только адреса, кратные sizeof(int).

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

В случае с нашей тестовой программой первый вызов malloc() распределил пять байт.

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

В связи с тем, что выравнивание malloc() обычно можно игнорировать, а выравнивание может способствовать незаметному переполнению буфера, Electric Fence предоставляет возможность управление выравниванием через переменную окружения ЕF_ALIGNMENT. Если эта переменная установлена, все результаты malloc() выравниваются в соответствии с ее значением. Например, если переменная установлена в значение 5, все результаты malloc() будут рассматриваться как кратные 5 (тем не менее, это значение не особенно полезно). Для отключения выравнивания памяти перед запуском программы установите ЕF_ALIGNMENT в 1. В среде Linux некорректно выровненный доступ в любом случае исправляются в ядре, несмотря на то, что в результате скорость выполнения программы может существенно снизиться. Программа будет функционировать корректно, если только в ней не присутствуют небольшие переполнения буфера.

Ниже приведен пример поведения тестовой программы, скомпонованной с Electric Fence, после установки ЕF_ALIGNMENT в 1.

$ export EF_ALIGNMENT=1

$ gdb broken

...

(gdb) run

Starting program: /usr/src/lad/code/broken

Electric Fence 2.2.0 Copyright (C) 1987 - 1999 Bruce Perens.

Program received signal SIGSEGV, Segmentation fault.

0x002a78c6 in strcpy() from /lib/tls/libc.so.6

(gdb) where

#0 0x002a78c6 in strcpy() from /lib/tls/libc.so.6

#1 0x08048522 in broken() at broken.c:15

#2 0x08048638 in main() at broken.с:47

На этот раз Electric Fence нашел переполнение буфера, которое произошло первым.

 

7.5.3. Другие средства

Electric Fence не только помогает обнаружить переполнение буфера, но и может найти недогрузку буфера (выполняя доступ к памяти, расположенной перед началом выделяемого malloc() буфера) и получает доступ к памяти, освобождаемой с помощью free(). Если переменная окружения EF_PROTECT_BELOW установлена в 1, Electric Fence перехватывает недогрузку буфера вместо его переполнения. Это происходит путем размещения недоступной области памяти непосредственно перед фактической областью памяти, возвращаемой функцией malloc(). При этом Electric Fence не сможет обнаружить переполнение буфера из-за страничной организации памяти, реализованной в большинстве процессоров. Выравнивание памяти может затруднить обнаружение переполнения буфера, однако оно не влияет на недогрузку буфера. Функция malloc() из Electric Fence всегда возвращает адрес памяти в начале страницы, которая всегда выровнена по границе слова.

Если EF_PROTECT_FREE установлена в 1, free() делает переданную ей область памяти недоступной, но не возвращает ее в пул свободной памяти. Если программа пытается получить доступ к этой памяти на любом этапе в будущем, ядро обнаружит несанкционированный доступ. Настройка EF_PROTECT_FREE помогает удостовериться, что код ни на одном этапе выполнения не использует память, освобожденную с помощью free().

 

7.5.4. Ограничения

Несмотря на то что Electric Fence выполняет неплохую работу по обнаружению переполнения буферов, выделенных malloc(), он не помогает отслеживать проблемы ни с глобальными, ни с локальными данными. Electric Fence также не обнаруживает утечки памяти, потому решать эту проблему придется другими средствами.

 

7.5.5. Потребление ресурсов

Хотя Electric Fence является мощным, легким в употреблении и быстрым инструментом (поскольку все проверки доступа осуществляются аппаратными средствами), за все это приходится платить свою цену. Большинство процессоров позволяют системе управлять доступом к памяти только в единицах, равных странице, за один раз. На процессорах Intel 80x86, например, каждая страница занимает 4096 байт. Вследствие того, что Electric Fence требует от malloc() установки двух разных областей памяти для каждого вызова (одна — позволяющая доступ, а другая — запрещающая), каждый вызов malloc() потребляет страницу памяти, или 4 Кбайт! Если в тестируемом коде распределяется множество небольших участков памяти, его компоновка с Electric Fence может легко увеличить потребление памяти программы на два или три порядка. При этом использование EF_PROTECT_FREE еще более усугубляет положение, поскольку память никогда не освобождается.

Для систем с большими относительно размера отлаживаемой программы объемами памяти при поиске источника определенной проблемы Electric Fence может действовать быстрее, чем Valgrind. Тем не менее, если для функционирования Electric Fence требуется организовать пространство для свопинга размером в 1 Гбайт, то Valgrind, вполне вероятно, окажется намного быстрее, даже несмотря на то, что он использует эмулятор, а не собственно центральный процессор.

 

Глава 8

Создание и использование библиотек

 

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

 

8.1. Статические библиотеки

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

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

ar res libname.a foo.o bar.о baz.o

Также в архив можно добавлять объектные файлы по одному.

ar res libname.a foo.o

ar res libname.a bar.о

ar res libname.a baz.o

В любом случае libname.a получится одинаковым. В команде использованы перечисленные ниже опции.

r Включает объектные файлы в библиотеку, заменяя любой уже существующий в архиве файл с таким же именем.
с Молча создает библиотеку, если таковой еще не существует.
s Поддерживает в таблице соответствие названий символов объектным файлам.

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

 

8.2. Совместно используемые библиотеки

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

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

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

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

• При обнаружении ошибки совместно используемую библиотеку можно заменить исправленной версией, а не компилировать повторно каждую программу, ее использующую.

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

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

Теперь стандартный формат двоичного файла практически на каждой платформе Linux представляет собой современный, расширяемый файловый формат ELF (Executable and Linking Format — формат исполняемых и компонуемых модулей), описанный в [24], ftp://tsx-11.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.g и ftp://tsx-11.mit.edu/pub/linux/packages/GCC/elf.ps.gz. Это значит, что практически на всех платформах Linux шаги, предпринимаемые для создания и использования разделяемых библиотек, совершенно одинаковы.

 

8.3. Разработка совместно используемых библиотек

 

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

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

 

8.3.1. Управление совместимостью

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

Например, разработчики и службы поддержки библиотеки С в Linux стараются поддерживать обратную совместимость для всех выпусков библиотеки С с одним и тем же старшим номером версии. Версия 5 библиотеки С прошла через пять небольших ревизий и, за некоторыми исключениями, программы, работающие с первой младшей версией, будут работать и с последней. (Исключения составляют неудачно написанные программы, основанные на неопределенном поведении библиотеки С или библиотеке С с ошибками, которые были исправлены в более новых версиях.) Ввиду того, что все библиотеки С версии 5 рассчитаны на обратную совместимость с предыдущими версиями, все они используют одно и то же имя soname — libc.so.5, относящееся к имени файла, в котором оно хранится — /lib/libc.so.5. m.r , где m — младший номер версии, a r — номер выпуска.

Приложения, которые компонуются с совместно используемой библиотекой, не компонуются непосредственно, например, с /lib/libc.so.6, даже если этот файл существует. Программа ldconfig, стандартная системная утилита, создает символическую ссылку /lib/libc.so.6 (soname) на /lib/libc-2.3.2.so, действительное имя библиотеки.

В результате упрощается модернизация совместно используемых библиотек. Для обновления версии 2.3.2 до 2.3.3 потребуется всего лишь скопировать новую версию libc-2.3.3.so в каталог /lib и запустить ldconfig. ldconfig просматривает все библиотеки с soname, равным libc.so.6, и создает символическую ссылку из soname на самую новую библиотеку, включающую это soname. Затем все приложения, скомпонованные с /lib/libc.so.6, автоматически используют новую библиотеку при последующих запусках, a /lib/libc-2.3.2.so можно смело удалить, поскольку потребность в ней полностью отпадает.

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

Итак, для компоновки с библиотекой С компоновщик находит /usr/lib/libc.so, указывающую на то, что нужно использовать /lib/libc.so.6, который является ссылкой на /lib/libc-2.3.2.so. Приложение компонуется с soname-именем libc-2.3.2.so — libc.so.6, и при запуске оно находит /lib/libc.so.6 и связывается с libc-2.3.2.so, поскольку libc.so.6 является символической ссылкой на libc-2.3.2.so.

 

8.3.2. Несовместимые библиотеки

Если новая версия библиотеки не должна быть совместимой с предшествующими ее версиями, ей потребуется присвоить другое имя soname. Например, для выпуска новой версии библиотеки С, не совместимой со старой версией, разработчики использовали soname libc.so.6 вместо libc.so.5. В результате делается акцент на несовместимости, а приложения, скомпонованные с разными версиями библиотеки, могут сосуществовать в одной системе. Приложения, скомпонованные с одной из версий libc.so.5, будут продолжать использовать последнюю версию библиотеки с soname libc.so.5, а приложения, скомпонованные с одной из версий libc.so.6, будут работать с последней версией библиотеки, соответствующей soname libc.so.6.

 

8.3.3. Разработка совместимых библиотек

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

1. Изменение или удаление интерфейсов экспортированных функций.

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

3. Изменение поведения функций, выходящее за пределы первоначальной спецификации.

Для поддержания совместимости версий библиотеки можно предпринимать следующие действия.

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

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

 

8.4. Сборка совместно используемых библиотек

Если вы разобрались с концепцией имен soname, все остальное просто. Достаточно следовать нескольким несложным правилам, которые перечислены ниже.

• Собирайте свой исходный код с указанием флага -fPIC для gcc. В результате сгенерируется независимый от места расположения код, который можно компоновать и загружать по любому адресу.

• Не используйте опцию компилятора -fomit-frame-pointer. Библиотеки по-прежнему будут работать, но отладчики станут бесполезными. Если в библиотеке будет найдена ошибка, пользователь не сможет осуществить обратную трассировку ошибки в коде.

• При компоновке библиотеки используйте gcc вместо ld. Компилятору С известно, как вызывать загрузчик для правильной компоновки, к тому же нет никакой гарантии, что интерфейс для ld останется неизменным.

• При компоновке библиотеки не забывайте предоставлять имя soname. Для этого используется специальная опция компилятора -Wl. Для сборки своей библиотеки используйте команду

gcc -shared -Wl, -soname, soname -о libname filelist liblist

где soname — имя soname, libname — имя библиотеки, включая полное имя версии, например, libc.so.5.3.12, filelist — список объектных файлов, которые нужно разместить в библиотеке, a liblist — список других библиотек, предоставляющих символы, к которым будет получать доступ эта библиотека. Последний элемент очень легко пропустить, поскольку без него библиотека будет работать в системе, в которой она создана, но может не работать в других ситуациях. Практически для любой библиотеки в список следует включать библиотеку С, поместив -lс в конце списка.

Чтобы создать файл libfоо.so.1.0.1 с soname-именем libfоо.so.1 из объектных файлов fоо.о и bar.о, используйте следующую команду:

gcc -shared -Wl,-soname,libfoo.so.1 -о libfoo.so.1.0.1 foo.o bar.о -lc

• He разбивайте на полосы библиотеку, если только не сталкиваетесь с окружением, где пространство ограничено. Разбитые на полосы библиотеки будут функционировать, но будут иметь такие же основные недостатки, что и библиотеки, собранные из объектных файлов, скомпилированных с -fomit-frame-pointer.

 

8.5. Инсталляция совместно используемых библиотек

 

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

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

2. Если нужно, чтоб компоновщик смог найти библиотеку без указания ее с помощью флажка -L библиотека , инсталлируйте библиотеку в /usr/lib или создайте символическую ссылку в /usr/lib по имени имя_библиотеки .so , которая указывает на файл совместно используемой библиотеки. Вы должны использовать относительную символическую ссылку (когда /usr/lib/libc.so указывает на ../../lib/libc.so.5.3.12), а не абсолютную (когда /usr/lib/libc.so указывает на /lib/libc.so.5.3.12).

3. Если нужно, чтобы компоновщик смог обнаружить библиотеку без ее инсталляции в системе (или до ее инсталляции), создайте ссылку имя_библиотеки .so в текущем каталоге. Затем используйте -L., чтоб указать gcc на поиск библиотек в текущем каталоге.

4. Если полный путь к каталогу, в который вы инсталлировали файл совместно используемой библиотеки, не перечислен в /etc/ld.so.conf, добавьте его в этот файл, указав в отдельной строке.

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

Создавать записи в /etc/ld.so.conf и запускать ldconfig нужно только тогда, когда библиотеки инсталлируются в качестве системных.

 

8.5.1. Пример

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

1: /* libhello.c */

2:

3: #include

4:

5: void print_hello (void) {

6:  printf("Добро пожаловать в библиотеку!\n");

7: }

Разумеется, необходима программа, которая использует библиотеку libhello.

1: / * usehello.c * /

2:

3: #include "libhello.h"

4:

5: int main(void) {

6:  print_hello();

7:  return 0;

8: }

Содержимое libhello.h оставлено в качестве упражнения для самостоятельной проработки. Для того чтобы скомпилировать и воспользоваться этой библиотекой без ее инсталляции в системе, выполните перечисленные ниже шаги.

1. С использованием флажка -fPIC соберите объектный файл совместно используемой библиотеки:

gcc -fPIC -Wall -g -с libhello.c

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

gcc -g -shared -Wl, -soname,libhello.so.0 -о libhello.so.0.0 libhello.о -lc

3. Создайте ссылку из soname на библиотеку:

ln -sf libhello.so.0.0 libhello.so.0

4. Создайте ссылку для использования компоновщиком при компиляции приложений с опцией -lhello:

ln -sf libhello.so.0 libhello.so

5. С помощью флажка -L. укажите компоновщику на необходимость поиска библиотек в текущем каталоге, а с помощью -lhello определите, с какой библиотекой выполнять компоновку:

gcc -Wall -g -с usehello.c -о usehello.o

gcc -g -о usehello usehello.o -L. -lhello

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

6. Теперь запустите usehello:

LD_LIBRARY_PATH=$(pwd) ./usehello

Переменная окружения LD_LIBRARY_PATH указывает системе места, где следует искать библиотеки (более детальная информация представлена в следующем разделе). Конечно, по желанию можно установить libhello.so.* в /usr/lib и избежать настройки переменной окружения LD_LIBRARY_PATH.

 

8.6. Работа с совместно используемыми библиотеками

 

Самый легкий способ работы с совместно используемыми библиотеками — игнорировать тот факт, что она совместная. Компилятор С автоматически задействует совместно используемые библиотеки вместо статических, если ему явно не указано обратное.

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

 

8.6.1. Использование деинсталлированных библиотек

После запуска программы динамический загрузчик обычно ищет необходимые программе библиотеки в кэше (/etc/ld.so.cache, созданном ldconfig) библиотек, которые находятся в каталогах, записанных в /etc/ld.so.conf. Однако если установлена переменная окружения LD_LIBRARY_PATH, поиск осуществляется сначала в каталогах, перечисленных в ней. Это значит, что если вы хотите использовать измененную версию библиотеки С при работе с определенной программой, эту библиотеку можно поместить в любой каталог и соответствующим образом изменить LD_LIBRARY_PATH. Например, некоторые версии браузера Netscape, скомпонованные с версией 5.2.18 библиотеки С, не будут работать вследствие ошибки сегментации при запуске со стандартной библиотекой С 5.3.12. Это происходит из-за более строгой политики malloc(). Многие помещают копию библиотеки С 5.2.18 в отдельный каталог, например, /usr/local/netscape/lib/, переносят туда исполняемый файл браузера Netscape и заменяют /usr/local/bin/netscape сценарием оболочки, который выглядит примерно так:

#!/bin/sh

export LD_LIBRARY_PATH=/usr/local/netscape/lib:$LD_LIBRARY_PATH

exec /usr/local/netscape/lib/netscape $*

 

8.6.2. Предварительная загрузка библиотек

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

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

LD_PRELOAD=/lib/libsomething.o exec /bin/someprogram $*

Кроме того, как и с zlibc, может возникнуть потребность предварительно загрузить библиотеку для всех программ в системе. Самый простой способ для этого — добавить в файл /etc/ld.so.preload строку, которая указывает библиотеку, подлежащую загрузке. Для случая zlibc строка будет выглядеть следующим образом:

/lib/uncompress.о

 

Глава 9

Системное окружение Linux

 

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

 

9.1. Окружение процесса

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

EDITOR или VISUAL При установке EDITOR или VISUAL у пользователя появляется возможность выбирать текстовый редактор для редактирования текстового файла. Использование двух различных переменных объясняется тем, что когда-то EDITOR применялась для телетайпной машины, a VISUAL  — для полноэкранного терминала.
LD_LIBRARY_PATH Обеспечивает разделенные двоеточиями пути к каталогам, в которых следует искать библиотеки. Обычно эту переменную устанавливать не нужно, поскольку в системном файле /etc/ld.so.conf есть вся необходимая информация. Модифицировать его в своих программах вряд ли придется; эта переменная предоставляет информацию для системного компоновщика времени выполнения, ld.so . Однако, как было описано в главе 8, LD_LIBRARY_PATH может оказаться полезной при разработке совместно используемых библиотек.
LD_PRELOAD Перечисляет библиотеки, которые должны быть загружены для переопределения символов в системных библиотеках. LD_PRELOAD , как и LD_LIBRARY_PATH , более подробно описана в главе 8.
PATH Предоставляет разделенный двоеточиями путь к каталогам, где следует искать исполняемые программы для запуска. Следует отметить, что в Linux (как и во всех вариантах Unix), в отличие от некоторых операционных систем, не принят автоматический поиск исполняемого файла в текущем каталоге. Для этого путь должен включать каталог . (точка). Более подробно о работе с этой переменной рассказывается в главе 10.
TERM Предоставляет информацию о типе терминала, установленного у пользователя; это определяет способ позиционирования символов на экране. Более подробно об этом читайте в главах 24 и 21.

 

9.2. Системные вызовы

 

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

При разработку программы написанный код работает в режиме пользователя (user mode). Драйверы устройств и файловые системы, наоборот, работают в режиме ядра (kernel mode). В пользовательском режиме программы тщательно защищены от повреждений, вызванных их взаимодействием друг с другом или остальной частью системы. Код, работающий в режиме ядра, имеет полный доступ к компьютеру и может делать или же разрушать все, что угодно.

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

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

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

 

9.2.1. Ограничения системных вызовов

Режим ядра защищен от влияния режима пользователя. Одна из таких защит состоит в том, что тип данных, передаваемых между режимами ядра и пользователя, ограничен, легко верифицируется и следует строгим соглашениям.

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

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

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

 

9.2.2. Коды возврата системных вызов

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

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

perror()

Печатает сообщение об ошибке. Передайте в функцию строку с информацией о том, что код намеревался предпринять.

if ((file = open(DB_PATH, O_RDONLY)) < 0) {

 perror("не удается открыть файл базы данных");

}

Функция perror() выведет сообщение, описывающее возникшую ошибку, а также объяснение того, что код собирался делать:

не удается открыть файл базы данных: No such file or directory

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

strerror()

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

if ((file = open(DB_PATH, O_RDONLY) ) < 0) {

 fprintf(stderr,

  "не удается открыть файл базы данных %s, %s\n",

  DB_PATH, strerror(errno));

}

sys_errlist

He очень хорошая альтернатива strerror(). sys_errlist — это массив размером sysnerr указателей на статические, доступные только для чтения символьные строки, которые описывают ошибки. Попытка записи в эти строки приводит к нарушению сегментации и сбросу дампа ядра.

if ((file = open(DB_PATH, O_RDONLY)) < 0) {

 if (errno < sys_nerr) {

  fprintf(stderr,

   "не удается открыть файл базы данных %s, %s\n",

   DB_PATH, sys_errlist[errno]);

 }

}

Этот массив не является ни стандартным, ни переносимым, и упоминается здесь лишь потому, что вы можете столкнуться с кодом, от него зависящим. Заменив такой код вызовом функции strerror(), вы получите существенный выигрыш.

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

 

9.2.3. Использование системных вызовов

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

Большинство, но не все, системные вызовы объявлены в . Фактически файл представляет собой универсальное вместилище практически для всех системных вызовов. Чтобы определить, какие включаемые файлы нужно использовать, обычно нужно обратиться к системным man-страницам. Хотя описания функций на man-страницах зачастую весьма лаконичны, там можно найти точные указания о том, какой файл должен быть включен для использования функции.

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

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

К счастью, существует другой способ решения такой проблемы. Многие версии программы man, включая используемую в системах Linux, позволяют указывать альтернативный путь поиска man-страниц. Прочтите man-страницу о самой программе man, чтобы определить, поддерживается ли в вашей версии man переменная окружения MANSECT и аргумент -S. Если переменная поддерживается, можно установить MANSECT в что-нибудь вроде 3:2:1:4:5:6:7:8:tcl:n:l:p:о. Просмотрите в файле конфигурации man (в большинстве систем Linux это /etc/man.config) текущую настройку MANSECT.

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

if (ioctl(fd, FN, data)) {

 /* обработка ошибки на основе errno */

}

Часто встречается следующая форма:

if (ioctl(fd, FN, data) < 0) {

 /* обработка ошибки на основе errno */

}

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

 

9.2.4. Общие коды возврата ошибок

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

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

Для определения того, какую ошибку можно ожидать от определенного системного вызова, обращайтесь к соответствующим man-страницам. В частности, с помощью команды man 3 errno можно получить список кодов ошибок, определенных POSIX. Тем не менее, ситуация часто изменяется, и man-страницы не всегда отвечают существующему состоянию дел. Если системный вызов возвращает неожиданный код ошибки, можно предположить, что скорее man-страница устарела, а не системный вызов дал сбой. Исходный код Linux поддерживается более тщательно, чем документация.

E2BIG Список аргументов слишком длинный. При запуске нового процесса с помощью exec() существует ограничение на длину задаваемого списка аргументов. См. главу 10.
EACCESS В доступе будет отказано. Эта ошибка возвращается системным вызовом access() , рассматриваемым в главе 11, и представляет собой более информативный код возврата, чем само состояние ошибки.
EAGAIN Возвращается при попытке выполнения неблокируемого ввода-вывода, если нет доступных данных. EWOULDBLOCK является синонимом EAGAIN . При блокируемом вводе-выводе системный вызов установил бы блокировку и ожидал бы данных.
EBADF Неправильный номер файла. Был передан номер файла, не ссылающийся на открытый файл, в функцию read() , close() , ioctl() или другой системный вызов, принимающий номер файла в качестве аргумента.
EBUSY Системный вызов mount() возвращает эту ошибку при попытке смонтировать файловую систему, которая уже смонтирована, или размонтировать файловую систему, которая в настоящий момент используется.
ECHILD Дочерние процессы отсутствуют. Возвращается семейством системных вызовов wait() . См. главу 10.
EDOM Это ошибка не системного вызова, а ошибка из библиотеки С системы. EDOM устанавливается математическими функциями, если аргумент выходит за пределы допустимого диапазона. (Это EINVAL для области функции.) Например, функция sqrt() не работает с комплексными числами и потому не принимает отрицательные аргументы.
EEXIST Возвращается creat() , mknod() или mkdir() , если файл уже существует, или функцией open() в том же случае, если указаны флаги O_CREAT и O_EXCL .
EFAULT Неверный указатель (указывающий на недоступную область памяти) был передан в качестве аргумента системному вызову. Обращение по этому указателю из пользовательской программы, которая произвела системный вызов, приведет к ошибке сегментации.
EFBIG Возвращается write() при попытке записи файла, который длиннее, чем может логически обработать файловая система (физические ограничения пространства во внимание не принимаются).
EINTR Системный вызов был прерван. Прерываемые системные вызовы рассматриваются в главе 12.
EINVAL Возвращается, если системный вызов получил недопустимый аргумент.
EIO Ошибка ввода-вывода. Обычно генерируется драйвером устройства для обозначения ошибки в оборудовании или неисправимой ошибку взаимодействия с устройством.
EISDIR Возвращается системными вызовами, требующими имя файла, например unlink() , если последний компонент в имени пути является каталогом, а не файлом, а данная операция не может быть применена к каталогу.
ELOOP Возвращается системными вызовами, которые принимают путь, если при разборе пути встречается слишком много символических ссылок в строке (то есть символические ссылки, указывающие на символические ссылки, которые, в свою очередь, указывают на символические ссылки и так далее). Текущее ограничение — 16 символических ссылок на строку.
EMFILE Возвращается, если для вызываемого процесса нельзя открыть больше файлов.
EMLINK Возвращается link() , если в компонуемом файле уже содержится максимальное количество ссылок для файловой системы (в стандартной файловой системе Linux этот максимум составляет 32 000).
ENAMETOOLONG Имя пути слишком длинное либо для системы, либо для файловой системы, к которой вы пытаетесь получить доступ.
ENFILE Возвращается, если ни один процесс системы не может открыть больше ни одного файла.
ENODEV Возвращается mount() , если запрошенный тип файловой системы не доступен. Возвращается open() при попытке открыть специальный файл для устройства, для которого нет ассоциированного драйвера в ядре.
ENOENT Файл или каталог не существует. Возвращается при попытке получить доступ к несуществующему файлу или каталогу.
ENOEXEC Ошибка исполняемого формата. Может появиться при попытке запустить (устаревший) а.out в системе, в которой отсутствует поддержка бинарных файлов а.out . Может также встречаться при попытке запуска бинарного файла формата ELF, собранного для другой архитектуры центрального процессора.
ENOMEM Не хватает памяти. Возвращается функциями brk() и mmap() при неудачной попытке распределения памяти.
ENOSPC Возвращается write() при попытке записать файл длиннее, чем объем свободного пространства в файловой системе.
NOSYS Системный вызов не реализован. Обычно происходит при запуске нового исполняемого файла на старом ядре, которое не поддерживает системный вызов.
ENOTBLK Системный вызов mount() возвращает эту ошибку при попытке смонтировать в качестве файловой системы файл, не являющийся специальным файлом блочного устройства.
ENOTDIR Промежуточный компонент пути существует, но не является каталогом. Возвращается любым системным вызовом, принимающим имя файла.
ENOTEMPTY Возвращается rmdir() , если удаляемый каталог не пуст.
ENOTTY Обычно встречается, когда приложение, которое пытается обратиться к терминалу, запущено с перенаправлением ввода или вывода в канал. Но также может встречаться при попытке совершить операцию ввода-вывода на неправильном типе устройства. Стандартное сообщение об ошибке в этом случае, "not a typewriter", может сбить с толку.
ENXIO Нет такого устройства или адреса. Обычно генерируется при попытке открыть специальный файл устройства, который ассоциируется с частью не установленного или не настроенного оборудования.
EPERM У процесса недостаточно полномочий для завершения операции. Эта ошибка обычно встречается в файловых операциях. См. главу 11.
EPIPE Возвращается write() , если читающая сторона канала или сокета закрыта и захвачен или проигнорирован сигнал SIGPIPE . См. главу 12.
ERANGE Не являясь ошибкой системного вызова, ERANGE устанавливается математическими функциями, если результат невозможно представить возвращаемым типом. Эта ошибка может также возникать в других функциях, если им передается слишком короткий буфер для возвращаемой строки. (Для диапазона этой ошибке соответствует EINVAL .)
EROFS Возвращается write() при попытке записать в файловую систему, доступную только для чтения.
ESPIPE Возвращается lseek() при навигации по файлу, дескриптор которого не поддерживает навигацию (включая файловые дескрипторы для каналов, именованных каналов и сокетов). См. главы 11 и 17.
ESRCH Нет такого процесса. См. главу 10.
ETXTBSY Возвращается open() при попытке открыть на запись запущенный исполняемый файл или совместно используемую библиотеку или любой другой файл, отображенный на память с установленным флажком MAP_DENYWRITE (см. главу 13). Чтобы избежать такого поведения, необходимо переименовать файл, сделать новую копию с таким же именем, как у старого файла, и работать с этой новой копией. См. главу 11 с обсуждением того, почему так происходит.
EXDEV Возвращается link() , если исходные и целевые файлы находятся в разных файловых системах.

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

 

9.3. Поиск заголовочных и библиотечных файлов

Заголовочные файлы в системе Linux хранятся в иерархии каталогов /usr/include. Именно там по умолчанию компилятор ищет включаемые файлы. (Заголовочные файлы могут храниться за пределами /usr/include, но тогда на них имеются ссылки внутри /usr/include. Например, на момент написания книги включаемые файлы системы X были расположены в /usr/X11R6/include/X11, но благодаря символической ссылке компилятор мог найти их через /usr/include/X11.)

С библиотеками дело обстоит практически так же, правда, с некоторыми нюансами. Библиотеки, которые считаются важными для загрузки системы (и ее отладки в случае необходимости), расположены в /lib. Другие системные библиотеки находятся в /usr/lib, кроме библиотек X11R6, которые хранятся в /usr/X11R6/lib. Компилятор по умолчанию будет искать стандартные системные библиотеки.

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