一篇解释arm启动代码中最生涩隐晦地方的好文

本文探讨了使用MMU进行地址重映射的启动代码结构,介绍了映像文件的基本组成及其加载过程,详细解释了堆栈初始化的过程,并讨论了启动代码的内容和初始化顺序。
 
使用MMU进行地址重映射的启动代码结构探讨
 
 

——使用AXD调试MMU地址映射程序手记(二)后记

黄振华                        http://aquarius.cublog.cn/

 

摘要:

本文是对《使用AXD调试MMU地址映射程序手记(二)》一文的补充,首先对编写启动代码所需要了解的基本知识和大多数初学者可能比较模糊的基本概念作了简单介绍,然后对启动代码的结构或说流程做了一些探讨。

事实上,对于一个简单的嵌入式应用,编写启动代码并不困难,但如果要在启动代码中使用MMU并完成地址重映射,有一些关键的步骤就值得商酌。

本文仅是笔者学习过程中的一点心得,纰漏之处在所难免,旨在抛砖引玉,给初学者一个思考的方向,更多以及更权威内容请参考文后所列的参考资料。

 

一、映像文件基本组成

映像文件加载时域包括RORW段,运行时域则包括RORWZI三个段。其中RORW段的内容在加载时和运行时是一样的,只是存储空间可能不同,而ZI段则是运行时由初始化函数创建的。

       RO段:Read-Only段,包括源程序中的CODE段,只读数据段(包括变量的初始化值——可以是任意变量,全局/局部、静态/动态变量的初值;还包括数据常量——这个常量也可以是全局的或局部的。也就是说,编译器既要为变量分配存储空间——变量是可读写的,并不放在RO段,又要为变量的初值分配存储空间,两者是两回事)。

       RW段:可读写段,主要指RW-DATA,也可能有RW-CODERW-DATA是指已经初始化的全局变量。

       ZI段:Zero-Initialized段,主要包括未初始化的全局变量,编译器用0值对其进行初始化。该段中的数据由于是变量,因而也是可读写的,但在映像文件加载时,并不为ZI段分配存储空间,虽然在ADS编译器的Memory map文件中认为Total RW Size = (RW Data + ZI Data)

 

       二、代码,数据和变量在映像文件中的位置

       上面简单总结了映像文件各段的组成。从程序的组成看,可以分为变量、数据和代码,其中变量又分为全局/局部的或静态/动态的,它们的存储空间又是如何分配的呢?

       代码:一般是只读的,由编译器分配存储空间并放到映像文件的RO段。

       数据:这里所指的数据都是常量(若可变则为变量),也包括指针常量,那么也属于只读的数据,也由编译器分配存储空间放到映像文件的RO段。

       变量:主要根据生存期来分,因为生存期是按在内存中的生存时间来定义的,而作用域与存储空间分配无关。

       1.全局变量和静态变量:包括静态局部变量和全局/静态指针变量在内,由编译器分配存储空间,已初始化的放到RW段,否则放到ZI段;

       2.动态变量:主要是指局部变量,包括局部指针变量在内,占用栈空间。

 

       三、启动过程中的堆栈初始化释疑

       堆与栈:对于ARM,堆是向上生长的,栈是向下生长的。

       局部变量占用栈(stack)空间(但其初始化值为数据,占用RO空间);

       程序中动态申请的如malloc()new函数申请的内存空间占用堆(heap)空间。

 

       ————×以下讨论不使用semihosting机制×————

 

       因此,在转入C应用程序前,必须要为C程序准备堆栈空间。根据具体的目标平台的存储器资源,要对堆栈的初始化函数__user_initial_stackheap( )进行移植,主要是正确设置堆(heap)和栈(stack)的地址。它可以使用CARM汇编语言来编写,并至少返回堆基址(保存在R0中),栈基址(保存在R1)可选。因而一个简单的汇编语言编写的__user_initial_stackheap( )函数如下:

              EXPORT        __user_initial_stackheap

__user_initial_stackheap

              LDR              R0, =0x20000        ;heap base

              LDR              R1, =0x40000        ;stack base, optional

              MOV             PC, R14

       该函数的C语言实现可见参考资料[6]P158页。

 

       注意,如果在工程中没有自定义这个函数,那么缺省情况下,编译器/链接器会把|Image$$ZI$$Limit|作为堆(heap)的基址(即把heapstack区放置在ZI区域的上方,这也被认为是标准的实现[7])。但是,如果使用scatter文件实现分散加载机制,链接器并不生成符号|Image$$ZI$$Limit|,这时就必须自己重新实现__user_initial_stackheap( )函数并且设置好堆基址和栈顶,否则链接时会报错。

 

       堆栈区还分为单区模型和双区模型,在双区模型中,还必须设置堆栈限制[4,6,7]

 

       关于重定义__user_initial_stackheap( )函数时几点要注意的地方:一是不要使用超过96字节的stack,二是不要影响到R12IP,用作进程间调用的暂存寄存器),三是按规则返回参数值(R0heap baseR1stack baseR2heap limitR3stack limit),四是让堆区保持8字节对齐[6]

 

       在启动代码中,还要对各个处理器模式的栈指针进行初始化。这个问题很容易与上面谈到的__user_initial_stackheap()函数的作用相混淆。可从以下几点来加以说明:

       1)在嵌入式应用中,启动代码分为两个部分:一是系统的初始化,包括中断向量表的建立、时钟、存储系统初始化、关键I/O口初始化、各处理器模式下的栈指针初始化等;二是应用程序初始化(或说C库函数初始化),包括RW段的搬移和ZI段的清零、C应用程序堆栈区的建立(__user_initial_stackheap()函数初始化堆栈指针)等。

       从这个意义上说,两者并没有直接关系。

 

       2)但两者并不是没有联系的。以单区模型的堆栈区为例,由于栈是向下生长的,堆是向上生长的,系统模式的栈指针(与用户模式相同,共用一个R13寄存器来描述)实际上定义了用户模式下单区模型堆栈区的上限,而__user_initial_stackheap()函数中指定的heap基址则成为该堆栈区的下限。

       因此,如果之前已经对系统模式(用户模式)的栈指针进行了初始化,则在重定义__user_initial_stackheap()函数时,就不需要重新定义stack base了。

 

       四、启动代码的内容和初始化顺序探讨

       前面已经指出,启动代码包括系统初始化以及应用程序运行环境的初始化两个部分,完成初始化后,就可以呼叫用户主程序了。参考资料[1][3][5]等都对两个部分的内容以及过程列出了非常清晰但又简单明了的步骤,这对于初学者来说稍微有点抽象。

如果不需要使用MMU进行地址重映射,那么,结合网上可以搜集的示例boot代码以及分析文档,加上自己动手移植和调试,也是比较容易理解的。如果是使用处理器自带的Remap控制寄存器来进行地址重映射,网上也有相关的代码,例如网友twentyoneboot代码【4510 bootloader的实现与分析(附源代码)】就非常清楚,另外,在《ARM学习报告》系列文章中也对其有详细的分析。

 

对于在启动过程中要使用MMU进行地址重映射的系统初始化顺序,在《使用AXD调试MMU地址映射程序手记(二)》一文中给出了一个参考步骤,并做了一定的说明。通过进一步参考权威资料,这里,对系统初始化顺序作了小的改进与修正如下:

①禁止所有中断→②初始化时钟→③初始化存储器→④初始化各模式下的栈指针→⑤初始化GPIO→⑥⑥⑥⑥HHHH拷贝映像文件到SDRAM→⑦建立地址重映射表→⑧使能MMU→⑨应用程序初始化(RW&ZI区)→⑩使能异常中断→⑾呼叫主程序(dummyOS)。

主要对使能异常中断和应用程序初始化的顺序做了调整,即先进行应用程序的初始化,再使能异常中断,可参考[3][10]

......

 

———————————————————————————————————————

【参考资料】

[1]ARM体系结构与编程》,杜春雷,清华大学出版社,20032

[2]ARM嵌入式系统开发——软件设计与优化》,沈建华译,20055

[3]《基于ARM的嵌入式系统程序开发要点》,费浙平,20038

[4]RealView编译工具开发者指南》,ARM Limited20031

[5]ADS Developer Guide》,ARM Limited200111

[6]ADS Compilers and Libraries Guide》,ARM Limited200111

[7]ADS Linker and Utilities Guide》,ARM Limited200111

[8]MAP文件认识初步》,JOHNNY LEE

[9]《堆与栈的区别》,未知网友

[10]《使用ADS1.2进行嵌入式软件开发》,ARM Limited

———————————————————————————————————————

【注】纰漏之处,恳请指正。转载请注明作者及出处。

查看章 STM32 keil mdk启动代码发分析_转2010年01月29日 星期五 13:50 ;// Stack Configuration ;// Stack Size (in Bytes) ;// Stack_Size EQU 0x00000200 ;//定义堆栈大小 AREA STACK, NOINIT, READWRITE, ALIGN=3 ;//定义一个数据段 按8字节对齐 ;AREA 伪指令用于定义一个代码段或数据段 NOINIT:指定此数据段仅仅保留了内存单元,而没有将各初始值入内存单元,或者将各个内存单元值初始化为0 Stack_Mem SPACE Stack_Size ;//保留Stack_Size大小的堆栈空间 分 配连续 Stack_Size 字节的存储单元并初始化为 0 __initial_sp ;//标号,代表堆栈顶部地址,后面有用 ;// Heap Configuration ;// Heap Size (in Bytes) ;// Heap_Size EQU 0x00000020 ;//定义堆空间大小 AREA HEAP, NOINIT, READWRITE, ALIGN=3 ;//定义一个数据段,8字节对齐 __heap_base Heap_Mem SPACE Heap_Size ;//保留Heap_Size的堆空间 __heap_limit ;//标号,代表堆末尾地址,后面有用 PRESERVE8 ;//指示编译器8字节对齐 THUMB ;//指示编译器为THUMB指令 ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY ;//定义只读数据段,其实放在CODE区,位于0地址 EXTERN NMIException EXTERN HardFaultException EXTERN MemManageException EXTERN BusFaultException EXTERN UsageFaultException EXTERN SVCHandler EXTERN DebugMonitor EXTERN PendSVC EXTERN SysTickHandler ;//声明这些符号在外部定义,同C ;//在××it.c中实现这些函数 ,中断就能自动调用了 EXPORT __Vectors EXPORT __initial_sp ;EXPORT:在程序中声明一个全局的标号__Vectors,该标号可在其他的件中引用;IMPORT:伪指令用于通知编译器要使用的标号在其他的源件中定义, ;但要在当前源件中引用,而且无论当前源件是否引用该标号,该标号均会被加入到当前源件的符号表中 __Vectors DCD __initial_sp ; Top of Stack //Cotex-M 要求此处为堆栈顶部地址 DCD Reset_Handler ; Reset Handler DCD NMIException ; NMI Handler DCD HardFaultException ; Hard Fault Handler DCD MemManageException ; MPU Fault Handler DCD BusFaultException ; Bus Fault Handler DCD UsageFaultException ; Usage Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVCHandler ; SVCall Handler DCD DebugMonitor ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSVC ; PendSV Handler DCD SysTickHandler ; SysTick Handler //一大堆的异常处理函数地址 ; External Interrupts EXTERN WWDG_IRQHandler EXTERN PVD_IRQHandler EXTERN TAMPER_IRQHandler EXTERN RTC_IRQHandler EXTERN FLASH_IRQHandler EXTERN RCC_IRQHandler EXTERN EXTI0_IRQHandler EXTERN EXTI1_IRQHandler EXTERN EXTI2_IRQHandler EXTERN EXTI3_IRQHandler EXTERN EXTI4_IRQHandler EXTERN DMAChannel1_IRQHandler EXTERN DMAChannel2_IRQHandler EXTERN DMAChannel3_IRQHandler EXTERN DMAChannel4_IRQHandler EXTERN DMAChannel5_IRQHandler EXTERN DMAChannel6_IRQHandler EXTERN DMAChannel7_IRQHandler EXTERN ADC_IRQHandler EXTERN USB_HP_CAN_TX_IRQHandler EXTERN USB_LP_CAN_RX0_IRQHandler EXTERN CAN_RX1_IRQHandler EXTERN CAN_SCE_IRQHandler EXTERN EXTI9_5_IRQHandler EXTERN TIM1_BRK_IRQHandler EXTERN TIM1_UP_IRQHandler EXTERN TIM1_TRG_COM_IRQHandler EXTERN TIM1_CC_IRQHandler EXTERN TIM2_IRQHandler EXTERN TIM3_IRQHandler EXTERN TIM4_IRQHandler EXTERN I2C1_EV_IRQHandler EXTERN I2C1_ER_IRQHandler EXTERN I2C2_EV_IRQHandler EXTERN I2C2_ER_IRQHandler EXTERN SPI1_IRQHandler EXTERN SPI2_IRQHandler EXTERN USART1_IRQHandler EXTERN USART2_IRQHandler EXTERN USART3_IRQHandler EXTERN EXTI15_10_IRQHandler EXTERN RTCAlarm_IRQHandler EXTERN USBWakeUp_IRQHandler ;//同上, DCD WWDG_IRQHandler ; Window Watchdog DCD PVD_IRQHandler ; PVD through EXTI Line detect DCD TAMPER_IRQHandler ; Tamper DCD RTC_IRQHandler ; RTC DCD FLASH_IRQHandler ; Flash DCD RCC_IRQHandler ; RCC DCD EXTI0_IRQHandler ; EXTI Line 0 DCD EXTI1_IRQHandler ; EXTI Line 1 DCD EXTI2_IRQHandler ; EXTI Line 2 DCD EXTI3_IRQHandler ; EXTI Line 3 DCD EXTI4_IRQHandler ; EXTI Line 4 DCD DMAChannel1_IRQHandler ; DMA Channel 1 DCD DMAChannel2_IRQHandler ; DMA Channel 2 DCD DMAChannel3_IRQHandler ; DMA Channel 3 DCD DMAChannel4_IRQHandler ; DMA Channel 4 DCD DMAChannel5_IRQHandler ; DMA Channel 5 DCD DMAChannel6_IRQHandler ; DMA Channel 6 DCD DMAChannel7_IRQHandler ; DMA Channel 7 DCD ADC_IRQHandler ; ADC DCD USB_HP_CAN_TX_IRQHandler ; USB High Priority or CAN TX DCD USB_LP_CAN_RX0_IRQHandler ; USB Low Priority or CAN RX0 DCD CAN_RX1_IRQHandler ; CAN RX1 DCD CAN_SCE_IRQHandler ; CAN SCE DCD EXTI9_5_IRQHandler ; EXTI Line 9..5 DCD TIM1_BRK_IRQHandler ; TIM1 Break DCD TIM1_UP_IRQHandler ; TIM1 Update DCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and Commutation DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare DCD TIM2_IRQHandler ; TIM2 DCD TIM3_IRQHandler ; TIM3 DCD TIM4_IRQHandler ; TIM4 DCD I2C1_EV_IRQHandler ; I2C1 Event DCD I2C1_ER_IRQHandler ; I2C1 Error DCD I2C2_EV_IRQHandler ; I2C2 Event DCD I2C2_ER_IRQHandler ; I2C2 Error DCD SPI1_IRQHandler ; SPI1 DCD SPI2_IRQHandler ; SPI2 DCD USART1_IRQHandler ; USART1 DCD USART2_IRQHandler ; USART2 DCD USART3_IRQHandler ; USART3 DCD EXTI15_10_IRQHandler ; EXTI Line 15..10 DCD RTCAlarm_IRQHandler ; RTC Alarm through EXTI Line DCD USBWakeUp_IRQHandler ; USB Wakeup from suspend ;//同上 AREA |.text|, CODE, READONLY ;//定义代码段 ; Reset Handler Reset_Handler PROC ;过程的开始 ;//Rset_Handler的实现 利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰 EXPORT Reset_Handler [WEAK] ;//在外部没有定义该符号时导出该符号,见HELP中[WEAK] IMPORT __main ;//导入符号,__main为 运行时库提供的函数;完成堆栈,堆的初始话 LDR R0, =__main ;//等工作,会调用下面定义的__user_initial_stackheap; BX R0 ;//跳到__main,进入C的世界 ENDP ;过程的结束 ALIGN ; User Initial Stack & Heap IF :DEF:__MICROLIB ;//如果使用micro lib,micro lib 描述见armlib.chm EXPORT __heap_base EXPORT __heap_limit ;//只导出几个定义 ELSE ;//如果使用默认C运行时库 IMPORT __use_two_region_memory EXPORT __user_initial_stackheap __user_initial_stackheap ;//则进行堆栈和堆的赋值,在__main函数执行过程中调用。 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 ;//OK ,完了 http://blog.csdn.net/chehlcy/archive/2010/01/09/5164472.aspx http://files.ourdev.cn/bbs_upload134190/files_11/ourdev_495775.txt ====================================================================== Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main LDR R0, =__main BX R0 ENDP 这段代码什么意思。 有2个地方不理解 一:PROC ENDP 二: [WEAK] 什么意思 ------------------------------------------------------------------------------- 一:PROC为子程序开始,ENDP为子程序结束 二:[weak]的意思就是弱。 怎么弱呢?如果你在其他地方一个同名函数,比如Reset_handler, 你的这个函数就可以取代它这个函数了。 语法格式: EXPORT 标号 {[WEAK]} EXPORT 伪指令用于在程序中声明一个全局的标号,该标号可在其他的件中引用。 EXPORT可用 GLOBAL 代替。标号在程序中区分大小, [WEAK] 选项声明其他的同名标号优先于该标号被引用。 使用示例: AREA Init , CODE , READONLY EXPORT Stest ;声明一个可全局引用的标号Stest…… END
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值