可执行程序 -> cpu执行第一条用户代码
这个流程中着重讲述的是 HEX 文件如何被烧写到 STM32 内部的指定地址处。(烧写到 STM32 中的可执行文件不仅只有 HEX 格式,还有 axf、bin。针对不同格式的可执行文件,用不同的工具进行烧写)。
而本篇文章将要详细地描述一个流程:
cpu执行第一条用户代码 -> 调用 __main 函数-> __rt_entry -> main函数
这里需要注意一下,__main 是 c 库中的一个函数,和 main 函数是有区别的!!!
启动文件内容描述
上图中的汇编关键字最好记住,因为比较常用。 在此基础上,我们继续深入一点。 DCD指令 STM32 启动文件中使用 DCD 指令的目的是:达到 4GB 全范围跳转。 LDR 指令只能跳到当前 PC 4kB 范围内,而 B 指令能跳转到 32MB 范围。 B . STM32 启动文件中使用 b . 语句的作用就是:防止程序跑飞。 副作用:触发了一个未知中断的时候会卡死在中断服务函数中,以至于你几乎都找不到!!!
注意:中断服务函数全部都是在启动文件中已经定义好了,如果在外部文件中定义中断服务函数,名称要和事先已经定义好的中断服务函数的名称一样,函数名称的不同代表着地址的不同,因为函数名称本质就是地址!!!
STM32启动流程
获取栈顶指针
跳转到复位中断函数
注意:当程序编译完成之后,SP栈顶指针就已经确定了。 MDK编译程序的组成: Code:代码域,它指的是编译器生成的机器指令,这些内容被存储到 ROM 区。 RO-data:Read Only data,只读数据域,它指程序中用到的只读数据,这些数据被存储在 ROM 区,因而程序不能修改其内容。C语言中 const 关键字定义的变量就是典型的 RO-data。 RW-data:Read Write data,可读写数据域,它指初始化为”非0值“的可读写数据,程序刚运行时,这些数据具有非0的初始值,且运行的时候它们会常驻在 RAM 区,因而应用程序可以修改其内容。C 语言中定义的全局变量,且定义时赋予“非0值”给该变量进行初始化。 ZI-data:Zero Initialie data,即 0 初始化数据,它指初始化为“0值”的可读写数据域。它与 RW-data 的区别是程序刚运行时这些数据初始值全都为 0,而后续运行过程与 RW-data 的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改其内容。例如 C 语言中使用定义的全局变量,且定义时赋予 “ 0 值” 给该变量进行初始化(若定义该变量时没有赋予初始值,编译器会把它当 ZI-data 来对待,初始化为 0)。 ZI-data 的栈空间(Stack)及堆空间(Heap):在 C 语言中,函数内部定义的局部变量属于栈空间,进入函数的时候会向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。而使用 malloc 动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于 ZI-data 区域的,这些空间都会被初始值化为 0 值。编译器给出的 ZI-data 占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用 malloc 动态申请堆空间,编译器会优化,不把堆空间计算在内)。 程序组件所属的区域:
程序组件 所属类别
机器代码指令 Code
常量 RO-data
初值非0的全局变量 RW-data
初值为0的全局变量 ZI-data
局部变量 ZI-data栈空间
使用 malloc 动态分配的空间 ZI-data堆空间
RW-data 和 ZI-data 它们仅仅是初始值不一样而已,应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如 STM32 的内部 FLASH,因而系统掉电后也能正常保存但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的。 程序状态区域的组成;
程序状态与区域 组成 程序执行时的只读区域(RO) Code+RO-data 程序执行时的可读写区域(RW) RW-data + ZI-data 程序存储时占用的ROM区 Code + RO-data + RW-data
最小启动配置(加个鸡腿)
注意:设置好 SP,就可以运行用户程序。 编写中断向量表
编写复位中断函数
设置堆栈指针 跳转到__main函数 至此,cpu执行第一条用户代码 -> 调用__main函数 分析完毕,接下来是,__main函数 -> __rt_entry -> main函数。 这里再次声明一下:__main 函数是 c 库中的一个函数,和用户编写的 main 函数是有区别的!!!
必备知识
必备知识中主要是用到了.map文件,双击红色箭头所指向的区域就可以打开!!!
用户程序在FLASH中的组织架构
上面两张图截取了镜像文件在 FLASH 上的内存分布。 从上面两张图可以知道,在程序的最开始处,存储的是数据段,这个数据段就是中断向量表,里面存储这所有中断函数的入口地址。 紧跟着的就是代码段,代码段包含了自己编写的用户代码和库函数。 之后又跟着数据段,这个数据段有个专有的名称,叫做代码常量区,也就是你定义的 const 类型的全局变量(记住不是const 类型的局部变量,const 类型的局部变量还是存储在栈区)会存储在这个区域。 特别注意,非常重要的知识点: 在代码常量区后面还有一个区,叫做读写数据区,这个区域中的数据最终要被拷贝到 SRAM 中去,因为 FLASH 只能读不能写(事实上可以进行写操作,只不过需要密钥而已,参考手册中有说明)而 SRAM 中的数据是可读可写的。 但是,.map 文件中并没有提到,也就是说你从 .map 文件中是找不到这个区的,
你能看到的最后一项就是代码常量区,因此这个地方一般情况下很难发现到,只有深入 __main 函数之后才可以知道。
值得注意的是:
在代码区中,不仅有Code、Data类型的数据,还有 WPAD!!! PAD 就是 padding 的意思,中文翻译过来就是填充的意思。作用:进行4字节对齐,提高cpu的取指速率。 也就是说,无论是指令还是数据,在内存中都要4个字节对齐,所表现出来的特征就是: 地址的最低两位都为 0,换成 16 进制来说,就是最后一个字母只能为 0、4、8、c。
用户数据在SRAM中的组织架构
在 SRAM 中,第一个区域叫做全局区,也有人叫静态区。你定义的全局变量(有初始值),静态变量都存放在这个区域当中。 这里需要说明一下一个特例: 比如你定义了一个全局变量:int a; 没有初始化的全局变量默认为 0,但要注意,并不是说没有初始化的全局变量就属于 .bss 段(网上有很多的博客都说错了),它还是属于全局区,它的值是编译器赋值给它的!!! 紧跟着的就是.bss段。
注意:.bss 段不被包含在可执行文件当中
定义的未初始化全局数组,未初始化的静态全局数组等等保存在 .bss 段。 接下来就是堆和栈,因为堆向上生长,栈向下生长,因此堆在栈的前面。 此时,我们得到一个非常重要的结论:栈顶指针的值 = RW-data + ZI-data。
大家可以想一下,为什么。 还有,由于当一个程序生成可执行文件之后,栈顶指针的值就确定了。 那也就是说,从栈顶指针处,到 SRAM 最后一个存储单元都处于未使用状态,也就是说,有一部分内存我们是没有使用的,这里需要注意!!!
加载地址 链接地址 运行地址 存储地址
加载地址:将指令或数据从地址 A 拷贝到地址 B,地址 A 就是加载地址。
链接地址:由链接脚本文件指出,链接的时候确定。
运行地址:程序在内存中运行时候的地址。
存储地址:指令或数据在 flash 中存放的存储地址,就是存储地址。
这里需要说明一下:
链接地址是静态的,在程序链接的时候确定。
运行地址是动态的,因为当你使用位置无关码(后面会提到)将程序从 A 地址拷贝到 B 地址处,那么运行地址就发生了改变。
存储地址就是加载地址,没有区别!!!
代码重定向 程序或数据的链接地址要和运行地址一致,但往往程序或数据的存储地址(加载地址)和运行地址不一样,因此需要代码重定向。 代码重定向:使用位置无关码将用户程序或数据从存储地址拷贝到运行地址。 用一句很精确的话来描述代码重定向:使逻辑地址与实际物理地址一一对应的过程。 这篇博客非常详细地描述了代码重定向的过程,读者特别需要注意的就是:MCU和MPU代码重定向的区别!!! 位置无关码 当程序或数据的链接地址和运行地址不一样的时候,此时只有位置无关码才能够正确被执行 位置无关码:依赖于程序当前运行的PC值,进行相对的跳转,导致的结果就是,无论代码在哪,总能达到指令正常运行的目的,因此是位置无关的。 位置有关码:不依赖当前PC值,是绝对跳转,只有程序运行在链接地址处时,才能达到指令的正常目的,因此是位置有关系的。
__main函数
作用:Initialization of the execution environment and execution of the application You can customize execution intialization by defining your own __main that branches to __rt_entry. The entry point of a program is at __main in the C library where library code:
Copies non-root (RO(不会拷贝,官方提供和实际实践有出入) and RW) execution regions from their load addresses to their execution addresses. Also, if any data sections are compressed, they are decompressed from the load address to the execution address.
Zeroes ZI regions.
Branches to __rt_entry.
If you do not want the library to perform these actions, you can define your own __main that branches to __rt_entry.(我们后面会自己实现 __main函数)
注意:__main 函数不会将 RO 段数据拷贝到执行地址处,虽然官方说明了
_rt_entry 函数
procedure The library function __rt_entry() runs the program as follows:
Sets up the stack and the heap by one of a number of means that include calling __user_setup_stackheap(), calling __rt_stackheap_init(), or loading the absolute addresses of scatter-loaded regions.
Calls __rt_lib_init() to initialize referenced library functions, initialize the locale and, if necessary, set up argc and argv for main().This function is called immediately after __rt_stackheap_init() and is passed an initial chunk of memory to use as a heap. This function is the standard ARM C library initialization function and it must not be reimplemented.
Calls main(), the user-level root of the application.
From main(), your program might call, among other things, library functions.
Calls exit() with the value returned by main().
entry 的是 ARM 汇编语法中程序的入口地址,GNU Assember 语法中 start 是程序的入口地址 __rt_lib 库函数是没有源文件,都已经编译完成了。 The symbol __rt_entry is the starting point for a program using the ARM C library. Control passes to __rt_entry after all scatter-loaded regions have been relocated to their execution addresses. Usage
The default implementation of __rt_entry:
Sets up the heap and stack.
Initializes the C library by calling __rt_lib_init.(ARMc库里面全面都是 .b .l 形式的库,没有源码)
Calls main().
Shuts down the C library, by calling __rt_lib_shutdown.
Exits.
__rt_entry must end with a call to one of the following functions:
exit()
Calls atexit()- registered functions and shuts down the library.
__rt_exit()
Shuts down the library but does not call atexit() functions.
_sys_exit()
Exits directly to the execution environment. It does not shut down the library and does not call atexit() functions.
自己实现 __main 函数
消除警告 提示:程序的首地址并不和程序的入口地址等效。 注意:ARM 汇编语法 entry 是一个程序的入口地址,GNU 汇编语法 start 是一个程序的入口地址。 我们已自己实现 __main 函数,ENTRY 已没有实质作用, 但为了避免 KEIL 警告,这里加上。
自己实现__rt_entry函数
你觉得你行吗?你知道要多少行代码吗,并且,没必要!!!
问题思考
为什么我们可以自己编写 __main 和 __rt_entry 因为库函数里面的 W__main 函数 和 __rt_entry 函数是弱函数。
弱函数定义时需要写红色箭头所指向的关键字。 当一个用户程序运行完以后,会出现什么情况 MCU的程序执行结束后去哪儿了
总结
_ _main函数 -> __rt_entry函数 -> main函数 介绍完毕。 本系列文章流程: 可执行程序 -> cpu执行第一条用户代码的流程 -> _ _main函数 -> __rt_entry函数 -> main函数 详细地阐述了可执行文件是如何被加载到 FLASH上,以及编写的用户程序(main函数)被调用之前经历了哪些步骤。 如果你对这些步骤了然于胸的时候,那么恭喜你,你已经很强了,大部分人是学不到这么深的,就算工作了很多年!!! 希望本系列的博文能够对你有所帮助!!! 最后,希望大家能够学有所成,未来可期。