|
||||||||||||
|
Глава 10Отладка По утверждению Software Engineering Institute (Институт программных разработок) и IEEE (Institute of Electrical and Electronics Engineers, Институт инженеров по электротехнике и электронике) в любом значимом фрагменте программного обеспечения первоначально всегда есть дефекты, примерно два на 100 строк программного кода. Эти ошибки приводят к тому, что программы и библиотеки не работают так, как требуется, часто заставляя программу вести себя иначе, чем предполагалось. Отслеживание ошибок, их идентификация и удаление могут потребовать от программиста больших затрат времени на этапе разработки. В этой главе мы рассмотрим недочеты программного обеспечения и некоторые средства и методы исследования характерных примеров ошибочного поведения. Это не то же самое, что тестирование (задача проверки работы программы во всех возможных условиях или обстоятельствах), хотя тестирование и отладка конечно же взаимосвязаны, и многие ошибки обнаруживаются в процессе тестирования. Будут обсуждаться следующие темы: □ типы ошибок; □ общие методы отладки; □ отладка с помощью gdb и других средств; □ проверка соблюдения условий (макрос assert); □ устранение ошибок использования памяти. Типы ошибокОшибка, как правило, возникает по одной из нескольких причин, каждая из которых предполагает конкретный метод выявления и устранения. □ Ошибки описания или спецификации. Если программа неверно определена, она, несомненно, не сможет выполняться, как требуется. Даже лучший программист в мире может порой написать неверную программу. Прежде чем приступить к программированию (или разработке), убедитесь в том, что вы точно знаете и четко представляете, что должна делать программа. Вы обнаружите и устраните множество ошибок спецификации (если не все), обсуждая требования и получая подтверждение их правильности у тех, кто будет применять вашу программу в дальнейшем. □ Ошибки проектирования или разработки. Перед созданием программы любого размера должны прорабатываться. Как правило, недостаточно просто сесть к клавиатуре компьютера, непосредственно набрать программный код и ждать, что программа сразу заработает. Нужно время, чтобы подумать о том, как написать программу, какие структуры данных потребуются и как они будут использоваться. Постарайтесь заранее разработать все в деталях, это убережет вас от многочисленных переработок программы в дальнейшем. □ Ошибки кодирования. Конечно, все делают ошибки при наборе. Создание программного кода из вашей разработки — неидеальный процесс. Именно здесь появляется много ошибок. Когда вы сталкиваетесь с ошибкой в программе, не упускайте возможности еще раз прочесть ваш исходный код или попросите об этом кого-нибудь. Просто поразительно, как много ошибок и недочетов можно обнаружить и устранить, обсуждая реализацию с кем-нибудь еще. Примечание □ Попытайтесь выполнить основную часть программы на бумаге, этот процесс называют формальным прогоном. Для наиболее важных подпрограмм запишите значения на входе и вычислите шаг за шагом выходные значения. Для отладки совсем не обязательно всегда применять компьютер, иногда именно компьютер создает проблемы. Даже разработчики, пишущие библиотеки, компиляторы и операционные системы, делают ошибки! С другой стороны, не спешите винить во всем используемые программные средства; гораздо вероятнее, что ошибка закралась в вашу новую программу, а не в компилятор. Общие методы отладкиСуществует несколько разных подходов к отладке и тестированию типовой программы Linux. Обычно разработчик запускает программу и смотрит, что происходит. Если программа не работает, необходимо решить, что с ней делать. Можно изменить программу и попробовать снова (анализ программного кода, метод проб и ошибок), можно попытаться получить больше информации о том, что происходит внутри программы (оснащение контрольными средствами) или можно непосредственно проанализировать работу программы (контролируемое выполнение). Отладка включает в себя пять следующих этапов: □ тестирование — поиск существующих изъянов или ошибок; □ стабилизация — обеспечение повторяемости ошибок; □ локализация — определение строки кода, отвечающей за ошибку; □ корректировка — исправление программного кода; □ проверка — подтверждение того, что исправление работает. Программа с ошибкамиДавайте рассмотрим пример программы, содержащей ошибки. Читая данную главу, вы будете пробовать отладить эту программу. Она написана во время разработки большой программной системы. Ее задача — протестировать единственную функцию sort, которая предназначена для реализации сортировки массива структур типа itemметодом "пузырька". Элементы сортируются по возрастанию поля key. Программа вызывает функцию sortдля сортировки контрольного примера, чтобы протестировать функцию. В реальной жизни вы никогда не стали бы обращаться к этому конкретному алгоритму из-за его очень низкой эффективности. Мы же применяем его, потому что он короткий, относительно простой и его легко превратить в неправильный. На самом деле в стандартной библиотеке языка С есть функция с именем qsort, выполняющая эту задачу. К сожалению, исходный код программы нелегко читается, в нем нет комментариев, и автор уже недоступен. Вам придется биться с ней самостоятельно, начиная с основной подпрограммы debug1.c. /* 1 */ typedef struct { /* 2 */ char *data; /* 3 */ int key; /* 4 */ } item; /* 5 */ /* 6 */ item array[] = { /* 7 */ {"bill", 3}, /* 8 */ {"neil", 4}, /* 9 */ {"john", 2}, /* 10 */ {"rick", 5}, /* 11 */ {"alex", 1}, /* 12 */ }; /* 13 */ /* 14 */ sort(a, n) /* 15 */ item *a; /* 16 */ { /* 17 */ int i = 0, j = 0; /* 18 */ int s = 1; /* 19 */ /* 20 */ for(; i < n && s != 0; i++) { /* 21 */ s = 0; /* 22 */ for(j = 0; j < n; j++) { /* 23 */ if(a[j].key > a[j + 1].key) { /* 24 */ item t = a[j]; /* 25 */ a[j] = a[j+1]; /* 26 */ a[j+1] = t; /* 27 */ s++; /* 28 */ } /* 29 */ } /* 30 */ n--; /* 31 */ } /* 32 */ } /* 33 */ /* 34 */ main() /* 35 */ { /* 36 */ sort(array,5); /* 37 */ } Теперь попытайтесь откомпилировать эту программу: $ сс -о debug1 debug1.с Она компилируется успешно без каких-либо сообщений об ошибках или предупреждений. Прежде чем выполнять эту программу, вставьте фрагмент кода для вывода результата. В противном случае вы не будете знать, отработала ли программа. Вы добавите несколько дополнительных строк для отображения массива после сортировки. Назовите новую версию debug2.c. /* 33 */ #include <stdio.h> /* 34 */ main() /* 35 */ { /* 36 */ int i; /* 37 */ sort(array, 5); /* 38 */ for(i = 0; i < 5; i++) /* 39 */ printf("array[3d] = (%s, %d)\n", /* 40 */ i, array[i].data, array[i].key); /* 41 */ } Этот дополнительный код, строго говоря, не является частью, позже добавленной программистом. Мы заставили вас добавить его только для тестирования программы. Следует быть очень внимательным, чтобы не внести новых ошибок в ваш тестовый код. Теперь снова откомпилируйте программу и на этот раз выполните ее: $ cc -о debug2 debug2.с $ ./debug2 Что произойдет, когда вы сделаете это, зависит от вашей версии Linux (или UNIX) и особенностей ее установки. В своих системах мы получили следующий результат: array[0] = {john, 2} array[1] = {alex, 1} array[2] = {(null), -1} array[3] = {bill, 3} array[4] = {neil, 4} В еще одной системе (запускающей другое ядро Linux) мы получили следующий вывод: Segmentation fault В вашей системе Linux вы увидите один из приведенных результатов или совсем другой. Мы рассчитывали получить приведенный далее вывод: array[0] = {alex, 1} array[1] = {john, 2} array[2] = {bill, 3} array[3] = {neil, 4} array[4] = {rick, 5} Ясно, что в данном программном коде есть серьезная ошибка. Он не выполняет сортировку корректно, если вообще работает, а если он завершается с ошибкой сегментации, то операционная система посылает сигнал программе, сообщая о том, что обнаружен несанкционированный доступ к памяти, и преждевременно завершает программу, чтобы не испортить данные в оперативной памяти. Способность операционной системы обнаружить несанкционированный доступ к памяти зависит от настройки оборудования и некоторых тонкостей реализации системы управления памятью. В большинстве систем объем памяти, выделяемый программе операционной системой, больше реально используемого. Если несанкционированный доступ осуществляется к этому участку памяти, оборудование может не выявить несанкционированный доступ. Вот почему не все версии Linux и UNIX сгенерируют сигнал о нарушении сегментации. Примечание Когда вы исследуете проблемы доступа к элементам массива, часто полезно увеличить размер этих элементов, поскольку это увеличит размер ошибки. Если вы читаете единственный байт за пределами массива байтов, это может вам сойти с рук, т.к. память, выделенная программе, будет округляться до величины, зависящей от операционной системы, возможно, равной 8 Кбайт. Если вы увеличите размер элемента массива, заменив элемент типа itemмассивом из 4096 символов, любое обращение к несуществующему элементу массива, возможно, окажется за пределами выделенной памяти. Каждый элемент массива равен 4 Кбайт, поэтому некорректно используемый участок памяти будет находиться за концом массива на расстоянии от 0 до 4 Кбайт. Если мы внесем эту поправку, назвав результат debug3.c, то получим ошибку сегментации в версиях Linux обоих авторов. /* 2 */ char data[4096]; $ сс -о debug3 debug3.с $ ./debug3 Segmentation fault Возможно, что какие-то варианты систем Linux или UNIX все еще не будут выдавать сообщение об ошибке сегментации. Когда стандарт ANSI С утверждает, что поведение не определено, на самом деле он разрешает программе делать все, что угодно. Это выглядит так, как будто мы написали не удовлетворяющую стандартам программу на языке С, и она может демонстрировать очень странное поведение! Как видите, изъян в программе переводит ее в категорию программ с непредсказуемым поведением. Анализ кодаКак мы упоминали ранее, часто, если программа не работает, как ожидалось, неплохо перечитать ее. Предположим, что мы просмотрели программный код примера этой главы и исправили в нем все очевидные ошибки. Примечание Существуют средства, которые могут помочь в анализе кода, одно из самых очевидных — компилятор. Он сообщит вам о любых имеющихся в вашей программе синтаксических ошибках. Примечание Чуть позже мы кратко обсудим и другие средства, lintи splint. Как и компилятор, они анализируют код и сообщают о фрагментах кода, которые могут быть некорректными. Оснащение средствами контроляОснащение средствами контроля — это вставка в программу кода для сбора дополнительной информации о поведении программы во время ее выполнения. Очень популярна вставка вызовов функции printfдля вывода значений переменных на разных стадиях выполнения программы. Вы можете с пользой для себя добавить несколько вызовов printf, но должны знать о том, что этот процесс повлечет за собой дополнительные редактирование и компиляцию при любом изменении программы и, конечно, вам придется удалить код, когда ошибки будут исправлены. Здесь могут помочь два метода оснащения средствами контроля. Первый использует препроцессор языка С для выборочного включения кода средств контроля так, что вам нужно только перекомпилировать программу для вставки или удаления отладочного кода. Сделать это можно очень просто, с помощью конструкций, подобных приведенным далее: #ifdef DEBUG printf("variable x has value = %d\n", x); #endif Вы можете компилировать программу с флагом компилятора -DDEBUGдля определения символического имени DEBUGи включения дополнительного кода и без этого флага — для удаления отладочного кода. Можно создать и более сложный вариант использования пронумерованных отладочных макросов: #define BASIC_DEBUG 1 #define EXTRA_DEBUG 2 #define SUPER_DEBUG 4 #if (DEBUG & EXTRA_DEBUG) printf... #endif В этом случае вы всегда должны определять макрос DEBUG, но можете настраивать объем отладочной информации или уровень детализации. Флаг компилятора -DDEBUG=5в нашем примере активизирует макросы BASIC_DEBUGи SUPER_DEBUG, но не EXTRA_DEBUG. Флаг DDEBUG=0отключит всю отладочную информацию. С другой стороны, вставка следующих строк устранит необходимость задания в командной строке DEBUG, если отладки не требуется. #ifndef DEBUG #define DEBUG 0 #endif Несколько макросов, определенных препроцессором С, могут предоставить отладочную информацию. Эти макросы раскрываются для предоставления сведений о текущей компиляции (табл. 10.1). Обратите внимание на то, что приведенные символические имена начинаются и заканчиваются двумя символами подчеркивания. Это стандартное правило для символических имен препроцессора, и вы должны аккуратно выбирать идентификаторы, чтобы избежать конфликтов. Термин "текущие" в предыдущих описаниях указывает на момент выполнения препроцессорной обработки, т.е. время и дата запуска компилятора и обработки файла. Таблица 10.1
Выполните упражнение 10.1. Упражнение 10.1. Отладочная информацияДалее приведена программа cinfo.c, которая выводит дату и время компиляции, если включен режим отладки. #include <stdio.h> # include <stdlib.h> int main() { #ifdef DEBUG printf("Compiled: " __DATE__ " at " __TIME__ "\n"); printf("This is line %d of file %s\n", __LINE__, __FILE__); #endif printf("hello world\n"); exit(0); } Когда вы откомпилируете эту программу с включенным режимом отладки (используя флаг -DDEBUG), то увидите следующие сведения о компиляции: $ cc -о cinfo -DDEBUG cinfo.c $ ./cinfo Compiled: Jun 30 2007 at 22:58:43 This is line 8 of file cinfo.c hello world $ Как это работает Препроцессор С, часть компилятора, отслеживает текущую строку и текущий файл во время компиляции. Он подставляет текущие (времени компиляции) значения этих переменных везде, где обнаруживает символические имена __LINE__и __FILE__. Дата и время компиляции становятся доступными аналогичным образом. Поскольку __DATE__и __TIME__— строки, вы можете объединить их в функции printfс помощью строк формата, т.к. в языке С ANSI смежные строки воспринимаются как одна. Отладка без перекомпиляции Прежде чем двигаться дальше, стоит отметить, что существует способ применения функции printf, позволяющий отлаживать программу без применения метода #ifdef DEBUG, требующего перекомпиляции программы перед ее использованием. Метод заключается во вставке глобальной переменной как флага отладки, разрешении опции -dв командной строке, которая дает возможность пользователю включить отладку даже после того, как программа была введена в эксплуатацию, и включении функции мониторинга процесса отладки. Теперь можно вкраплять в код программы строки, подобные следующим: if (debug) { sprintf(msg, ...) write_debug(msg) } Записывать вывод отладки следует в стандартный поток ошибок stderrили, если это не годится из-за характера программы, используйте возможности мониторинга, предоставляемые функцией syslog. Если вы вставляете в программу подобную трассировку для решения проблем, возникающих на этапе разработки, просто оставьте этот код в программе. Если вы будете чуть внимательнее, чем всегда, такой подход не вызовет никаких проблем. Выигрыш проявится, когда программа будет введена в эксплуатацию; если пользователи обнаружат проблему, они смогут выполнить программу в режиме отладки и диагностировать ошибки для вас. Вместо известия о том, что программа выдает сообщение о нарушении сегментации, они смогут написать, что конкретно делает программа в ходе выполнения, а не только описать свои действия. Разница может оказаться огромной. У этого метода есть явный недостаток: программа становится больше, чем должна быть. В большинстве случаев это, скорее, мнимая проблема, чем реальная. Программа может стать на 20–30% больше, но чаще всего это не оказывает никакого существенного влияния на ее производительность. Снижение производительности наступает при увеличении размера на несколько порядков, а не на небольшую величину. Контролируемое выполнениеВернемся к примеру программы. У вас есть ошибка. Вы можете изменить программу, вставив в нее дополнительный код для вывода значений переменных по мере выполнения программы, или применить отладчик для контроля над выполнением программы и просмотра ее состояния в ходе выполнения. В коммерческих UNIX-системах есть ряд отладчиков, набор которых зависит от поставщика системы. Наиболее распространенные — adb, sdb, idebug и dbx. Более сложные отладчики позволяют просматривать с некоторой степенью детализации состояние программы на уровне исходного кода. Именно к таким относится отладчик GNU, gdb, который может применяться в системах Linux и многих вариантах UNIX. Существуют и внешние интерфейсы (или программы-клиенты) для gdb, делающие его более удобным для пользователя; к таким программам относятся xxgdb, KDbg и ddd. Некоторые IDE, например, те, с которыми вы познакомились в главе 9, также предоставляют средства отладки или внешний интерфейс для gdb. У редактора Emacs даже есть средство (gdb-mode), позволяющее запускать gdb в вашей программе, устанавливать точки останова и построчно просматривать выполнение исходного кода. Для подготовки программы к отладке необходимо откомпилировать ее с одной или несколькими специальными опциями. Эти опции заставляют компилятор вставлять в программу дополнительную отладочную информацию. Она включает в себя идентификаторы и номера строк — сведения, которые отладчик может использовать, чтобы показать пользователю, до какого места в исходном программном коде дошло выполнение. Флаг -g— один из обычно применяемых при компиляции программы с последующей отладкой. Вы должны указывать его при компиляции всех исходных файлов, которые нуждаются в отладке, а также для компоновщика, чтобы могли применяться специальные версии стандартной библиотеки С, обеспечивающие поддержку режима отладки в библиотечных функциях. Программа компилятора передаст флаг компоновщику автоматически. Отладка может применяться и с библиотеками, не откомпилированными для этой цели, но с меньшей гибкостью. Отладочная информация может увеличить исполняемый файл во много (до десяти) раз. Несмотря на увеличение размера исполняемого файла (он займет больше места на диске), объем памяти, необходимый для выполнения программы, практически остается тем же самым. Перед вводом программы в эксплуатацию неплохо удалить из нее отладочную информацию, но только после того, как программа полностью отлажена. Примечание Отладка с помощью gdbДля отладки программы вы можете применять отладчик проекта GNU, gdb. Это очень мощный отладчик, который распространяется бесплатно и может использоваться на многих платформах UNIX. Он также служит отладчиком по умолчанию в системах Linux. gdb перенесен на многие другие платформы и может применяться для отладки встроенных систем реального времени. Запуск gdbПерекомпилируйте программу примера для отладки и запустите gdb: $ cc-g -o debug3 debug3.c $ gdb debug3 GNU gdb 6.6 Copyright (C) 2006 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i586-suse-linux"... Using host libthread_db library "/lib/libthread_db.so.1". (gdb) У gdb есть довольно подробная интерактивная система помощи и полное справочное руководство, представляемое как набор файлов, которые можно просматривать с помощью программы infoили из редактора Emacs. (gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points data -- Examining data files -- Specifying and examining files internals -- Maintenance commands obscure -- Obscure features running -- Running the program stack -- Examining the stack status -- Status inquiries support -- Support facilities tracepoints -- Tracing of program execution without stopping the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help all" for the list of all commands. Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous, (gdb) Сам по себе отладчик gdb — приложение, выполняющееся в текстовом режиме, но он предоставляет несколько сокращенных клавишных команд для выполнения повторяющихся задач. Во многих версиях есть редактирование в командной строке с хронологией команд, так что вы можете прокрутить список назад и выполнить ту же команду снова (попробуйте воспользоваться клавишами перемещения курсора). Все версии отладчика поддерживают "пустую команду"; нажатие клавиши <Enter> выполняет последнюю команду еще раз. Это особенно удобно при проверке выполнения программы в построчном режиме с помощью команд stepили next. Для завершения работы gdb применяйте команду quit. Выполнение программыВыполнить программу можно с помощью команды run. Любые аргументы, переданные вами команде run, пересылаются в программу как ее собственные аргументы. В данном случае вам не нужны никакие аргументы. Предположим, что ваша система, как и системы обоих авторов, теперь генерирует сообщение о нарушении сегментации памяти. Если нет, читайте дальше. Вы узнаете, что делать, когда одна из ваших программ действительно сгенерирует сообщение о нарушении сегментации. Если вы не получили такого сообщения, но хотите поработать с этим примером во время чтения книги, когда первая из проблем, связанных с доступом к памяти, будет устранена, можно взять программу из файла debug4.c. (gdb) run Starting program: /home/neil/BLP4e/chapter10/debug3 Program received signal SIGSEGV, Segmentation fault. 0x0804846f in sort (a=0x804a040, n=5) at debug3.c:23 23 /* 23 */ if(a[j].key > a[j+1].key) { (gdb) Программа, как и прежде, выполняется неверно. Когда программа дает сбой, gdb указывает причину и местонахождение. Теперь вы можете выяснять первопричину проблемы. В зависимости от ядра вашей системы, версий библиотеки С и компилятора сбой программы может произойти в другом месте, например в строке 25, когда элементы массива меняются местами, а не в строке 23, когда сравниваются поля keyэлементов массива. Если это так, вы увидите следующее сообщение: Program received signal SIGSEGV, Segmentation fault. 0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25 25 /* 25 */ a[j] = a[j+1]; Вы все равно можете продолжать следить за примером сеанса работы gdb, который описывается далее. Трассировка стекаПрограмма была остановлена при выполнении функции sortв строке 23 исходного файла debug3.c. Если при компиляции вы не включили в программу дополнительную отладочную информацию ( cc -g), то не сможете увидеть, где программа дала сбой, и использовать имена переменных для просмотра данных. Увидеть, как вы добрались до этого места, можно с помощью команды backtrace: (gdb) backtrace #0 0x0804846f in sort (a=0x804a040, n=5) at debug3.c:23 #1 0x08048583 in main() at debug3.c:37 (gdb) Это очень простая программа и трассировка у нее короткая, т.к. вы не вызывали много функций из других функций. Вы только видите, что sortбыла вызвана из mainв строке 37 того же файла debug3.c. Обычно проблема гораздо сложнее, и команда backtraceприменяется для определения маршрута, который привел к месту ошибки. Эта информация очень полезна при отладке функций, вызываемых из множества разных мест. У команды backtraceесть сокращенная форма btи для совместимости с другими отладчиками есть команда where, выполняющая ту же функцию. Просмотр переменныхОтладчик вывел данные в момент остановки программы, и в трассировке стека показаны значения аргументов функции. Функция sortбыла вызвана с параметром а, значение которого 0х804а040. Это адрес массива. Обычно он в различных системах разный и зависит от используемых компилятора и операционной системы. Сбойная строка 23 — сравнение одного элемента массива с другим: /* 23 */ if (a[j].key > a[j+1].key) { Отладчик можно применять для просмотра содержимого параметров функции, локальных переменных и глобальных данных. Команда (gdb) print j $1 = 4 Вы видите, что у локальной переменной jзначение 4. Любые значения, выводимые командами gdb, подобными данной, сохраняются для будущего использования в псевдопеременных. В данном случае переменной $1присвоено значение 4, на случай, если она вам позже понадобится. Последующие команды будут сохранять свои результаты в переменных $2, $3и т.д. Значение переменной j, равное 4, означает, что программа попыталась выполнить оператор if (а[4].key > а[4+1].key) У массива array, который вы передали функции sort, только пять элементов, которые пронумерованы от 0 до 4. Поэтому данный оператор считывает несуществующий элемент массива array[5]. Переменная цикла jприняла некорректное значение. Если ваша программа завершилась в строке 25, система обнаружила чтение за пределами массива, только когда взялась за перестановку элементов массива, выполнив оператор /* 25 */ а[j] = a[j+1]; который при j, равной 4, дает в результате а[4] = а[4+1]; Просмотреть элементы передаваемого массива можно, применив выражение в команде (gdb) print а[3] $2 = {data = "alex", '\0' <repeats 4091 times>, key = 1} (gdb) Отладчик gdb сохраняет результаты выполнения команд в псевдопеременных вида $<номер>. Результат последней команды всегда хранится в псевдопеременной $, а предыдущей — в $$. Это позволяет результат одной команды использовать в другой. Например: (gdb) print j $3 = 4 (gdb) print a[$-1].key $4 = 1 Вывод листинга программыВы можете в программе gdb вывести на экран исходный текст программы с помощью команды list. Она выводит фрагмент кода, расположенного рядом с текущей позицией. Последующие вызовы listвыведут остальной текст. Команде listможно задать в качестве аргумента имя функции, и команда отобразит фрагмент текста в этом месте программы, или можно указать пару номеров строк, и на экране появится текст программы, находящийся между этими строками. (gdb) list 18 /* 18 */ int s = 1; 19 /* 19 */ 20 /* 20 */ for(; i < n && s != 0; i++) { 21 /* 21 */ s = 0; 22 /* 22 */ for(j = 0; j < n; j++) { 23 /* 23 */ if(a[j].key > a[j+1].key) { 24 /* 24 */ item t = a[j]; 25 /* 25 */ a[j] = a[j+1]; 26 /* 26 */ a[j+1] = t; 27 /* 27 */ s++; (gdb) В строке 22 задано выполнение цикла до тех пор, пока переменная jменьше n. В данном случае nравна 5, поэтому у jбудет последнее значение 4, слишком большое. Значение 4 приводит к сравнению а[4]с а[5]и возможной их перестановке. Единственное решение этой конкретной проблемы — исправить условие завершения цикла на следующее: j < n-1. Давайте внесем это изменение, назовем новую программу debug4.c, откомпилируем ее и попробуем снова выполнить. /* 22 */ for(j = 0; j < n-1; j++) { $ cc -g -o debug4 debug4.с $ ./debug4 array[0] = {john, 2} array[1] = {alex, 1} array[2] = {bill, 3} array[3] = {neil, 4} array[4] = {rick, 5} Программа все еще не работает, поскольку она вывела неверно отсортированный список. Попробуем применить gdb для пошагового выполнения программы. Установка точек остановаДля обнаружения места сбоя в программе необходимо иметь возможность проследить за тем, что делает программа во время выполнения. Остановить ее в любой момент можно с помощью точек останова. Они останавливают программу и передают управление отладчику. Вы сможете проверить переменные и затем разрешить программе продолжить выполнение. В функции sortесть два цикла. Внешний цикл с переменной цикла iвыполняется для каждого элемента массива один раз. Внутренний или вложенный цикл меняет местами элемент с последующим, расположенным ниже в списке. Это создает эффект всплытия пузырьков, поднимая вверх меньшие элементы. После каждого выполнения внешнего цикла самый большой элемент опускается на дно. Вы можете убедиться в этом, остановив программу на внешнем цикле и просмотрев состояние массива. Для установки точек останова применяется ряд команд. Их перечень получен отладчиком gdb с помощью команды help breakpoint: (gdb) help breakpoint Making program stop at certain points. List of commands: awatch -- Set a watchpoint for an expression break -- Set breakpoint at specified line or function catch -- Set catchpoints to catch events clear -- Clear breakpoint at specified line or function commands -- Set commands to be executed when a breakpoint is hit condition -- Specify breakpoint number N to break only if COND is true delete -- Delete some breakpoints or auto-display expressions delete breakpoints -- Delete some breakpoints or auto-display expressions delete checkpoint -- Delete a fork/checkpoint (experimental) delete mem -- Delete memory region delete tracepoints -- Delete specified tracepoints disable -- Disable some breakpoints disable breakpoints -- Disable some breakpoints disable display -- Disable some expressions to be displayed when program stops disable mem -- Disable memory region disable tracepoints -- Disable specified tracepoints enable -- Enable some breakpoints enable delete -- Enable breakpoints and delete when hit enable display -- Enable some expressions to be displayed when program stops enable mem -- Enable memory region enable once -- Enable breakpoints for one hit enable tracepoints -- Enable specified tracepoints hbreak -- Set a hardware assisted breakpoint ignore -- Set ignore-count of breakpoint number N to COUNT rbreak -- Set a breakpoint for all functions matching REGEXP rwatch -- Set a read watchpoint for an expression tbreak -- Set a temporary breakpoint tcatch -- Set temporary catchpoints to catch events thbreak -- Set a temporary hardware assisted breakpoint watch -- Set a watchpoint for an expression Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous. Установите точку останова в строке 21 и выполните программу: $ gdb debug4 (gdb) break 21 Breakpoint 1 at 0x8048427: file debug4.c, line 21. (gdb) run Starting program: /home/neil/BLP4e/chapter10/debug4 Breakpoint 1, sort (a=0x804a040, n=5) at debug4.c:21 21 /* 21 */ s = 0; Вы можете вывести значение массива и затем с помощью команды contразрешить программе продолжить выполнение. Это позволит программе выполняться до тех пор, пока она не натолкнется на следующую точку останова, в нашем случае это снова строка 21. В любой момент времени может быть активно несколько точек останова: (gdb) print array[0] $1 = (data = "bill", '\0' <repeats 4091 times>, key = 3) Для вывода нескольких последовательных элементов массива можно применить конструкцию @<число>, чтобы заставить gdb вывести указанное количество элементов массива. Для того чтобы вывести все пять элементов, можно использовать следующую команду: (gdb) print array[0]@5 $2 = {{data = "bill", '\0' <repeats 4091 times>, key = 3}, { data = "neil", '\0' <repeats 4091 times>, key =4}, { data = "john", '\0' <repeats 4091 times>, key =2}, { data = "rick", '\0' <repeats 4091 times>, key =5}, { data = "alex", '\0' <repeats 4091 times>, key = 1}} Учтите, что вывод немного подчищен, чтобы его легче было читать. Поскольку это первый проход цикла, массив еще не изменен. Когда вы разрешите программе продолжить выполнение, то увидите последовательные перестройки массива array, происходящие по мере выполнения программы: (gdb) cont Continuing. Breakpoint 1, sort (a=0x8049580, n=4) at debug4.c:21 21 /* 21 */ s = 0; (gdb) print array[0]@5 $3 = {{data = "bill", '\0' <repeats 4091 times>, key = 3}, { data = "john", '\0' <repeats 4091 times>, key =2}, { data = "neil", '\0' <repeats 4091 times>, key = 4}, { data = "alex", '\0' <repeats 4091 times>, key =1}, { data = "rick", '\0' <repeats 4091 times>, key =5}} (gdb) Можно воспользоваться командой display, чтобы задать в gdb автоматическое отображение массива при каждой остановке программы в точке останова: (gdb) display array[0]@5 1: array[0]@5 = {{data = "bill", '\0' <repeats 4091 times>, key = 3}, { data = "john", '\0' <repeats 4091 times>, key = 2}, { data = "neil", '\0' <repeats 4091 times>, key = 4}, { data = "alex", '\0' <repeats 4091 times>, key = 1}, { data = "rick", '\0' <repeats 4091 times>, key, = 5}} Более того, вы можете изменить точку останова таким образом, что вместо остановки программы она просто отобразит данные, которые вы запросили, и продолжит выполнение. Для этого примените команду commands. Она позволит указать, какие команды отладчика выполнять при попадании в точку останова. Поскольку вы уже указали отображение, вам нужно лишь задать команду в точке останова для продолжения выполнения: (gdb) commands Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". > cont > end Теперь, когда вы разрешите программе продолжить выполнение, она продолжается до завершения, выводя значение массива каждый раз, когда оказывается вблизи внешнего цикла. (gdb) cont Continuing. Breakpoint 1, sort (a=0x8049684, n=3) at debug4.c:21 21 /* 21 */ s = 0; 1: array[0]@5 = {{data = "john", '\000' <repeats 4091 times>, key = 2}, { data = "bill", '\000' <repeats 4091 times>, key =3}, { data = "alex", '\000' <repeats 4091 times>, key =1}, { data = "neil", '\000' <repeats 4091 times>, key =4}, { data = "rick", '\000' <repeats 4091 times>, key = 5}} array[0] = {john, 2} array[1] = {alex, 1} array[2] = {bill, 3} array[3] = {neil, 4} array[4] = {rick, 5} Program exited with code 025. (gdb) Отладчик gdb сообщает о том, что программа завершается с необычным кодом завершения. Это происходит потому, что программа сама не вызывает exitи не возвращает значение из функции main. Код завершения в данном случае не имеет смысла, значимый код должен предоставляться вызовом функции exit. Кажется, что программа не выполняет внешний цикл столько раз, сколько ожидалось. Вы можете увидеть, что значение параметра n, используемого в условии завершения цикла, уменьшается при каждом достижении точки останова. Это значит, что цикл не будет выполняться нужное число раз. Дело в уменьшении nв строке 30. /* 30 */ n--; Это попытка оптимизировать программу за счет того, что в конце каждого прохода внешнего цикла наибольший, элемент arrayокажется внизу и поэтому остается меньше элементов для сортировки. Но как видно, это мешает внешнему циклу и создает проблемы. Простейший способ исправления (хотя есть и другие) — удалить ошибочную строку. Давайте проверим, применив отладчик для корректировки, устранило ли такое исправление проблему. Вставка исправлений с помощью отладчикаВы уже видели, что можно применять отладчик для установки точек останова и просмотра значений переменных. Применив точки останова с заданными действиями, можно проверить исправление, называемое "заплатой", перед тем, как изменять текст программы и выполнять ее повторную компиляцию. В данном случае нужно остановить программу в строке 30 и увеличить переменную n. В дальнейшем, когда строка 30 выполнится, значение останется неизменным. Давайте перезапустим программу с самого начала. Прежде всего вы должны удалить вашу точку останова и отладочный вывод. С помощью команды info можно увидеть, какие точки останова и какой вывод вы включили: (gdb) info display Auto-display expressions now in effect: Num Enb Expression 1: y array[0]@5 (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x08048427 in sort at debug4.c:21 breakpoint already hit 3 times cont Вы можете либо отключить эти точки останова, либо удалить их совсем. Если их отключить, у вас останется возможность включить их позже, когда понадобится. (gdb) disable break 1 (gdb) disable display 1 (gdb) break 30 Breakpoint 2 at 0x8048545: file debug4.c, line 30. (gdb) commands 2 Type commands for when breakpoint 2 is hit, one per line. End with a line saying just "end". >set variable n = n+1 >cont >end (gdb) run Starting program: /home/neil/BLP4e/chapter10/debug4 Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30 30 /* 30 */ n--; Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30 30 /* 30 */ n--; Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30 30 /* 30 */ n--; Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30 30 /* 30 */ n--; Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30 30 /* 30 */ n--; array[0] = {alex, 1} array[1] = {john, 2} array[2] = {bill, 3} array[3] = {neil, 4} array[4] = {rick, 5} Program exited with code 025. (gdb) Программа выполняется полностью и выводит корректный результат. Теперь можно внести изменения и переходить к тестированию ее с большим объемом данных. Дополнительные сведения о gdbОтладчик проекта GNU — исключительно мощный инструмент, способный снабжать множеством сведений о внутреннем состоянии выполняющихся программ. В системах, поддерживающих средство аппаратно устанавливаемых контрольных точек, можно применять gdb для наблюдения за изменениями переменных в режиме реального времени. Аппаратно устанавливаемые контрольные точки — это функция некоторых ЦПУ; такие процессоры способны автоматически останавливаться при возникновении определенных условий, обычно доступе к памяти в заданной области. Кроме того, gdb может следить (watch) за выражениями. Это означает, что с потерей производительности gdb может остановить программу, когда выражение принимает конкретное значение, независимо от того, в каком месте программы выполнялось вычисление. Точки останова можно устанавливать со счетчиками и условиями, так что они включаются только после фиксированного числа проходов или при выполнении условия. Отладчик gdb также способен подключаться к уже выполняющимся программам. Это очень полезно при отладке клиент-серверных систем, поскольку вы сможете отлаживать некорректно ведущий себя серверный процесс во время выполнения без необходимости останавливать и перезапускать его. Можно компилировать программы, например, с помощью строки gcc -O -g, чтобы получить преимущества от применения оптимизации и отладочной информации. Недостаток заключается в том, что оптимизация может слегка переупорядочить текст программы, поэтому, когда вы будете выполнять программу в пошаговом режиме, может оказаться, что вы "скачете вперед и назад" по строкам, чтобы добиться того эффекта, что и в первоначальном тексте программы. Отладчик gdb можно также применять для отладки аварийно завершившихся программ. Системы Linux и UNIX при аварийном завершении программы часто создают дамп ядра в файле с именем core. Это отображение карты памяти программы, которое содержит значения глобальных переменных в момент возникновения сбоя. Вы сможете использовать gdb для того, чтобы определить место в программе, вызвавшее аварийное завершение. Дополнительную информацию см. в интерактивном справочном руководстве к gdb. Отладчик gdb доступен в соответствии с требованиями Общедоступной лицензии проекта GNU и его поддерживает большинство систем UNIX. Мы настоятельно рекомендуем вам, как следует изучить его. Дополнительные средства отладкиПомимо полнофункциональных отладчиков, таких как gdb, Linux-системы обычно предоставляют и другие средства, которые можно применять для поддержки процесса отладки. Некоторые из них снабжают статической информацией о программе, другие обеспечивают динамический анализ. Статический анализ предоставляет сведения только об исходном тексте программы. Программы ctags, cxref и cflow работают с исходными файлами и предлагают полезные данные о вызовах функций и их месте в программе. Динамический анализ предоставляет информацию о том, как программа ведёт себя во время выполнения. Программы prof и gprof предлагают сведения о том, какие функции были выполнены, и сколько времени заняло их выполнение, Давайте рассмотрим некоторые из этих средств и их вывод. Не все они будут доступны во всех системах, хотя у многих из этих средств есть свободно распространяемые версии. Lint удаление ошибок из ваших программПервые системы UNIX предоставляли утилиту lint. Эта программа по существу — препроцессор компилятора С со вставленными тестами, обеспечивающими некоторые проверки с точки зрения здравого смысла и вывод предупреждений. Среди прочего она обнаруживает случаи применения переменных до того, как им было присвоено значение, или случаи неиспользования аргументов функций. Более современные компиляторы C могут ценой производительности времени компиляции формировать аналогичные предупреждения. Утилиту lint, как таковую, обогнала стандартизация языка С. Поскольку средство основывалось на раннем компиляторе С, оно совсем не справляется с синтаксисом ANSI. Есть несколько коммерческих версий lintдля UNIX и одна версия в Интернете для Linux, названная splint. Она известна под именем LClint, как часть проекта MIT (Massachusetts Institute of Technology, Массачусетский технологический институт), занимающегося разработкой средств формального описания. splint, средство подобное lint, может предоставлять полезные обзорные комментарии к программному коду. Найти splintможно по адресу http://www.splint.org. Далее приведена первоначальная версия (debug0.c) программы-примера, которую вы уже отладили. /* 1 */ typedef struct { /* 2 */ char *data; /* 3 */ int key; /* 4 */ } item; /* 5 */ /* 6 */ item array[j] = { /* 7 */ {"bill", 3}, /* 8 */ {"neil", 4}, /* 9 */ {"john", 2}, /* 10 */ {"rick", 5}, /* 11 */ {"alex", 1}, /* 12 */ }; /* 13 */ /* 14 */ sort(a, n) /* 15 */ item *a; /* 16 */ { /* 17 */ int i = 0, j = 0; /* 18 */ int s; /* 19 */ /* 20 */ for(; i < n & s != 0; i++) { /* 21 */ s = 0; /* 22 */ for(j = 0; j < n; j++) { /* 23 */ if(a[j].key > a[j+1].key) { /* 24 */ item t = a[j]; /* 25 */ a[j] = a[j+1]; /* 26 */ a[j+1] = t; /* 27 */ s++; /* 28 */ } /* 29 */ } /* 30 */ n--; /* 31 */ } /* 32 */ } /* 33 */ /* 34 */ main() /* 35 */ { /* 36 */ sort(array,5); /* 37 */ } В этой версии есть проблема в строке 20, где вместо предполагаемого оператора &&применяется оператор &. Далее приведен отредактированный пример вывода splint, выполненной с этой версией программы. Обратите внимание на то, как она обнаруживает проблемы в строке 20 — тот факт, что вы не инициализировали переменную sи что возможны проблемы с условием из-за некорректного оператора. neil@susel03:~/BLP4e/chapter10> splint -strict debug0.c Splint 3.1.1 --- 19 Mar 2005 debug0.c:7:18: Read-only string literal storage used as initial value for unqualified storage: array[0].data = "bill" A read-only string literal is assigned to a non-observer reference. (Use -readonlytrans to inhibit warning) debug0.c:8:18: Read-only string literal storage used as initial value for unqualified storage: array[1].data = "neil" debug0.c:9:18: Read-only string literal storage used as initial value for unqualified storage: array[2].data = "john" debug0.с:10:18: Read-only string literal storage used as initial value for unqualified storage: array[3].data = "rick" debug0.c:11:18: Read-only string literal storage used as initial value for unqualified storage: array[4].data = "alex" debug0.с:14:22: Old style function declaration Function definition is in old style syntax. Standard prototype syntax is preferred. (Use -oldstyle to inhibit warning) debug0.с: (in function sort) debug0.c:20:31: Variable s used before definition An rvalue is used that may not be initialized to a value on some execution path. (Use -usedef to inhibit warning) debug0.с:20:23: Left operand of & is not unsigned value (boolean): i < n & s != 0 An operand to a bitwise operator is not an unsigned values. This may have unexpected results depending on the signed representations. (Use -bitwisesigned to inhibit warning). debug0.c:20:23: Test expression for for not boolean, type unsigned int: i < n & s != 0 Test expression type is not boolean or int. (Use -predboolint to inhibit warning); debug0.с:25:41: Undocumented modification of a[]: a[j] = a[j + 1] An externally-visible object is modified by a function with no /*@modifies@*/ comment. The /*@modifies ... @*/ control comment can be used to give a modifies list for an unspecified function. (Use -modnomods to inhibit warning) debug0.c:26:41: Undocumented modification of a[]: a[j + 1] = t debug0.c:20:23: Operands of & are non-integer (boolean) (in post loop test): i < n & s != 0 A primitive operation does not type check strictly. (Use -strictops to inhibit warning) debug0.с:32:14: Path with no return in function declared to return int There is a path through a function declared to return a value on which there is no return statement. This means the execution may fall through without returning a meaningful result to the caller. (Use -noret to inhibit warning) debug0.с:34:13: Function main declared without parameter list A function declaration does not have a parameter list. (Use -noparams to inhibit warning) debug0.с: (in function main) debug0.с:36:22: Undocumented use of global array A checked global variable is used in the function, but not listed in its globals clause. By default, only globals specified in .lcl files are checked. To check all globals, use +allglobals. To check globals selectively use /*@checked@*/ in the global declaration. (Use -globs to inhibit warning) debug0.с:36:17: Undetected modification possible from call to unconstrained function sort: sort An unconstrained function is called in a function body where modifications are checked. Since the unconstrained function may modify anything, there may be undetected modifications in the checked function. (Use -modunconnomods to inhibit warning) debug0.c:36:17: Return value (type int) ignored: sort(array, 5) Result returned by function call is not used. If this is intended, can cast result to (void) to eliminate message. (Use -retvalint to inhibit warning) debug0.c:37:14: Path with no return in function declared to return int debug0.c:6:18: Variable exported but not used outside debug0: array A declaration is exported, but not used outside this module. Declaration can use static qualifier. (Use -exportlocal to inhibit warning) debug0.c:14:13: Function exported but not used outside debug0: sort debug0.c:15:17: Definition of sort debug0.c:6:18: Variable array exported but not declared in header file A variable declaration is exported, but does not appear in a header file. (Used with exportheader.) (Use -exportheadervar to inhibit warning) debug0.c:14:13: Function sort exported but not declared in header file A declaration is exported, but does not appear in a header file. (Use -exportheader to inhibit warning) debug0.c:15:17: Definition of sort Finished checking - 22 code warnings $ Утилита выражает неудовольствие по поводу объявления функций в старом стиле (не ANSI) и несоответствия типов значений, возвращаемых функциями, и самими величинами, которые они возвращают (или нет) в действительности. Эти предупреждения не влияют на работу программы, но должны быть выведены. Она также обнаружила две реальные ошибки в следующем фрагменте кода: /* 18 */ int s; /* 19 */ /* 20 */ for(; i < n & s != 0; i++) { /* 21 */ s = 0; Средство splint определило (выделенные цветом строки предыдущего вывода), что переменная sиспользуется в строке 20, но не была при этом инициализирована, и что оператор &стоит на месте более обычного оператора &&.В данном случае старшинство оператора изменяет значение условия и создает проблему в программе. Обе эти ошибки были исправлены при чтении исходного текста программы до запуска процесса отладки. Несмотря на то, что пример мало изобретателен и служит только для демонстрации, подобные ошибки регулярно возникают в реальных программах." Средства, отслеживающие вызовы функцийТри утилиты — ctags, cxrefи cflow— формируют часть стандарта X/Open и, следовательно, должны включаться в системы, представляемые как системы UNIX с программными средствами разработки. Примечаниеctags Программа ctagsсоздает алфавитный указатель функций. Для каждой функции вы получаете перечень мест в программе, где она применяется, как алфавитный указатель к книге. ctags [-a] [-f filename] sourcefile sourcefile ... ctags -x sourcefile sourcefile ... По умолчанию ctagsсоздает в текущем каталоге файл с именем tags, содержащий для каждой функции, объявленной в любом из входных файлов исходного кода, строки следующего вида: announce app_ui.c /^static void announce(void) / Каждая строка файла содержит имя функции, файл, в котором она объявлена, и регулярное выражение, которое можно использовать для поиска описания функции в файле. Некоторые редакторы, например Emacs, могут применять файлы этого вида для навигации в исходном тексте программы. Кроме того, с помощью опции -хв программе ctags(если она доступна в вашей версии программы) вы можете формировать строки аналогичного вида в стандартном файле вывода. find_cat 403 appui.с static cdc_entry find_cat( Можно перенаправить вывод в другой файл с помощью опции -f filenameи добавить его в конец существующего файла, указав опцию -а. cxref Программа cxrefанализирует исходный текст на языке С и формирует перекрестные ссылки. Она показывает, где в программе упоминается каждое символическое имя (переменная, директива #defineи функция). Программа создает отсортированный список с указанием места определения каждого идентификатора, которое помечается звездочкой, как показано далее: SYMBOL FILE FUNCTION LINE BASENID prog.с -- *12 *96 124 126 146 156 166 BINSIZE prog.с -- *30 197 198 199. 206 BUFMAX prog.с -- *44 45 90 BUFSIZ /usr/include/stdio.h -- *4 EOF /usr/include/stdio.h -- *27 argc prog.с -- 36 prog.с main *37 61 81 argv prog.с -- 36 prog.с main *38 61 calldata prog.с -- *5 prog.с main 64 188 calls prog.с -- *19 prog.с main 54 На машине одного из авторов этой книги предыдущий вывод был сгенерирован в каталоге с исходными файлами приложения с помощью команды $ cxref *.с *.h но точный синтаксис зависит от версии. См. документацию к вашей системе и интерактивное справочное руководство для получения дополнительной информации о том, включена ли программа cxrefи как ее применять. cflow Программа cflowвыводит дерево вызовов функций — схему, показывающую, какие функции вызывают другие функции, какие функции вызываются этими другими и т.д. Эта схема полезна для выяснения структуры программы, понимания ее принципов действия и наблюдения за влиянием изменений, внесенных в функцию. Некоторые версии программы cflowмогут работать с объектными файлами так же, как с исходными. Подробности см. в интерактивном справочном руководстве. Далее приведен пример вывода, полученный версией cflow(cflow-2.0), которая есть в Интернете и поддерживается Марти Лейснером (Marty Leisner). 0 file_ungetc {prcc.c 997} 1 main {prcc.c 70} 2 getopt {} 3 show_all_lists {prcc.c 1070} 4 display_list {prcc.c 1056} 5 printf {} 6 exit {} 7 exit {} 9 usage {prcc.c 59} 10 fprintf {} 11 exit {} Пример информирует о том, что функция mainвызывает (среди прочих) функцию show_all_listsи что show_all_listsв свою очередь вызывает функцию display_list, которая вызывает функцию printf. У этой версии cflowесть опция -i, которая формирует инвертированный потоковый граф. Утилита cflowперечисляет для каждой функции другие функции, вызывающие данную. Звучит не очень понятно, но на самом деле все просто. Далее приведен пример: 19 display_list {prcc.c 1056} 20 show_all_lists {prcc.c 1070} 21 exit {} 22 main {prcc.c 70} 23 show_all_lists {prcc.c 1070} 24 usage {prcc.c 59} 25 ... 74 printf {} 75 display_list {prcc.c 1056} 76 maketag {prcc.c 4 87} 77 show_all_lists {prcc.c 1070} 78 main {prcc.c 70} 79 ... 99 usage {prcc.c 59} 100 main {prcc.c 70} В примере показано, что функцию exit, например, вызывают функции main, show_all_listsи usage. Выполнение профилирования с помощью prof/gprofМетодика, зачастую полезная при попытках выяснить проблемы снижения производительности программы, называется профилированием выполнения (execution profiling). Профиль программы, обычно поддерживаемый специальными опциями компилятора и вспомогательными программами, показывает, где программа тратит время. Программа prof(и ее эквивалент в проекте GNU, gprof) выводит отчёт из файла трассировки выполнения, который формируется во время выполнения профилируемой программы. Профилируемый исполняемый файл создается с помощью флага компилятора -p(для prof) или флага -pg(для gprof). $ cc -pg -о program program.с Программа компонуется со специальной библиотекой С, и в нее включается контрольный код. В конкретных системах он может отличаться, но общая цель — такая организация программы, которая позволяет часто прерывать выполнение и записывать этап выполнения. Контрольные данные записываются в файл mon.out (gmon.out для gprof) в текущем каталоге. $ ./program $ ls -ls 2 -rw-r--r-- 1 neil users 1294 Feb 4 11:48 gmon.out Программа prof/gprof читает эти контрольные данные и выводит отчет. См. подробности, касающиеся опций программы, в интерактивном справочном руководстве. Далее в качестве примера приведен вывод (сокращенный) программы gprof. cumulative self self total time seconds seconds calls ms/call ms/call name 18.5 0.10 0.10 8664 0.01 0.03 doscan [4] 18.5 0.20 0.10 mcount (60) 14.8 0.28 0.08 43320 0.00 0.00 _number [5] 9.3 0.33 0.05 8664 0.01 0.01 _format_arg [6] 7.4 0.37 0.04 112632 0.00 0.00 _ungetc [8] 7.4 0.41 0.04 8757 0.00 0.00 _memccpy [9] 7.4 0.45 0.04 1 40.00 390.02 _main [2] 3.7 0.47 0.02 53 0.38 0.38 _read [12] 3.7 0.49 0.02 w4str [10] 1.9 0.50 0.01 26034 0.00 0.00 _strlen [16] 1.9 0.51 0.01 8664 0.00 0.00 strncmp [17] Проверки соблюдения условийНесмотря на то, что вставка на этапе разработки программы с помощью условной компиляции отладочного кода, такого как вызовы printf, распространена, иногда оставлять такие сообщения в поставляемой программе непрактично. Но часто проблемы возникают во время работы программы из-за некорректных допущений или исходных данных, а не из-за ошибок кодирования. Это события, которых "не может быть никогда". Например, функция может быть написана в расчете на то, что ее входные параметры будут в определенном диапазоне. Если передать ей некорректные данные, она может сделать некорректной работу всей системы. В тех случаях, когда внутренняя логика системы нуждается в подкреплении, X/Open предоставляет макрос assert, применяемый для проверки правильности исходных данных и остановки выполнения программы в противном случае. #include <assert.h> void assert(int expression) Макрос assertвычисляет выражение и, если оно не равно нулю, выводит некоторую диагностическую информацию о стандартной ошибке и вызывает функцию abortдля завершения программы. Заголовочный файл assert.h определяет макросы в зависимости от определения флага NDEBUG. Если NDEBUGопределен во время обработки заголовочного файла, assertопределяется по существу как ничто. Это означает, что вы можете отключить проверки заданных выражений во время компиляции, компилируя с опцией -DNDEBUGили вставив перед включением файла assert.h строку #define NDEBUG в каждый исходный файл. Этот метод применения порождает проблему. Если вы используете assertво время тестирования, но отключите макрос в рабочем коде, в вашем рабочем коде может оказаться менее строгая проверка, чем применявшаяся в процессе его тестирования. Обычно макросы assertне оставляют включенными в рабочем коде — вряд ли вам понравится рабочий код, предоставляющий пользователю недружелюбное сообщение assert failedи останавливающий программу. Быть может, лучше написать свою отслеживающую ошибки подпрограмму, которая проверяет выражение, использовавшееся в макросе, но не нуждается в полном отключении в рабочем коде. Вы также должны убедиться в том, что у выражения макроса assertнет побочных эффектов. Например, если вы применяете вызов функции с побочным эффектом, этот побочный эффект не проявится в рабочем коде с отключенными макросами assert. Выполните упражнение 10.2. Упражнение 10.2. Программа assert.c.Далее приведена программа assert.c, определяющая функцию, которая должна принимать положительное значение. Она защищает от ввода некорректного аргумента благодаря применению макроса assert. После включения заголовочного файла assert.h и функции "квадратный корень", проверяющей положительное значение параметра, вы можете писать функцию main. #include <stdio.h> #include <math.h> #include <assert.h> #include <stdlib.h> double my_sqrt(double x) { assert(x >= 0.0); return sqrt(x); } int main() { printf("sqrt +2 = %g\n", my_sqrt(2.0)); printf("sqrt -2 = %g\n", my_sqrt(-2.0)); exit(0); } Теперь при выполнении программы вы увидите нарушение в макросе assertпри передаче некорректного значения. Точный формат сообщения о нарушении условия макроса assert в разных системах разный. $ сс -о assert assert.с -lm $ ./assert sqrt +2 = 1.41421 assert: assert.c:7: my_sqrt: Assertion 'x >= 0.0' failed. Aborted $ Как это работает Когда вы попытаетесь вызвать функцию my_sqrtс отрицательным числом, макрос assertдаст сбой. Он предоставляет файл и номер строки, в которой нарушено условие и само нарушенное условие. Программа завершается прерыванием abort. Это результат вызова abortмакросом assert. Если вы перекомпилируете программу с опцией -DNDEBUG, макрос assertне компилируется, и вы получаете NaN(Not a Number, не число) — значение, указывающее на неверный результат при вызове функции sqrtиз функции my_sqrt. $ cc -о assert -DNDEBUG assert.с -lm $ ./assert sqrt +2 = 1.41421 sqrt -2 = nan $ Некоторые более старые версии математической библиотеки генерируют исключение для математической ошибки, и ваша программа будет остановлена с сообщением "Floating point exception" ("Исключение для числа с плавающей точкой") вместо возврата NaN. Устранение ошибок использования памятиРаспределение динамической памяти — богатый источник ошибок, которые трудно выявить. Если вы пишете программу, применяющую функции mallocи freeдля распределения памяти, важно внимательно следить за блоками, которые вы выделяете, и быть уверенным в том, что не используется блок, который вы уже освободили. Обычно блоки памяти выделяются функцией mallocи присваиваются переменным-указателям. Если переменная-указатель изменяется, и нет других указателей, указывающих на блок памяти, он становится недоступным. Это утечка памяти, вызывающая увеличение размера программы. Если вы потеряете большой объем памяти, скорость работы вашей системы, в конце концов, снизится, и система уйдет за пределы памяти. Если вы записываете в область, расположенную после конца выделенного блока (или перед началом блока), вы с большой долей вероятности повредите структуры данных, используемые библиотекой malloc, следящей за распределением памяти. В этом случае в какой-то момент времени вызов mallocили даже freeприведет к нарушению сегментации, и ваша программа завершится аварийно. Определение точного места возникновения сбоя может оказаться очень трудной задачей, поскольку нарушение могло возникнуть задолго до события, вызвавшего аварийное завершение программы. Неудивительно, что существуют коммерческие и бесплатные средства, способные помочь в решении проблем этих двух типов. Например, есть много разных версий функций mallocи free, которые содержат дополнительный код для проверки выделения и освобождения блоков памяти и пытаются учесть двойное освобождение блока и другие типы неправильного использования памяти. ElectricFenceБиблиотека ElectricFence была разработана Брюсом Перенсом (Bruce Perens). Она доступна как необязательный компонент в некоторых дистрибутивах Linux, таких как Red Hat (Enterprise и Fedora), SUSE и openSUSE, и может быть легко найдена в Интернете. Это средство пытается применять виртуальную память системы Linux для защиты памяти, используемой функциями mallocи free, и аварийного останова программы в момент повреждения памяти. Выполните упражнение 10.3. Упражнение 10.3. Применение библиотеки ElectricFenceДалее приведена программа efence.c, которая выделяет память с помощью функции mallocи пишет данные за концом выделенного блока. Познакомьтесь с ней и посмотрите, что произойдет. #include <stdio.h> #include <stdlib.h> int main() { char *ptr = (char *)malloc(1024); ptr[0] = 0; /* Теперь пишет за пределы блока */ ptr[1024] = 0; exit(0); } Когда вы откомпилируете и выполните программу, то не увидите некорректного поведения. Однако вероятно, что область памяти, выделенная malloc, повреждена, и вы, в конце концов, попадете в беду. $ cc -о efence efence.с $ ./efence $ Тем не менее, если вы возьмете ту же самую программу и скомпонуйте ее с библиотекой ElectricFence (libefence.a), то получите немедленный отклик: $ cc -о efence efence.с -lefence $ ./efence Electric Fence 2.2.0 Copyright (С) 1987-1999 Bruce Perens <bruce@perens.com> Segmentation fault $ Выполнение под контролем отладчика позволяет получить подробное описание проблемы; $ cc -g -о efence efence.с -lefence $ gdb efence (gdb) run Starting program: /home/neil/BLP4e/chapter10/efence Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens bruce@perens.com Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 1024 (LWP 1869)] 0x08048512 in main () at efence.c:10 10 ptr[1024] = 0; (gdb) Как это работает Библиотека ElectricFence заменяет функцию mallocи связанные с ней функции версиями, применяющими аппаратные средства виртуальной памяти для защиты от несанкционированного доступа к памяти. При возникновении подобного обращения к памяти порождается сигнал нарушения сегментации и программа останавливается. valgrindСредство valgrindспособно обнаруживать многие из обсуждавшихся нами проблем (упражнение 10.4). Прежде всего, оно умеет находить ошибки доступа, к массиву и утечки памяти. Это средство, возможно, не включено в ваш дистрибутив Linux, но его можно найти на Web-сайте http://valgrind.org. Для применения valgrindдаже не требуется перекомпиляции программы, и вы можете находить ошибки доступа к памяти в выполняющейся программе. Данное средство заслуживает внимания; оно применяется в основных разработках, включая среду KDE версии 3. Упражнение 10.4. Средство valgrind Далее приведена программа checker.c, которая выделяет некоторый объем памяти, читает область памяти и записывает данные за пределами выделенного участка, а затем делает выделенный участок недоступным. #include <stdio.h> #include <stdlib.h> int main() { char *ptr = (char *)malloc(1024); char ch; /* Неинициализированное чтение */ ch = ptr[1024]; /* Запись за пределами блока */ ptr[1024] = 0; /* Потеря блока */ ptr = 0; exit(0); } Для применения valgrindвы просто выполняете команду valgrind, передав ей опции, задающие нужные виды проверок, и далее указав программу для выполнения с ее аргументами (если таковые есть). При выполнении программы с valgrindвы увидите множество обнаруженных проблем: $ valgrind --leak-check=yes -v ./checker ==4780== Memcheck, a memory error detector. ==4780== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al. ==4780== Using LibVEX rev 1732, a library for dynamic binary translation. ==4780== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP. ==4780== Using valgrind-3.2.3, a dynamic binary instrumentation framework. ==4780== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al. ==4780== --4780-- Command line --4780-- ./checker --4780-- Startup, with flags: --4780-- --leak-check=yes --4780-- -v --4780-- Contents of /рroc/version: --4780-- Linux version 2-6.20.2-2-default (geeko@buildhost) (gcc version 4.1.3 20070218 (prerelease) (SUSE Linux)) #1 SMP Fri Mar 9 21:54:10 UTC 2007 --4780-- Arch and hwcaps: X86, x86-sse1-sse2 --4780-- Page sizes: currently 4096, max supported 4096 --4780-- Valgrind library directory: /usr/lib/valgrind --4780-- Reading syms from /lib/ld-2.5.so (0x4000000) --4780-- Reading syms from /home/neil/BLP4e/chapter10/checker (0x8048000) --4780-- Reading syms from /usr/lib/valgrind/x86-linux/memcheck (0x38000000) --4780-- object doesn't have a symbol table --4780-- object doesn't have a dynamic symbol table --4780-- Reading suppressions file: /usr/lib/valgrind/default.supp --4780-- REDIR: 0x40158B0 (index) redirected to 0x38027EDB (???) --4780-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_core.so (0x401E000) --4780-- object doesn't have a symbol table --4780-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so (0x4021000) --4780-- object doesn't have a symbol table ==4780= WARNING: new redirection conflicts with existing -- ignoring it --4780-- new: 0x040158B0 (index ) R-> 0x04024490 index --4780-- REDIR: 0x4015A50 (strlen) redirected to 0x4024540 (strlen) --4780-- Reading syms from /lib/libc-2.5.so (0x4043000) --4780-- REDIR: 0x40ADFF0 (rindex) redirected to 0x4024370 (rindex) --4780-- REDIR: 0x40AAF00 (malloc) redirected to 0x4023700 (malloc) ==4780== Invalid read of size 1 ==4780== at 0x804842C: main (checker.с: 10) ==4780== Address 0x4170428 is 0 bytes after a block of size 1,024 alloc'd ==4780== at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==4780== by 0x8048420: main (checker.c: 6) =4780= ==4780== Invalid write of size 1 ==4780== at 0x804843A: main (checker.с: 13) ==4780== Address 0x4170428 is 0 bytes after a block of size 1,024 alloc'd ==4780== at 0x4 023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==4780== by 0x8048420: main (checker.c: 6) --4780-- REDIR: 0x40A8BB0 (free) redirected to 0x402331A (free) --4780-- REDIR: 0x40AEE70 (memset) redirected to 0x40248A0 (memset) ==4780== ==4780== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 3 from 1) ==4780== ==4780== 1 errors in context 1 of 2: ==4780== Invalid write of size 1 ==4780== at 0x804843A: main (checker.с: 13) ==4780== Address 0x4170428 is 0 bytes after a block of size 1,024 alloc'd ==4780== at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==4780== by 0x80484 20: main (checker.c: 6) ==4780== ==4780== 1 errors in context 2 of 2: ==4780== Invalid read of size 1 ==4780== at 0x804842C: main (checker.c:10) ==4780== Address 0x4170428 is 0-bytes after a block of size 1,024 alloc'd ==4780== at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==4780== by 0x8048420: main (checker.с: 6) --4780-- --4780-- supp: 3 dl-hack3 ==4780== ==4780== IN SUMMARY: 2 errors from 2 contexts (suppressed: 3 from 1) ==4780== ==4780== malloc/free: in use at exit: 1,024 bytes in 1 blocks. ==4780== malloc/free: 1 allocs, 0 frees, 1,024 bytes allocated. ==4780== ==4780== searching for pointers to 1 not-freed blocks. ==4780== checked 65,444 bytes. ==4780== ==4780== ==4780== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==4780== at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==4780== by 0x8048420: main (checker.c: 6) ==4780== ==4780== LEAK SUMMARY: ==4780== definitely lost: 1,024 bytes in 1 blocks. ==4780== possibly lost: 0 bytes in 0 blocks. ==4780== still reachable: 0 bytes in 0 blocks. ==4780== suppressed: 0 bytes in 0 blocks. --4780-- memcheck: sanity checks: 0 cheap, 1 expensive --4780-- memcheck: auxmaps: 0 auxmap entries (0k, 0M) in use --4780-- memcheck: auxmaps: 0 searches, 0 comparisons --4780-- memcheck: SMs: n_issued = 9 (144k, 0M) --4780-- memcheck: SMs: n_deissued = 0 (0k, 0M) --4780-- memcheck: SMs: max_noaccess = 65535 (1048560k, 1023M) --4780-- memcheck: SMs: max_undefined = 0 (0k, 0M) --4780-- memcheck: SMs: max_defined = 19 (304k, 0M) --4780-- memcheck: SMs: max_non_DSМ = 9 (144k, 0M) --4780-- memcheck: max sec V bit nodes: 0 (0k, 0M) --4780-- memcheck: set_sec_vbits8 calls: 0 (new: 0, updates: 0) --4780-- memcheck: max shadow mem size: 448k, 0M --4780-- translate: fast SP updates identified: 1,456 ( 90.3%) --4780-- translate: generic_known SP updates identified: 79 ( 4.9%) --4780-- translate: generic_unknown SP updates identified: 76 ( 4.7%) --4780-- tt/tc: 3,341 tt lookups requiring 3,360 probes --4780-- tt/tc: 3,341 fast-cache updates, 3 flushes --4780-- transtab: new 1,553 (33,037 -> 538,097; ratio 162:10) [0 scs] --4780-- transtab: dumped 0 (0 -> ??) --4780-- transtab: discarded 6 (143 -> ??) --4780-- scheduler: 21,623 jumps (bb entries). --4780-- scheduler: 0/1,828 major/minor sched events. --4780-- sanity: 1 cheap, 1 expensive checks. --4780-- exectx: 30,011 lists, 6 contexts (avq 0 per list) --4780-- exectx: 6 searches, 0 full compares (0 per 1000) --4780-- exectx: 0 cmp2, 4 cmp4, 0 cmpAll $ Вы видите, что обнаружены некорректные считывания и записи, и интересующие нас блоки памяти приводятся с указанием места, которое для них отведено. Для прерывания выполнения программы в ошибочном месте можно применить отладчик. У программы valgrindесть много опций, включая подавление ошибок определенного типа и обнаружение утечки памяти. Для выявления такой утечки в примере вы должны использовать одну из опций, передаваемых valgrind. Для контроля утечек памяти после завершения программы следует задать опцию --leak-check=yes. Список опций можно получить с помощью команды valgrind --help. Как это работает Программа выполняется под контролем средства valgrind, которое перехватывает действия, совершаемые программой, и выполняет множество проверок, включая обращения к памяти. Если обращение относится к выделенному блоку памяти и некорректно, valgrind выводит сообщение. В конце программы выполняется подпрограмма "сбора мусора", которая определяет, есть ли выделенные и неосвобожденные блоки памяти. Об этих потерянных блоках выводится сообщение. РезюмеВ этой главе обсуждались некоторые методы и средства отладки. Система Linux предоставляет ряд мощных инструментов для удаления ошибок из ваших программ. Вы устранили несколько ошибок в программе с помощью отладчика cflowи splint. В заключение были рассмотрены проблемы, возникающие при использовании динамически распределяемой памяти, и некоторые средства, способные помочь обнаружить их, например ElectricFence и valgrind. Утилиты, обсуждавшиеся в этой главе, в основном хранятся на FTP-серверах в Интернете. Авторы, имеющие к ним отношение, могут порой сохранять авторские права на них. Информацию о многих утилитах можно найти в архиве Linux, по адресу http://www.ibiblio.org/pub/Linux. Мы надеемся, что новые версии будут появляться на этом Web-сайте по мере их выхода в свет. |
|
||||||||||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||
|