背景

一年前使用bochs虚拟机,在x86系统上实现了一个分时操作系统内核,为了进一步理解ARM体系架构,于是我打算基于Cortex-M7内核来编写一个实时操作系统内核,增强对操作系统的理解。

硬件启动流程

无论是什么CPU,启动后的第一行代码肯定是汇编,下面对比一下stm32f7系列和x86的启动流程对比:

芯片架构x86Cortex-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
  • 在链接脚本中存放数据段之前先使用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 LoopCopyDataInit

      CopyDataInit:
      ldr r4, [r2, r3]
      str r4, [r0, r3]
      adds r3, r3, #4

      LoopCopyDataInit:
      adds r4, r0, r3
      cmp r4, r1
      bcc CopyDataInit

      /* Zero fill the bss segment. */
      ldr r2, =_sbss
      ldr r4, =_ebss
      movs r3, #0
      b LoopFillZerobss

      FillZerobss:
      str r3, [r2]
      adds r2, r2, #4

      LoopFillZerobss:
      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) }

通过上面的链接脚本可以得出两个存储器中的段布局:

RAMFlash
data段中断向量表
bss段代码段
堆段只读数据段
栈段data段初始值

如果是一个没有操作系统的单片机程序,RAM并没有被完全利用起来,因为它没有实现内存管理,在bss段和堆之间还有很大一片内存未得到充分利用。

引导程序如何跳转用户程序

引导程序如果选择FLASH启动,那么从存储器首地址开始就是中断向量表,而ResetHandler放在了第一个中断向量中,只需要取出中断向量,然后进行跳转就可以了。

汇编程序工作总结

  1. 定义中断向量表
  2. 初始化全局变量
  3. 初始化系统时钟和VTOR寄存器
  4. 初始化C库
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐