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

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

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

#include <unwind.h> // 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 не пропущены) и, действительно, он даже прослеживает путь от main к коду инициализации загрузки. Так что, по-видимому, проблема не в скрипте компоновщика или параметрах компилятора. (Кроме того, после разборки было подтверждено, что в этом тесте ARM7 указатель кадра также не используется).

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

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

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

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

Спасибо!


person hugov    schedule 03.08.2010    source источник
comment
Множество дубликатов, в том числе stackoverflow.com/questions/77005/   -  person    schedule 03.08.2010
comment
Нил: Ты прочитал вопрос? (рядом с заголовком и жирной печатной строкой?) Он получает обратную трассировку, но в ней отсутствуют некоторые вызываемые функции.   -  person IanH    schedule 03.08.2010
comment
Это было полезно для печати обратной трассировки в проектах Android NDK.   -  person chrisvarnz    schedule 16.07.2014
comment
Решил ли какой-либо из ответов ваши проблемы?   -  person tothphu    schedule 29.10.2014


Ответы (6)


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

person King Sumo    schedule 04.08.2011
comment
Я понятия не имею, что делает эта опция, но вам также может понадобиться указать --no-merge-exidx-entries при связывании. old.nabble.com/Stack-backtrace-for-ARM- Thumb-td29264138.html - person Justin L.; 16.11.2012
comment
@ДжастинЛ. - ссылка в настоящее время мертва, FWIW. - person 500 - Internal Server Error; 15.05.2017

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

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

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

person doron    schedule 03.08.2010
comment
Вы можете уменьшить количество ложных срабатываний, используя дизассемблированный исполняемый файл для ручной реконструкции кадров стека; посмотрите на первые несколько инструкций каждой функции для подсчета стековых регистров и любых дальнейших настроек указателя стека. - person Mike Seymour; 03.08.2010
comment
Придирка: некоторые платформы ARM используют указатель кадра (обычно r11). Но это здесь не важно, так как вопрошающий утверждает, что его платформа не имеет значения. - person Mike Seymour; 03.08.2010
comment
Майк: да, я мог бы сделать это (сам)... но наверняка есть какой-то код или библиотека, которые я могу использовать, которые уже делают это?! Конечно, в контексте исключений каждый возможный кадр стека должен содержать необходимые метаданные (как минимум, размер), чтобы раскрутить стек. Таким образом, учитывая, что обработка исключений работает, почему собственный разматыватель gcc не может сделать это за меня? - person hugov; 04.08.2010
comment
@hugov: обработка исключений должна знать, какие объекты уничтожить, куда перейти и в какое состояние восстановить стек. Ему не нужно знать полный стек вызовов, поэтому я не ожидаю, что смогу восстановить полную трассировку стека, если компилятор специально не решит поддерживать это. Исходя из вашего опыта, я предполагаю, что это не так, но я могу ошибаться. - person Mike Seymour; 04.08.2010
comment
@Mike Seymour - технически ассемблер ARM даже не имеет встроенной концепции стека. Ближе всего мы подошли к инструкциям LDM и STM. Таким образом, вы можете реализовать стек как угодно. Вызов процедуры ARM, который используется для большинства стандартных ARM ABI, не поддерживает указатель кадра, но ничто, кроме совместимости, не остановит вас от использования указателя кадра. - person doron; 04.08.2010
comment
@deus: Действительно, хотя в Thumb есть инструкции push и pop, которые предполагают полный нисходящий стек с r13 в качестве указателя стека, поэтому концепция стека там проскользнула в сборку. Текущий ABI не имеет концепции указателя фрейма, но в более старых версиях были варианты, которые позволяли разматывать в те дни, когда для этого нельзя было полагаться на отладочную информацию. - person Mike Seymour; 04.08.2010
comment
Ваше предложение искать в стеке указатели инструкций — это в основном то, что делает github.com/armink/CmBacktrace, по крайней мере, ложные срабатывания лучше, чем ложные отрицания. - person satur9nine; 14.01.2020

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

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

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


Изменить/обновить:

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

person IanH    schedule 03.08.2010
comment
Он говорит, что функции есть (не встроенные), но он не сказал, проверял ли он, вызываются ли функции или переходили к ним. - person IanH; 03.08.2010
comment
@DeadMG: отрицательный голос, безусловно, резкий. Вызовы хвоста обычно оптимизируются таким образом при компиляции для ARM, и эта оптимизация даст точно наблюдаемые результаты. - person Mike Seymour; 03.08.2010
comment
ОП специально сказал, что проверил дизассемблер. - person Puppy; 03.08.2010
comment
@DeadMG: Он сказал, что проверил, что функции были вызваны, а не встроены, но, возможно, он пропустил функции, заканчивающиеся ветвью, а не возвратом. Это не то, что вы заметите, если внимательно не прочитаете каждую инструкцию. Конечно, ваши голоса принадлежат вам, и вы можете распоряжаться ими так, как считаете нужным. - person Mike Seymour; 04.08.2010
comment
@DeadMG: Даже взглянув на разборку, если вы не знаете об этой оптимизации, вы легко можете проследить, есть ли вызов или прыжок. Я все еще думаю, что здесь проблема - другой ответ интересен, но он не объясняет, почему в трассировке есть только func3() и main(). (а не только func3() и func2()). - person IanH; 04.08.2010
comment
Чтобы уточнить: упрощенный игрушечный код в исходном посте мог выполнить оптимизацию обратного вызова/перехода, но в фактическом коде есть вещи по обе стороны от вызова, которые не могут (и я убедился, что они не оптимизируются). прочь. В начале и в конце каждой функции есть push/pop, а следующая функция в цепочке вызывается с помощью инструкции blx (Thumb2). - person hugov; 04.08.2010

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

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

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

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

person user3351650    schedule 25.02.2014

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

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

}

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

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

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

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

person driedler    schedule 05.04.2017
comment
Хакерский, несколько работает. Я мог бы получить 2 кадра стека, используя этот метод. _Unwind_Backtrace() и libunwind дали мне все 4 кадра. - person Alexei Khlebnikov; 27.04.2018

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

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

person Mike Seymour    schedule 04.08.2010
comment
Возможно, хотя я почти уверен (на 99,9%), что информация о DWARF на самом деле не попадает в двоичный образ, запрограммированный во флэш-память. Как бы я проверил? - person hugov; 05.08.2010