找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 12130|回复: 5
收起左侧

STM32在MAIN函数之前做了什么事

[复制链接]
ID:241309 发表于 2017-10-20 15:55 | 显示全部楼层 |阅读模式
在学习STM32的时候了解了在MAIN函数之前程序干了什么,其中有一些内容是参考别人的。

main函数之前执行的启动程序:
在执行main函数之前还有一些引导的程序,以开发板的型号为例,启动程序为startup_stm32f10x_cl.s:
开始的部分是声明的内容
【1】MODULE 控制指令是用来标记 modules 源码的开始,后边的 ?cstartup 是模块的名字,此文档的最后的 END 表明模块的结束,与前面的MODULE对应。
【2】SECTION 指令是声明段,一个段不能同时包含 public symbol 和 pubweak symbol ,模块只有在相同的名字的模块没有被链接进来的时候才会被链接进来。
    语法格式:SECTION section:type [flag] [(align)]
        align,是用于指定地址对齐到 2^align,他的取值是 0 到 30
        flag,取值NOROOT、ROOT、REORDER、NOREORDER,默认是ROOT,NOROOT表示如果这个段中的符号没有被引用,将会被连接器舍弃,即可被优化。ROOT表示不可被优化。REORDER表示开始一个新的名字是 section 的段(section),NOREORDER表示开始一个新的名字为 section 的片段(fragment),多个片段组成一个段(section)。
        type,memory 的类型,取值是 CODE、CONST、DATA
        section,段的名字
【3】EXTERN 用导入其他模块的 symbol(符号)。
【4】PUBLIC 导出 symbol(符号)。
【1】DATA 表示下边中的标签是 32 位的标签,THUMB 表示下边的标签是 16 位的标签,所谓的标签是 地址的别名,不占用代码空间,给编译器看的。
【2】 DCD 是数据定义或者 重定位指令,为的是定义一个值,或者保留 memory,DCD 别名是 DC32,用于声明一个 32 位的常量,这部分是中断向量表的内容,需要注意的是,他们的顺序不能改变,此部分会放到 flash 的最开始部分,当系统启动的时候会加载前一个地址,第一个地址是 C 程序的栈的栈顶地址,第二个地址是向量表的开始地址,中断发生时会根据向量表的首地址和偏移量来找到程序的入口。
【3】sfe 指令作用是返回栈的结尾,因为栈的增长方向是反方向的。
【1】THUMB 表明下边是 thumb 指令。
【2】Reset_Handler 在开机或者复位的时候执行
    R0 = SystemInit
    跳转到 SystemInit 函数,并将处理器切换到 thumb 状态
    R0 = __iar_program_start
    跳转到 __iar_program_start 函数,状态也是切换到 thumb 状态
【3】此处的 __iar_program_start 在程序中找不到是因为它已经被封装到了 IAR 自带的C库启动代码中了,当我们编译的时候,在项目属性的 linker,library中勾选了 Automatic runtime library ,就告诉了编译器用库中的 __iar_program_start ,具体实现了什么,我们可以查看 IAR 工具为我们提供的源码,具体路径在IAR 安装目录下的 arm\src\lib\thumb ,我们可以看到有的文件分别的提供了 汇编代码和 c 代码。
其中的 cmain.s 文件中有
可以看到在执行过__iar_program_start函数之后就进入了main函数。
分析启动过程:
当STM32复位后,首先从地址0x0000_0000处取出栈顶地址放入MSP(主堆栈指针)寄存器中(即寄存器R13)。接着从地址0x0000_0004处取出复位向量地址放入PC寄存器中(即寄存器R15),再接着程序就从复位处理函数处(Reset_Handler)开始执行。
对比两张图片可以发现,R13所指向的地址与0x0000_0000地址的内容相等;R15所指向的地址与0x0000_0004地址的内容相等。
接下来我们分析Reset_Handler做了哪些事:
我们可以看到,只是执行了两个函数SystemInit和__iar_program_start,通过单步跟踪发现在SystemInit里进行清理中断,时钟初始化,Flash读取初始化等操作。
而在__iar_program_start里实际上只执行了__low_level_init和__iar_data_init2两个函数,因为__low_level_init里只是将R0赋0x1,所以再将R0和0x0进行比较结果不相等(CMP R0, #0x0),__iar_init$$done这个函数没有得到执行。
系统复位时,Cortex-M3从代码区偏移0x0000'0000处获取栈顶地址,用来初始化MSP(主堆栈指针)寄存器的值。接下来从代码区偏移0x0000'0004获取第一个指令的跳转地址。这些地址,是CM3要求放置中断向量表的地方。
这里是一个程序的启动区的反汇编:
__vector_table:
  08004000  2600      
  08004002  2000      
  08004004  7E1D      (注解:下面的文字老是说 IAP 很奇怪 不是 IAR么)
  08004006  0800      (转发人注解:我知道 IAP 意思是“在应用编程”。)
这个程序是由IAP程序来启动的,IAP程序获取0x0800'4000处的MSP值(0x20002600),并设置为MSP的值,即主堆栈最大范围是0x2000'0000~0x2000'25FF。接下来IAP程序获取0x0800'4004处的Reset_Handler的地址(0x0800'7E1D),并跳转到Reset_Handler()执行。
IAP在这里完全是模仿了Cortex-M3的复位序列,也就是说,在没有IAP的系统上,CM3只能从0x0800'0000获取MSP,从0x0800'0004获取第一条指令所处地址。而IAP就存在在0x0800'0000这个地址上,IAP的启动,已经消耗掉了这个复位序列,所以IAP要启动UserApp程序的时候,也是完全模仿Cortex-M3的复位序列的。
接下来我们看看复位后第一句指令——Reset_Handler()函数里有什么。
若我们使用的是ST公司标准外设库,那么已经有了现成的Reset_Handler,不过他是弱定义——PUBWEAK,可以被我们重写的同名函数覆盖。一般来说,我们使用的都是ST提供的Reset_Handler,在V3.4版本的库中,可以在startup_stm32f10x_xx.s中找到这个函数:
        PUBWEAK Reset_Handler
        SECTION .text:CODE:REORDER(2)
Reset_Handler
        LDR     R0, =SystemInit
        BLX     R0
        LDR     R0, =__iar_program_start
        BX      R0
看来ST没有做太多的事,他只调用了自家库提供的SystemInit函数进行系统时钟、Flash读取的初始化,并把大权交给了__iar_program_start这个IAR提供的“内部函数”了,我们就跟紧这个__iar_program_start跳转,看看IAR做了什么,上面一段代码的反汇编如下:
       Reset_Handler:
__iar_section$$root:
  08007E1C  4801      LDR          R0, [PC, #0x4]; LDR     R0, =SystemInit
  08007E1E  4780      BLX          R0;BLX     R0
  08007E20  4801      LDR          R0, [PC, #0x4];LDR R0, =__iar_program_start
  08007E22  4700      BX           R0;BX      R0
  08007E24  6C69      
  08007E26  0800      
  08007E28  7D8D      
  08007E2A  0800     
细心的观众会发现地址是0x0800'7E1C,比我们查到的0x0800'7E1D差了1,这是ARM家族的遗留问题,因为ARM处理器的指令至少是半字对齐的(16位THUMB指令集 or 32位ARM指令集),所以PC指针的LSB是常为0的,为了充分利用寄存器,ARM公司给PC的LSB了一个重要的使命,那就是在执行分支跳转时,PC的LSB=1,表示使用THUMB模式,LSB=0,表示使用ARM模式,但在最新的Cortex-M3内核上,只使用了THUMB-2指令集挑大梁,所以这一位要常保持1,所以我们查到的地址是0x0800'7E1D(C=1100,D=1101),放心,我们的CM3内核会忽略掉LSB(除非为0,那么会引起一个fault),从而正确跳转到0x0800'7E1C。
从0x0800'7E20处的加载指令,我们可以算出__iar_program_start所处的位置,就是当前PC指针(0x0800'7E24),再加上4,即0x0800'7E28处的所指向的地址——0x0800'7D8D(0x0800'7D8C),我们跟紧着跳转,__iar_program_start果然在这里:
__iar_program_start:
  08007D8C  F000F88C  BL           __low_level_init
  08007D90  2800      CMP          R0, #0x0
  08007D92  D001      BEQ          __iar_init$$done
  08007D94  F7FFFFDE  BL           __iar_data_init2
  08007D98  2000      MOVS         R0, #0x0
  08007D9A  F7FDFC49  BL          main
我们看到IAR提供了__low_level_init这个函数进行了“底层”的初始化,进一步跟踪,我们可以查到__low_level_init这个函数做了些什么,是不是我们想象中的不可告人。
__low_level_init:
  08007EA8  2001      MOVS         R0, #0x1
  08007EAA  4770      BX           LR
__low_level_init出乎想象的简单,只是往R0寄存器写入了1,就立即执行"BX LR"回到调用处了,接下来,__iar_program_start检查了R0是否为0,为0,则执行__iar_init$$done,若不是0,就执行__iar_data_init2。__iar_init$$done这个函数很简单,只有2句话,第一句是把R0清零,第二句就直接"BL main",跳转到main()函数了。不过既然__low_level_init已经往R0写入了1,那么我们还是得走下远路——看看__iar_data_init2做了些什么,虽然距离main只有一步之遥,不过这中间隐藏了编译器的思想,我们得耐心看下去。
__iar_data_init2:
  08007D54  B510      PUSH         {R4,LR}
  08007D56  4804      LDR          R0, [PC, #0x10]
  08007D58  4C04      LDR          R4, [PC, #0x10]
  08007D5A  E002      B            0x8007D62
  08007D5C  F8501B04  LDR          R1, [R0], #0x4
  08007D60  4788      BLX          R1
  08007D62  42A0      CMP          R0, R4
  08007D64  D1FA      BNE          0x8007D5C
  08007D66  BD10      POP          {R4,PC}
  08007D68  7C78      
  08007D6A  0800      
  08007D6C  7C9C     
  08007D6E  0800     
看来IAR迟迟不执行main()函数,就是为了执行__iar_data_init2,我们来分析分析IAR都干了些什么坏事~
首先压R4,LR入栈,然后加载0x0800'7C78至R0,0x0800'7C9C至R4,马上跳转到0x0800'7D62执行R0,R4的比较,结果若是相等,则弹出R4,PC,然后立即进入main()。不过IAR请君入瓮是自不会那么快放我们出来的——结果不相等,跳转到0x0800'7D5C执行,在这里,把R0指向的地址——0x0800'7C78中的值——0x0800'7D71加载到R1,并且R0中的值自加4,更新为0x0800'7C7C,并跳转到R1指向的地址处执行,这里是另一个IAR函数:__iar_zero_init2:
__iar_zero_init2:
  08007D70  2300      MOVS         R3, #0x0
  08007D72  E005      B            0x8007D80
  08007D74  F8501B04  LDR          R1, [R0], #0x4
  08007D78  F8413B04  STR          R3, [R1], #0x4
  08007D7C  1F12      SUBS         R2, R2, #0x4
  08007D7E  D1FB      BNE          0x8007D78
  08007D80  F8502B04  LDR          R2, [R0], #0x4
  08007D84  2A00      CMP          R2, #0x0
  08007D86  D1F5      BNE          0x8007D74
  08007D88  4770      BX           LR
  08007D8A  0000      MOVS         R0, R0
__iar_data_init2还没执行完毕,就跳转到了这个__iar_zero_inti2,且看我们慢慢分析这个帮凶——__iar_zero_inti2做了什么。
__iar_zero_inti2将R3寄存器清零,立即跳转到0x0800'7D80执行'LDR          R2, [R0], #0x4',这句指令与刚才在__iar_data_init2见到的'LDR          R1, [R0], #0x4'很类似,都为“后索引”。这回,将R0指向的地址——0x0800'7C7C中的值——0x0000'02F4加载到R2寄存器,然后R0中的值自加4,更新为0x0800'7C80。接下来的指令检查了R2是否为0,显然这个函数没那么简单想放我我们,R2的值为2F4,我们又被带到了0x0800'7D74处,随后4条指令做了如下的事情:
1、将R0指向的地址——0x0800'7C80中的值——0x2000'27D4加载到R1寄存器,然后R0中的值自加4,更新为0x0800'7C84。
2、将R1指向的地址——0x2000'27D4中的值——改写为R3寄存器的值——0,然后R1中的值自加4,更新为0x2000'27D8。
3、R2自减4
4、检查R2是否为0,不为0,跳转到第二条执行。不为,则执行下一条。
这简直就是一个循环!——C语言的循环for(r2=0x2F4;r2-=4;r!=0){...},我们看看循环中做了什么。
第一条指令把一个地址加载到了R1——0x2000'27D4 是一个RAM地址,以这个为起点,在循环中,对长度为2F4的RAM空间进行了清零的操作。那为什么IAR要做这个事情呢?消除什么记录么?用Jlink查看这片内存区域,可以发现这片区域是我们定义的全局变量的所在地。也就是说,IAR在每次系统复位后,都会自动将我们定义的全局变量清零0。
清零完毕后,接下来的指令"LDR          R2, [R0], #0x4"将R0指向的地址——0x0800'7C84中的值——0加载到R2寄存器,然后R0中的值自加4,更新为0x0800'7C88。随后检查R2是否为0,这里R2为0,执行'BX LR'返回到__iar_data_init2函数,若是不为0,我们可以发现又会跳转至“4指令”处进行一个循环清零的操作。
读到这里,我们应该可以猜到IAR的意图了:__iar_data_init2一开始加载了0x0800'7C78至R0,0x0800'7C9C至R4,[R0,R4]就是一段启动代码区,在这个区域内保存了要“处理”的所有地址与信息——执行的函数地址或者参数,实际上,这片区域也有一个名字,叫做:Region$$Table$$Base。在这个区域内,程序以R0为索引,R4为上限,当R0=R4,__iar_data_init2执行完毕,跳转至main()函数。
好了,保持我们这个猜想,继续跟踪我们的PC指针——我们回到了__iar_data_init2函数中,第一件事就是比较R0,R4的值,可惜的是,仍然不相等,我们又被带到了0x0800'7D5C,至此,我们应该能看出这是一个__iar_data_init2的“主循环”,这也验证了我们对IAR意图的猜想~
  __iar_data_init2中的“主循环”:
  08007D5C  F8501B04  LDR          R1, [R0], #0x4
  08007D60  4788      BLX          R1
  08007D62  42A0      CMP          R0, R4
我们可以等价写为:for(r0=0x0800'7C78,r4=0x0800'7C9C;r0!=r4;r0+=4){...}
此时,我们的R0为0x0800'7C88,经过“指令1”,R0变为0x0800'7C8C,R1为0x0800'7C55。我们来看看,7C55处,IAR又要执行何种操作。
__iar_copy_init2:
  08007C54  B418      PUSH         {R3,R4}
  08007C56  E009      B            0x8007C6C
  08007C58  F8501B04  LDR          R1, [R0], #0x4
  08007C5C  F8502B04  LDR          R2, [R0], #0x4
  08007C60  F8514B04  LDR          R4, [R1], #0x4
  08007C64  F8424B04  STR          R4, [R2], #0x4
  08007C68  1F1B      SUBS         R3, R3, #0x4
  08007C6A  D1F9      BNE          0x8007C60
  08007C6C  F8503B04  LDR          R3, [R0], #0x4
  08007C70  2B00      CMP          R3, #0x0
  08007C72  D1F1      BNE          0x8007C58
  08007C74  BC12      POP          {R1,R4}
  08007C76  4770      BX           LR
这是一个名为__iar_copy_init2的函数,他执行了什么"copy"操作呢?
首先压R3,R4入栈,然后跳转到0x0800'7C6C,从R0——Region$$Table$$Base中取出参数0x238放入R3,接下来的指令大家应该都熟悉了,0x238不为0,所以我们被带至7C58处,再次从Region$$Table$$Base中取出参数0x0800'7F14放入R1,从Region$$Table$$Base取出参数0x2000'2AC8放入R2处。细心的观众应该能察觉这和__iar_zero_init2中取参数的几乎一样:先取出大小,随后取出了地址——只不过这里多出了1个地址,没错这就是"copy",随后的指令
  08007C60  F8514B04  LDR          R4, [R1], #0x4
  08007C64  F8424B04  STR          R4, [R2], #0x4
  08007C68  1F1B      SUBS         R3, R3, #0x4
  08007C6A  D1F9      BNE          0x8007C60
则是另一个“4指令”,指令1将R1指向地址的数据读到R4,指令2将R2指向地址的数据改写为R4的数据,指令3、4是完成一个循环。
说到这里大家都应该明白了——这就是一个"copy"的操作,从Flash地址0x0800'7F14起,将长度0x238的数据拷贝到RAM地址0x2000'2AC8中。
通过Jlink,我们可以看到这片区域是我们定义的并且已初始化的全局变量。也就是说,每次复位后,IAR在此处进行全局变量的初始化。
在这“4指令”执行完毕后,再次从Region$$Table$$Base中取出参数,为0,比较之后条件符合,函数返回__iar_data_init2。
此时的R0已经为0x0800'7C9C与R4相等,__iar_data_init2终于完成它的使命。
  08007D98  2000      MOVS         R0, #0x0
  08007D9A  F7FDFC49  BL           main
将R0清零以后,IAR放弃主动权,把PC指针交给了用户程序的入口——main()。
但请注意,这里使用的是BL指令进行main跳转,也就是说,main函数只是IAR手中的一个子程序,若是main函数执行到了结尾,接下来则会执行exit等IAR提供的“退出”函数。这些函数,等待下回分解~
总之,IAR在启动main()函数以前,执行了Reset_Handler,调用SystemInit()(ST库提供)进行时钟,Flash读取初始化,并转入__iar_program_start中执行__low_level_init与__iar_data_init2,并在__iar_data_init2中,先后调用__iar_zero_init2与__iar_copy_init2对全局变量、全局已初始化变量进行相应的初始化操作。最后,调用main()函数执行。
这就是IAR在启动main()函数之前做的事情,它并没有那么神秘,只要花些时间,就可以跟跟踪分析出这个过程。
STM32的启动分析
一、STM32的复位序列
当STM32产生复位后,做的第一件事就是读取下列两个32位整数的值:
1、从地址0x0000,0000处取出MSP(主堆栈指针)的初始值放入MSP寄存器中;
2、从地址0x0000,0004处取出复位向量放入PC寄存器中,然后从PC中存取的地
址出取指并开始执行。
图1:复位序列
请注意,这与传统的ARM架构以及其他的单片机完全不同,他们复位后一般是从
0x0000,0000地址处取出第一条指令并执行,而一般0x0000,0000都是一条跳转指令。而在STM32中,在0地址处提供的是MSP的初始值,然后紧跟着就是向量表(上电复位时向量表是被默认放在0x04地址处,但是通过修改向量表偏移量寄存器(VTOR)可以将其定义在其他位置)。另外,向量表中的数值是32位的地址,而不是跳转指令,系统会自动将该数值存入PC寄存器中后从该32为地址指向的地址出开始执行,这有点像指针的指针。
图2:初始化MSP及PC的初始化的一个范例因为SMT32使用的是向下生长的满栈,
所以MSP初始值必须是堆栈内存的末地址加1。举例来说,如果你的堆栈区域在
0x20007C00-0x20007FFF之间,那么MSP的初始值就必须是0x20008000。 向量表跟随在MSP的初始化之后——也就是第2个标目。要注意因为STM32是在Thumb态下执行,所以向量表中每个数值必须把LSB(最低权重位)置1.正是因为这个原因,图2中就是用0x101来表示0x100.当0x100处的指令得到执行后,就正是开始了程序的执行。在这之前MSP是必须的,因为可能第1条指令还没来得及执行,就发生了NMI(不可屏蔽中断)或者其他的Fault,MSP初始化好后就已经为他们的服务例程准备好了堆栈。
二、STM323种启动模式
在STM32中,可以通过BOOT[1:0]引脚选择三种不同的启动模式,如表1:
表1:STM32的三种启动模式 根据选定的启动模式,主闪存存储器、系统存储器或SRAM可以按照以下方式访问:
1、 从主闪存存储器启动:
主闪存存储器被映射到启动空间(0x0000,0000),但仍然能够在原有的地址(0x8000,0000)访问它,即闪存存储器的内容可以在两个地
址区域访问,0x0000,0000或者0x8000,0000.
2、从系统存储器启动:
系统存储器被映射到启动空间(0x0000,0000),但仍然能够在原有的地址(0x1FFF,F000)访问它。
3、 从内置SRAM启动:
只能在0x2000,0000开始的地址区访问SARM。当从内置的SRAM中启动,在应用程序的初始化代码中,必须使用NVIC的异常表和偏移寄存器,从新映射向量表到SRAM之中。
《stm32权威指南》中有这样的说法,待理解。

完整的Word格式文档51黑下载地址(共12页):
STM32在main函数之前执行的启动程序.docx (392.57 KB, 下载次数: 43)

评分

参与人数 1黑币 +50 收起 理由
admin + 50 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

ID:1 发表于 2017-10-21 00:27 | 显示全部楼层
好资料,51黑有你更精彩!!!
回复

使用道具 举报

ID:324843 发表于 2018-5-8 10:44 | 显示全部楼层
资料不错, 可惜没有积分下载个全的
回复

使用道具 举报

ID:222998 发表于 2018-10-22 10:52 | 显示全部楼层
楼主厉害了  分析的很详细   学习了
回复

使用道具 举报

ID:372307 发表于 2018-10-22 20:32 | 显示全部楼层
学习了
回复

使用道具 举报

ID:474500 发表于 2019-2-1 10:08 | 显示全部楼层
感谢楼主,可惜没有积分下载
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表