带你快速理解ARM启动文件

  • 预备知识
  • ARM的科普
  • ARM汇编
  • 堆和栈
  • 栈(stack)
  • 堆(heap)
  • 代码解读(KEIL)
  • 代码解读(IAR)
  • 程序到底如何运行

小编写在前面的话:


这篇博文是小编在学习的过程中不懂就查,查完整理再加上自己的理解后的结果。涉及的内容广度会比较大,包括ARM和汇编科普、堆栈概念、汇编代码解读、程序运行。希望能够基于startup_stm32l071xx.s代码将上述几个方面的知识平铺开来,让大家能够读懂ARM启动文件。

预备知识

此处针对ARM小白的快速科普喔~

ARM的科普

相信大家都听说过CPU,CPU的学名叫中央处理器,它是一个机器的大脑,可以用于计算和控制机器等。世界上又各种大大小小的机器,有的复杂有的简单,就产生了不同的架构需求。ARM和x86就是我们比较熟悉的CPU的两个不同的架构。ARM是RISC(精简指令集)CPU,而x86是Intel公司出品的CISC(复杂指令集)CPU。ARM架构偏向于处理简单任务,应用在如智能传感终端上。而Intel公司出品的CPU(x86,x64等)偏向于处理更复杂的任务,应用在如台式机、服务器处理器上。

ARM汇编

如果你有一个ARM的工程文件,必不可少的就是它的启动文件,就那stm32l071这款芯片举例,它的启动文件就叫做startup_stm32l071xx.s,打开它,你就看到了ARM汇编代码。汇编是低级语言(不是说语言很简单,而是更贴近计算机底层的语言。小编觉得会这个低级语言的人十分高级),它直接描述/控制着CPU的运行。

堆和栈

主要说说两个非常基本的内存分配区:堆和栈。

栈偏高效,堆偏灵活。为什么这么说呢?

栈(stack)

  1. 系统自动分配释放:栈的地址都有专门的寄存器提供存放,也有专门的指令实现进栈、出栈操作,这就决定了栈的效率比较高。
  2. 所分配的内存是连续的,从高地址向低地址分配(低地址方向是出栈入栈的口)。栈的最大容量是系统预先规定好的
  3. 栈区存放的内容与函数有关(有函数就有栈):断点地址(返回地址)、函数参数、函数内部动态局部变量、函数返回的数据。

堆(heap)

  1. 由程序员malloc分配,free释放:堆是由用户高级语言提供的,指令的封装比栈复杂,所以效率没有栈高。如果程序员没有free,则分配的内存一直存在直到程序结束后被os回收。
  2. 所提供的内存不连续,从低地址向高地址分配。每一次malloc得到的内存块连续,但上一次malloc和下一次malloc得到的内存块不连续,需要用链表串起来。堆的大小受限于系统中有效的虚拟内存。
  3. 堆区存放的数据内容由用户决定:数据、数组、结构体、字符串。

在内存分配上还有:
guard栈保护区:用来检测栈是否溢出。当栈发生溢出时,操作系统可以很容易地检测到这种情况。对于不具备MMU(内存管理单元)的小型嵌入式系统,栈保护区也可以用来存储写入到里面的数据。

global/static variables全局区:全局变量和静态变量存放区,程序结束后由系统释放。

:频繁使用heap会产生内存碎片,应该遵循先申请后释放的原则来避免在堆中产生碎片。

代码解读(KEIL)

如果我在使用C语言开发智能终端,却不知道程序到底是从哪开始,要往哪里去的,我就还是不能说我明白这个程序,所以,今天我们就一起来看看运行ARM工程最最最开始的代码——启动文件startup_stm32l071xx.s。
:汇编代码中的分号;表示注释。

//声明一个栈
Stack_Size   EQU   0x00000400 
		AREA  STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem    SPACE  Stack_Size
__initial_sp

EQU 等值命令 EQU 0x00000400 相当于#define Stack_Size 0x00000400,只是一个声明,并未分配地址。

AREA:告诉编译器一个新的代码段或者数据段。STACK表示段名(可任意命名);NOINIT表示不初始化;READWRITE表示可读可写;ALIGN=3表示按照2^3对齐,即8字节对齐。

SPACE:用于分配大小为Stack_Size的内存空间

__initial_sp表示栈的结束地址(栈是由高向低地址生长的)

//声明一个堆
Heap_Size    EQU   0x00000200
	AREA  HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem    SPACE  Heap_Size
__heap_limit
	PRESERVE8
	THUMB

对于堆来说,有一些和栈类似就不再赘述。说说不一样的地方:

__heap_base表示堆的起始地址。

__heap_limit表示堆的结束地址。(堆是由低向高地址生长的)

PRESERVE8:指定当前文件的堆按照8字节对齐,这是keil编译器的一个编程要求。

THUMB:表示后面指令兼容THUMB指令(THUMB指令是ARM以前的指令集)

//向量声明
AREA  RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size

定义一个名为RESET的数据段,权限为仅可读。

声明的三个symbol可供外部文件调用。

(EXPORT使symbol有全局属性,如果是IAR编译器,则使用的是GLOBAL这个指令。)

//向量表
Vectors    DCD   __initial_sp       ; Top of Stack
	DCD   Reset_Handler       		; Reset Handler
	DCD   NMI_Handler        		; NMI Handler
	DCD   HardFault_Handler     	; Hard Fault Handler
	DCD   0             ; Reserved
	DCD   0             ; Reserved
	DCD   0             ; Reserved
	DCD   0             ; Reserved
	DCD   0             ; Reserved
	DCD   0             ; Reserved
	DCD   0             ; Reserved
	DCD   SVC_Handler        ; SVCall Handler
        ...
__Vectors_End
__Vectors_Size  EQU  __Vectors_End - __Vectors
                AREA    |.text|, CODE, READONLY //定义一个名为.text的代码段,可读。

:DCD相当于C语言当中的&,定义地址。
向量表从FLASH的0地址开始放置,以4个字节为一个单位,地址0存放的是栈顶地址。0x04存放的是复位程序的地址,以此类推。

; Reset handler routine
Reset_Handler  PROC
	EXPORT Reset_Handler         [WEAK]//弱定义
	IMPORT __main
	IMPORT SystemInit			
	LDR   R0, =SystemInit	//调用SystemInit初始化系统时钟
	BLX   R0
	LDR   R0, =__main	//调用main初始化用户堆栈,引导程序进入__main
	BX   R0
	ENDP

接下来有很多中断服务函数。但是这些函数在这里是空的,只是占了个位置。真正的中断服务函数程序需要我们在外部c文件里面重新实现。

如果我们在使用某个外设的时候开启了某个中断,却忘记写中断服务程序,那么当中断被触发,程序就会跳到这里的启动文件里空的中断并无限循环,程序就死了。

NMI_Handler   PROC
	EXPORT NMI_Handler          [WEAK]
	B    . //跳转到. 表示无限循环
	ENDP
HardFault_Handler    PROC
    EXPORT  HardFault_Handler              [WEAK]
    B       .
    ENDP
    ……
    ALIGN //对指令或者数据存放的地址进行对齐,后面会跟立即数,若缺省则表示4字节对齐。

用户堆栈初始化,在keil的option for target中的target标签可设置。

arm java 启动项目 arm启动文件_Stack

汇编代码如下:

;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
    //判断是否定义了__MICROLIB
	IF   :DEF:__MICROLIB //若已经定义,则赋予下面三个symblos全局属性
	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

代码解读(IAR)

在小编的IAR工程中,引用的启动文件和keil又不相同了。在IAR的库中提供了cstartup.s, cmain.s, cexit.s文件。即使你用的是KEIL,也不妨看看IAR这部分,也许有些知识点是上面没有出现的。

//MODULE...END表示开始和结束,?cstartup是模块的名字
MODULE ?cstartup
...
END

代码的启动函数在cstartup.s中

SECTION(声明段)。

语法格式:SECTION section:type [flag] [(align)]

section是段的名字;

type是memory的类型,取值是CODE, CONST,DATA。

flag取值有:NOROOT, ROOT, REORDER, NOREORDER。默认是ROOT,表示不可被优化。NOROOT表示如果这个段中的符号没有被引用,将会被连接器舍弃。REORDER表示开始一个新的名字是section的段(section)。NOREORDER表示开始一个新名字为section的片段(fragment),多个fragment组成一个section。

align,用于指定地址对齐到2^align(align取值范围在0到30)

;; Forward declaration of sections.
//分了两个段:数据段和代码段
SECTION CSTACK:DATA:NOROOT(3)
SECTION .intvec:CODE:NOROOT(2)
//表示导入其它的模块
EXTERN __iar_program_start
EXTERN SystemInit
    
PUBLIC __vector_table
//PUBLIC说明当前的模块所引用的标识符(__vector_table)可以被其它的模块引用。

启动文件的引导地址可以自己定义,如果使用默认的配置,IAR在编译时,将会使用IAR自己的系统库作为引导。

arm java 启动项目 arm启动文件_arm_02


DATA表示以下的标签是32位的标签,THUME表示以下的标签是16位的标签。(标签是地址的别名,不占用代码空间,给编译器看的。)

DATA
__vector_table
	DCD   sfe(CSTACK)
	DCD   Reset_Handler       ; Reset Handler
    ...

中断向量表的顺序不能变化,此部分在flash的最开始部分。当系统启动的时候会加载前两个地址,第一个地址是c程序栈的栈顶地址,第二个地址是向量表的开始地址。中断发生时会根据向量表的首地址和偏移量来找到程序的入口。

sfe指令的作用:返回栈的结尾。(因为栈的增长方向是反方向的)

ARM核分为两个状态:ARM和THUMB。两者的区别是:指令的长度不同。

Cortex-M3只有Thumb-2状态和调试状态。Thumb-2状态时arm和thumb状态的结合和优化。

ARM的M系列主要用Thumb指令,ARM9和A系列主要用ARM指令。

两状态的切换:

ARM->THUMB 执行BX指令后,R0[0]=1,

THUMB->ARM 执行BX指令后,R0[0]=0,

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Default interrupt handlers.
;;
	THUMB
	PUBWEAK Reset_Handler
	SECTION .text:CODE:NOROOT:REORDER(2)
Reset_Handler

	LDR   R0, =SystemInit//将SystemInit寄存器地址加载到R0
	BLX   R0             //程序跳转到R0中的地址执行,并将处理器切换为THUMB状态,将PC地址保存到R14。
	LDR   R0, =__iar_program_start//把__iar_program_start地址加载到寄存器R0。__iar_programe_start就是程序的入口函数。
	BX   R0				//程序跳转到R0中的地址执行,并将处理器切换为THUMB状态/

B{条件} 目标地址 //最简单的跳转指令,立即跳转。

BL{条件} 目标地址 //带链接的跳转。先将当前指令的下一条指令地址(PC)保存在LR寄存器(R14),然后跳转到目标地址。

BX{条件} 目标地址 //带状态切换的跳转。跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令(最低位为0),也可以是Thumb指令(最低为为1)。

BLX{条件} 目标地址 //带链接和状态切换的跳转(ARM/Thumb指令的跳转)。结合BX与BL功能。

程序到底如何运行

首先,在汇编器中编写有一个短模块Reset_Handler()(在代码解读里描述过),系统已启动就立即执行。主要是为应用程序的运行模式初始化堆栈指针还有一些必须的配置。

简单看一下函数实现,不做详解。

void Reset_Handler(void)
{
	uint32_t *pSrc, *pDest;
	/* Initialize the relocate segment */
	pSrc = &_etext;
	pDest = &_srelocate;
	if (pSrc != pDest) {
		for (; pDest < &_erelocate;) {
			*pDest++ = *pSrc++;
		}
	}
	/* Clear the zero segment */
	for (pDest = &_szero; pDest < &_ezero;) {
		*pDest++ = 0;
	}
	/* Set the vector table base address */
	pSrc = (uint32_t *) & _sfixed;
	SCB->VTOR = ((uint32_t) pSrc & SCB_VTOR_TBLOFF_Msk);
	
	/* Overwriting the default value of the NVMCTRL.CTRLB.MANW bit (errata reference 13134) */

	NVMCTRL->CTRLB.bit.MANW = 1;
	/* Initialize the C library */
	__libc_init_array();
	/* Branch to main function */

	main();

	/* Infinite loop */
	while (1);

}

所以在main()函数之前还有一些初始化处理,然后才进入我们编写的应用程序main()。