本帖最后由 1455555 于 2023-11-19 17:44 编辑
目录 1、 相关基础概念 2、 启动模式 3、 启动文件 A、启动文件的作用 B、启动文件中的汇编指令 C、启动文件代码 a 开辟栈空间 b 开辟堆空间 c、中断向量表定义 d、复位程序代码 d-1 __main d-1-1 __scatterload d-1-2 __scatterload_null d-1-3 _scatterload_copy d-1-4 __scatterload_zeroinit d-2 __rt_entry d-2-1 __user_setup_stackheap d-2-1-1 __user_inital_stackheap d-2-2 __rt _entry_main e、中断服务函数 f、用户堆栈初始化 D、启动文件内容示意图
1、相关基础概念 在学习启动文件之前,要了解一些有关的概念。 存储芯片根据断电后是否保留存储的信息可分为易失性存储芯片(RAM)和非易失性存储芯片(ROM)。非易失性存储器芯片在断电后亦能持续保存代码及数据,分为闪型存储器(Flash Memory)与只读存储器(Read-OnlyMemory),其中闪型存储器是主流,而闪型存储器又主要是NAND Flash和NOR Flash。APM32使用的为NOR FLASH。 如图1所示,内存可分为6 个储存数据段和3种存储属性区,下面是它们的简要介绍。 ZI:Zeroinitialized 的缩写,包含初始化为 0 的数据(ZIdata)。 Bss: Block Started by Symbol。储存未初始化的,或初始化为0的全局变量和静态变量。 Heap:由程序员分配。 Stack:系统自动分配,是用户存放程序临时创建的局部变量,由系统自动分配和释放。 RW:已经初始化的全局变量和静态变量 Data:数据段,储存已初始化且不为0的全局变量和静态变量。static声明的变量放在data段。 RO:只读 Text:代码段,储存程序代码。 Constdata:储存只读变量。const修饰的只读变量,C语言中,const修饰的局部变量存放在栈上,全局变量放在constdata段。
但需要注意的是,在计算FLASH的使用大小时,要将RW字段算入FLASH中。因为编译器为了完成所有 RW 段数据赋值,其先将 RW 段的所有初值,先保存到 Flash 中,程序执行时,再 Flash 中的数据搬运到 RAM 中,所以 RW 段既占用 Flash又占用 RAM,且占用的空间大小是相等的。 为什么RW段会放到两个区呢?当你断电后,RAM里所有的数据都会丢失,那我们已经初始化不为零的全局变量该怎么办?这时候,就是将存在FLASH里的RW字段复制到RAM里面,栈也就在这个地方被引入。 举个例子,运行APM32F411SDK中的LED_TOGGLE例程,得到如图2结果:
RAM Size = RW-Data+ZI-Data = 1464+516+72=2052B Flash Size = Code+RO-Data+RW-Data = 72+1632=1704B。 2、启动模式 如图3所示,APM32一共有三种不同的启动模式。 Flash memory启动方式 当boot0 = 0,boot1=x时,启动地址:0x08000000 是内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。基本上都是采用这种模式。 有部分MCU可以从外部的FLASH启动,比如STM32H750 System memory启动方式 当boot0 = 1;boot1 = 0时,启动地址:0x1FFF0000从系统存储器启动,这种模式启动的程序功能是由厂家设置的。系统存储器是芯片内部一块特定的区域,开发板在出厂时,在这个区域内部预置了一段BootLoader,也就是我们常说的ISP程序,这是一块ROM,出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader 中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。 下载步骤: 1、将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader 2、最后在BootLoader的帮助下,通过串口下载程序到Flash中 3、程序下载完成后,需要将BOOT0设置为GND,手动复位,从Flash中启动。 SRAM启动方式 当boot0 = 1;boot1 = 1时,启动地址:0x20000000 内置SRAM,一般用于程序调试。
3、启动文件 以内部FLASH启动为例,学习启动文件相关知识。启动文件由汇编编写,是系统上电复位后第一个执行的程序。 A、启动文件的作用 启动文件主要做了以下工作: 1、初始化堆栈指针 SP= _initial_sp 2、初始化程序计数器指针 PC= Reset_Handler 3、设置堆和栈的大小 4、初始化中断向量表 5、配置外部 SRAM 作为数据存储器(可选) 6、配置系统时钟,通过调用 SystemInit 函数(可选) 7、调用 C 库中的_main 函数初始化用户堆栈,最终调用 main 函数。 B、启动文件中的汇编指令
图片4中提及的是比较常见的汇编指令,如果在阅读汇编代码时,发现不了解的汇编指令可以在KEIL编译器中搜索。具体步骤为MDK->Help->uVision Help。如图5,6所示。
C、启动文件代码 以APM32F411的启动代码为例,版本是:APM32F4xx_SDK_V1.4,启动文件名称是: startup_apm32f411.s。截图按照启动文件代码顺序。阅读者可以前往珠海极海半导体有限公司(geehy.com),极海半导体官网下载。 a 开辟栈空间
30 行 EQU:宏定义的伪指令,给数字常量取一个符号名,类似与C中的 define。定义栈大小为0x00000400字节,即 1024B(1KB),常量的符号是 Stack_Size。 31 行 AREA 汇编一个新的代码段或者数据段。段名为STACK,段名可以任意命名;NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照2^3 对齐,即8 字节对齐。 33行 SPACE 分配内存指令,分配大小为Stack_Size 字节连续的存储单元给栈空间。 34 行__initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。 栈顶地址,可以通过.map 文件查看。
我们定义Stack_Size 的大小是 0x00000400,栈顶地址__initial_sp是0x200006a8,那栈底地址是0x200006a8 - Stack_Size0x400= 0x200002a8。栈是从高往低生长,所以每使用一个栈空间地址,栈顶地址__initial_sp 就减一。
b 开辟堆空间
开辟堆的大小为0x00000200(512 字节),段名为 HEAP,不初始化,可读可写,8 字节对齐。 __heap_base表示堆的起始地址,__heap_limit 表示堆的结束地址。 PRESERVE8:指示编译器按照 8 字节对齐。 THUMB:指示编译器之后的指令为 THUMB 指令。
Thumb指令,Thumb代码使用的指令数要比ARM代码多约30%~40%,但最终生成的目标代码所需的存储空间约为ARM代码的60%~70%(因为每条指令所占空间是arm指令的一半)。在存储器是32位的情况下,ARM性能较好,因为同样的代码编译的结果Thumb指令将会比ARM多,Thumb指令仍旧花费指令周期来从32-bit块内存预取。在16-bit内存上,即使有比ARM多的代码,这时Thumb性能也较好,因为Thumb每一条指令预取需要一个周期而每条ARM指令需要两个周期。 ARM规定:PC 最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令(0)还是 Thumb 指令(1)。如图,在单步调试代码时,0x080001CA处,BX R3指令意为跳转至R3寄存器,此时R3寄存器里的值为0x080001D5,最后一位为奇数。单步执行,发现代码运行至0x080001D5处,原因为BIT0的1,并不表示实际地址,而是代表使用THUMB指令。
c、中断向量表定义 在地址0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。 中断向量表被放置在代码段的最前面。例如:当我们的程序在FLASH 运行时,那么向量表的起始地址是:0x0800 0000。地址 0x0800 0000 存放的是栈顶地址。DCD:以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler 中断函数入口地址。从代码上看,向量表中存放的都是中断服务函数的函数名,所以C 语言中的函数名对芯片来说实际上就是一个地址。
如图所示,代码定义了一个数据段,名字为RESET, READONLY 表示只读。EXPORT 表示声明一个标号具有全局属性,可被外部的文件使用。__Vectors为向量表起始地址,__Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。 DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
内核使用了向量表查表机制。向量表是一个WORD(32 位整数)数组, 每个下标对应一种异常,该下标元素的值则是该异常服务函数的入口地址。举个例子,如果发生了异常SVCall,则 NVIC 会计算出偏移移量是 11x4=0x2C,取出服务例程的入口地址并跳入。 要注意的是:地址0x0000 0000 并不是入口地址,而是给出了复位后 MSP 的初值。F407 的向量表格中红色框住部分是系统内核异常。85 个可屏蔽中断通道。
d、复位程序代码 定义一个段命为.text,只读的代码段,在 CODE 区。 利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。 167 行子程序开始 168 行声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务。169行和170 行 IMPORT 表示该标号来自外部文件。 171 行 LDR 表示从存储器中加载字到一个存储器中。SystemInit 是一个标准的库函数,在 system_stm32f4xx.c 文件中定义,主要作用是配置系统时钟。 172 行 BLX 表示跳转到由寄存器给出的地址,把跳转前的下条指令地址保存到LR。 173 行把__main 的地址给 R0。__main 是一个标准的C 库函数,最终调用main 函数去到 C 的世界。 174 行 BX 表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main 地址,最终调用 main 函数,不返回,进入 C 的世界。 175 行 ENDP 表示子程序结束。
d-1 __main 当编译器发现定义了main 函数,那么就会自动创建_main。主要包含下面两个函数。 __scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了ZI 运行域的初始化工作。 __rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数 (1)段是__main 函数, (2)段是__scatterload 函数, (3)段是__scatterload_null 函数。当程序运行到__main 函数,先跳转到__scatterload 函数运行,执行__scatterload 函数。
后文的说明中有观察寄存器的值,为了查看寄存器的方便和文章的统一性,这里给出调试时的汇编窗口,但反汇编代码更清晰直观,所以截出反汇编文件中相同部分的图片。这两张图内容是一样的,选其一看即可。 d-1-1 __scatterload 当程序运行到__main 函数,先跳转到__scatterload 函数运行,执行完__scatterload 函数后,R10 和 R11 会被赋值。
d-1-2 __scatterload_null
第1、2 行比较 r10、r11 是否相等,如果不等则跳转到 0x080001B6。 第4行是把 0x080001AF 赋值给 lr,即是保存_scatterload_null 的入口地址; 第5 行是把 r10 对应地址存放的 4 个字复制到 r0-r3 中,执行后r0,r1,r2,r3,r10的值都得到了更改。 此时, R0: 0x080007BC 表示的是加载域起始地址。 R1: 0x20000000 为运行域地址。 R2: 0x00000048 为要复制的 RWData 大小,也可以在 map 文件查找得知。 R3:0x080001D5 是_scatterload_copy 函数的起始地址。
d-1-3 _scatterload_copy
通过__scatterload_null 函数的最后一行跳转到_scatterload_copy函数。_scatterload_copy 复制好 RWData 后,最后跳转回到__scatterload_null。回到__scatterload _null 函数,判断 r10 和 r11 是否相等,不等,代码继续运行,最后跳转到r3 寄存器存的地址。 此时是循环回来再执行完__scatterload_null 函数后,即将进入__scatterload_zeroinit函数,先来看一下 r0 到 r3 的值变化。 R0: 0x08000804 表示的是加载域结束地址。 R1: 0x20000048 为 ZI 段的起始地址。 R2: 0x00000660 为 ZI 段大小,即ZI Data 大小,也可以在 map 文件查找得知。 R3: 0x080001F1 是__scatterload_zeroinit函数的起始地址。
d-1-4 __scatterload_zeroinit __scatterload_zeroinit 代码对ZI 段清零的过程,从ZI 段的起始地址 0x20000048 开始,大小为0x00000660,进行清零操作。最后跳转回__scatterload 函数。
这一小部分的执行顺序可以用下图表示。 第一步:使用__scatterload和__scatterload_null进行数据处理,得到__scatterload_copy函数所需要的关键数据。 第二步:执行__scatterload_copy函数,完成负责把 RW 输出段从装载域地址复制到运行域地址。 第三步:执行__scatterload_null,继续进行数据处理,得到__scatterload_zeroinit函数所需关键数据。 第四步:执行__scatterload_zeroinit函数,完成了ZI 运行域的初始化工作。 第五步:跳回__scatterload函数,准备执行__rt_entry函数。
d-2 __rt_entry d-2-1 __user_setup_stackheap __rt_entry 函数开始就先调用__user_setup_stackheap函数来建立堆栈。
__user_setup_stackheap 函数的第一条指令是保存函数的返回地址。第二条指令是跳转到__user_libspace 进行一些微库的初始化工作,后面的几条语句是建立一个临时栈。 d-2-1-1 程序跳转到__user_inital_stackheap 进行用户栈的初始化。
下面为__user_inital_stackheap 代码,这段代码就是启动文件中初始化堆栈的代码。
d-2-2 __rt _entry_main 运行到__rt _entry_main,到main(),执行到我们自己写的程序。
e、中断服务函数 B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。 开启了中断,但忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,B 指令作用下跳转到一个‘.’中,无限循环。
f、用户堆栈初始化 ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,空缺则表示4字节对齐。要注意的是,这个不是 ARM 的指令,是编译器的。 383 行判断是否定义了__MICROLIB。 385 行到 387 行如果定义__MICROLIB,声明__initial_sp、__heap_base 和__heap_limit 这三个标号具有全局属性,可被外部的文件使用。__initial_sp 表示栈顶地址,__heap_base 表示堆起始地址,__heap_limit 表示堆结束地址。 337 行没有定义__MICROLIB,使用默认的C 库运行。堆栈的初始化由 C 库函数__main 来完成。 接下来指令为:保存堆起始地址;保存栈大小;保存堆大小;保存栈顶指针;跳转到LR 标号给出的地址,不用返回。 406 行 END 表示到达文件的末尾,文件结束
D、启动文件内容示意图
|