Cortex-M3启动过程-Keil#

让我们从编译后说起(以STM32为例)。

1. ELF文件#

在Keil中一个STM32项目被编译后,会生成一个ELF格式的.axf文件(在项目根目录下的Objects文件夹中),该文件中包含有数据段、代码段、调试段等各种信息,调试程序使用的就是ELF文件,ELF文件的格式如下:

2. BIN文件#

但是最终通过JLink下载到STM32内部Flash的不是ELF文件而是BIN文件,BIN文件中是纯粹的指令代码和数据,没有其他冗余的数据。

将ELF文件中冗余的数据去掉,就是BIN文件的内容。如下图所示:

3. BSS段#

BSS段(Block Started by Symbol)中存放的是初始值为零的全区和静态变量。

但是因为BSS段内容全为零,所以在ELF文件和BIN文件中均不包含BSS段的内容,只保存有BSS段的地址和大小信息

程序会在运行的时候根据BSS段地址和大小信息,在相应的内存空间中初始化BSS段。在Keil中这个任务是由ARM C库完成的。

4. 堆和栈#

此处讨论的是STM32系统的堆和栈。

栈是一块由系统专用的内存空间,在发生函数调用时存储函数的局部变量和参数,在Keil中栈的初始化是由ARM C库完成的。栈的操作由CPU自动完成(C语言中的函数调用被编译为汇编时,就会有相应的出入栈操作指令)。

如下图所示,函数开始调用时会有入栈操作指令(当然函数调用结束时会有出栈操作):

堆是一块可以由程序员自由使用的内存空间,在Keil中堆的初始化和管理都是由ARM C库完成的。堆的使用是程序员通过调用malloc(),free()等ARM C库函数完成的。

事实上,堆和栈都是一块预留的内存空间,在程序运行前其内容是没有意义的,所以在ELF文件和BIN文件中均没有堆和栈的内容,只有堆栈的地址和大小信息

STM32在上电启动时,ARM C库会根据BIN文件中堆栈的地址和大小信息,在相应的内存空间初始化堆和栈。

5. ELF文件视图,装载视图和运行视图#

LR_IROM1 0x08000000 0x00010000  {

  ER_IROM1 0x08000000 0x00010000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }

  RW_IRAM1 0x20000000 0x00005000  {
   .ANY (+RW +ZI)
  }
}

  • ELF文件视图:STM32项目编译后,首先会生成ELF文件,ELF文件中包含大量的调试信息,这是程序真正运行时所不需要的。

  • BIN文件视图:所以下载程序时,要先将ELF文件转换成BIN文件,然后再烧写到STM32的Flash中。

  • 运行视图:STM32上电后,程序开始运行,首先会将Flash文件中的 .data段复制到SRAM中,并在SRAM中相应的位置初始化堆栈和.bss段。最好这些工作后跳转到用户的main函数,执行用户编写的业务逻辑。

ELF文件转BIN文件的方法:

# keil_installed_root/ARM/ARMCC/bin/fromelf.exe

fromelf.exe --bin --output test.bin test.axf

下载器与keil配合使用时,可以直接使用ELF文件烧写Flash,这是因为从ELF文件到BIN文件的过程是由Keil自动完成的。

5.1 装载(BIN文件)视图#

最终烧写到STM32内部FLash中的内容如下图所示:

但是代码段(.text)中为什么会包含这些东西呢?

5.2 运行视图#

程序运行时,除了代码段和数据段,还有堆、栈、BSS段。

根据《The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors》 - Chapter 4.8中的描述:

Cortex®-M3 CPU上电后,默认从0x0000 0000地址处取得栈顶地址(MSP),通过ICode bus从0x0000 0004地址处取得PC的值(复位向量),然后开始执行代码。

  • 那么怎么保证编译后的程序,0x0000 0000地址处是栈顶地址(MSP),0x0000 0004地址处是PC的值(复位向量)呢?

依靠的是链接脚本和启动文件

启动文件:

  .section .isr_vector,"a",%progbits
  .type g_pfnVectors, %object
  .size g_pfnVectors, .-g_pfnVectors


g_pfnVectors:

  .word _estack
  .word Reset_Handler
  .word NMI_Handler
  .word HardFault_Handler

链接脚本:

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);    /* end of "RAM" Ram type memory */

_Min_Heap_Size = 0x200 ;    /* required amount of heap  */
_Min_Stack_Size = 0x400 ;   /* required amount of stack */

/* Memories definition */
MEMORY
{
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 64K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 512K
}

/* Sections */
SECTIONS
{
  /* The startup code into "FLASH" Rom type memory */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

在启动文件中定义了isr_vector段,并且isr_vector段中开头两个字就是:

.word _estack
.word Reset_Handler

在连接脚本中我们可以看到_estack的定义:

# 其实就是内存的最高地址
_estack = ORIGIN(RAM) + LENGTH(RAM);    /* end of "RAM" Ram type 

这样设置的话,栈就是向下生长的。

并且在连接脚本中将isr_vector段放在了存储器的首地址,也就是0x00000000地址处,所以0x00000000地址处就是栈顶地址,0x000000004地址处就是复位向量地址。