背景
一年前使用bochs虚拟机,在x86系统上实现了一个分时操作系统内核,为了进一步理解ARM体系架构,于是我打算基于Cortex-M7内核来编写一个实时操作系统内核,增强对操作系统的理解。
硬件启动流程
无论是什么CPU,启动后的第一行代码肯定是汇编,下面对比一下stm32f7系列和x86的启动流程对比:
芯片架构 | x86 | Cortex-M7 |
---|---|---|
第一段代码 | bios | 固化于芯片内部的一段引导程序 |
第二段代码 | 外部存储介质中的MBR (512bye) 通常称之为boot 加载到内存中后开始运行 | 根据用户选择的自举模式,跳转到不同的地址启动 |
第三段代码 | loader程序 | 无 |
第四段代码 | 操作系统内核 | 无 |
对比两种架构,可以发现有很大的不同,带着疑问去思考是个学习的好方法,也许有人会有一些疑问。
x86的四段代码都做了什么工作?
- 首先,BIOS是固化在主板上的一段程序,由主板厂商完成,当它完成基本的初始化之后和上电自检后,开始从外部存储介质中查找MBR。
- 由于MBR只有512字节,boot无法完成复杂的功能,因此它只做一件事,从文件系统中查找loader,加载到内存中然后进行跳转。
- loader程序的长度远大于512字节,这时候就可以进行更多复杂的操作,第一步是把操作系统文件从外部加载到内存中,第二步是做一些响应的初始化和准备工作,然后从16位的实模式跳转到32位的保护模式,如果是64位的操作系统,还需要从32位保护模式跳转到 IA-32e 模式,这些功能无法在512字节中完成,所以才出现了loader。
- 操作系统启动之后接管所有硬件和软件。
启动汇编文件和链接脚本
前面介绍了Cortex-M7的启动流程,stm32f7就是基于该架构的一款芯片,在芯片内部除了CPU还集成了外设,RAM和Flash,这里我们主要讲解的是操作系统,操作系统和内核相关,和外设基本没有太多关系,因此对外设不做过多了解,RAM和Flash是计算机的存储器,所以我们需要对其启动流程,功能进行深入探究。
绝大多数情况下我们编写的代码都是保存在flash中,选择的启动方式也是从片内flash启动,当芯片内部的引导程序执行完成后,从0x8000004地址取出第一段代码所在的地址,然后开始运行,到这里可能就会有人有疑问了,程序难道不是从0x8000000开始执行的吗?没关系,下面我们就一步步来分解stm32f7的这段汇编程序吧。
查看启动流程的最好的方式就是链接脚本,所以第一步先看链接脚本:
程序的入口和存储器的定义
/* 定义了代码的入口为Reset_Handler */
ENTRY(Reset_Handler)
/* 栈顶指针的初始值,可以用来确定栈和堆的地址范围 */
_estack = 0x20080000; /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */
/* MEMORY指令在嵌入式系统中比较常见,用于定义不同的存储器 */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 512K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
}
- ENTRY用于指定函数的入口为Reset_Handler
- 链接脚本可以直接定义变量,如果变量在程序中已经定义过,那么仅仅是对该变量赋予初始值,_estack 是中断向量表中的第一项,在这里是为了赋予初值,用于初始化栈顶指针。
- _Min_Heap_Size 和 _Min_Stack_Size没有放到最终的程序中,只是为了用于计算使用的RAM是否超过存储器的大小而设置的,一旦所用的内存超过存储器大小就会报错终止编译。
- MEMORY指令在嵌入式系统中比较常见,需要根据存储器实际大小进行定义。
编译后的代码如何放入存储器
/* 将指定的段链接到存储器对应的地址上 */
SECTIONS
{
/* 将中断向量表放到Flash中,第一段数据的起始地址就是从存储器首地址0x8000000开始 */
.isr_vector :
{
. = ALIGN(4);
/* KEEP保证该段数据即使未被使用也不会被编译器去除,因为编译器默认情况下会将未使用的全局变量给去掉 */
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
- FLASH中的第一段是isr_vector,也就是中断向量表,下面是架构手册中列出的中断向量表,他们不是指令,在Cortex-M7架构中有一个寄存器VTOR,用于记录中断向量表的位置,当发生中断后会会从该寄存器指向的位置查找中断向量,然后跳转到中断服务程序。
- 系统启动之后VTOR的默认值是0,这其实是指向了引导程序中的中断向量表,芯片启动之后也会进行部分外设的初始化,系统检查等工作,当异常产生的时候会从0地址开始查找中断服务程序,当跳转到用户编写的代码之后,必须重新设置VTOR,否则产生异常或中断时会跳转到错误的位置。
- 从0x8000000开始存储中断向量,最多有255个中断向量,再加上SP初始值刚好占据1K的空间,即0x8000000~0x80001FF。
/* 当中断向量表存储完之后再把所有的代码段翻入到flash中 */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
/* 在链接脚本中可以定义全局变量,_etext 相当于一个4字节的全局变量,表示代码段的末尾 */
_etext = .; /* define a global symbols at end of code */
} >FLASH
/* 只读数据也放入到Flash中 */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >FLASH
- 紧跟其后的是代码段和只读数据段,他们都位于FLASH中,并且大小会随着代码变化。
/* 此处省略一些和我们关系不大的段 */
/* 位于汇编启动文件中的一个全局变量,代表数据段在Flash中的首地址 */
_sidata = LOADADDR(.data);
/* 数据段中存放的是有初始值的全局变量,首先要随固件一起烧录到Flash中 */
/* 在Flash中的地址叫做LMA(加载地址),在RAM中的地址叫做VMA(运行地址)*/
/* 当程序开始运行之后需要从LMA加载到VMA进行全局变量初始化 */
/* 前面的几个段中没有使用AT,那么编译时他们的LMA和VMA相同 */
/* 使用AT相当于重新指定了一个存储位置 */
.data :
{
. = ALIGN(4);
/* 位于汇编启动文件中的一个全局变量,代表数据段在RAM中的首地址 */
_sdata = .;
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
/* 位于汇编启动文件中的一个全局变量,代表数据段在RAM中的尾地址 */
_edata = .;
} >RAM AT> FLASH
/* _sidata _sdata _edata 这三个全局变量,可以完成C语言中数据段的初始化工作 */
/* Uninitialized data section */
. = ALIGN(4);
.bss :
{
/* 位于汇编启动文件中的一个全局变量,代表BSS段在RAM中的首地址 */
_sbss = .; /* define a global symbol at bss start */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
/* 位于汇编启动文件中的一个全局变量,代表BSS段在RAM中的尾地址 */
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM
/* _sbss _ebss 这两个全局变量,可以完成C语言中BSS段的初始化工作 */
/* BSS段的值全为0,虽然也需要初始化,但是不需要单独存储其初始值到Flash中,所以不用AT */
- 然后是数据段和bss段,这些都是运行过程中需要使用的变量,其中初始值不为0的段叫做数据段,其余没有初始值的或者为0的叫做BSS段。
- data段的初始值需要在用户程序启动之后从FLASH加载到RAM中,之后才能正常运行程序,而初始值在FLASH中的位置,加载到内存中的哪个地址,以及加载的长度,似乎我们都不知道,只有编译器知道。
- 带着这些疑问我们来看一下汇编文件中对_sidata,_sdata ,_edata,_sbss,_ebss 这几个变量的描述
- /* start address for the initialization values of the .data section.
defined in linker script /
.word _sidata
/ start address for the .data section. defined in linker script /
.word _sdata
/ end address for the .data section. defined in linker script /
.word _edata
/ start address for the .bss section. defined in linker script /
.word _sbss
/ end address for the .bss section. defined in linker script */
.word _ebss
- /* start address for the initialization values of the .data section.
- 在链接脚本中存放数据段之前先使用LOADADDR(.data)获取数据段的起始地址作为_sidata的初始值,然后将当前指针的位置存放到_sdata中,当所有段放完之后再把当前指针的位置赋予_edata,有了这三个值就可以在程序启动之后把初始值从FLASH搬到内存中。
- 而BSS段的值全为0,把以_sbss开始,到_ebss结束的位置初始化为0即可,这些工作都由芯片厂商提供的汇编文件完成了,我们不需要过度关心下面是汇编中的代码
-
Reset_Handler:
ldr sp, =_estack /* set stack pointer *//* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInitCopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit/* Zero fill the bss segment. */
ldr r2, =_sbss
ldr r4, =_ebss
movs r3, #0
b LoopFillZerobssFillZerobss:
str r3, [r2]
adds r2, r2, #4LoopFillZerobss:
cmp r2, r4
bcc FillZerobss/* Call the clock system initialization function./
bl SystemInit
/ Call static constructors /
bl __libc_init_array
/ Call the application’s entry point.*/
bl main
bx lr
.size Reset_Handler, .-Reset_Handler
-
- 当全局变量初始化完成之后开始调用SystemInit 初始化时钟,其中包含了VTOR的设置,保证中断可以正常响应。
- __libc_init_array的描述可以看下这个链接,这里不做介绍
- 然后跳转到main函数运行用户代码
/* 上面的data段和bss段以及堆栈在运行时是需要放到RAM中的 */
/* 当这些段的大小超过存储器大小,链接器将报错,通过这样的方式来实现检查 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
.ARM.attributes 0 : { *(.ARM.attributes) }
通过上面的链接脚本可以得出两个存储器中的段布局:
RAM | Flash |
---|---|
data段 | 中断向量表 |
bss段 | 代码段 |
堆段 | 只读数据段 |
栈段 | data段初始值 |
如果是一个没有操作系统的单片机程序,RAM并没有被完全利用起来,因为它没有实现内存管理,在bss段和堆之间还有很大一片内存未得到充分利用。
引导程序如何跳转用户程序
引导程序如果选择FLASH启动,那么从存储器首地址开始就是中断向量表,而ResetHandler放在了第一个中断向量中,只需要取出中断向量,然后进行跳转就可以了。
汇编程序工作总结
- 定义中断向量表
- 初始化全局变量
- 初始化系统时钟和VTOR寄存器
- 初始化C库