验证Cortex-M0 / M3 / M4 / M7上的内存地址

哈Ha!

关于前一天的政权放松,在一个相邻帖子的评论中,有关微控制器的文章都只是被LED闪烁而感到愤慨,以及我懒得恢复的我的标准博客过早地死了,我将在这里传递有关一个令人遗憾的小亮点的有用材料。使用Cortex-M内核的一个技巧-检查随机地址的有效性。


一种非常有用且同时由于某种原因而在Cortex-M微控制器(全部)上未描述的现成功能之一是能够检查内存中地址的正确性。 借助它,您可以确定闪存,RAM和EEPROM的大小,确定特定外围设备和寄存器在特定处理器上的存在,消除掉落的进程,同时保持OS的整体运行状况等。

在正常模式下,当您到达Cortex-M3 / M4 / M7上不存在的地址时,会抛出BusFault异常,并且在没有其处理程序的情况下,它会升级为HardFault。 在Cortex-M0上,没有“详细”异常(MemFault,BusFault,UsageFault),并且任何故障都会立即升级为HardFault。

通常,您不能忽略HardFault-例如,这可能是硬件故障的结果,设备的进一步行为将变得不可预测。 但是在特定情况下,可以并且应该这样做。

Cortex-M3和Cortex-M4:未实现的BusFault


在Cortex-M3及更高版本上,检查地址的有效性非常简单-必须通过FAULTMASK寄存器禁止所有异常(显然,不可屏蔽的异常除外),专门禁用BusFault处理,然后戳入要检查的地址,并查看BFAR寄存器中的BFARVALID标志是否,即总线故障地址寄存器。 如果您选择了它,那么您将拥有BusFault,即 地址不正确。

代码看起来像这样,来自标准(非供应商)CMSIS的所有定义和功能,因此它可以在任何M3,M4或M7上运行:

bool cpu_check_address(volatile const char *address) { /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */ static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; /* Clear BFARVALID flag by writing 1 to it */ SCB->CFSR |= BFARVALID_MASK; /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */ uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; /* probe address in question */ *address; /* Check BFARVALID flag */ if ((SCB->CFSR & BFARVALID_MASK) != 0) { /* Bus Fault occured reading the address */ is_valid = false; } /* Reenable BusFault by clearing BFHFNMIGN */ SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; } 

Cortex-M0和Cortex-M0 +


如前所述,使用Cortex-M0和Cortex-M0 +,一切都变得更加复杂,因为它们没有BusFault和所有相应的寄存器,并且异常会立即升级为HardFault。 因此,只有一种出路-使HardFault处理程序能够理解该异常是故意引起的,然后返回调用它的函数,并在其中传递一个指示HardFault的标志。

这完全是在汇编程序中完成的。 在下面的示例中,寄存器R5设置为1,并且两个“幻数”被写入寄存器R1和R2。 如果在尝试将值加载到要检查的地址后发生HardFault,则它必须检查R1和R2的值,如果它们找到了必要的数字,请将R5设置为零。 R5的值通过牢固地绑定到该寄存器的特殊变量传输到syshech代码中,要汇编到汇编器中的地址是隐式的,我们只知道在arm-none-eabi中,函数的第一个参数位于R0中。

 bool cpu_check_address(volatile const char *address) { /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */ (void)address; /* R5 will be set to 0 by HardFault handler */ /* to indicate HardFault has occured */ register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" /* set default R5 value */ "ldr r1, =0xDEADF00D \n" /* set magic number */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic to be sure */ "ldrb r3, [r0] \n" /* probe address */ ); return result; } 

HardFault处理程序的最简单形式的代码如下所示:

 __attribute__((naked)) void hard_fault_default(void) { /* Get stack pointer where exception stack frame lies */ __asm__ volatile ( /* decide if we need MSP or PSP stack */ "movs r0, #4 \n" /* r0 = 0x4 */ "mov r2, lr \n" /* r2 = lr */ "tst r2, r0 \n" /* if(lr & 0x4) */ "bne use_psp \n" /* { */ "mrs r0, msp \n" /* r0 = msp */ "b out \n" /* } */ " use_psp: \n" /* else { */ "mrs r0, psp \n" /* r0 = psp */ " out: \n" /* } */ /* catch intended HardFaults on Cortex-M0 to probe memory addresses */ "ldr r1, [r0, #0x04] \n" /* read R1 from the stack */ "ldr r2, =0xDEADF00D \n" /* magic number to be found */ "cmp r1, r2 \n" /* compare with the magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x08] \n" /* read R2 from the stack */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic number to be found */ "cmp r1, r2 \n" /* compare with 2nd magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x18] \n" /* read PC from the stack */ "add r1, r1, #2 \n" /* move to the next instruction */ "str r1, [r0, #0x18] \n" /* modify PC in the stack */ "ldr r5, =0 \n" /* set R5 to indicate HardFault */ "bx lr \n" /* exit the exception handler */ " regular_handler: \n" /* here comes the rest of the fucking owl */ ) 

离开异常处理程序时,Cortex将确保被处理程序破坏的寄存器(R0-R3,R12,LR,PC ...)扔到堆栈上。 第一个片段-它已经存在于大多数现成的HardFault处理程序中,除了那些用纯裸机编写的片段外-确定哪个堆栈:在OS中工作时,它可以是MSP或PSP,并且它们具有不同的地址。 在裸机项目中,通常先验地安装MSP堆栈(主堆栈指针),而不进行检查-因为由于缺少进程,PSP(进程堆栈指针)无法存在。

确定所需的堆栈并将其地址放入R0后,我们从中读取值R1(偏移量0x04)和R2(偏移量0x08),并将其与魔术字进行比较(如果两者均匹配)-我们从堆栈中读取PC值(偏移量0x18),加2 (2个字节-Cortex-M *上指令的大小),然后将其保存回堆栈。 如果不这样做,当我们从处理程序中返回时,我们将发现自己实际上是在引起异常的同一条指令上,并且将始终循环运行。 附录2将我们移至返回时的下一条指令。

*更新。 在评论中,出现了关于Cortex-M上指令大小的问题,我将在此处做出正确答案:在这种情况下,崩溃会导致LDRB指令,该指令在ARMv7-M架构中可用,有两种版本-16位和32位。 如果至少满足以下条件之一,则将选择第二个选项:

  • 作者明确指出了指令LDRB.W而不是LDRB(我们没有)
  • 使用高于R7的寄存器(对于我们-R0和R3)
  • 指定的偏移量大于31个字节(我们没有偏移量)


在所有其他情况下(即,当操作数符合指令的16位版本的格式时),汇编程序必须选择16位版本。

因此,在我们的情况下,总是会有一个2字节的指令需要跳过,但是如果您强烈地编辑代码,则可以使用选项。

接下来,在R5中写入0,以作为进入HardFault的指标。 R3之后的寄存器不会在特殊寄存器之前保存到堆栈中,并且退出处理程序时,它们不会以任何方式还原,因此破坏或不破坏它们是我们的良心。 在这种情况下,我们有意将R5从1更改为0。

中断处理程序的返回仅以一种方式完成。 进入处理程序时,会在LR寄存器中写入一个特殊值EXC_RETURN,这是写入PC以退出处理程序所必需的-不仅是写入它,而且还需要使用POP或BX命令来执行(例如,“ mov pc,lr,不起作用” ,尽管在您看来这是第一次,但确实可行。 BX LR看起来像是尝试去一个无意义的地址(在LR中,类似于0xFFFFFFF1的东西与我们需要返回的过程的真实地址无关),但实际上处理器在PC上看到了该值(它将去往何处)自动),它将从堆栈中恢复寄存器并继续执行我们的过程-由于我们手动将堆栈中PC的数量增加了2,因此在调用HardFault之后执行下一个过程。

您当然可以在哪里清楚地了解所有偏移量和命令。

好吧,或者如果幻数不可见,则所有内容都将移交给regular_handler,然后遵循常规的HardFault处理过程-通常,这是一个将寄存器值打印到控制台,决定下一步如何处理处理器的函数,等等。

RAM大小确定


使用所有这些都非常简单明了。 我们要编写一种固件,该固件可在具有不同RAM数量的几个微控制器上工作,而每次都在完整程序中使用RAM吗?

是的,很简单:

 static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) { char *address = base; do { address += block; if (!cpu_check_address(address)) { break; } } while ((uint32_t)(address - base) < maxsize); return (uint32_t)(address - base); } uint32_t get_cpu_ram_size(void) { return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024); } 

那么就需要maxsize,以便在它与下一个地址块之间的RAM可能达到最大数量的情况下,cpu_check_address不会中断。 在此示例中,它是80 KB。 探查所有地址也没有任何意义-只需看一下数据表,看看两个控制器模型之间的最小可能步骤是什么,并将其设置为块。

以编程方式过渡到位于任何地方中间的引导加载程序


有时您可以做一些更复杂的技巧-例如,假设您想以编程方式跳转至工厂出厂的引导加载程序STM32,以通过UART或USB切换至固件更新模式,而不必费心编写引导加载程序。

STM32引导加载程序位于您需要转到的称为系统内存的区域,但是存在一个问题-该区域不仅在不同系列的处理器上而且在同一系列的不同型号上具有不同的地址(可以在AN2606上找到史诗般的铭牌)。第22至26页)。 通常,当您将相应功能添加到平台而不是仅添加到特定产品时,您需要多功能性。

在CMSIS文件中,系统内存的起始地址也丢失了。 无法通过Bootloader ID来确定它,因为 这是一个麻烦的问题-Bootloader ID位于系统内存的最后一个字节中,这使我们回到了地址问题。

但是,如果我们查看STM32存储卡,将会看到类似以下内容:


在这种情况下,我们对系统内存环境感兴趣-例如,最上面是一个曾经可编程的区域(并非所有STM32)和选项字节(全部)。 不仅在不同的模型中,而且在不同的STM32系列中,都观察到这种结构,唯一的区别在于OTP的存在以及系统内存和选件之间地址中的间隙的存在。

但是对于我们来说,最重要的是选项字节的起始地址在常规的CMSIS标头中-在此处被称为OB_BASE。

更简单。 我们编写该函数以从指定地址向下或向上搜索第一个有效或无效地址:

 char *cpu_find_next_valid_address(char *start, char *stop, bool valid) { char *address = start; while (true) { if (address == stop) { return NULL; } if (cpu_check_address(address) == valid) { return address; } if (stop > start) { address++; } else { address--; } }; return NULL; } 

然后从Option字节向下看,首先是系统内存或与其相邻的OTP的末尾,然后是两次遍历,然后是系统内存的开始:

 /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; 

毫不费力地,我们将其安排到一个函数中,该函数查找系统内存的开头并跳转到该内存,即它启动引导加载程序:

 static void jump_to_bootloader(void) __attribute__ ((noreturn)); /* Sets up and jumps to the bootloader */ static void jump_to_bootloader(void) { /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; if (!c) { NVIC_SystemReset(); } uint32_t boot_addr = (uint32_t)c; uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr); uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4); void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr); /* Reset the stack pointer */ __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); } 

这取决于特定的处理器型号……什么都没有。 逻辑不适用于在OTP和系统内存之间存在漏洞的模型-但我没有检查是否有任何问题。 将积极使用OTP-检查。

其他技巧仅适用于从代码中调用引导加载程序的通常过程-不要在初始化外围设备,时钟速度等之前忘记重置堆栈指针并调用离开引导加载程序的过程。由于其极简主义,引导加载程序可以阻塞初始化外围设备并期望它处于默认状态。 从程序中的任意位置调用引导加载程序的一个好方法是写入RTC备份寄存器或简单地写入幻数存储器中的已知地址,程序重新启动并在初始化该数的最初阶段进行检查。

PS由于处理器存储卡中的所有地址在最坏的情况下都按4对齐,因此通过以4个字节(而不是1个字节)的增量逐步访问它们的想法将大大加快上述过程。

重要告示


注意:请注意,在特定控制器上,特定地址的有效性并不一定表示该地址可能实际存在功能。 例如,控制某些可选外围模块的寄存器地址可能是有效的,尽管该模块本身不存在该模块。 从制造商的角度来看,最有趣的技巧是可能的,通常是出于对不同处理器型号使用相同晶体的原因。 但是,在大多数情况下,这些程序可以工作并且证明非常有用。

Source: https://habr.com/ru/post/zh-CN437256/


All Articles