“ARM 程序”是指在 ARM 系统中正在执行的程序,而非保存在 ROM 中的 bin 映像(image)文件。这一点清注意区别。

一个 ARM 程序包含 3 部分: RO, RW 和 ZI。

由以上 3 点说明可以理解为:

1.1 ARM 芯片的启动过程概述

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(1)

图 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,如下图所示。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(2)

图 1.2 Reset_Handler

继续单步运行,程序跳入__main(C Library 的代码,并非用户代码),如下图所示。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(3)

图 1.3 __main

继续单步运行,经过一系列的代码(主要是 scatter load 过程)后,程序进入__rt_entry(同样是由 C Library 管理,并非用户代码)。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(4)

图 1.4 __rt_entry

继续单步运行,再次经过一系列的代码之后(主要是堆栈和堆的初始化以及 C Library 的初始化),程序进入 main()。如下图所示。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(5)

图 1.5 进入 main 函数

以上是整个测试代码启动过程的跟踪调试的大致过程,这个过程对 ARM 系列的芯片来说都是相同的,不同的是里面具体的细节。

1.2.3 详细的启动过程使用的依然是上面的测试代码,详细启动过程如下图所示。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(6)

图 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.2.5 __rt_entry

__rt_entry 符号是使用 ARM C 库的程序的起点。将所有分散加载区重定位到其执行地址后,会将控制权传递给__rt_entry。其有如下缺省实现:

注意:最后两步是在程序退出 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 库之间的主要差异是:

1.4 x.map

想要更好的了解启动代码的运行机制,我们就有必要了解一下由 Keil 的链接器“armlink”生成的描述文件即 x.map 文件。

1.4.1 关于链接器

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(7)

图 1.7 目标文件的组成

上图即是 armlink 的链接器为程序清单 1.1 所示的测试代码生成的.map 文件中的一部分,其描述了镜像文件的组成信息,其中可以明显的看到其由两部分构成:

可见我们在上文中所描述的启动过程中看到的__main、 __rt_entry、 __scatterload 以及__rt_lib_init 等,就是 C Library 中的代码。同理,我们每次烧录的可执行的 ARM 的 bin 文件中不仅有开发者编写的代码,还有C Library 的代码。

1.4.2 RW 段在 RAM 的存放

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(8)

图 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 中的指令(启动程序)至少应该有这样的功能:

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 文件):

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(9)

Prog2 编译出来后的信息如下(来自.map 文件):

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(10)

以上两个程序编译出来后的信息可以看出: 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 文件):

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(11)

Prog4 编译出来后的信息如下(来自.map 文件):

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(12)

以上两个程序编译出来后的信息可以看出: 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 文件):

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(13)

Prog4 编译出来后的信息如下(来自.map 文件):

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(14)

编译的结果完全符合推测,只有 ZI 数据相差了 1 个字节。这个字节正是未初始化的一个字符型变量“a”所引起的。

注意: 如果一个变量被初始化为 0,则该变量的处理方法与未初始化华变量一样放在 ZI区域。即: ARM C 程序中,所有的未初始化变量都会被自动初始化为 0。以上代码是再 ADS 下编译的, keil 环境下与之不同,比如在 keil 下生成 ZI 数据段就必须定义一个大于 8 字节的未初始化或初始化位 0 的变量,且必须在源代码中引用此变量才会在链接的描述文件中看到其生成的 ZI 文件。

1.6 缺省内存映射

对于没有描述内存映射的映像,链接器根据缺省内存映射放置代码和数据。如下图所示。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(15)

图 1.9 缺省内存映射

注意:这个内存映射并非对所有芯片都有效,不同的芯片的内存映射是不同的。在

Cortex-M3 中的内存映射与上图是一致的。

1.7 内存模型

在 Keil for ARM 下你可以选择以下任意内存模型:

1. 单内存区

堆栈从内存区顶部向下增长。堆从内存区底部向上增长。这是缺省设置。由堆管理的内存从来不会缩减。不能将通过调用 free()释放的堆内存再次用于其他用途。

2. 双内存区

一个内存区用于堆栈,另一个内存区用于堆。堆区大小可以是零。堆栈区可以位于分配的内存中,也可以从执行环境中继承。要使用双区模型而不是缺省的单区模型,请使用以下任一方法:

例如下图所示,此代码来自 startup_LPC17xx.s。

cortex-m3定义的内核图是怎样的(Cortex-M3内核芯片进阶之启动过程详解)(16)

如果使用双区内存模型,并且未提供任何堆内存,则无法调用 malloc()、使用 stdio 或获取main()的命令行参数。如果将堆区大小设置为 0,并且将__user_heap_extend()定义为可扩展堆的函数,则会在需要时创建堆。

,