ARM ContexM指令集
以下内容为STM32的启动文件所涉及到的指令
参考文档:ARM汇编指南v5.06

ARM-ContexM系列启动文件深入解析 startup_stm32h750xx.s
注释部分
;********************************************************************************
;* File Name : startup_stm32h750xx.s
;* @author MCD Application Team
;* Description : STM32H7xx devices vector table for MDK-ARM toolchain.
;* This module performs:
;* - Set the initial SP
;* - Set the initial PC == Reset_Handler
;* - Set the vector table entries with the exceptions ISR address
;* - Branches to __main in the C library (which eventually
;* calls main()).
;* After Reset the Cortex-M processor is in Thread mode,
;* priority is Privileged, and the Stack is set to Main.
;* <<< Use Configuration Wizard in Context Menu >>>
;******************************************************************************
;* @attention
;*
;* Copyright (c) 2018 STMicroelectronics.
;* All rights reserved.
;*
;* This software is licensed under terms that can be found in the LICENSE file
;* in the root directory of this software component.
;* If no LICENSE file comes with this software, it is provided AS-IS.
;*
;*******************************************************************************
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>以上为启动文件的注释部分
这个文件包含了为 MDK-ARM 工具链中的 STM32H7xx 设备准备的向量表。
- 设置初始堆栈指针(SP)。
- 设置初始程序计数器(PC),其值等于
Reset_Handler。 - 使用异常的 ISR 地址设置向量表条目。
- 分支到 C 库中的
__main(最终会调用main()函数)。
处理器状态描述
复位后,Cortex-M 处理器处于线程模式,优先级为特权级,堆栈设置为主堆栈。
Stack_Size EQU 0x400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_spEQU指令
通过查找可查看该指令的使用方法

该指令 为数字常亮,寄存器相对值或者PC相对值提供符号名称,以上还提供了语法以及示例代码片
使用EQU定义常量,类似于C语言中的 #define
Stack_Size EQU 0x400所以以上汇编语句解释为:定义一个常量Stack_Size 并赋值 0x400 (1024)
指定栈空间大小
AREA指令
AREA指令指示汇编程序汇编一个新的代码段或数据段
语法

sectionname 该部分名称:
arrt可选项较多,这里只展示用到的:

NOINIT解释为 该数据段未初始化的或被初始化为零

READWRITE 解释为 该部分可读可写,这是代码区域的默认设置

ALIGN=expression 解释为 用于指定该区域(或称为 section)的对齐边界,默认情况下,ELF(可执行与可链接格式)的 section 是按照四字节边界对齐的。expression 可以是从 0 到 31 的任何整数值。
例如,如果 expression 是 10,那么 section 将会按照 2的10次方=1024 字节,即 1KB 的边界进行对齐。
我们这里是ALIGN=3 即8字节对齐
ALIGN=expression 属性允许开发者指定内存区域的对齐边界,这对于优化内存访问和满足特定硬件要求是非常重要的。
注意:注意事项:
- 对于 ARM 指令 section,不应使用
ALIGN=0或ALIGN=1。 - 对于 Thumb 指令 section,不应使用
ALIGN=0。
AREA STACK, NOINIT, READWRITE, ALIGN=3所以以上代码可解释为:STACK 区域是未初始化的或被初始化为零,这个区域是可读写的,并且其起始地址是 8 字节对齐的。
SPACE指令
该指令保留一个归零的内存快。

{label} SPACE expr
label :可选标签 expr 要填充的字节数或0
Stack_Mem SPACE Stack_Size以上代码解释为:在内存中为堆栈预留空间的。Stack_Size这是一个之前用 EQU 定义的常量,表示要预留的空间的大小,单位是字节。
__initial_sp
- 这是一个标签,表示初始堆栈指针的位置。在这个例子中,由于 ARM Cortex-M 架构使用的是全降序堆栈,
__initial_sp通常会被设置为堆栈区域的末尾。(栈顶,向下生长)
为什么要这么写?
- 在 ARM Cortex-M 微控制器中,
__initial_sp的值会被放在向量表的第一个位置,即启动地址0x00000000处。 - 当微控制器复位时,这个值会被自动加载到堆栈指针寄存器(SP)中,以初始化堆栈。
- 通过这种方式,
__initial_sp为程序提供了一个初始的、有效的堆栈空间,这对于程序的运行是至关重要的。
因此,__initial_sp 的定义方式是基于 ARM 架构的工作原理和堆栈的使用方式。这样的定义方式确保了程序能够在启动时获得一个正确初始化的堆栈指针。
__heap_base
__heap_limit
Heap_Size EQU 0x200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit以上所涉及到的指令与初始化堆栈指令相同,应该可以理解是什么作用,这里不同点在于为什么要这样写
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit__heap_base- 这是一个标签,表示堆区域的开始地址。
Heap_Mem SPACE Heap_Size- 在
HEAP区域中预留了Heap_Size0x200(512字节)的空间。Heap_Mem是这块空间的开始地址。
- 在
__heap_limit- 这是一个标签,表示堆区域的结束地址。
为什么要这么写?
__heap_base和__heap_limit用于定界堆区域。这两个标签分别表示堆的开始和结束,它们可以被运行时环境用来管理堆内存的分配和释放。Heap_Mem SPACE Heap_Size用于实际预留堆内存。这块内存可以被动态分配给程序中需要动态分配内存的部分,例如,通过malloc函数分配的内存。- 在运行时,堆管理器会使用
__heap_base和__heap_limit来确定堆的边界,以便知道哪些地址可以安全地分配,以及何时堆已满。
这样的组织方式为运行时环境提供了必要的信息,以正确、有效地管理堆内存。
为什么 __initial_sp 不需要像 __heap_base 和 __heap_limit 那样定义:
- 堆栈是全降序的,所以
__initial_sp通常会被设置为堆栈区域的末尾。这意味着,不需要像__heap_limit那样单独标记堆栈的结束位置。 - 堆栈的开始位置(即堆栈的底部)通常是固定的,由
AREA定义的区域和SPACE伪指令决定,所以也不需要像__heap_base那样单独标记堆栈的开始位置。
PRESERVE8指令
THUMB 指令

PRESERVE8 指令用于指定当前文件是否保持堆栈的八字节对齐。
- 如果代码保持八字节对齐的堆栈,应使用
PRESERVE8或PRESERVE8 {TRUE}来设置 PRES8 构建属性。 - 如果代码不保持八字节对齐的堆栈,应使用
PRESERVE8 {FALSE}来确保不设置 PRES8 构建属性。
PRESERVE8 ; 表示当前文件保持堆栈的八字节对齐
PRESERVE8 {FALSE} ; 表示当前文件不保持堆栈的八字节对齐
THUMB

ARM指令集分为 32位的ARM指令,16位的THUMB指令,以及后来的既支持32位又支持16位的THUMB 2指令
THUMB 指令在 ARM 汇编中用于声明使用 Thumb 指令集,但它不特指 Thumb-2。
PRESERVE8
THUMBAREA指令扩展

定义的内存区域的名称 RESET
DATA 这表示该区域用于存储数据。DATA 区域通常用于存储初始化的数据。

READONLY指示该区域不能被写入,只是可读的

AREA RESET, DATA, READONLY以上解释为:定义了一个名为 RESET 的只读数据区域。这个区域通常会包含程序的启动代码和其他不会在运行时被修改的数据。
EXPORT指令
EXPORT 指令用于声明一个符号(例如变量或函数)是全局的,这意味着这个符号可以被其他的源文件访问和使用。

EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_SizeEXPORT __Vectors- 这声明了一个名为
__Vectors的全局符号。通常,这个符号代表了向量表的开始位置。向量表通常包含了一系列的地址,这些地址指向了处理器异常和中断的处理函数。
- 这声明了一个名为
EXPORT __Vectors_End- 这声明了一个名为
__Vectors_End的全局符号。这个符号通常代表了向量表的结束位置。
- 这声明了一个名为
EXPORT __Vectors_Size- 这声明了一个名为
__Vectors_Size的全局符号。这个符号通常用于表示向量表的大小。
- 这声明了一个名为
用途:
__Vectors:- 其他模块或链接器脚本可能会使用这个符号来定位向量表的开始位置。
__Vectors_End和__Vectors_Size:- 这些符号可以被用来计算向量表的大小,或者用于确保其他数据和代码不会与向量表重叠。
总结:
通过 EXPORT 指令导出的这些符号,允许其他源文件、模块或链接器脚本访问向量表的相关位置和信息,这对于系统的配置和运行是至关重要的。
DCD指令
DCD指令分配一个或多个内存字,按四字节边界对齐,并定义内存初始运行时候的内容

__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog interrupt ( wwdg1_it)
DCD PVD_AVD_IRQHandler ; PVD/AVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
DCD FLASH_IRQHandler ; FLASH
……………………………………
省略内容
……………………………………
DCD LPUART1_IRQHandler ; LP UART1 interrupt
DCD 0 ; Reserved
DCD CRS_IRQHandler ; Clock Recovery Global Interrupt
DCD ECC_IRQHandler ; ECC diagnostic Global Interrupt
DCD SAI4_IRQHandler ; SAI4 global interrupt
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins
__Vectors_End__Vectors:向量表的标签,表示向量表的开始位置。__Vectors_End向量表的标签,表示向量表的结束位置。DCD:用于定义一个 32 位的字(word)。它用于在内存中存储一个 32 位的值。__initial_sp:表示初始堆栈指针的值各种 Handler
DCD Reset_Handler到DCD SysTick_Handler是 Cortex-M 处理器的系统异常处理函数。DCD 0 ; Reserved表示这些位置是保留的,没有分配给任何异常处理函数。DCD WWDG_IRQHandler到DCD WAKEUP_PIN_IRQHandler是特定于设备的外部中断处理函数。
这段代码主要定义了 ARM Cortex-M 微控制器的向量表,包括系统异常、保留位置和外部中断。向量表的每个条目都包含了一个指向相应处理函数的地址。这些地址在复位时被加载,以及在相应的异常或中断发生时被调用。
异常与架构相关通常不会变,中断由厂商设计,同种内核可能存在差异
AREA指令扩展
EQU指令前面已经提到过
AREA这里的CODE

__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY这段代码定义了向量表的大小,并且定义了一个新的代码区域。下面是对这段代码的详细解释:
1. __Vectors_Size EQU __Vectors_End - __Vectors
__Vectors_Size是一个符号,用EQU定义为__Vectors_End - __Vectors的值。__Vectors_End和__Vectors分别是向量表的结束和开始位置的标签。- 所以,
__Vectors_Size代表了向量表的大小(以字节为单位)。
2. AREA |.text|, CODE, READONLY
AREA指令用于定义一个新的内存区域。|.text|是这个区域的名称。这个名称通常用于存放程序的代码部分。CODE表示这个区域用于存放代码。READONLY表示这个区域是只读的,通常代码区域都是只读的,以防止程序在运行时修改自己的代码。
总结
这段代码计算了向量表的大小,并且定义了一个名为 .text 的只读代码区域。这是 ARM 汇编语言程序组织内存和代码的一种常见方式。
看下面的代码片,指令较多,详解如下
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
PROC指令


PROC指令标记着函数的开始
Reset_Handler PROC
; 这里是 Reset_Handler 过程的体,包含了一系列汇编指令
ENDPEXPORT指令
前面提过这个指令
EXPORT Reset_Handler [WEAK]
EXPORT指令用于将Reset_Handler符号导出,使其成为一个全局符号。这意味着Reset_Handler可以在其他汇编或 C 文件中被访问或链接。Reset_Handler通常是程序启动时执行的第一个函数,它负责进行一些初始化操作。[WEAK]属性指定Reset_Handler为一个弱符号。- 弱符号允许用户在其他地方重新定义。如果存在同名的强符号,那么链接器会使用那个强符号的定义,而忽略弱符号的定义。
- 如果没有其他地方定义同名的强符号,那么链接器会使用这个弱符号的定义。
- 在嵌入式系统中,
Reset_Handler通常是在复位时由硬件调用的第一个函数,它会进行一些必要的系统初始化。 - 通过将
Reset_Handler定义为弱符号,用户可以在他们自己的代码中提供Reset_Handler的一个不同实现,以覆盖默认的实现。
示例:
如果你有一个默认的 Reset_Handler 实现,但在某些情况下,你想要提供一个不同的实现,你可以在另一个文件中定义一个同名的强符号,这个新定义会覆盖原来弱符号的定义。
IMPORT指令
IMPORT SystemInit
IMPORT __main
IMPORT 指令用于导入在其他模块或文件中定义的符号。这允许你在当前模块中引用这些符号,而不需要知道它们的具体实现或地址。
IMPORT SystemInit
- 这条指令导入了一个名为
SystemInit的符号。SystemInit通常是一个函数,用于进行系统初始化,例如配置系统时钟、初始化 RAM 等。 SystemInit的具体实现可能在其他的汇编文件或 C 文件中,但可以在当前模块中被调用。
IMPORT __main
- 这条指令导入了一个名为
__main的符号。__main通常是 C 运行时库提供的一个函数,它负责初始化 C 环境(例如初始化全局变量),然后调用main函数。 __main的具体实现是由 C 运行时库提供的,用户通常不需要关心其实现细节。
LDR指令

LDR有多重用法,这里只解释用到的这种
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0LDR R0, =SystemInit 这条指令有一个特殊的用法。这条指令的意图是将 SystemInit 的地址加载到寄存器 R0 中。
LDR:是 Load Register 的缩写,用于从内存中加载数据到寄存器。R0:是目标寄存器,用于存储加载的数据。=SystemInit:这里的等号=表示取SystemInit的地址。SystemInit通常是一个函数,这里是获取这个函数的地址。
实际操作:
实际上,由于 LDR 指令通常用于从内存地址加载数据,直接加载一个符号地址(如函数地址)需要一些额外的步骤。汇编器会进行一些处理来实现这一点:
- 汇编器会在某个地方存储
SystemInit的地址值。 LDR R0, =SystemInit会被转换为加载该地址值到R0寄存器的操作。
示例:
假设 SystemInit 函数的地址是 0x08004000,那么汇编器和链接器会处理为类似以下的操作:
LDR R0, =0x08004000 ; 将 SystemInit 函数的地址加载到 R0 寄存器中用途:
这种用法通常出现在需要调用函数或者访问变量的场合。,在启动文件中,需要调用 SystemInit 函数来进行一些硬件的初始化操作。在这种情况下,使用 LDR R0, =SystemInit 来获取 SystemInit 函数的地址,然后通过 BLX R0 或其他跳转指令来调用该函数。
这里一样就不解释了
LDR R0, =__mainBLX、BX指令
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0BLX (Branch with Link and Exchange) 指令:
- 作用:
BLX指令用于调用函数,并且可以在 ARM 和 Thumb 指令集之间切换。它会将返回地址(下一条要执行的指令的地址)保存到链接寄存器LR中。 - 特点:
BLX可以实现状态切换,即从 ARM 状态跳转到 Thumb 状态,或从 Thumb 状态跳转到 ARM 状态。
BX (Branch and Exchange) 指令:
- 作用:
BX指令用于实现分支跳转,并且可以在 ARM 和 Thumb 指令集之间切换。但与BLX不同,BX不会保存返回地址。 - 特点:
BX通常用于实现函数返回操作,它可以从一个状态的函数返回到另一个状态的调用者。
BLX用于函数调用,会保存返回地址到LR寄存器,并可以在 ARM 和 Thumb 之间切换。BX用于分支跳转,通常用于函数返回,不会保存返回地址,并可以在 ARM 和 Thumb 之间切换。
LDR R0, =SystemInit
这条指令将 SystemInit 函数的地址加载到 R0 寄存器中。
BLX R0
这条指令使用 BLX 来调用 SystemInit 函数。BLX 指令会将下一条指令的地址(即 LDR R0, =__main 的地址)保存到链接寄存器 LR 中,然后跳转到 R0 寄存器中的地址(即 SystemInit 函数的地址)执行。如果 SystemInit 是 Thumb 模式的代码,而当前是 ARM 模式,那么还会进行状态切换。
SystemInit 函数通常用于进行一些系统初始化的工作,例如配置时钟、初始化内存等。
LDR R0, =__main
在 SystemInit 函数执行完毕并返回后,这条指令将 __main 函数的地址加载到 R0 寄存器中。__main 是由 C 运行时库提供的,它会进行一些额外的初始化工作,然后调用 main 函数。
BX R0
最后,BX R0 指令用于跳转到 R0 寄存器中的地址(即 __main 函数的地址)执行。如果 __main 是 Thumb 模式的代码,而当前是 ARM 模式,那么还会进行状态切换。BX 指令不会保存返回地址,因为在这个上下文中,__main 函数通常不会返回。
ENDP指令
ENDP 指令用于标记一个过程(Procedure)或函数(Function)的结束。它与 PROC 指令配对使用。
Function_Name PROC
; 函数或过程的具体指令
ENDP
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP以上的B .没有遇到过
B .这是一个无限循环,B . 指令表示无条件跳转到当前地址,即创建了一个无限循环。这通常用于异常处理函数中,当异常发生时,如果没有提供具体的处理逻辑,程序会停在这个无限循环中。
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_AVD_IRQHandler [WEAK]
EXPORT TAMP_STAMP_IRQHandler [WEAK]
……………………
省略部分
……………………
WWDG_IRQHandler
PVD_AVD_IRQHandler
TAMP_STAMP_IRQHandler
WAKEUP_PIN_IRQHandler
B .
ENDP
ALIGN- 为这些处理程序(例如
WWDG_IRQHandler)提供自定义的处理逻辑,您可以在您的代码中定义一个同名的函数。由于这个新定义没有[WEAK]属性,它将覆盖原来的弱定义。 - 如果不提供自定义的处理程序,那么当相关的中断发生时,控制权将转到
Default_Handler,并在B .指令处无限循环。
ALIGN指令
ALIGN 指令在 ARM 汇编中用于将当前位置对齐到指定的边界。



这个指令会确保接下来的代码或数据从一个四字节边界开始。如果当前位置已经是四字节对齐的,那么 ALIGN 指令不会有任何效果。如果当前位置不是四字节对齐的,那么会在当前位置和下一个四字节边界之间插入适量的填充字节
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END IF :DEF:__MICROLIB
………………
………………
ELSE
………………
………………这一行是一个条件编译指令,它会检查是否定义了 __MICROLIB 符号。进行条件选择
:DEF: 是一个条件符号,用于检查一个符号是否被定义
在keil中表现为

IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap勾选之后执行
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limitEXPORT 指令用于声明符号,使得这些符号可以被其他模块访问。这里,如果 __MICROLIB 被定义,那么 __initial_sp、__heap_base 和 __heap_limit 这三个符号会被导出。
否则执行
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheapIMPORT指令用于声明在其他模块中定义的符号。这里,如果__MICROLIB没有被定义,那么会导入__use_two_region_memory符号。- 同时,
__user_initial_stackheap符号会被导出。
__use_two_region_memory 和 __user_initial_stackheap 解释 :
相关链接:https://developer.arm.com/documentation/dui0475/i/CJAGBBEG
__use_two_region_memory 和 __user_initial_stackheap 是 ARM 编译器和运行时系统中的特殊符号,它们与内存管理和系统初始化有关。
__use_two_region_memory
这个符号通常与 ARM 的两区域内存模型相关。在两区域内存模型中,栈和堆是分开的,它们各自占用内存的不同区域。如果系统配置为使用这种模型,那么 __use_two_region_memory 符号通常会被定义。这个符号主要用于编译时,帮助编译器理解内存模型的配置,以生成正确的代码。
__user_initial_stackheap
__user_initial_stackheap 是一个用户可以提供的函数,用于初始化和配置栈和堆的内存区域。当 ARM 运行时系统启动时,它会调用这个函数来设置栈和堆的初始位置和大小。这个函数通常会返回一个结构,其中包含了栈和堆的初始位置和大小信息。
这个函数通常在系统不使用 MicroLib 时被定义和使用。MicroLib 是 ARM 提供的一个小型 C 库,适用于资源受限的嵌入式系统。
标号__user_initial_stackheap,表示用户堆栈初始化程序入口。
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
ENDLDR R0, =Heap_Mem:将Heap_Mem的地址加载到R0寄存器中。Heap_Mem是堆的起始地址。LDR R1, =(Stack_Mem + Stack_Size):将Stack_Mem + Stack_Size的结果加载到R1寄存器中。这是栈的结束地址。LDR R2, = (Heap_Mem + Heap_Size):将Heap_Mem + Heap_Size的结果加载到R2寄存器中。这是堆的结束地址。LDR R3, = Stack_Mem:将Stack_Mem的地址加载到R3寄存器中。Stack_Mem是栈的起始地址。BX LR指令用于从当前子程序返回。LR(Link Register)通常存储着返回地址。- ALIGN 确保四字节对其
ENDIF:标志着一个IF条件编译块的结束。END:标志着汇编文件的结束。