首页
社区
课程
招聘
[原创] 从STM32L4看ARM裸板的bootloader
发表于: 2020-4-11 12:49 15577

[原创] 从STM32L4看ARM裸板的bootloader

2020-4-11 12:49
15577

Fundamental, fundamental, fundamental, ...without the fundamental, all those fancy magics won't work.

有经验的程序员都知道源码需要进行编译、链接、封装,然后才能执行。那你知道如何为一块CPU编写并编译程序吗?知道编译后的程序如何写入MCU、并让CPU加载运行的吗?

早先收到阿里云提供的Developer kit开发板,对他们的RTOS进行体验,就是下面这款:

devkit.png

不得不说,使用aos全家桶运行、烧写和调试代码都非常方便;而且最近看发现还支持最小化定制裁剪,根据自己的需求下载对应的代码,算是咱256G小硬盘的福音了:)

不过今天不是分析阿里的RTOS(AliOS Things),也不是把玩这块开发板,而是借助其中的MCU来探索下裸板的开发和运行之路。

在开始为一块MCU编程之前,我们要做的第一件事就是先查看这个MCU的文档。例如,如果我们想写一个helloworld程序,那么就至少需要知道:

对于我们而言,手上的MCU型号是STM32L496VGTx,因此这些大部分都能在stm32l496ae datasheet中查看到。首先,在datasheet中我们知道STM32L496VGTx中的CPU是ARM Cortex-M4,内存SRAM为320KB,内部含有1MB的Flash。

根据ARM的文档中关于Cortex-m4 中断向量表的介绍,我们可以看到保存第一条指令地址的地址为0x0004

vec.png

其中0x0000保存的是栈的地址。也就是说,CPU复位之后,会首先将0x0000地址的内容加载到栈寄存器sp中,然后将0x0004地址的内容加载并保存到指令寄存器pc中,然后才开始执行第一条指令。

CPU执行每条指令,本质上包含5步:取指、译码、执行、访存、写回。如果不影响状态,多条指令的5步可以交错,这就称为CPU的流水线,现代CPU都包含多级流水线的设计和其他的优化来提升执行速度。……扯远了,说这个主要是强调一点:CPU实际运行的第一条指令的地址为*(addr *)0x0004。而前面两条"指令",即加载sp和加载pc,实际上是通过CPU硬件的有限状态机实现的。

还是在ARM的文档Memory-Model中,可以看到我们的芯片内存映射的结构大致如下:

mm.png

在32位的寄存器下,有大约4GB的寻址空间。其中ARM只定义了一个大概的范围,地址空间的实际映射其实和厂商的设计有比较大的关系。比如在我们的STM32L4 MCU中,实际的映射如下:

mm1.png

需要注意的是flash地址空间,为0x08000000 ~ 0x08100000,大小为0x10000正好是datasheet中所说的1MB。还有就是APB的地址空间,因为APB总线通常是用来控制外设的,比如我们下面会用到的串口(UART)。

Talk is cheap,接下来就是实际的编码,我们的目标是在CPU上电启动后马上打印“HelloWorld”,没有其他多余的操作。

在打印HelloWorld之前,我们先确保MCU能够正常启动并运行我们的代码。为此,需要正确编译和链接我们的程序。根据上面ARM初始化向量表的定义,我们先写个汇编文件startup_m4.s

Reset_Handler是我们实际运行的第一条指令地址,其地址写在中断向量表的0x04偏移处。对于其他的中断处理程序,我们先简单放一部分到Default_Handler中。

有了代码,还需要链接到对应的地址中,执行这项任务的就是linker脚本。通常我们使用ld时也会调用默认的linker脚本,可以通过ld --verbose命令查看,不过默认的链接脚本无法满足我们的需求,所以根据上面的文档,我们写一个简单的链接脚本m4.ld如下:

编译并链接我们的程序:

最后生成的是ELF程序,为了在裸板上运行,需要将无用的信息去掉,只保留纯粹的代码和数据:

如果想要了解更多链接脚本的语法和含义,可以参考官方的文档——Linker Scripts

有了starup.bin之后,就可以使用对应的接口写入Flash,对于我们这块开发板引出的接口是ST-LINK,所以可以直接使用stlink程序来写,前面说了Flash地址为0x08000000

当然,你也可以使用其他工具,比如我最喜欢的OpenOCD。使用openocd需要自己对接口进行适配,其中包含了很多预置的配置,例如对于我们手上的开发板,可以使用以下配置:

值得一提的是,openocd的配置使用的是裁剪过的TCL语言,使用前可以花一两个小时先了解下。

OpenOCD中内置了gdbserver,不过如果你用openOCD+gef进行调试的话,很可能会遇到错误。经过查看代码和相关的资料,我发现openocd的gdbserver会将程序状态字寄存器命名为xPSR而不是传统的cpsr,所以我写了个gdb脚本解决这个问题:

烧写成功后复位使用JTAG接口进行调试,可以看到进入了我们的程序中:

jtag.png

PS:由于我们的大部分中断都没有处理,所以单步调试触发中断后程序很可能跑飞:)

说句题外话,生成的starup.bin就是我们常说的固件,实际上在逆向分析时从flash读出来的数据也就是这个格式,从0x00地址开始。比如,分析这个固件的时候通常使用的方法是:

其他工具也可以用类似的方法将首地址rebase进行分析,但关键是要知道对应芯片的中断向量表定义,这样才能找到真正的入口函数。

现在有了骨架,可以实现真正的功能了。在操作系统中,我们printf("hello world")本质上是经过系统调用让内核把数据写到标准输出,但是在裸板上可没那么方便,一切都要自己操作。

打印数据到串口的功能通过UART实现,而UART是连接在CPU的APB总线上的。在软件上向UART发送数据实际上是通过向APB总线发送数据到UART硬件对应的接口,发送数据的操作通过将APB总线的读写映射为MMIO实现,简单来说就是通过CPU向内存读写数据实现总线上的读写操作。

在前面的图片中我们能看到APB总线的MMIO映射地址为0x40000000,那么UART在哪个地址呢?可以通过STM32的应用文档中查看;或者更简单地,直接查看STM32的驱动文件stm32l496xx.h

在stm32l496xx中,APB总线连接了6个串口,起始地址分别是:

UART地址空间的定义是:

对应硬件接口:

usart.png

软件中对UART的读写主要通过对UART本身的寄存器操作实现,例如向串口写一个字节就是:USART->TDR = 0x41,具体的写入内容根据型号有所差异,在STM32F4XX的驱动中相关代码如下:

对于我们STM32L4XX的MCU,在官方的cube中代码实现为stm32l4xx_cube/Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_uart.cHAL_UART_Transmit函数,虽然相对复杂,但本质上也大同小异。

实际上在MCU中printf和puts等函数的实现都是通过逐字节写入UART寄存器实现的。所以我们新建一个c文件并定义最简单的print函数如下:

然后在之前的Reset_Handler稍加修改,令其跳转到我们的主程序执行:

最后编译并重新链接:

监听串口的数据并重新烧写,一个硬核的HelloWorld就完成了!

如果串口是USART而不是UART,那么可能需要经过一些额外的配置,具体可以参考USART vs UART: Know the difference

在实际工程中,真正进入用户程序之前需要初始化好各个硬件外设,配置好基本的中断处理程序。这部分代码一般是由MCU vendor提供的,作为Bootloader(Boot ROM)固化。当然我们这里是绕过MCU直接针对CPU编写程序,以展示软硬件之间的微妙联系。

本文主要介绍了CPU上电从硬件到软件的启动过程,其中一个关键概念就是中断向量表,这是所有First Stage Bootloader都需要理解和实现的地方。此外还介绍了如何通过控制串口在裸机上实现了一简单的HelloWorld应用,这实际上是一个简化的外设驱动,即通过总线读写外设寄存器来封装外部硬件的调用,这部分代码在内核中也是相当常见的。

 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2020-5-3 11:08 被evilpan编辑 ,原因: 拒绝标题党从我做起:)
收藏
免费 3
支持
分享
最新回复 (5)
雪    币: 4064
活跃值: (4402)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
感谢分享!
2020-4-11 14:09
0
雪    币: 15698
活跃值: (19008)
能力值: ( LV12,RANK:300 )
在线值:
发帖
回帖
粉丝
3
mark,楼主辛苦了
2020-4-11 15:30
0
雪    币: 2002
活跃值: (607)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
标题党啊,你这个在stm32嵌入式入门教学里属于初级课程,咋变成硬核shellcode了
2020-4-13 21:09
0
雪    币: 17106
活跃值: (13873)
能力值: ( LV15,RANK:595 )
在线值:
发帖
回帖
粉丝
5
hackings 标题党啊,你这个在stm32嵌入式入门教学里属于初级课程,咋变成硬核shellcode了[em_7]
感谢建议,已经进行了相应的修改
2020-5-3 11:10
0
雪    币: 453
活跃值: (261)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
认真读了一遍,楼主的基本功真扎实
2020-7-24 16:44
0
游客
登录 | 注册 方可回帖
返回