2.6. Отладка

2.6.1. Отладчик

Отладчик, поставляемый с FreeBSD, называется gdb (GNU debugger). Он запускается по команде

    % gdb progname
         

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

    M-x gdb RET progname RET
         

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

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

Наконец, если вы находите, что его выдача команд в стиле командной строки в текстовом режиме неудобна, то в Коллекции Портов для него имеется графический инструмент xxgdb.

Этот раздел предназначен быть введением в использование gdb и не покрывает специальные вопросы, такие, как отладка ядра.

2.6.2. Запуск программы в отладчике

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

    ... (no debugging symbols found) ...

при запуске gdb, то определите, что программа не была откомпилирована с опцией -g.

В приглашении gdb наберите команду break main. Это укажет отладчику пропустить предварительный подготовительный код программы и начать сразу с вашего кода. Теперь выдайте команду run для запуска программы--она начнет выполняться с подготовительного кода и затем будет остановлена отладчиком при вызове main(). (Если вы когла-либо удивлялись, откуда вызывается main(), то теперь вы должны знать!).

Теперь вы можете выполнить программу построчно по шагам, нажимая n. Если вы попали на вызов функции, то можете перейти в нее, нажав s. Оказавшись в вызове функции, вы можете вернуться из пошагового выполнения функции нажатием f. Вы можете также использовать команды up и down для просмотра вызывающей подпрограммы.

Вот простой пример того, как выявить ошибку в программе при помощи gdb. Это наша программа (с намеренно допущенной ошибкой):

    #include <stdio.h>
    
    int bazz(int anint);
    
    main() {
        int i;
    
        printf("This is my program\n");
        bazz(i);
        return 0;
    }
    
    int bazz(int anint) {
        printf("You gave me %d\n", anint);
        return anint;
    }
         

Эта программа устанавливает значение переменной i равным 5 и передает ее в функцию bazz(), которая выводит переданное нами число.

При компиляции и запуске программы мы получили

    % cc -g -o temp temp.c
    % ./temp
    This is my program
    anint = 4231
         

Это не то, что мы ожидали! Самое время посмотреть, что же происходит!

    % gdb temp
    GDB is free software and you are welcome to 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.
    GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc.
    (gdb) break main               Пропуск начального кода
    Breakpoint 1 at 0x160f: file temp.c, line 9.    gdb устанавливает точку останова в main()
    (gdb) run                   Запуск до вызова main()
    Starting program: /home/james/tmp/temp      Программа начинает работать
    
    Breakpoint 1, main () at temp.c:9       gdb останавливается в main()
    (gdb) n                       Переход к следующей строке
    This is my program              Программа выводит
    (gdb) s                       Переход в bazz()
    bazz (anint=4231) at temp.c:17          gdb выводит стек вызовов
    (gdb)
         

Минуточку! Как параметр anint оказался равным 4231? Разве мы не присвоили ему значение 5 в функции main()? Давайте перейдем к функции main() и взглянем туда.

    (gdb) up                   Переход вверх по стеку вызовов
    #1  0x1625 in main () at temp.c:11      gdb выводит стек вызовов
    (gdb) p i                   Вывод значения переменной i
    $1 = 4231                   gdb выводит 4231
         

О боже! Судя по коду, мы забыли инициализировать переменную i. Вы хотели сделать вот что

    ...
    main() {
        int i;
    
        i = 5;
        printf("This is my program\n");
    ...
         

но забыли про строку i=5;. Так как мы не присвоили начальное значение для i, то переменная принимает случайное значение, оказывающее в соответствующей области памяти при работе программы, и в нашем случае это оказалось число 4231.

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

2.6.3. Исследование файла дампа

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

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

    (gdb) core progname.core
         

Если вы не в том же каталоге, что и файл дампа, то вам нужно сначала выполнить команду dir /path/to/core/file.

Вы должны увидеть нечто вроде следующего:

    % gdb a.out
    GDB is free software and you are welcome to 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.
    GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc.
    (gdb) core a.out.core
    Core was generated by `a.out'.
    Program terminated with signal 11, Segmentation fault.
    Cannot access memory at address 0x7020796d.
    #0  0x164a in bazz (anint=0x5) at temp.c:17
    (gdb)
         

В этом случае программа называлась a.out, так что файл дампа называется a.out.core. Мы можем видеть, что программа завершилась аварийно из-за попытки доступа к области памяти, ей недоступной, в функции bazz.

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

    (gdb) bt
    #0  0x164a in bazz (anint=0x5) at temp.c:17
    #1  0xefbfd888 in end ()
    #2  0x162c in main () at temp.c:11
    (gdb)
         

При сбое программы была вызвана функция end(); в этом случае функция bazz() была вызвана из main().

2.6.4. Подключение к работающей программе

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

В этой ситуации нужно запуститть еще один отладчик gdb, воспользоваться командой ps для поиска идентификатора порожденного процесса и выполнить команду

    (gdb) attach pid
         

в gdb, после чего отлаживать программу обычным образом.

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

    ...
    if ((pid = fork()) < 0)     /* _Always_ check this */
        error();
    else if (pid == 0) {        /* child */
        int PauseMode = 1;
    
        while (PauseMode)
            sleep(10);  /* Wait until someone attaches to us */
        ...
    } else {            /* parent */
        ...
         

Теперь все, что вам нужно сделать, это подключиться к порожденному процессу, установить значение переменной PauseMode в 0 и дождаться возврата из вызова функции sleep()!