Подтвердить что ты не робот

Как получить обратную трассировку стека вызовов? (глубоко встроенный, без поддержки библиотеки)

Я хочу, чтобы мои обработчики исключений и функции отладки могли печатать обратные трассировки стека вызовов, в основном так же, как функция библиотеки backtrace() в glibc. К сожалению, моя библиотека C (Newlib) не предоставляет такой вызов.

У меня есть что-то вроде этого:

#include <unwind.h&gt // GCC internal unwinder, part of libgcc
_Unwind_Reason_Code trace_fcn(_Unwind_Context *ctx, void *d)
{
    int *depth = (int*)d;
    printf("\t#%d: program counter at %08x\n", *depth, _Unwind_GetIP(ctx));
    (*depth)++;
    return _URC_NO_REASON;
}

void print_backtrace_here()
{
    int depth = 0;
    _Unwind_Backtrace(&trace_fcn, &depth);
}

который в основном работает, но полученные следы не всегда полны. Например, если я делаю

int func3() { print_backtrace_here(); return 0; }
int func2() { return func3(); }
int func1() { return func2(); }
int main()  { return func1(); }

backtrace показывает только функции func3() и main(). (Это пример игрушки, но я проверил разборку и подтвердил, что эти функции здесь полностью, а не оптимизированы или встроены.)

Обновление:. Я пробовал этот код backtrace в старой системе ARM7, но с теми же (или, по крайней мере, как можно более эквивалентными) параметрами компилятора и компоновщиком script, и он печатает правильный, полный backtrace (т.е. func1 и func2 не пропущены), и даже в этом случае он даже возвращает предыдущий код в код инициализации загрузки. По-видимому, проблема связана не с компоновщиком script или с параметрами компилятора. (Кроме того, подтверждено из разборки, что в этом тесте ARM7 не используется указатель фрейма).

Код компилируется с помощью -fomit-frame-pointer, но моя платформа (bare metal ARM Cortex M3) определяет ABI, который в любом случае не использует указатель кадра. (В предыдущей версии этой системы использовался старый APCS ABI на ARM7 с принудительными кадрами стека и указателем кадра, а backtrace как здесь, который отлично работал).

Вся система скомпилирована с -fexception, которая обеспечивает необходимые метаданные, используемые _Unwind, в файл ELF. (_Unwind предназначен для обработки исключений, я думаю).

Итак, мой вопрос: Существует ли "стандартный", приемлемый способ получения надежных обратных трасс во встроенных системах с использованием GCC?

Я не возражаю, если вам придётся возиться со скриптами-компоновщиками и кодом crt0, но не хочу, чтобы у вас были какие-либо шансы на саму программу.

Спасибо!

4b9b3361

Ответ 1

Для этого вам нужно -funwind-tables или -fasynchronous-unwind-tables В некоторых целях это необходимо для правильной работы _Unwind_Backtrace!

Ответ 2

gcc возвращает оптимизацию. В func1() и func2() он не вызывает func2()/func3() - вместо этого он переходит к func2()/func3(), поэтому func3() может сразу вернуться к main().

В вашем случае func1() и func2() не нужно настраивать фрейм стека, но если они это сделают (например, для локальных переменных), gcc все равно может сделать оптимизацию, если вызов функции является последней инструкцией - Затем он очищает стек до перехода к func3().

Посмотрите на сгенерированный код ассемблера, чтобы увидеть его.


Edit/Update:

Чтобы убедиться, что это причина, выполните что-то после вызова функции, которая не может быть переупорядочена компилятором (например, с использованием возвращаемого значения). Или просто попробуйте выполнить компиляцию с -O0.

Ответ 3

Поскольку платформы ARM не используют указатель на фрейм, вы никогда не знаете, насколько велик стек и не может просто вывести его из одного возвращаемого значения в R14.

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

Если вы используете чистые исполняемые файлы ELF, вы можете отделять символы отладки от исполняемого файла выпуска. gdb может помочь вам узнать, что происходит с вашего стандартного дампа ядра unix

Ответ 4

Некоторые компиляторы, такие как GCC, оптимизируют вызовы функций, как вы упомянули в примере. Для работы фрагмента кода нет необходимости хранить промежуточные указатели возврата в цепочке вызовов. Это нормально, чтобы вернуться от func3() до main(), поскольку промежуточные функции не делают ничего лишнего, кроме вызова другой функции.

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

Если вы используете GCC, попробуйте -fno-optimize-sibling-calls

Другим удобным вариантом GCC является -mno-sched-prolog, что предотвращает переупорядочение команды в прологе функции, что очень важно, если вы хотите разобрать побайтовый код, как это делается здесь: http://www.kegel.com/stackcheck/checkstack-pl.txt

Ответ 5

Имеется ли в вашем исполняемом файле информация об отладке, от компиляции с опцией -g? Я думаю, что это требуется для получения полной трассировки стека без указателя кадра.

Вам может понадобиться -gdwarf-2, чтобы убедиться, что он использует формат, который включает в себя информацию для размотки.

Ответ 6

Это взломанно, но я нашел, что он работает достаточно хорошо, учитывая объем пространства в коде /RAM:

Предполагая, что вы используете режим ARM THUMB, выполните компиляцию со следующими параметрами:

-mtpcs-frame -mtpcs-leaf-frame  -fno-omit-frame-pointer

Для извлечения стоп-кода используется следующая функция. Обратитесь к комментариям за дополнительной информацией:

/*
 * This should be compiled with:
 *  -mtpcs-frame -mtpcs-leaf-frame  -fno-omit-frame-pointer
 *
 *  With these options, the Stack pointer is automatically pushed to the stack
 *  at the beginning of each function.
 *
 *  This function basically iterates through the current stack finding the following combination of values:
 *  - <Frame Address>
 *  - <Link Address>
 *
 *  This combination will occur for each function in the call stack
 */
static void backtrace(uint32_t *caller_list, const uint32_t *caller_list_end, const uint32_t *stack_pointer)
{
    uint32_t previous_frame_address = (uint32_t)stack_pointer;
    uint32_t stack_entry_counter = 0;

    // be sure to clear the caller_list buffer
    memset(caller_list, 0, caller_list_end-caller_list);

    // loop until the buffer is full
    while(caller_list < caller_list_end)
    {
        // Attempt to obtain next stack pointer
        // The link address should come immediately after
        const uint32_t possible_frame_address = *stack_pointer;
        const uint32_t possible_link_address = *(stack_pointer+1);

        // Have we searched past the allowable size of a given stack?
        if(stack_entry_counter > PLATFORM_MAX_STACK_SIZE/4)
        {
            // yes, so just quite
            break;
        }
        // Next check that the frame addresss (i.e. stack pointer for the function)
        // and Link address are within an acceptable range
        else if((possible_frame_address > previous_frame_address) &&
                ((possible_frame_address < previous_frame_address + PLATFORM_MAX_STACK_SIZE)) &&
               ((possible_link_address  & 0x01) != 0) && // in THUMB mode the address will be odd
                (possible_link_address > PLATFORM_CODE_SPACE_START_ADDRESS &&
                 possible_link_address < PLATFORM_CODE_SPACE_END_ADDRESS))
        {
            // We found two acceptable values

            // Store the link address
            *caller_list++ = possible_link_address;

            // Update the book-keeping registers for the next search
            previous_frame_address = possible_frame_address;
            stack_pointer = (uint32_t*)(possible_frame_address + 4);
            stack_entry_counter = 0;
        }
        else
        {
            // Keep iterating through the stack until be find an acceptable combination
            ++stack_pointer;
            ++stack_entry_counter;
        }
    }

}

Вам нужно будет обновить #defines для вашей платформы.

Затем вызовите следующее, чтобы заполнить буфер текущим стеком вызовов:

uint32_t callers[8];
uint32_t sp_reg;
__ASM volatile ("mov %0, sp" : "=r" (sp_reg) );
backtrace(callers, &callers[8], (uint32_t*)sp_reg);

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