理解RO、RW、ZI
要理解RO、RW、ZI需要先了解以下背景知识:
1.ARM程序的组成
此处所说的“ARM程序”是指在ARM系统中正在运行的程序,而非保存在ROM中的二进制映像文件(image),这点类似于进程和程序之间的关系。
一个ARM程序包含3部分:RO、RW和ZI:
- RO是指令和常量
- RW是具有初值的全局或静态变量
- ZI是没有初值的全局或静态变量(也包括初值为0)
一般其他人都把RW和ZI解释为已初始化变量和未初始化变量,我认为这种描述方法容易产生这是一种程序是静态的错觉,所以我使用了有初值和没有初值这种描述方法。下面用一段代码做示例:
1 | char *p = "hello"; // p属于RW,"hello"属于RO |
2.ARM映像文件的组成
所谓ARM映像文件就是指烧录到ROM中的二进制文件,也称为image文件。以下用image来称呼它。
image文件只包含RO和RW。
之所以image文件不包含ZI数据,是因为ZI都是0,没必要包含,只需要程序在运行之前,把计划要用来放置ZI数据的区域清零即可。
Q:局部非静态变量属于哪里?比如上面示例代码中的变量c和d
A:在栈里。栈是一个和ZI类似的区域,不占用映像文件空间,在程序运行前确定好放在哪里即可,和ZI不同的地方是栈不会被清零。
局部变量又叫自动变量,自动的意思是这个变量会被自动的创建和销毁,当程序执行到这个变量被定义的时候,就自动的在栈里面给它分配一个空间,然后把代码中编写的初始值填进去,当程序离开此变量所在作用域的时候,作用域内的所有栈空间一起被收回。编译器在生成代码的时候会自动加入这个分配和回收的过程。
关于局部变量的入栈我特地研究实验了一下,点击了解。
3.ARM程序的执行过程
从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。
实际上,ROM中的指令至少应该有这样的功能:
- 将RW从ROM中搬到RAM中,因为在程序运行时RW不能处于ROM中,这样就无法修改它了
- 将计划放置ZI(未初始化或初始化为0的全局变量)的RAM区域全部清零。因为程序运行时ZI并不在image中,所以需要程序根据编译器给出的ZI地址及大小来将相应的RAM区域清零。
这两项工作合起来就可以称为“分散加载”的过程了。这里的分散是相对于聚合的,程序运行之前,其在image中的状态显然是聚合的,RO、RW挤在一起。在程序运行之后,一方面RO、RW、ZI会被分别搬运到不同的地方去,这是分散的第一层意思;另一方面,即使是RO、RW内部也不是铁板一块,而是由很多个部分组成,它们都可以被搬运到不同的地方去,这是分散的第二层意思。分散加载文件的作用就是在程序运行的最开始的时候,指定RO、RW、ZI中的各个部分需要被搬运到什么地方去。
从上面分析可知,在分散加载完成之前,RW和ZI还不存在,其中的变量(全局变量)自然也不存在。所以在分散加载完成之前,绝对不能访问全局变量,否则会有不可预料的后果。
常见的ROM、RAM器件类型
常见ROM:NAND Flash,Nor Flash
常见RAM:SRAM,DRAM,SDRAM,DDRAM
Cortex-M系列MCU一般是片上有一个Nor Flash(又叫IROM,internal ROM),一个SRAM(又叫IRAM,internal RAM),同时也可以外接Flash、SDRAM等,所以可能会存在多个、多种类型的ROM和RAM。
分散加载文件
分散加载文件概念
对于分散加载文件的概念,在《ARM体系结构与编程》一书中的第11章有详细介绍。
分散加载文件(即scatter file,后缀为.scf)是一个文本文件,通过编写一个分散加载文件来告诉ARM连接器在生成映像文件时,如何分配RO、RW、ZI等数据的存放地址。
如果使用Keil的话,会按照MCU的类型自动生成一个最基本的分散加载文件,当然我们也可以设置keil使用我们自定义的分散加载文件,后面我会再详细讨论。
分散加载文件的格式
分散加载描述文件是一个文本文件,它向链接器描述目标系统的存储器映射。分散加载文件指定了:
- 每个加载区的地址和最大尺寸
- 每个加载区的属性
- 从每个加载区派生的执行区
- 每个执行区的执行地址和最大尺寸
- 每个执行区的输入节(也叫输入段)
从描述文件的格式就可以看出加载区、执行区、输入段的层次关系。
分散加载文件基本点
- 编译后输出的映像文件中,各段是首尾相连的,中间没有空闲的区域,而各段的先后关系是根据连接时参数的先后顺序决定的armlinker file1.o file2.0…
- scatter文件用于将编译后的映像文件中的特定段加载到多个分散的指定内存区域。(注意分散加载这件事不是由scatter文件自己来做的,而是由一段代码来做的,这段代码就是编译器根据scatter文件生成的)
- 有两类域(region):执行域(execution region,可以是ROM区域或RAM区域)和加载域(load region,一般是ROM区域)
- 加载域:就是编译之后得到的image文件烧写到rom中的一段区域,所有的RO、RW、ZI都包括在其中。(ZI部分因为是全零的,所以没必要放进去,但是image里面一定包含ZI部分的描述信息,比如它有多大,这个信息是分散加载时需要的。)
- 执行域:就是把加载域进行“解压缩”后的样子。比如一种可能的“解压”方法:RO没有变动还是在ROM中,RW被移到了SRAM中,而ZI被放置在SDRAM中。
- scatter文件本身并不能对image实现“解压缩”,它是链接器的输入参数,链接器读入scatter文件之后,会根据其中的各种地址、大小等参数生成实现分散加载的代码。既然scatter文件包含了所有RO,这一段启动代码自然也要包含,它在scatter中的符号就是
*(InRoot$$Sections),这段代码是__main()(库中的函数)的一部分。这就是为什么在启动文件中要跳到__main而不是main()的原因之一。 - 起始地址与加载地址重合的执行域称为root region,
*(InRoot$$Sections)必须放在这个执行域中,否则链接的时候会报错。∗(+RO)包含了∗(InRoot$$Sections),所以如果在root region 中用到了*(+RO)可以不再指定*(InRoot$$Sections)。
下面是一个简单的分散加载文件示例:
