Post-mortem отладка на Cortex-M

в 9:33, , рубрики: C, c++, Cortex, cortex-m, cortex-m0, cortex-m4, KEIL, minidump, stacktrace, stm32, минидамп, отладка, программирование микроконтроллеров, стектрейс, стэктрейс

Post-mortem отладка на Cortex-M

Post-mortem отладка на Cortex-M - 1

Предыстория:

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

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

На соплях был прикручен отладочный UART, в который я стал выводить логи. Стало легче, часть проблем решилась. Но потом случился assert и все завертелось.

В моем случае макрос для ассерта выглядит как-то так:

#define USER_ASSERT( statement )                                           
    do                                                                     
    {                                                                      
        if(! (statement) )                                                 
        {                                                                  
            DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!n",      
                                 __LINE__, __FILE__ );                     
                                                                           
            __disable_irq();                                               
            while(1)                                                       
            {                                                              
                __BKPT(0xAB);                                              
                if(0)                                                      
                    break;                                                 
            }                                                              
        }                                                                  
    } while(0)

__BKPT(0xAB) — это программная точка останова; если ассерт происходит под отладкой, то отладчик просто останавливается на проблемной строчке, очень удобно.

По некоторым ассертам сразу понятно, что их вызвало – потому что в логе видно имя файла и номер строки, на котором ассерт сработал.

Но по происходившему ассерту было понятно только, что переполнился массив – точнее, самодельная обертка над массивом, которая проверяет выход за границы. Из-за этого в логе было видно только имя файла “super_array.h” и номер строки в нем же. А какой конкретно массив – непонятно. Из окружающих логов тоже неясно.

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

Поскольку я пишу в uVision Keil 5 с компилятором armcc, дальнейший код проверялся только под ним. Еще я использовал С++11, потому что уже 2019 год на дворе, пора уже.

Stacktrace

Разумеется, первое, что приходит в голову – но блин, ведь когда на нормальном настольном компе происходит ассерт, в консоль выводится стектрейс, типа как на КДПВ. Из стектрейса обычно можно понять, какая последовательность вызовов привела к ошибке.
Окей, значит мне тоже нужен стектрейс. Как бы его сделать?

Может быть, если бросить исключение, он сам выведется?

Кидаем исключение и не ловим его, видим вывод “SIGABRT” и вызов _sys_exit. Не прокатило, ну и ладно, не очень-то и хотелось исключения разрешать.

Погуглить, как это другие люди делают.

Все способы платформозависимые (не слишком удивительно), для gcc под POSIX есть backtrace() и execinfo.h. Для Кейла не нашлось ничего внятного. Роняем скупую слезу. Придется лезть в стек руками.

Лезем в стек руками

Теоретически, все довольно просто.

  1. Адрес возврата из текущей функции находится в регистре LR, адрес текущей вершины стека (в смысле, последнего элемента в стеке) – в регистре SP, адрес текущей команды — в регистре РС.
  2. Каким-то образом находим размер стекового кадра для текущей функции, шагаем по стеку на такое расстояние, находим там адрес возврата для предыдущей функции и повторяем так, пока не прошагаем стек до конца.
  3. Как-то сопоставляем адреса возвратов с номерами строк в файлах с исходным кодом.

Окей, для начала – как узнать размер стекового кадра?

На опциях по-умолчанию – судя по всему, никак, он просто хардкодится компилятором в «пролог» и «эпилог» каждой функции, в команды, которые выделяют и освобождают кусок стека под кадр.
Но, к счастью, у armcc есть опция --use_frame_pointer, которая выделяет регистр R11 под Frame Pointer – т.е. указатель на стековый кадр предыдущей функции. Отлично, теперь можно будет прошагать по всем стековым кадрам.

Теперь – как сопоставить адреса возвратов со строками в файлах с исходниками?

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

Вздыхаем. Значит, честный стектрейс – такой, чтобы в отладочный вывод сразу выводились имена функций и номера строк – не выйдет. Но можно выводить адреса, а потом на компе их сопоставлять с функциями и номерами строк, благо отладочная инфа в проекте все-таки есть.

Но это выглядит очень печально, потому что придется парсить .map-файл, в котором указаны диапазоны адресов, которые занимает каждая функция. А потом еще отдельно парсить файлы с дизассемблированным кодом, чтобы найти конкретную строчку. Резко возникает желание забить.

Плюс внимательное разглядывание документации на опцию --use_frame_pointer позволяет увидеть вот эту страницу, которая говорит, что эта опция может привести к падениям в HardFault в случайные моменты времени. Мда.
Ладно, думаем дальше.

А как это делает отладчик?

А ведь отладчик как-то показывает стек вызовов даже без frame pointer’a. Ну, понятно, как, у IDE ведь под рукой есть вся отладочная инфа, ей не составляет труда сопоставить адреса и имена функций. Хм.

При этом у той же Visual Studio есть такая штука – minidump – когда падающее приложение генерирует маленький файлик, который потом скармливаешь студии и она восстанавливает состояние приложения на момент падения. И можно все переменные рассмотреть, по стеку погулять с комфортом. Хм еще раз.

А ведь это вроде как довольно просто. Надо всего лишь каждый день втирать в ягодицы густой советский продолжение по ссылке заполнить стек значениями, которые были там в момент падения и, видимо, восстановить состояние регистров. Да и все, вроде бы?

Опять же, разбиваем эту идею на подзадачи.

  1. На микроконтроллере нужно пройти по стеку, для этого нужно получить текущее значение SP и адрес начала стека.
  2. На микроконтроллере нужно вывести значения регистров.
  3. В IDE нужно как-то затолкать все значения из «минидампа» обратно в стек. И значения регистров тоже.

Как получить текущее значение SP?

Желательно, не марая рук об ассемблер. В Кейле, к счастью, есть специальная функция (intrinsic) — __current_sp(). В gcc не сработает, но мне и не надо.

Как получить адрес начала стека? Поскольку я пользуюсь своим скриптом для защиты от переполнения (про который я писал здесь ), стек у меня лежит в отдельной линкерной секции, которую я называл REGION_STACK.
Значит, его адрес начала можно узнать у линкера, с помощью странных переменных с долларами в названиях.

Методом проб и ошибок подбираем нужное имя — Image$$REGION_STACK$$ZI$$Limit, проверяем, работает.

Пояснение

Это волшебный символ, который создается на этапе линковки, поэтому строго говоря, он не является константой этапа компиляции.
Чтобы им воспользоваться, нужно разыменование:

extern unsigned int Image$$REGION_STACK$$ZI$$Limit;

using MemPointer = const uint32_t *;
// чтобы получить значение, нужно разыменование
static const auto stack_upper_address = (MemPointer) &(
Image$$REGION_STACK$$ZI$$Limit );

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

Как вывести значения регистров?

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

Действительно нужны только Link Register (LR), который хранит адрес возврата из текущей функции, SP, с которым мы уже разобрались и Program Counter (PC), который хранит адрес текущей команды.

Опять же, я не смог найти варианта, который работал бы с любым компилятором, но для Кейла снова есть intrinsic-функции: __return_address() для LR и __current_pc() для РС.
Отлично. Осталось затолкать все значения из минидампа обратно в стек, а значения регистров – в регистры.

Как загрузить "минидамп" в память?

Сначала я планировал воспользоваться командой отладчика LOAD, которая позволяет загружать значения из .hex или .bin-файла в память, но быстро выяснил, что LOAD почему-то не загружает значения в RAM.
И регистры я бы этой командой заполнить все равно бы не смог.

Ну и ладно, это все равно потребовало бы слишком много телодвижений, конвертить текст в bin, конвертить bin в hex...

К счастью, у Кейла есть симулятор, а для симулятора можно писать скрипты на некоем убогом С-подобном языке. И в этом языке есть возможность писать в память! Для этого есть специальные функции типа _WDWORD и _WBYTE. Собираем все идеи в кучу, и получаем вот такой код.

Весь код:

#define USER_ASSERT( statement )                                           
    do                                                                     
    {                                                                      
        if(! (statement) )                                                 
        {                                                                  
            DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!n",      
                                 __LINE__, __FILE__ );                     
                                                                           
            print_minidump();                                              
            __disable_irq();                                               
            while(1)                                                       
            {                                                              
                __BKPT(0xAB);                                              
                if(0)                                                      
                    break;                                                 
            }                                                              
        }                                                                  
    } while(0)

// это специальный символ, который генерирует линкер
// это размер стека, регион для которого я сам так назвал в scatter-файле
extern unsigned int Image$$REGION_STACK$$ZI$$Limit;

void print_minidump()
{

// если компилятор - armcc или arm-clang
#if __CC_ARM || ( (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050))

    using MemPointer = const uint32_t *;

    // чтобы получить значение, нужно разыменование
    static const auto stack_upper_address = (MemPointer) &(Image$$REGION_STACK$$ZI$$Limit );
    // стек растет в сторону уменьшения адресов, т.е. в данный момент заполнен кусок
    // между SP и stack_upper_address

    auto LR = __return_address();
    auto PC = __current_pc();
    auto SP = __current_sp();

    auto i = 0;

    DEBUG_PRINTF("nCopy the following function for simulator to .ini-file, n"
                 "start fresh debug session in simulator and call __load_minidump() from command window.n"
                 "You should be able to see the call stack in CallStack windownn");

    DEBUG_PRINTF("func void __load_minidump() {n ");

    for( MemPointer stack = (MemPointer)SP; stack <= stack_upper_address; stack++ )
    {
        DEBUG_PRINTF("_WDWORD (0x%p, 0x%08x); ", stack, *stack );

        // лень выдумывать нормальный способ выводить красивый столбик текста
        if( i == 1 )
        {
            DEBUG_PRINTF("n ");
            i=0;
        }
        else
        {
            i++;
        }
    }

    DEBUG_PRINTF("n LR = 0x%08x;", LR );
    DEBUG_PRINTF("n PC = 0x%08x;", PC );
    DEBUG_PRINTF("n SP = 0x%08x;", SP );
    DEBUG_PRINTF("n}n");

#endif

}

Для загрузки минидампа нам нужно создать .ini-файл, скопировать в него функцию __load_minidump, добавить этот файл в автозапуск – Project -> Options for Target -> Debug и на разделе Use Simulator прописать этот .ini-файл в графе “Initialization file”.

Теперь просто заходим в отладку на симуляторе и, не запуская отладку, вызываем в окне команд функцию __load_minidump().
И вуаля, нас телепортирует в функцию print_minidump на строку, в которой сохранился РС. А в окне Callstack+Locals видно стек вызовов.

Примечание:

Функция специально названа с двумя подчеркиваниями в начале, потому что если название функции или переменной в симуляторном скрипте случайно совпадет с названием в коде проекта, то Кейл не сможет ее вызвать. Стандарт С++ запрещает использовать имена с двумя подчеркиваниями в начале, поэтому вероятность совпадения имен снижается.

В принципе, это все. Насколько я смог проверить, минидамп работает и для обычных функций и для обработчиков прерываний. Будет ли он работать для всяких извращений с setjmp/longjmp или alloca – не знаю, поскольку извращения не практикую.

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

Потом я еще немного погуглил и нашел похожую штуку для gcc и gdb – CrashCatcher.

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

Автор: Amomum

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js