• Типы ошибок
  • Общие методы отладки
  • Программа с ошибками
  • Анализ кода
  • Оснащение средствами контроля
  • Контролируемое выполнение
  • Отладка с помощью gdb
  • Запуск gdb
  • Выполнение программы
  • Трассировка стека
  • Просмотр переменных
  • Вывод листинга программы
  • Установка точек останова
  • Вставка исправлений с помощью отладчика
  • Дополнительные сведения о gdb
  • Дополнительные средства отладки
  • Lint удаление ошибок из ваших программ
  • Средства, отслеживающие вызовы функций
  • Выполнение профилирования с помощью prof/gprof
  • Проверки соблюдения условий
  • Устранение ошибок использования памяти
  • ElectricFence
  • valgrind
  • Резюме 
  • Глава 10

    Отладка

    По утверждению Software Engineering Institute (Институт программных разработок) и IEEE (Institute of Electrical and Electronics Engineers, Институт инженеров по электротехнике и электронике) в любом значимом фрагменте программного обеспечения первоначально всегда есть дефекты, примерно два на 100 строк программного кода. Эти ошибки приводят к тому, что программы и библиотеки не работают так, как требуется, часто заставляя программу вести себя иначе, чем предполагалось. Отслеживание ошибок, их идентификация и удаление могут потребовать от программиста больших затрат времени на этапе разработки.

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

    Будут обсуждаться следующие темы:

    □ типы ошибок;

    □ общие методы отладки;

    □ отладка с помощью gdb и других средств;

    □ проверка соблюдения условий (макрос

    assert
    );

    □ устранение ошибок использования памяти.

    Типы ошибок

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

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

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

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

    Примечание

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

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

    Общие методы отладки

    Существует несколько разных подходов к отладке и тестированию типовой программы 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 сгенерируют сигнал о нарушении сегментации.

    Примечание

    Некоторые библиотечные функции, такие как

    printf
    , в определенных обстоятельствах также будут препятствовать некорректному доступу, например при использовании указателя
    null
    .

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

    Если вы увеличите размер элемента массива, заменив элемент типа

    item
    массивом из 4096 символов, любое обращение к несуществующему элементу массива, возможно, окажется за пределами выделенной памяти. Каждый элемент массива равен 4 Кбайт, поэтому некорректно используемый участок памяти будет находиться за концом массива на расстоянии от 0 до 4 Кбайт.

    Если мы внесем эту поправку, назвав результат debug3.c, то получим ошибку сегментации в версиях Linux обоих авторов.

    /* 2 */ char data[4096];


    $ сс -о debug3 debug3.с

    $ ./debug3

    Segmentation fault

    Возможно, что какие-то варианты систем Linux или UNIX все еще не будут выдавать сообщение об ошибке сегментации. Когда стандарт ANSI С утверждает, что поведение не определено, на самом деле он разрешает программе делать все, что угодно. Это выглядит так, как будто мы написали не удовлетворяющую стандартам программу на языке С, и она может демонстрировать очень странное поведение! Как видите, изъян в программе переводит ее в категорию программ с непредсказуемым поведением.

    Анализ кода

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

    Примечание

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

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

    Примечание

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

    gcc -Wall -pedantic -ansi

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

    Wall
    . Она генерирует полезную информацию при обнаружении ошибок в программе.

    Чуть позже мы кратко обсудим и другие средства,

    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

    Макрос Описание
    __LINE__
    Десятичная константа, предоставляющая номер текущей строки
    __FILE__
    Строка, предоставляющая имя текущего файла
    __DATE__
    Строка в форме "ммм дд гггг", текущая дата
    __TIME__
    Строка в форме "чч:мм:сс", текущее время

    Выполните упражнение 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
    — один из обычно применяемых при компиляции программы с последующей отладкой. Вы должны указывать его при компиляции всех исходных файлов, которые нуждаются в отладке, а также для компоновщика, чтобы могли применяться специальные версии стандартной библиотеки С, обеспечивающие поддержку режима отладки в библиотечных функциях. Программа компилятора передаст флаг компоновщику автоматически. Отладка может применяться и с библиотеками, не откомпилированными для этой цели, но с меньшей гибкостью.

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

    Примечание

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

    strip <файл>
    .

    Отладка с помощью 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) {

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

    print
    отображает содержимое переменных и других выражений:

    (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];

    Просмотреть элементы передаваемого массива можно, применив выражение в команде

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

    (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 с программными средствами разработки.

    Примечание

    Эти утилиты и другие, упоминаемые в этой главе, могут не входить в состав вашего дистрибутива Linux. Если они пропущены, можно поискать их реализации в Интернете. Хорошая отправная точка (для дистрибутивов Linux, поддерживающих формат RPM-пакетов) — Web-сайты http://rpmfind.net и http://rpm.pbone.net. Можно попытаться поискать в нескольких репозитариях для конкретных дистрибутивов, включая http://ftp.gwdg.de/pub/opensuse/ для openSUSE, http://rpm.livna.org для Fedora и http://packages.slackware.it/ для Slackware.

    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 предоставляет ряд мощных инструментов для удаления ошибок из ваших программ. Вы устранили несколько ошибок в программе с помощью отладчика gdb и познакомились с некоторыми средствами статического анализа, такими как

    cflow
    и
    splint
    . В заключение были рассмотрены проблемы, возникающие при использовании динамически распределяемой памяти, и некоторые средства, способные помочь обнаружить их, например ElectricFence и
    valgrind
    .

    Утилиты, обсуждавшиеся в этой главе, в основном хранятся на FTP-серверах в Интернете. Авторы, имеющие к ним отношение, могут порой сохранять авторские права на них. Информацию о многих утилитах можно найти в архиве Linux, по адресу http://www.ibiblio.org/pub/Linux. Мы надеемся, что новые версии будут появляться на этом Web-сайте по мере их выхода в свет. 








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх