Nixon 4 24 января, 2019 Опубликовано 24 января, 2019 (изменено) · Жалоба Задача по обратной раскрутке стека вызовов у меня возникает регулярно. Понятно, что в режиме отладки за вас это делает отладчик, но часто нужно делать пост-анализ сбоев в рантайме. Ничего готового я не нашел, хотя возможно все лежит на поверхности. Сейчас я реализовал это через одно место (жутко процессоро- и компиляторо- зависисимо - 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. По коррекции стека перед выходом определяю размер текущего фрейма стека с предположением что первым в стек компилятор кладет адрес возврата. Ну и дальше рекурсия. Возможно у кого-то есть более грамотное решение. Изменено 14 ноября, 2022 пользователем haker_fox Добавил теги к популярной теме. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
AlexandrY 3 24 января, 2019 Опубликовано 24 января, 2019 · Жалоба А что так хитро сделан подсчет битов в поле <registers>? Вместо того чтобы быстрее посчитать можно было бы залогить какие регистры сохраняются и дополнительно проверить валидность состава регистров. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 241 24 января, 2019 Опубликовано 24 января, 2019 · Жалоба 4 часа назад, Nixon сказал: Задача по обратной раскрутке стека вызовов у меня возникает регулярно. Понятно, что в режиме отладки за вас это делает отладчик, но часто нужно делать пост-анализ сбоев в рантайме. ... Возможно у кого-то есть более грамотное решение. Не понятно - что именно нужно? Чем не устраивает просто захватить/выдать_в_лог N слов от верхушки стека? А дальше: листинг в руки и "раскрутка" - по нему врукопашную. И непонятно - что Вы собираетесь найти по *pc своим кодом? Функция вполне себе запросто может завершаться безусловным переходом - Ваш код выдаст ерунду. Это самый распространённый случай. Не говоря уже о множестве других вариантов. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Nixon 4 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 15 часов назад, jcxz сказал: Не понятно - что именно нужно? Чем не устраивает просто захватить/выдать_в_лог N слов от верхушки стека? А дальше: листинг в руки и "раскрутка" - по нему врукопашную. И непонятно - что Вы собираетесь найти по *pc своим кодом? Функция вполне себе запросто может завершаться безусловным переходом - Ваш код выдаст ерунду. Это самый распространённый случай. Не говоря уже о множестве других вариантов. Сохранить верхушку стека недостаточно. Функция FOO() в которой возник assert вызывается из многих мест, цепочка вызовов достаточно длинная, и нужно знать на каком именно этапе произошло формирование ошибочного набора параметров для FOO(). Кроме того место для логгирования сбоев ограничено. Но как вариант просто сохранять кадр стека (побольше) имеет место быть. В любом случае нужна ручная работа. По поводу возврата из функции не через POP - тоже верно, такое возможно, можно учесть и этот случай, но я в своем проекте такого не встречал (возможно это зависит от уровня оптимизации). Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Nixon 4 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 15 часов назад, AlexandrY сказал: А что так хитро сделан подсчет битов в поле <registers>? Была такая функция подсчета битов, я ее тупо скопипастил. 15 часов назад, AlexandrY сказал: Вместо того чтобы быстрее посчитать можно было бы залогить какие регистры сохраняются и дополнительно проверить валидность состава регистров. А какая разница что считать - сохраненные регистры или восстановленные? Разве что искать вверх придется только команды коррекции стека и PUSH. Но их тоже может не быть. Я честно говоря надеялся, что существует что-то стандартное для раскрутки стека вызовов. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Nixon 4 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 15 часов назад, jcxz сказал: Функция вполне себе запросто может завершаться безусловным переходом - Ваш код выдаст ерунду. Это самый распространённый случай. Не говоря уже о множестве других вариантов. Да, спасибо, нашел такое. Добавил еще поиск такого способа выхода Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Arlleex 183 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 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)) с какими параметрами произошел вызов. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
haker_fox 61 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 18 hours ago, jcxz said: Чем не устраивает просто захватить/выдать_в_лог N слов от верхушки стека? Я у себя в обработчике хардфолта так и стал делать с недавних пор. Печатаю оба стека на всякий случай: MSP, PSP, естественно с указанием активного. Ограничился 32 значениями. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 241 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 4 часа назад, Nixon сказал: Сохранить верхушку стека недостаточно. Функция FOO() в которой возник assert вызывается из многих мест, цепочка вызовов достаточно длинная, и нужно знать на каком именно этапе произошло формирование ошибочного набора параметров для FOO(). И....? Что мешает посмотреть листинги функций и узнать это? Цитата По поводу возврата из функции не через POP - тоже верно, такое возможно, можно учесть и этот случай, но я в своем проекте такого не встречал (возможно это зависит от уровня оптимизации). Вы это серьёзно??? А сколько раз вы смотрели в окно дизассемблера отладчика? И у Вас в коде нет ни одной функции, которая бы не вызывала другие функции и содержала мало аргументов? Любая такая функция заканчивается BX LR. Не верю! У Вас в коде нет и одного ветвления в конце функции, где компилятор сделал бы в конце переход на завершение, вместо того чтобы влепить отдельную POP ...? Тоже как-то не верится. Посмотрите внимательнее в свои листинги. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 241 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 4 часа назад, Nixon сказал: Да, спасибо, нашел такое. Добавил еще поиск такого способа выхода А про все остальные методы передачи управления конечно забыли. А передача управления в ARM может производиться любой командой, меняющей PC. И LDR и ADD и SUB и т.п. Чтобы найти точку выхода из произвольного места произвольной функции в общем случае, нужно построить дизассемблер с искусственным интеллектом, который будет трассировать ход выполнения программы, эмулируя всю систему команд CPU (включая и FPU тоже) и содержимое всех регистров и всей памяти. Всё остальное будет работать только иногда, в некоторых случаях. Причём, если при очередной компиляции оно выдавало вменяемый результат, то после следующей перекомпиляции того же кода, выдаст ерунду. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
haker_fox 61 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 1 hour ago, jcxz said: Всё остальное будет работать только иногда, в некоторых случаях. Причём, если при очередной компиляции оно выдавало вменяемый результат, то после следующей перекомпиляции того же кода, выдаст ерунду. Т.е. всё-таки нужно смотреть в распечатку стека и листинг? Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 241 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 1 минуту назад, haker_fox сказал: Т.е. всё-таки нужно смотреть в распечатку стека и листинг? Я вроде это с самого начала говорил. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Integro 0 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 47 minutes ago, jcxz said: Чтобы найти точку выхода из произвольного места произвольной функции в общем случае, нужно построить дизассемблер с искусственным интеллектом Как тогда это делает отладчик показывая Вам callstack? Исуственный интелект!? Сомневаюсь! Вообще, @Nixon, фича норм, сам о такой подумывал, вот только руки до этого не дошли. Подобный функционал реализован в либах работы с ESP32, но это не ARM архитектура. Кроме того у них есть python скрипы которые сразу по crashdumр и elf покажут удобочитаемый callstack. Подробнее здесь: core_dump.c, panic.c но если честно, полезного там мало. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Nixon 4 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба Меня скорее всего спасает то, что функции имеющие assert довольно тяжелые, и потому они имеют только вариант завершения с POP. P.S. Особенность проблемы в том, что ошибка возникает ОЧЕНЬ редко, на объекте и оперативно просмотреть дамп нет возможности. Коррекция самой ошибки производится сбросом с последующим восстановлением переменных всех машин состояний. В принципе как бы и решена проблема. Но хотелось бы разобраться в чем ее истоки. Проект довольно большой, многоуровневый (писали его с полдесятка человек) и потому дамп нужно хранить очень большой, а места очень мало. Вот и хотелось обойтись только стеком вызовов. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 241 25 января, 2019 Опубликовано 25 января, 2019 · Жалоба 16 минут назад, Integro сказал: Как тогда это делает отладчик показывая Вам callstack? Исуственный интелект!? Сомневаюсь! У отладичка есть дополнительная информация о структуре исходника, получаемая из подключаемой отладочной инфы. У кода ТС этой инфы нет. Или попробуйте объяснить как код ТС-а будет искать точку выхода из функции если на пути PC ему встретился switch, который скомпилился в команду LDR PC, .... с таблицей переходов (по case-ам), которая лежит сразу за LDR PC, ... ? Как код ТС-а определит, что после LDR PC, ... не команды, а данные и каков их размер? Это с учётом того, что default-ветку switch-а оптимизатор может просто выкинуть (из-за неиспользуемости). Или вообще: компилятор может построить код switch-а не на LDR PC, ... с таблицей, а на ADD PC, Rx (если размеры кода по всем веткам case одинаковы). Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться