Как предотвратить переупорядочение вызовов функции, которая выводит журнальные сообщения на C?

Вопрос или проблема

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;
}

Общие рекомендации

  1. Проверка параметров функций: Серьезный подход к обработке входных данных может помочь избавиться от неожиданных сбоев в работе приложения. Хоть вы и не хотите защищать приложение от плохого кода, проверка критических входных данных может значительно упростить отладку.

  2. Используйте статический анализ: Используйте инструменты статического анализа кода, чтобы поймать потенциальные ошибки до компиляции.

  3. Документация и комментарии: Добавьте понятные комментарии к коду, чтобы другие разработчики понимали важность порядка вызова логирования.

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

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

Капча загружается...