Форум программистов, компьютерный форум, киберфорум
Наши страницы
Evg
Войти
Регистрация
Восстановить пароль
Рейтинг: 2.75. Голосов: 4.

Встроенная в программу печать стека при помощи внешнего отладчика

Запись от Evg размещена 15.02.2012 в 23:07
Обновил(-а) Evg 22.05.2014 в 23:57

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему

Причин для этого несколько.

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

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

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

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





Автор идеи - era.

Данный пример касается работы под unix'ами. Возможно, что всё это дело можно повторить и под Windows, но я слабо знаком с системным программированием под Windows, а потому по этому поводу сказать что-либо вразумительное затрудняюсь.

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

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

Сразу приведу исходники. Файл t.c - это образ нашей отлаживаемой программы. Модуль gdb.c и его интерфейсный файл gdb.h можно рассматривать как некую отладочную библиотеку, которую мы привинчиваем к нашей тестовой программе. Тестовая программа содержит ошибочную ситуацию и ASSERT, который должен эту ошибочную ситуацию поймать. В точке ASSERT'а помимо печати места исходника, где поймалась ситуация, делается дополнительная печать процедурного стека на момент срабатывания ASSERT'а. Что-то подробно расписывать не буду - всё написано в комментариях

Файл t.c

C
/* Файл t.c */
#include <stdio.h>
#include "gdb.h"
 
#define ASSERT(cond) \
  do \
  { \
    if (cond) \
      ; \
    else \
      { \
        fprintf (stderr, "==========================\n"); \
        fprintf (stderr, "Assertion \"%s\" failed at %s:%d\n", #cond, __FILE__, __LINE__); \
        fprintf (stderr, "==========================\n"); \
        gdb_PrintStackGDB(); \
        fprintf (stderr, "==========================\n"); \
      } \
  } while (0)
 
void
func1 (int x)
{
  /* Предполагаем, что x надодится в диапазоне от 0 до 9 включительно */
  ASSERT (x >=0 && x < 10);
}
 
/* func2 - чтобы в стеке вызовов появилась ещё одна функция */
void
func2 (int x)
{
  func1 (x);
}
 
int
main (int argc, char **argv)
{
  int i;
 
  /* Инициализация модуля работы с отладчиком */
  gdb_SetProgName (argv[0]);
 
  /* Здесь мы допустили ошибку и вместо "<" написали "<=".
   * ASSERT в функции func1 поймаемт нам эту ситуацию */
  for (i = 0; i <= 10; i++)
    func2 (i);
 
  return 0;
}

Файл gdb.c

C
/* Файл gdb.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
 
/* Печать ошибки после вызова системной функции */
#define SYS_ERROR(msg) \
  fprintf (stderr, "*** stack print error *** at %s %d: %s (%s)\n", \
           __FILE__, __LINE__, msg, strerror (errno));
 
/* ВНИМАНИЕ! Макрос использовать только внутри gdb_PrintStackGDB, потому
 * как содержит return. При таком подходе у нас останется неудалённым
 * временный файл, но я не стал заморачиваться с этим, чтобы не усложнять
 * программу, потому как такие ситуации мы считаем исключительными */
#define SYS_ASSERT(cond, err_msg) \
  do \
  { \
    if (cond) \
      ; \
    else \
      { \
        SYS_ERROR (err_msg); \
        return 0; \
      } \
  } while (0)
 
/* При запуске отладчика нужно будет указать путь до бинарника, чтобы
 * отладчик знал,откуда читать символьную таблицу и отладочную информацию.
 * В linux'е это можно сделать через ссылку /proc/self/exe, но это
 * не есть переносимый способ (т.е. на других unix'ах такое не отработает).
 * Поэтому самый простой вариант - это вынуть имя файла через argv[0],
 * но он не прокатит, если программа запускается через PATH или если
 * подсовывается фиктивный argv[0] */
static char *gdb_ProgramName;
 
void
gdb_SetProgName (char *prog_name)
{
  gdb_ProgramName = prog_name;
}
 
/* В случае удачи возвращаем 1 (true), иначе 0 (false). На тот случай,
 * если "наверху" это зачем-то надо */
int
gdb_PrintStackGDB (void)
{
  char buff[L_tmpnam];
  const char *scr_file_name;
  FILE *scr_file;
  pid_t child_pid;
 
  /* Создаем имя временного файла. Использовать "правильные" интерфейсы
   * типа tempnam или tmpfile не получится, потому как имя файла нам надо
   * передать в gdb, а с этими интерфейсами имя файла не доступно.
   * При линковке программы под linux'ом мы увидим предупреждение что-то типа
   * "warning: the use of `tmpnam' is dangerous, better use `mkstemp'" */
  scr_file_name = tmpnam (buff);
  SYS_ASSERT (scr_file_name != NULL, "failed to gen tmp file name");
 
  /* В файле формируем приказы для gdb. Этот файл будет передан по опции
   * в отладчик. В нешем случае нам нужно напечатать стек (bt) и выйти (q) */
  scr_file = fopen (scr_file_name, "w");
  SYS_ASSERT (scr_file, "child: failed access gdb script");
  fprintf (scr_file, "bt\nq\n");
  fclose (scr_file);
 
  /* Форкаемся для запуска отладчика в дочернем процессе и ожижания
   * в родительском. Перед fork'ом можно сделать создать pipe, в дочернем
   * процессе через dup2 замкнуть не него stdout и stderr, а в родительском
   * процессе прочитать оттуда текст (выдачу отладчика) с тем, чтобы
   * отфильтровать ненужные строки. Я этого делать не стану, дабы не усложнять
   * программу. Тем более, что в разных версиях отладчика печать может выглядеть
   * по разному */
  child_pid = fork();
  SYS_ASSERT (child_pid >= 0, "fork failed");
 
  if (child_pid == 0)
    {
      char pid_str[32];
 
      /* Дочерний процесс: запускаем gdb
       * Отладчику передаём pid процесса, к которому он должен прицепиться.
       * Поскольку мы после fork'а находимся в дочернем процессе, то для нас
       * это означает pid родительского процесса */
      sprintf (pid_str, "%d", (int)getppid());
 
      /* Исходим из предположения, что gdb находится в путях, прописанных
       * в PATH. Если у кого-то это не так, то вместо "gdb" надо будет прописать
       * полный путь до отладчика */
      execlp ("gdb", "gdb", gdb_ProgramName, pid_str, "-q", "--batch", "-x", scr_file_name, NULL);
 
      /* В случае успешного запска сюда не попадём */
      SYS_ASSERT (0, "child: failed to exec gdb");
    }
  else
    {
      pid_t exited_pid;
      int child_status;
 
      /* Родительский процесс: ожидаем завершение работы дочернего процесса
       * (из-под которого запускается gdb) */
      exited_pid = wait (&child_status);
      SYS_ASSERT (exited_pid == child_pid, "parent: error waiting child to die");
      SYS_ASSERT (WIFEXITED (child_status) && WEXITSTATUS (child_status) == 0,
                  "parent: abnormal child termination");
    }
 
  /* Временный файл надо удалятьручками */
  unlink (scr_file_name);
 
  return 1;
}

Файл gdb.h

C
/* Файл gdb.h */
#ifndef __GDB_H__
#define __GDB_H__
 
extern void
gdb_SetProgName (char *prog_name);
 
extern int
gdb_PrintStackGDB (void);
 
#endif /* __GDB_H__ */


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

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

Код:
$ gcc -g t.c gdb.c
/tmp/ccZhSSyU.o: In function `gdb_PrintStackGDB':
gdb.c:60: warning: the use of `tmpnam' is dangerous, better use `mkstemp'
Исполнение. Здесь привожу результат, полученный на Ubuntu-9.10

Код:
$ ./a.out 
==========================
Assertion "x >=0 && x < 10" failed at t.c:23
==========================
0x00d02422 in __kernel_vsyscall ()
#0  0x00d02422 in __kernel_vsyscall ()
#1  0x0046a72d in wait () from /lib/tls/i686/cmov/libc.so.6
#2  0x08048aa5 in gdb_PrintStackGDB () at gdb.c:105
#3  0x080487dd in func1 (x=10) at t.c:23
#4  0x08048818 in func2 (x=10) at t.c:30
#5  0x08048846 in main (argc=1, argv=0xbf94b774) at t.c:44
A debugging session is active.

	Inferior 1 [process 7760] will be detached.

Quit anyway? (y or n) [answered Y; input not from terminal]
==========================
Обратите внимание на то, что печатается не чистый стек в нужной нам точке, а несколько расширенный. Причина этого вполне понятна. В интересующей нас точке мы вызываем функцию gdb_PrintStackGDB, а та в свою очередь ещё паровоз системных функций и в итоге программа в ожидании отладчика будет висеть немного в другом месте. Однако часть стека, начинающаяся с gdb_PrintStackGDB - это именно то, что нам нужно.

В моём примере я сделал самый простой способ: сделал обычный fork без каких-либо манипуляций с дескрипторами вывода (stdout и stderr), в результате чего вся выдача из-под отладчика попала в стандартную печать. При желании (об этом написано в комментарии около вызова fork'а) можно было бы создать pipe, замкнуть на один его конец десrрипторы stdout и stdin (в результате чего вся печать отладчика попала бы в трубопровод), а с другого конца трубопровода эти данные прочитать. Такой сложный способ пригодится, например, в том случае, когда мы заходит отфильтровать в печати лишние строки, выдаваемые отладчиком. Или, например, когда мы печать хотим отобразить в файл (а не в консоль). На мой взгляд такая постановка задачи была бы интересна тем, кто начинает изучать системное программирование под unix. В своём примере я этого делать не буду, оставлю это тем, кому интересно сделать самому.

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

Выдача на i386-linux (gentoo)

Код:
==========================
Assertion "x >=0 && x < 10" failed at t.c:23
==========================
Using host libthread_db library "/lib/libthread_db.so.1".
0xb7fe0424 in __kernel_vsyscall ()
#0  0xb7fe0424 in __kernel_vsyscall ()
#1  0xb7f0215b in fork () from /lib/libc.so.6
#2  0x080488d8 in gdb_PrintStackGDB () at gdb.c:77
#3  0x08048743 in func1 (x=10) at t.c:23
#4  0x0804877b in func2 (x=10) at t.c:30
#5  0x080487af in main (argc=Cannot access memory at address 0x0
) at t.c:44
The program is running.  Quit anyway (and detach it)? (y or n) [answered Y; input not from terminal]
==========================

Выдача на sparc-linux

Код:
==========================
Assertion "x >=0 && x < 10" failed at t.c:23
==========================
0xf7e98f04 in wait () from /lib/libc.so.6
#0  0xf7e98f04 in wait () from /lib/libc.so.6
#1  0x00010ca0 in gdb_PrintStackGDB () at gdb.c:105
#2  0x000108e4 in func1 (x=10) at t.c:23
#3  0x0001092c in func2 (x=10) at t.c:30
#4  0x00010970 in main (argc=1, argv=0xffe89a04) at t.c:44
The program is running.  Quit anyway (and detach it)? (y or n) [answered Y; input not from terminal]
==========================

Выдача на sparc-solaris

Код:
==========================
Assertion "x >=0 && x < 10" failed at t.c:23
==========================
Retry #1:
Retry #2:
Retry #3:
Retry #4:
[New LWP 1]
0xff31f9b4 in _wait () from /usr/lib/libc.so.1
#0  0xff31f9b4 in _wait () from /usr/lib/libc.so.1
#1  0x00010d10 in gdb_PrintStackGDB () at gdb.c:105
#2  0x000109bc in func1 (x=10) at t.c:23
#3  0x000109f4 in func2 (x=10) at t.c:30
#4  0x00010a44 in main (argc=1, argv=0xffbff9ac) at t.c:44
==========================


Отладчик gdb позволяет задать при запуске файл сценария, в котором записаны инструкции, которые отладчик исполнит во время запуска. В файле gdb.c смотрите запись текста в файл scr_file. В нашем случае в файл сценария мы помещаем приказы "bt" (backtrace - печать стека) и "q" (quit). Можно было бы воткнуть приказ что-то типа "подняться по стеку на 3 активации вверх (чтобы попасть в реальную точку вызова gdb_PrintStackGDB) и напечатать значение переменной x". Такой вариант можно тыкнуть в какую-нибудь критическую точку, а потом в длинном запуске посмотреть, из каких мест программы и с какими значениями переменных вызывалась нужная функция. В общем, возможностей море и ограничиваются они только собственной фантазией и теми способами отладки, к которым вы привыкли.

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

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

Использование внешнего отладчика для печати стека - не единственный способ. См. http://www.cyberforum.ru/faq/thread235921.html#post2115288, а так же замечания относительно этого механизма: раз и два

Ещё одно интересное применение данной технологии - помимо печати стека ещё и сбросить core файл: http://www.cyberforum.ru/cpp-linux/thread421216.html. Полезность данного механизма заключается в том, что перед сбросом core-файла можно сначала проанализировать, нужен ли он вообще. А так же сразу же правильным образом назвать core-Файл (потому что в варианте "по умолчанию" создаются файлы с именами core.<pid>, и в случае большого количества файлов невозможно будет нормально понять, какой core-файл к какому тестовому примеру относился). При этом в процессе работы могут быть проблемы, связанные с тем, что библиотеки с опциями типа -fomit-frame-pointer, из-за чего становится невозможной раскрутка стека

Одна из возможных проблем, которая делает невозможной применение указанной нанотехнологии - исчерпание пользовательского стека, в результате чего обработчику сигнала просто негде работать, т.к. ему нужно стековое пространство. Постановка проблемы и способ решения. FIXME включить всё это дело в исходник программы и написать комментарий
Просмотров 3321 Комментарии 4
Всего комментариев 4
Комментарии
  1. Старый комментарий
    едва ли это так
    Цитата:
    Правда по текущему состоянию выясняется, что на i386 файл core сбрасывается кривой и нормально это дело работает только в варианте со статической линковкой
    Запись от Super-Windоws размещена 28.09.2012 в 16:00 Super-Windоws вне форума
  2. Старый комментарий
    Аватар для Evg
    Цитата:
    Сообщение от Super-Windоws
    едва ли это так
    Осуждение продолжено здесь. Суть проблемы более-менее выяснили, текст поправил
    Запись от Evg размещена 02.10.2012 в 16:49 Evg вне форума
  3. Старый комментарий
    Аватар для ISergey
    Цитата:
    Данный пример касается работы под unix'ами. Возможно, что всё это дело можно повторить и под Windows, но я слабо знаком с системным программированием под Windows, а потому по этому поводу сказать что-либо вразумительное затрудняюсь.
    http://www.codeproject.com/Articles/...-the-callstack
    Запись от ISergey размещена 26.03.2014 в 20:41 ISergey вне форума
  4. Старый комментарий
    Аватар для Evg
    А что такое класс StackWalker? Это что-то среди стандартных библиотек visual c? При таком раскладе это скорее всего аналоги linux'овых функций http://www.cyberforum.ru/faq/thread2...ml#post2115288. Что есть не совсем то
    Запись от Evg размещена 26.03.2014 в 21:52 Evg вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru