一个 ARM 程序包含 3 部分: RO, RW 和 ZI。
RO 便是只读数据,是程序中指令和常量;RW 是可读写的数据,程序中已初始化变量;ZI 是程序中未初始化的变量和初始化为 0 的变量。由以上 3 点解释可以理解为:
1.1 ARM 芯片的启动过程概述

图 3.1 ARM 芯片的启动过程详解
把稳,以上的过程并非绝对的,不同的 ARM 架构或是不同的代码以上的实行过程是不同的。
复位处理程序是在汇编器中编写的短模块,系统一启动就立即实行。复位处理程序最少要为运用程序的运行模式初始化堆栈指针。对付具有本地内存系统(如缓存、 TCM、 MMU 和MPU)的处理器,某些配置必须在初始化过程的这一阶段完成。复位处理程序在实行之后,常日跳转到__main 以开始 C 库初始化序列。
__main 卖力设置内存,而__rt_entry 卖力设置运行时环境。 __main 实行代码和数据复制、解压缩以及 ZI 数据的零初始化。然后,它跳转到__rt_entry,设置堆栈和堆、初始化库函数和静态数据,并调用任何顶级 C++布局函数。然后, __rt_entry 跳转到运用程序的入口 main()。主运用程序结束实行后, __rt_entry 将库关闭,然后把掌握权交还给调试器。函数标签 main()具有分外含义。 main()函数的存在逼迫链接器链接到__main 和__rt_entry 中的初始化代码。如果没有标记为 main()的函数,则没有链接到初始化序列,因而部分标准 C 库功能得不到支持。
1.2 结合代码来看 ARM 芯片的启动过程
1.2.1 调试环境的搭建及测试代码
利用 keil for ARM( uVision4)的软件仿照,工程的硬件设定为 LPC1700 系列,不该用microlib。这里不该用 microlib,则系统自动加载标准 C Library,这样我们才能看到标准的 ARM芯片的标准启动过程。随后我们会对 microlib 进行磋商。测试代码如下:
程序清单 3.1 启动过程测试代码#include "LPC17xx.h" / LPC17xx 外设寄存器 /int main (void){
SystemInit();//系统初始化
while (1) {}
}
我们的测试代码利用的是一段最大略的代码,代码本身只包含 CMSIS 标准的必备文件,即stdint.h、 core_cm3.h、 core_cm3.c、 system_LPC17xx.h、 system_LPC17xx.c、 LPC17xx.h、startup_LPC17xx.s 和 main.c。
1.2.2 跟踪启动代码开始调试之前,须将工程设置的 DEBUG 栏中取消掉 Run to main()的勾选,否则代码会直接运行到 main()函数,我们也就无法看到芯片的启动过程了。然后启动调试,最前辈入 Reset_Handler,如下图所示。
图 1.2 Reset_Handler
连续单步运行,程序跳入__main(C Library 的代码,并非用户代码),如下图所示。
图 1.3 __main
连续单步运行,经由一系列的代码(紧张是 scatter load 过程)后,程序进入__rt_entry(同样是由 C Library 管理,并非用户代码)。
图 1.4 __rt_entry
连续单步运行,再次经由一系列的代码之后(紧张是堆栈和堆的初始化以及 C Library 的初始化),程序进入 main()。如下图所示。
图 1.5 进入 main 函数
以上是全体测试代码启动过程的跟踪调试的大致过程,这个过程对 ARM 系列的芯片来说都是相同的,不同的是里面详细的细节。
1.2.3 详细的启动过程利用的依然是上面的测试代码,详细启动过程如下图所示。
图 1.6 芯片启动的详细过程
上图显示的便是测试代码在 LPC17xx 上启动运行的全过程(详细过程),由此图可见LPC17xx 的启动过程与图 1.1 所示的启动过程是基本同等的,但是还有差别,可以说是图 1.1所示启动过程的简化版。把稳:并非所有代码的启动过程全部相同,此启动过程与所利用的链接器、用户代码以及其所集成的 C Library 密切干系。此图为由“armlink”的链接器为程序清单 1.2 所示的测试代码产生的启动过程,其他情形可能会有一些差异(比如有的代码的启动过程就会运行一段 RW段的解压代码等,而本例程中则没有)。
1.2.4 __main
若程序利用的是 C 或 C++措辞编写的代码,那么 C/C++程序的入口是在 C Library 中的__main。库代码在此处实行以下操作。
1 将非根(RO 和 RW)实行区从其加载地址复制到实行地址(这里的根区指的便是__main 和__rt_entry)。其余,如果压缩了任何数据节,则会将它们从加载地址解压缩到实行地址(此数据压缩及解压缩过程并不是对所有代码都会实行);将 ZI 区清零;跳转到__rt_entry。1.2.5 __rt_entry
__rt_entry 符号是利用 ARM C 库的程序的出发点。将所有分散加载区重定位到其实行地址后,会将掌握权通报给__rt_entry。其有如下缺省实现:
设置堆和堆栈。调用__rt_lib_init 以初始化 C Library。调用 main()。调用__rt_lib_shutdown 以关闭 C Library。退出。把稳:末了两步是在程序退出 main()函数时才会实行,而嵌入式程序一样平常都是去世循环,以是基本不会实行这两个过程。还有,以长进程是对标准 C Library 而言,不包括利用 microlib的情形。
1.2.6 __rt_lib_init
这是库初始化函数,它与__rt_lib_shutdown()合营利用。
这是库初始化函数。它是紧靠__rt_stackheap_init()后面调用的,并通报一个要用作堆的初始内存块。此函数是标准 ARM 库初始化函数,不能重新实现此函数。
1.3 关于 microlib
microlib 是缺省 C 库的备选库。它旨在与须要装入到极少量内存中的深层嵌入式运用程序合营利用。这些运用程序不在操作系统中运行。 microlib 进行了高度优化以使代码变得很小。它的功能比缺省 C 库少,并且根本不具备某些 ISO C 特性。某些库函数的运行速率也比较慢,例如, memcpy()。microlib 与缺省 C 库之间的紧张差异是:
microlib 不符合 ISO C 库标准。不支持某些 ISO 特性,并且其他特性具有的功能也较少;microlib 不符合 IEEE 754 二进制浮点算法标准;microlib 进行了高度优化以使代码变得很小;无法对区域设置进行配置。缺省 C 区域设置是唯一可用的区域设置;不能将 main()声明为利用参数,并且不能返回内容;不支持 stdio,但未缓冲的 stdin、 stdout 和 stderr 除外;microlib 对 C99 函数供应有限的支持;microlib 不支持操作系统函数;microlib 不支持与位置无关的代码;microlib 不供应互斥锁来防止非线程安全的代码;microlib 不支持宽字符或多字节字符串;与 stdlib 不同, microlib 不支持可选择的单或双区内存模型。 microlib 只供应双区内存模型,即单独的堆栈和堆区。1.4 x.map
想要更好的理解启动代码的运行机制,我们就有必要理解一下由 Keil 的链接器“armlink”天生的描述文件即 x.map 文件。
1.4.1 关于链接器
图 1.7 目标文件的组成
上图即是 armlink 的链接器为程序清单 1.1 所示的测试代码天生的.map 文件中的一部分,其描述了镜像文件的组成信息,个中可以明显的看到其由两部分构成:
由 User Code 天生的目标文件;由 C Library 天生的目标文件。可见我们在上文中所描述的启动过程中看到的__main、 __rt_entry、 __scatterload 以及__rt_lib_init 等,便是 C Library 中的代码。同理,我们每次烧录的可实行的 ARM 的 bin 文件中不仅有开拓者编写的代码,还有C Library 的代码。
1.4.2 RW 段在 RAM 的存放
图 1.8 由 armlink 天生的 RW 段在 RAM 中存放的描述
1.5 关于 ARM 程序的 Memory 管理
1.5.1 ARM 镜像文件的组成(image)
所谓 ARM 映像文件便是指烧录到 ROM 中的 bin 文件,也成为 image 文件。以下用 Image文件来称呼它。 Image 文件包含了 RO 和 RW 数据(把稳:不包含 ZI 数据)。之以是 Image 文件不包含 ZI 数据,是由于 ZI 数据都是 0,没必要包含,只要程序运行之前将 ZI 数据所在的区域(实行区域)一律清零即可。包含进去反而摧残浪费蹂躏存储空间。
1.5.2 关于 image 文件(镜像文件)
从以上两点可以知道,烧录到 ROM 中的 image 文件与实际运行时的 ARM 程序之间并不是完备一样的。因此就有必要理解 ARM 程序是如何从 ROM 中的 image 到达实际运行状态的。实际上, RO 中的指令(启动程序)至少该当有这样的功能:
将 RW 从 ROM 中搬到 RAM 中,由于 RW 是变量,变量不能存在 ROM 中。将 ZI 所在的 RAM 区域全部清零,由于 ZI 区域并不在 Image 中,以是须要程序根据编译器给出的 ZI 地址及大小来将相应得 RAM 区域清零。 ZI 中也是变量,同理:变量不能存在 ROM 中。在程序运行的最初阶段, RO 中的指令(启动程序)完成了这两项事情后(也便是分散加载的过程) C 程序才能正常访问变量。否则只能运行不含变量的代码。说了这么多可能还是有些含糊, RO, RW 和 ZI 到底是什么,下面我将给出几个例子,最直不雅观的来解释 RO, RW, ZI在 C 措辞中的含义。1.5.3 RO 段
看下面两段程序,它们之间差了一条语句,这条语句便是声明一个字符常量。因此按照之前的内容,它们之间该当只会在 RO 数据中相差一个字节(字符常量为 1 字节)。
Prog1:
#include <stdio.h>
void main(void)
{
;
}
Prog2:
#include <stdio.h>
const char a = 5;
void main(void){
;
}
Prog1 编译出来后的信息如下(来自.map 文件):
Prog2 编译出来后的信息如下(来自.map 文件):
以上两个程序编译出来后的信息可以看出: Prog1 和 Prog2 的 RO 包含了 Code 和 RO Data两类数据。他们的唯一差异便是 Prog2 的 RO Data 比 Prog1 多了 1 个字节。这正和之前的推测同等。如果增加的是一条指令而不是一个常量,则结果该当是 Code 数据大小有差别。
1.5.4 RW 段
同样再看两个程序,它们之间只相差一个“已初始化的变量”,按照之前所讲的,已初始化的变量该当是算在 RW 中的,以是两个程序之间该当是 RW 大小有差异。Prog3:
#include <stdio.h>
void main(void)
{;
}
Prog4:
#include <stdio.h>
char a = 5;
void main(void)
{
;
}
Prog3 编译出来后的信息如下(来自.map 文件):
Prog4 编译出来后的信息如下(来自.map 文件):
以上两个程序编译出来后的信息可以看出: Prog1 和 Prog2 的 RO 包含了 Code 和 RO Data两类数据。他们的唯一差异便是 Prog2 的 RO Data 比 Prog1 多了 1 个字节。这正和之前的推测同等。如果增加的是一条指令而不是一个常量,则结果该当是 Code 数据大小有差别。
1.5.5 ZI 段(初始化为 0 或未初始化的变量)
再看两个程序,他们之间的差别是一个未初始化的变量“a”,从之前的理解中,该当可以推测,这两个程序之间该当只有 ZI 大小有差别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a;
void main(void)
{
;
}
Prog3 编译出来后的信息如下(来自.map 文件):
Prog4 编译出来后的信息如下(来自.map 文件):
编译的结果完备符合推测,只有 ZI 数据相差了 1 个字节。这个字节正是未初始化的一个字符型变量“a”所引起的。
把稳: 如果一个变量被初始化为 0,则该变量的处理方法与未初始化华变量一样放在 ZI区域。即: ARM C 程序中,所有的未初始化变量都会被自动初始化为 0。以上代码是再 ADS 下编译的, keil 环境下与之不同,比如在 keil 下天生 ZI 数据段就必须定义一个大于 8 字节的未初始化或初始化位 0 的变量,且必须在源代码中引用此变量才会在链接的描述文件中看到其天生的 ZI 文件。
1.6 缺省内存映射
对付没有描述内存映射的映像,链接器根据缺省内存映射放置代码和数据。如下图所示。
图 1.9 缺省内存映射
创建一个可以在个中实行 C 或 C++程序的环境。这包括:创建一个堆栈;创建一个堆(如果须要);初始化程序所用的库的部分组成内容;调用 main()以开始实行程序;支持程序利用 ISO 定义的函数;捕获运行时缺点和旗子暗记,如果须要,还可以在涌现缺点或程序退出时终止实行。把稳:这个内存映射并非对所有芯片都有效,不同的芯片的内存映射是不同的。在
Cortex-M3 中的内存映射与上图是同等的。
1.7 内存模型
在 Keil for ARM 下你可以选择以下任意内存模型:
1. 单内存区
堆栈从内存区顶部向下增长。堆从内存区底部向上增长。这是缺省设置。由堆管理的内存从来不会缩减。不能将通过调用 free()开释的堆内存再次用于其他用场。
2. 双内存区
一个内存区用于堆栈,另一个内存区用于堆。堆区大小可以是零。堆栈区可以位于分配的内存中,也可以从实行环境中继续。要利用双区模型而不是缺省的单区模型,请利用以下任一方法:
汇编措辞中的 IMPORT __use_two_region_memory;C 中的#pragma import(__use_two_region_memory)。例如下图所示,此代码来自 startup_LPC17xx.s。
如果利用双区内存模型,并且未供应任何堆内存,则无法调用 malloc()、利用 stdio 或获取main()的命令行参数。如果将堆区大小设置为 0,并且将__user_heap_extend()定义为可扩展堆的函数,则会在须要时创建堆。