Вопрос или проблема
typedef enum {
A = 0,
B = 1,
C = 2
} my_enum;
/**
* @warning Если random равен NULL или blabla недопустим, поведение этой функции неопределено.
*/
int foo(int *random, my_enum blabla)
{
//Вывод: [DBG][<timestamp>][<thread_name>][foo:<line_no>]: Вход - Привет, мир!
LOG_DEBUG_ENTERING("%s", "Привет, мир!");
int ret_code = 0;
*random = 50;
ret_code = do_something(*random, blabla);
//Вывод: [DBG][<timestamp>][<thread_name>][foo:<line_no>]: Выход - <ret_code>
LOG_DEBUG_EXITING("%d", ret_code);
return ret_code;
}
Я разрабатываю переносимое многопоточное приложение, которое может работать как на встроенных, так и на не встроенных системах, и совместимо с разными компиляторами, которые поддерживают как минимум c99.
Некоторые платформы, на которых я работаю, не имеют отладчика, поэтому перехват ошибок довольно сложен, и единственным инструментом, который может мне помочь в этих ситуациях, являются журнальные сообщения, генерируемые самим приложением.
Поэтому во все функции этого приложения я добавил макросы LOG_DEBUG_ENTERING()
и LOG_DEBUG_EXITING()
, чтобы иметь возможность отслеживать выполнение приложения.
Эти макросы/функции разработаны для того, чтобы гарантировать, что сообщение будет выведено, как только они закончат выполняться, или, в крайнем случае, сообщение будет помещено в очередь где-то (возможно, в другом процессе) для того, чтобы быть выведенным как можно скорее. Это зависит от реализации и платформы, в любом случае все сообщения будут выведены, даже если приложение аварийно завершится из-за ошибки.
Вышеуказанное – более или менее реальный пример в моем приложении.
Если foo()
вызывается одним из следующих способов, это приведет к аварийному завершению приложения:
int random;
foo(NULL, A); //Приложение завершится с ошибкой, когда foo() попытается присвоить 100 адресу, на который указывает random
foo(&random, 1000); //Авария, потому что поведение do_something() не определено, если blabla выходит за допустимые пределы
Я могу легко выяснить, где в коде приложение аварийно завершилось, по журнальным сообщениям, но я беспокоюсь, что компилятор может изменить порядок вызова макросов, например так:
int foo(int *random, my_enum blabla)
{
int ret_code = 0;
*random = 50;
ret_code = do_something(*random, blabla);
//Вывод: [DBG][<timestamp>][<thread_name>][foo:<line_no>]: Вход - Привет, мир!
LOG_DEBUG_ENTERING("%s", "Привет, мир!");
//Вывод: [DBG][<timestamp>][<thread_name>][foo:<line_no>]: Выход - <ret_code>
LOG_DEBUG_EXITING("%d", ret_code);
return ret_code;
}
В этом случае приложение завершится с ошибкой, не выведя никаких сообщений.
Как я могу предотвратить изменение порядка вызова LOG_DEBUG_ENTERING()
компилятором/ЦП?
ИЗМЕНЕНИЕ
Входные параметры foo()
и do_something()
не проверяются намеренно, идея заключается в том, что мы не защищаем наше приложение от плохого кода, но все же хотим иметь возможность ловить эти ошибки при необходимости.
Ответ или решение
Для предотвращения изменения порядка вызовов макросов LOG_DEBUG_ENTERING()
и LOG_DEBUG_EXITING()
в функции foo()
, можно использовать несколько подходов. Давайте рассмотрим их по порядку.
1. Использование конструкции goto
Можно использовать оператор goto
, который позволяет явно управлять порядком выполнения кода. В таком случае вы сможете гарантировать, что сначала будет выполнен вызов LOG_DEBUG_ENTERING()
, а затем LOG_DEBUG_EXITING()
, даже если произойдет ошибка.
int foo(int *random, my_enum blabla)
{
LOG_DEBUG_ENTERING("%s", "Hello world!");
int ret_code = 0;
if (random == NULL) {
ret_code = -1; // Примеры обработки ошибок
goto exit; // Переход сразу к выходу, если random NULL
}
*random = 50;
ret_code = do_something(*random, blabla);
exit:
LOG_DEBUG_EXITING("%d", ret_code);
return ret_code; // Здесь возвращаем результат
}
2. Использование макросов с do { ... } while (0)
Создайте макросы LOG_DEBUG_ENTERING
и LOG_DEBUG_EXITING
, которые будут реализованы в виде конструкции do { ... } while (0)
. Это ограничит их действие и гарантирует, что они будут выполнены в строго заданном порядке.
#define LOG_DEBUG_ENTERING(format, ...) do { \
// Реализация логирования входа \
} while(0)
#define LOG_DEBUG_EXITING(format, ...) do { \
// Реализация логирования выхода \
} while(0)
int foo(int *random, my_enum blabla)
{
LOG_DEBUG_ENTERING("%s", "Hello world!");
int ret_code = 0;
if (random == NULL) {
ret_code = -1;
goto exit;
}
*random = 50;
ret_code = do_something(*random, blabla);
exit:
LOG_DEBUG_EXITING("%d", ret_code);
return ret_code;
}
3. Использование __attribute__((cleanup))
в GCC
Если вы используете GCC, вы можете воспользоваться хорошей возможностью для обеспечения порядка вызова, используя атрибут __attribute__((cleanup))
. С его помощью можно определять "очистители", которые гарантированно будут вызваны при выходе из функции.
Создайте вспомогательную функцию для логирования выхода:
void log_exit(int ret_code) {
LOG_DEBUG_EXITING("%d", ret_code);
}
int foo(int *random, my_enum blabla) {
LOG_DEBUG_ENTERING("%s", "Hello world!");
int ret_code = 0;
if (random == NULL) {
ret_code = -1;
goto exit;
}
*random = 50;
ret_code = do_something(*random, blabla);
exit:
log_exit(ret_code);
return ret_code;
}
Общие рекомендации
-
Проверка параметров функций: Серьезный подход к обработке входных данных может помочь избавиться от неожиданных сбоев в работе приложения. Хоть вы и не хотите защищать приложение от плохого кода, проверка критических входных данных может значительно упростить отладку.
-
Используйте статический анализ: Используйте инструменты статического анализа кода, чтобы поймать потенциальные ошибки до компиляции.
-
Документация и комментарии: Добавьте понятные комментарии к коду, чтобы другие разработчики понимали важность порядка вызова логирования.
С помощью этих методов вы сможете минимизировать риск повторного порядка вызова ваших логирующих функций и гарантировать, что ваше приложение будет вести коррекцию логов, даже в случае возникновения проблем.