Перейти к содержанию
    

Раскрутка стека вызовов при анализе сбоев

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

Ничего готового я не нашел, хотя возможно все лежит на поверхности. Сейчас я реализовал это через одно место (жутко процессоро- и компиляторо- зависисимо  -  IAR и Thumb-2). 

void assertSave ( void)
{
    uint32_t   sp;
    uint16_t * pc;
    uint32_t   report[8];
    uint32_t   count;

    __ASM volatile ("mov %0, sp" : "=r" (sp) );
    __ASM volatile ("mov %0, pc" : "=r" (pc) );
    memset(report, 0, sizeof(report));
    for (uint32_t i = 0; i < 8; i++) {
        while (1) {
            // check pc
            if (((uint32_t)pc < IFLASH_START_ADDRESS) || ((uint32_t)pc > IFLASH_END_ADDRESS)) break;
            // check sp
            if (((uint32_t)sp < IRAM_START_ADDRESS)   || ((uint32_t)sp > IRAM_END_ADDRESS))   break;
            // find POP or POP.W
            if (((*pc & 0xFF00) == 0xBD00) || (*pc == 0xE8BD)) {
                // count = number of registers to be restored by POP command
                if (*pc == 0xE8BD) {
                    count = (uint32_t)(*(pc + 1) & 0x1FFF);
                } else {
                    count = (uint32_t)(*pc & 0x00FF);
                }
                count = count - ((count >> 1) & 0x55555555);
                count = (count & 0x33333333) + ((count >> 2) & 0x33333333);
                count = (((count + (count >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
                // checking previous command is ADD SP,SP,XX
                if ((*(pc - 1) & 0xFF00) == 0xB000) {
                    // count += size of the stack correction before POP
                    count += (uint8_t)(*(pc - 1) & 0x00FF);
                }
                // getting stack frame of previous function
                sp += count << 2;
                // getting continue address of previous function 
                pc  = (uint16_t *)(*(uint32_t *)sp & 0xFFFFFFFE);
                sp += 4;
                // save address of CALLSTACK
                report[i] = (uint32_t)pc - 4;
                break;
            }
            pc += 1;
        }
    }
    // write to blackbox
    blackBoxWrite(report, sizeof(report));
}

По сути я просматриваю от текущего PC до ближайшего возврата через POP или POP.W. По коррекции стека перед выходом определяю размер текущего фрейма стека с предположением что первым в стек компилятор кладет адрес возврата. Ну и дальше рекурсия.

Возможно у кого-то есть более грамотное решение.

Изменено пользователем haker_fox
Добавил теги к популярной теме.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

А что так хитро сделан подсчет битов в поле <registers>? 
Вместо того чтобы быстрее посчитать можно было бы залогить какие регистры сохраняются и дополнительно проверить валидность состава регистров. 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

4 часа назад, Nixon сказал:

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

...

Возможно у кого-то есть более грамотное решение.

Не понятно - что именно нужно? Чем не устраивает просто захватить/выдать_в_лог N слов от верхушки стека? А дальше: листинг в руки и "раскрутка" - по нему врукопашную.

И непонятно - что Вы собираетесь найти по *pc своим кодом? Функция вполне себе запросто может завершаться безусловным переходом - Ваш код выдаст ерунду. Это самый распространённый случай. Не говоря уже о множестве других вариантов.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

15 часов назад, jcxz сказал:

Не понятно - что именно нужно? Чем не устраивает просто захватить/выдать_в_лог N слов от верхушки стека? А дальше: листинг в руки и "раскрутка" - по нему врукопашную.

И непонятно - что Вы собираетесь найти по *pc своим кодом? Функция вполне себе запросто может завершаться безусловным переходом - Ваш код выдаст ерунду. Это самый распространённый случай. Не говоря уже о множестве других вариантов.

 

Сохранить верхушку стека недостаточно. Функция FOO() в которой возник assert вызывается из многих мест, цепочка вызовов достаточно длинная, и нужно знать на каком именно этапе произошло формирование ошибочного набора параметров для FOO(). Кроме того место для логгирования сбоев ограничено. Но как вариант просто сохранять кадр стека (побольше) имеет место быть. В любом случае нужна ручная работа. По поводу возврата из функции не через POP - тоже верно, такое возможно, можно учесть и этот случай, но я в своем проекте такого не встречал (возможно это зависит от уровня оптимизации).

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

15 часов назад, AlexandrY сказал:

А что так хитро сделан подсчет битов в поле <registers>?

Была такая функция подсчета битов, я ее тупо скопипастил. 

15 часов назад, AlexandrY сказал:

Вместо того чтобы быстрее посчитать можно было бы залогить какие регистры сохраняются и дополнительно проверить валидность состава регистров. 

А какая разница что считать - сохраненные регистры или восстановленные? Разве что искать вверх придется только команды коррекции стека и PUSH. Но их тоже может не быть. 

 

Я честно говоря надеялся, что существует что-то стандартное для раскрутки стека вызовов.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

15 часов назад, jcxz сказал:

Функция вполне себе запросто может завершаться безусловным переходом - Ваш код выдаст ерунду. Это самый распространённый случай. Не говоря уже о множестве других вариантов.

 

Да, спасибо, нашел такое. Добавил еще поиск такого способа выхода

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

16 минут назад, Nixon сказал:

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

Я для себя сделал следующий механизм...

trap.h:

#define trap(name, ...) trap_##name(__VA_ARGS__)
#define trap_EMPTY(...) trap_SystemTrap(0, SOURCE_FILE_ID, __LINE__)
#define trap_CATCH(...) trap_SystemTrap(1, SOURCE_FILE_ID, __LINE__, __VA_ARGS__)


void trap_SystemTrap(const u8 TrapType, const u8 FileNumber, const u32 LineNumber, ...);

 

trap.c:

void trap_SystemTrap(const u8 TrapType, const u8 FileNumber, const u32 LineNumber, ...)
{
  ... // тут какой-то код, сохраняющий в NVM-лог параметры этой функции, а также N слов от
      // верхушки стека, при необходимости.
  
  return;
}

 

Пример и правило использования:

stm8s003.c:

#include "trap.h"

#include "stm8s003.h"
#include "stm8s003_nvm.h"


#define SOURCE_FILE_ID FILE_STM8S003_NVM_C


bool nvm_WriteByteProgramMemory(const u16 ByteNumber, const u8 Byte, const bool FixedTimeOperation)
{
  assert(inrange(MCU_PAGE_NVM_SIZE, 0, 257));
  assert(inrange(PROGRAM_NVM_END, 0, 65535));
  
  bool ErrorCode = assertbool(FALSE);
  
  if(ByteNumber < MCU_PROGRAM_NVM_SIZE)
  {
    if(ByteNumber >= OB->UBCSR * MCU_PAGE_NVM_SIZE)
    {
      volatile u8 *const PNVM = (u8 *)(PROGRAM_NVM_START + ByteNumber);
      
      if(*PNVM != Byte)
      {
        NVM->PUKR = NVM_PUKR_UNLOCK_KEY1;
        NVM->PUKR = NVM_PUKR_UNLOCK_KEY2;
        
        if(bitset(NVM->SR, NVM_SR_PNVMUNLOCKED))
        {
          definebits(NVM->CR1, NVM_CR1_FIXBYTEPROGTIME, FixedTimeOperation);
          
          *PNVM = Byte;
          
          const u8 Status = NVM->SR;
          resetbits(NVM->SR, NVM_SR_PNVMUNLOCKED);
          if(bitsdiffer(Status, NVM_SR_ENDPROG, NVM_SR_PROGPROTPAGE))
            trap(CATCH, Status);
          
          resetbits(NVM->CR1, NVM_CR1_FIXBYTEPROGTIME);
          
          if(*PNVM != Byte)
            trap(CATCH, *PNVM, Byte);
        }
        else
          trap(EMPTY);
      }
    }
    else
      ErrorCode = assertbool(TRUE);
  }
  else
    trap(CATCH, ByteNumber);
  
  return ErrorCode;
}

 

То есть существует 3 основных типа ошибок:

1. Обрабатываемая ошибка (по сути функция возвращает значение ErrorCode в вызывающую, а та уже принимает решение, что делать дальше).

2. trap(EMPTY) разворачивается в вызов функции trap_SystemTrap().

3. trap(CATCH, ...) разворачивается в вызов функции trap_SystemTrap(...) с передачей в нее переменного списка аргументов.

Обращаю внимание, что при вызове trap() (как EMPTY, так и CATCH), в функцию trap_SystemTrap() автоматически передастся идентификатор текущего файла исходного кода SOURCE_FILE_ID и номер строки, из которой вызвался trap().

Защелкнув это в энергонезависимой памяти, можно зрительно увидеть в исходнике, где конкретно сработала ловушка, а также (для trap(CATCH)) с какими параметрами произошел вызов.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

18 hours ago, jcxz said:

Чем не устраивает просто захватить/выдать_в_лог N слов от верхушки стека?

Я у себя в обработчике хардфолта так и стал делать с недавних пор. Печатаю оба стека на всякий случай: MSP, PSP, естественно с указанием активного. Ограничился 32 значениями.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

4 часа назад, Nixon сказал:

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

И....? Что мешает посмотреть листинги функций и узнать это?

Цитата

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

Вы это серьёзно??? А сколько раз вы смотрели в окно дизассемблера отладчика?  :wink:

И у Вас в коде нет ни одной функции, которая бы не вызывала другие функции и содержала мало аргументов? Любая такая функция заканчивается BX LR.

Не верю!

У Вас в коде нет и одного ветвления в конце функции, где компилятор сделал бы в конце переход на завершение, вместо того чтобы влепить отдельную POP ...? Тоже как-то не верится.

Посмотрите внимательнее в свои листинги.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

4 часа назад, Nixon сказал:

Да, спасибо, нашел такое. Добавил еще поиск такого способа выхода

А про все остальные методы передачи управления конечно забыли. :russian_ru:  А передача управления в ARM может производиться любой командой, меняющей PC. И LDR и ADD и SUB и т.п.

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

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

1 hour ago, jcxz said:

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

Т.е. всё-таки нужно смотреть в распечатку стека и листинг?

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

1 минуту назад, haker_fox сказал:

Т.е. всё-таки нужно смотреть в распечатку стека и листинг?

Я вроде это с самого начала говорил.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

47 minutes ago, jcxz said:

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

Как тогда это делает отладчик показывая Вам callstack? Исуственный интелект!? Сомневаюсь!



Вообще, @Nixon, фича норм, сам о такой подумывал, вот только руки до этого не дошли.

Подобный функционал реализован в либах работы с ESP32, но это не ARM архитектура. Кроме того у них есть python скрипы которые сразу по crashdumр и elf покажут удобочитаемый callstack. Подробнее здесь: core_dump.c, panic.c но если честно, полезного там мало.
 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Меня скорее всего спасает то, что функции имеющие assert довольно тяжелые, и потому они имеют только вариант завершения с POP. 

P.S. Особенность проблемы в том, что ошибка возникает ОЧЕНЬ редко, на объекте и оперативно просмотреть дамп нет возможности. Коррекция самой ошибки производится сбросом с последующим восстановлением переменных всех машин состояний. В принципе как бы и решена проблема. Но хотелось бы разобраться в чем ее истоки. Проект довольно большой, многоуровневый (писали его с полдесятка человек) и потому дамп нужно хранить очень большой, а места очень мало. Вот и хотелось обойтись только стеком вызовов.  

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

16 минут назад, Integro сказал:

Как тогда это делает отладчик показывая Вам callstack? Исуственный интелект!? Сомневаюсь!

У отладичка есть дополнительная информация о структуре исходника, получаемая из подключаемой отладочной инфы. У кода ТС этой инфы нет.

Или попробуйте объяснить как код ТС-а будет искать точку выхода из функции если на пути PC ему встретился switch, который скомпилился в команду LDR PC, .... с таблицей переходов (по case-ам), которая лежит сразу за LDR PC, ... ? Как код ТС-а определит, что после LDR PC, ... не команды, а данные и каков их размер? Это с учётом того, что default-ветку switch-а оптимизатор может просто выкинуть (из-за неиспользуемости). Или вообще: компилятор может построить код switch-а не на LDR PC, ... с таблицей, а на ADD PC, Rx (если размеры кода по всем веткам case одинаковы).

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

Гость
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...