Лог запуска функций при помощи GCC

Давайте представим, что вы делаете устройство, которое должно работать годами, но почему-то процесс вылетает через некоторое время без сообщения причин.
Ситуация довольно распространенная в мире встраиваемых систем. И если при программировании для микроконтроллеров такая ситуация решается при помощи сторожевого таймера (watchdog), то тут такой подход не подойдет. Ведь система не зависла, просто планировщик завершил один процесс.
Есть выход подобный сторожевому таймеру — супервизор. Вы создаете отдельное ПО, которое будет следить за тем, выполняется ли ваша программа и если нет, сигнализировать/перезапускать/выключаться и так далее.
Но это все не решает корня проблемы. Почему вылетает ПО?
Само по себе оно этого не сделает. На то есть причины. Самая обыденная — указатель, который пытается получить доступ к чужой памяти. Но как это отследить?
В принципе ответ напрашивается сам. Необходимо вести лог запуска функций. Самый простой вариант для этого — разметистить в начале и конце функции, которые пишут в лог имя функции и время. Но это рутинно и очень долго.
К счастью в GCC есть метод сделать это практически автоматически. Для этого можно использовать конструктор и деструктор функции.
Для этого ваш компилятор должен быть собран с флагом

-finstrument-functions

В большинстве дистрибутивов Linux компилятор поставляется с этим флагом.

Давайте рассмотрим на примере как реализовать логирование запуска функций при помощи GCC.
Создайте файл main.c

#include <stdio.h>
 
void foo() {
    printf("foo\n");
}
 
int main() {
    foo(); 
    return 0;
}

Самый обычный hello world.
Теперь нужно оприделить функции логирования.
Писать лог будем в файл.
trace.c:

#include 
#include 
 
static FILE *fp_trace;
 
void __attribute__ ((constructor)) trace_begin(void)
{
    fp_trace = fopen("trace.out", "w");
}
 
void __attribute__ ((destructor)) trace_end(void)
{
    if(fp_trace != NULL) 
    {
        fclose(fp_trace);
    }
}
 
void __cyg_profile_func_enter(void *func,  void *caller)
{
    if(fp_trace != NULL) 
    {
        fprintf(fp_trace, "e %p %p %lu\n", func, caller, time(NULL) );
    }
}
 
void __cyg_profile_func_exit(void *func, void *caller)
{
    if(fp_trace != NULL)
    {
        fprintf(fp_trace, "x %p %p %lu\n", func, caller, time(NULL));
    }
}

Две последние функции выполняются при входе/выходе из каждой вашей функции и записывают в файл адрес функции, откуда она вызвана и время. Также при помощи символов e и x метится вход это или выход.

Компилируем.

$ gcc -finstrument-functions -g -c -o main.o main.c
$ gcc -c -o trace.o trace.c
$ gcc main.o trace.o -o main

После запуска main вы увидите в текущем каталоге файл trace.out следующего содержания.

e 0x4006f8 0x7f604e869fe0 1420879817
e 0x4006c6 0x40071c 1420879817
x 0x4006c6 0x40071c 1420879817
x 0x4006f8 0x7f604e869fe0 1420879817

Это уже необходимые нам данные. Но формат конечно оставляет желать лучшего. И тут есть два пути хардкорный — смотреть в map файле что и откуда вызвано или же воспользоваться утилитой addr2line.
Забугорные товарищи же вообще предлагают такой скрипт:

#!/bin/sh
if test ! -f "$1"
then
 echo "Error: executable $1 does not exist."
 exit 1
fi
if test ! -f "$2"
then
 echo "Error: trace log $2 does not exist."
 exit 1
fi
EXECUTABLE="$1"
TRACELOG="$2"
while read LINETYPE FADDR CADDR CTIME; do
 FNAME="$(addr2line -f -e ${EXECUTABLE} ${FADDR}|head -1)"
 CDATE="$(date -Iseconds -d @${CTIME})"
 if test "${LINETYPE}" = "e"
 then
 CNAME="$(addr2line -f -e ${EXECUTABLE} ${CADDR}|head -1)"
 CLINE="$(addr2line -s -e ${EXECUTABLE} ${CADDR})"
 echo "Enter ${FNAME} at ${CDATE}, called from ${CNAME} (${CLINE})"
 fi
 if test "${LINETYPE}" = "x"
 then
 echo "Exit  ${FNAME} at ${CDATE}"
 fi
done < "${TRACELOG}"

Получаем уже человекочитабельный результат.

$ ./readtracelog.sh main trace.out 
Enter main at 2015-01-10T10:50:17+0200, called from ?? (??:0)
Enter foo at 2015-01-10T10:50:17+0200, called from main (hello.c:9)
Exit  foo at 2015-01-10T10:50:17+0200
Exit  main at 2015-01-10T10:50:17+0200

Вот и все. Надюсь поможет.

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий