Получение обратного адреса исключения на ARM Cortex M0

Я пытаюсь получить адрес возврата обработчика IRQ в моем коде. Моя цель - сохранить значение ПК непосредственно перед истечением сторожевого таймера и перед сбросом в целях отладки, используя WDT_IRQHandler(). Я также тестирую этот подход с другими IRQ, чтобы проверить, понял ли я идею. Но, похоже, нет.

Я прочитал доступную документацию. Я понял, что когда происходит исключение, в стек помещаются 8 регистров: R0, R1, R2, R3, R12, LR, PC и XPSR.

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

  • получить адрес sp с помощью __builtin_frame_address(0);
  • добавьте к нему смещение в стеке ПК (0x18) и прочитайте значение, которое предположительно является значением, которое будет восстановлено на ПК при возврате обработчика.

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

Код работает нормально, поэтому я думаю, что у меня проблема с пониманием того, как он работает.

Если я проверяю разборку, в некоторых IRQ к SP добавляется константа перед POPping (?)

00001924: 0x000009b0 ...TE_IRQHandler+280   add     sp, #36 ; 0x24
00001926: 0x0000f0bd ...TE_IRQHandler+282   pop     {r4, r5, r6, r7, pc}

В других IRQ этого не происходит.

Я понимаю, что может случиться так, что в стек будет помещено больше регистров, так как я могу быть уверен, с какого смещения получить ПК?

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

1 ответ

Решение

Вы не можете полагаться на указатель стека внутри обработчика C по двум причинам:

  1. Регистры всегда помещаются в активный стек для выгруженного кода. Обработчики всегда используют основной стек (MSP). Если прерывание прерывает код режима потока, который выполняется из стека процессов (PSP) затем регистры будут переданы в PSP и вы никогда не найдете их в стеке обработчиков;
  2. Процедура C, вероятно, зарезервирует некоторое место в стеке для локальных переменных, и вы не знаете, сколько это будет, поэтому вы не сможете найти регистры.

Вот как я обычно это делаю:

void WDT_IRQHandler_real(uint32_t *sp)
{
    /* PC is sp[6] (sp + 0x18) */
    /* ... your code ... */
}

/* Cortex M3/4 */
__attribute__((naked)) void WDT_IRQHandler()
{
    asm volatile (
        "TST   LR, #4\n\t"
        "ITE   EQ\n\t"
        "MRSEQ R0, MSP\n\t"
        "MRSNE R0, PSP\n\t"
        "LDR   R1, =WDT_IRQHandler_real\n\t"
        "BX    R1"
    );
}

/* Cortex M0/1 */
__attribute__((naked)) void WDT_IRQHandler()
{
    asm volatile (
        "MRS R0, MSP\n\t"
        "MOV R1, LR\n\t"
        "MOV R2, #4\n\t"
        "TST R1, R2\n\t"
        "BEQ WDT_IRQHandler_call_real\n\t"
        "MRS R0, PSP\n"
    "WDT_IRQHandler_call_real:\n\t"
        "LDR R1, =WDT_IRQHandler_real\n\t"
        "BX  R1"
    );
}

Хитрость заключается в том, что обработчик представляет собой небольшой фрагмент сборки (я использовал голую функцию с GCC asm, вы также можете использовать отдельный файл asm), который передает указатель стека на настоящий обработчик. Вот как это работает (для M3/4):

  • Начальная стоимость LR в обработчике исключений известен как EXC_RETURN (подробнее здесь). Его биты имеют различное значение, мы заинтересованы в том, чтобы EXC_RETURN[2] является 0 если активный стек был MSP а также 1 если активный стек был PSP;
  • TST LR, #4 проверки EXC_RETURN[2] и устанавливает флаги состояния;
  • MRSEQ R0, MSP перемещает MSP в R0 если EXC_RETURN[2] == 0;
  • MRSNE R0, PSP перемещает PSP в R0 если EXC_RETURN[2] == 1;
  • В заключение, LDR/BX переходит к реальной функции (R0 это первый аргумент).

Вариант M0/1 аналогичен, но использует ветви, поскольку ядро не поддерживает блоки IT.

Это решает MSP/PSP и, поскольку он запускается перед любой стековой операцией, генерируемой компилятором, он обеспечивает надежный указатель. Я использовал простую (не связанную) ветвь для функции, потому что мне не нужно ничего делать после нее и LR уже хорошо идти. Это экономит несколько циклов и LR тяни / поп. Также все используемые регистры находятся в R0-R3 диапазон царапин, поэтому нет необходимости сохранять их.

Другие вопросы по тегам