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

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

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

#include <unwind.h&gt // GCC's 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(); }

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

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

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

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

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

Я не против возиться со скриптами компоновщика и кодом crt0, если это необходимо, но я не хочу иметь никаких шансов для самой цепочки инструментов.

Спасибо!

Ответы [ 6 ]

10 голосов
/ 04 августа 2011

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

7 голосов
/ 03 августа 2010

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

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

7 голосов
/ 03 августа 2010

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

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

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


Редактировать / Update:

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

3 голосов
/ 25 февраля 2014

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

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

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

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

1 голос
/ 06 апреля 2017

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

Предполагая, что вы используете режим 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);

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

0 голосов
/ 04 августа 2010

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

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

...