STM32启动文件.s解析


基本介绍

启动文件由汇编编写,是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作:
1、初始化堆栈指针 SP = _initial_sp
2、初始化程序计数器指针 PC = Reset_Handler
3、设置堆和栈的大小
4、初始化中断向量表
5、配置外部 SRAM 作为数据存储器(可选)
6、配置系统时钟,通过调用 SystemInit 函数(可选)
7、调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数
在这里插入图片描述

实践详解

栈空间开辟

栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,。如果程序出现了莫名其妙的错误,并进入了 HardFault 的时候,就要考虑是不是栈空间不够大,溢出了。
在这里插入图片描述

33 行 EQU:宏定义的伪指令,给数字常量取一个符号名,类似与 C 中的 define。定义栈大小为 0x00000400 字节,即 1024B(1KB),常量的符号是 Stack_Size。
35 行 AREA 汇编一个新的代码段或者数据段。段名为 STACK,段名可以任意命名;NOINIT 表示不初始化,READWRITE 表示可读可写,ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。
36 行 SPACE 分配内存指令,分配大小为 Stack_Size 字节连续的存储单元给栈空间。
37 行__initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。

在之前介绍的map文件里可以看到对应的地址空间:
在这里插入图片描述
我们定义 Stack_Size 的大小是 0x00000400,栈顶地址__initial_sp 的地址是 0x20000788,栈低地址是0x20000388。内存大小刚好就是 Stack_Size 的大小。栈是从高往低生长,所以每使用一个栈空间地址,栈顶地址__initial_sp 就减一。

堆空间的开辟

堆主要用于动态内存的分配,像 malloc()、calloc()和 realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
在这里插入图片描述
开辟堆的大小为 0x00000200(512 字节),段名为 HEAP,不初始化,可读可写,8 字节对齐。__heap_base表示堆的起始地址,__heap_limit 表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。
在这里插入图片描述
PRESERVE8:指示编译器按照 8 字节对齐。
THUMB:指示编译器之后的指令为 THUMB 指令。

中断向量表定义

在这里插入图片描述
定义一个数据段,名字为 RESET, READONLY 表示只读。EXPORT 表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End 和__Vectors_Size 三个标号具有全局性,可被外部的文件使用。
在这里插入图片描述

当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了向量表查表机制。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。上图是F103的向量表。

如果发生了异常 SVCall,则 NVIC 会计算出偏移移量是 0x2C,然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类:地址 0x0000 0000 并
不是什么入口地址,而是给出了复位后 MSP 的初值。

灰色部分是系统内核异常。表格中位置 0 到 59 是外部中断,CM3内核的芯片最大支持 240 个外部中断,具体使用多少个由芯片厂家设计决定。如这个表格中的 103 芯片只是使用了 60 个。这里说的外部中断是相对内核而言。
在这里插入图片描述
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。

DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。中断向量表被放置在代码段的最前面。例如:当我们的程序在 FLASH 运行时,那么向量表的起始地址是:0x0800 0000。地址 0x0800 0000 存放的是栈顶地址。
DCD:以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler中断函数入口地址。
从代码上看,向量表中存放的都是中断服务函数的函数名, C 语言中的函数名对芯片来说实际上就是一个地址。

复位程序

定义一个段命为.text,只读的代码段,在 CODE 区。
在这里插入图片描述
利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
145 行子程序开始
146 行声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务。WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号。
147 行和 148 行 IMPORT 表示该标号来自外部文件。这里表示 SystemInit 和__main 这两个函数均来自外部的文件。
149 行 LDR 表示从存储器中加载字到一个存储器中。SystemInit 是一个标准的库函数,在 system_stm32f1xx.c 文件中定义,主要作用是配置系统时钟、还有就是初始化 FSMC/FMC总线上外挂的 SRAM(可选),前面说配置外部 SRAM 作为数据存储器(可选)就是这个。
150 行 BLX 表示跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
151 行把__main 的地址给 R0。__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈和变量等,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因,如果不调用__main,那么程序最终就不会调用我们 C 文件里面的main,也就无法正常运行。
152 行 BX 表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main地址,最终调用 main 函数,不返回,进入 C 的世界。
153 行 ENDP 表示子程序结束。
在这里插入图片描述

对于_main 函数的分析

_main 和 main 是两个完全不同的函数。_main 代码是编译器自动创建的,因此无法找到_main 代码。当编译器发现定义了 main 函数,那么就会自动创建_main。程序经过汇编启动代码,执行到__main()后,可以看出有两个大的函数:
__scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了 ZI运行域的初始化工作。
__rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数。

中断服务程序

在这里插入图片描述
中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号。
分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。
在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现。
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个‘.’中,无限循环。

用户堆栈初始化

ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。
在这里插入图片描述
IF, ELSE, ENDIF 是汇编的条件分支语句。
331 行判断是否定义了__MICROLIB。关于__MICROLIB 这个宏定义,我们是在 KEIL里面配置,MicroLIB 是 MDK 自带的微库,是缺省 C 库的备选库,MicroLIB 进行了高度优化使得其代码变得很小,功能比缺省 C 库少。MicroLIB 是没有源码的,只有库。
在这里插入图片描述
勾选了 Use MicroLIB 就代表定义了__MICROLIB 这个宏。
333 行到 335 行如果定义__MICROLIB,声明__initial_sp、__heap_base 和__heap_limit这三个标号具有全局属性,可被外部的文件使用。__initial_sp 表示栈顶地址,__heap_base表示堆起始地址,__heap_limit 表示堆结束地址。
337 行没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的 C 库运行。堆栈的初始化由 C 库函数__main 来完成。
339 行 IMPORT 声明__use_two_region_memory 标号来自外部文件。
340 行 EXPORT 声明__user_initial_stackheap 具有全局属性,可被外部的文件使用。
342 行标号__user_initial_stackheap,表示用户堆栈初始化程序入口。
接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。
344 行保存堆起始地址。
345 行保存栈大小。
346 行保存堆大小。
347 行保存栈顶指针。
348 行跳转到 LR 标号给出的地址,不用返回。
354 行 END 表示到达文件的末尾,文件结束。

系统启动流程

Cortex-M3内核复位后的起始地址和中断向量表的位置可以被重映射。通过启动模式的选择,有以下 3 种情况:
1、通过 boot 引脚设置可以将中断向量表定位于 SRAM 区,即起始地址为 0x2000000,同时复位后 PC 指针位于 0x2000000 处;
2、通过 boot 引脚设置可以将中断向量表定位于 FLASH 区,即起始地址为 0x8000000,同时复位后 PC 指针位于 0x8000000 处;
3、 通过 boot 引脚设置可以将中断向量表定位于内置 Bootloader 区。

Cortex-M3 内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在 Cortex-M3 内核复位后,会自动从起始地址的下一个 32 位空间取出复位中断入口向量,跳转执行复位中断服务程序。

复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复位状态后,CM3 内核做的第一件事就是读取下列两个 32 位整数的值:
(1)从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址。
(2)从地址 0x0800 0004 处取出程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令。下面用示意图表示

传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。而在 CM3 内核中,0 地址处提供 MSP 的初始值,然后就是向量表,向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是 Reset_Handler 这个函数。
在这里插入图片描述

ARM 规定:PC最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令(0)还是 Thumb 指令(1),因为 CM3 主要执行 Thumb 指令,所以这些指令的最低位都是 1(都是奇数),使用 0x080001CD 来表达地址 0x080001CC 。 当0x080001CD 处的指令得到执行后,就正式开始了程序的执行(即去到 C 的世界)。所以在此之前初始化 MSP 是必需的。
因为可能第 1 条指令还没执行就会被 NMI (可屏蔽中断)或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
在这里插入图片描述

地址 0x08000000 的值是 0x20000788,地址 0x08000004 的值是0x080001CD,即堆栈指针 SP = 0x20000788,程序计数器指针 PC = 0x080001CD。因为 CM3内核是小端模式,所以倒着读。

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值