scatter文件 分析笔记
Scatter File
—ARM中的RO、RW和ZI 数据说明(1)
一直以来对于ARM体系中所描述的RO,RW和ZI数据存在似是而非的理解,这段时间对其仔细了解了一番,发现了一些规律,理解了一些以前书本上有的但是不理解的东西,我想应该有不少人也有和我同样的困惑,因此将我的一些关于RO,RW和ZI的理解写出来,希望能对大家有所帮助。
要了解RO,RW和ZI需要首先了解以下知识:
ARM程序的组成
此处所说的“ARM程序”是指在ARM系统中正在执行的程序,而非保存在ROM中的bin映像(image)文件,这一点清注意区别。
一个ARM程序包含3部分:RO,RW和ZI
RO是程序中的指令和常量
RW是程序中的已初始化变量
ZI是程序中的未初始化的变量
由以上3点说明可以理解为:
RO就是readonly,
RW就是read/write,
ZI就是zero
ARM映像文件的组成
所谓ARM映像文件就是指烧录到ROM中的bin文件,也称为image文件。以下用Image文件来称呼它。
Image文件包含了RO和RW数据。
之所以Image文件不包含ZI数据,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间。
Q:为什么Image中必须包含RO和RW?
A:因为RO中的指令和常量以及RW中初始化过的变量是不能像ZI那样“无中生有”的。
ARM程序的执行过程
从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。
实际上,RO中的指令至少应该有这样的功能:
1. 将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。
2. 将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中
在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。
说了上面的可能还是有些迷糊,RO,RW和ZI到底是什么,下面我将给出几个例子,最直观的来说明RO,RW,ZI在C中是什么意思。
1.RO
看下面两段程序,他们之间差了一条语句,这条语句就是声明一个字符常量。因此按照我们之前说的,他们之间应该只会在RO数据中相差一个字节(字符常量为1字节)。
Prog1:
#include <stdio.h>
void main(void)
{
;
}
Prog2:
#include <stdio.h>
const char a = 5;
void main(void)
{
;
}
Prog1编译出来后的信息如下:
======================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 96 0 Grand Totals
======================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
======================================================================
Prog2编译出来后的信息如下:
======================================================================
Code RO Data RW Data ZI Data Debug
948 61 0 96 0 Grand Totals
======================================================================
Total RO Size(Code + RO Data) 1009 ( 0.99kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1009 ( 0.99kB)
======================================================================
以上两个程序编译出来后的信息可以看出:
Prog1和Prog2的RO包含了Code和RO Data两类数据。他们的唯一区别就是Prog2的RO Data比Prog1多了1个字节。这正和之前的推测一致。
如果增加的是一条指令而不是一个常量,则结果应该是Code数据大小有差别。
2.RW
同样再看两个程序,他们之间只相差一个“已初始化的变量”,按照之前所讲的,已初始化的变量应该是算在RW中的,所以两个程序之间应该是RW大小有区别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a = 5;
void main(void)
{
;
}
Prog3编译出来后的信息如下:
======================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 96 0 Grand Totals
======================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
======================================================================
Prog4编译出来后的信息如下:
======================================================================
Code RO Data RW Data ZI Data Debug
948 60 1 96 0 Grand Totals
======================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 97 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1009 ( 0.99kB)
======================================================================
可以看出Prog3和Prog4之间确实只有RW Data之间相差了1个字节,这个字节正是被初始化过的一个字符型变量“a”所引起的。
3.ZI
再看两个程序,他们之间的差别是一个未初始化的变量“a”,从之前的了解中,应该可以推测,这两个程序之间应该只有ZI大小有差别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a;
void main(void)
{
;
}
Prog3编译出来后的信息如下:
======================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 96 0 Grand Totals
======================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
======================================================================
Prog4编译出来后的信息如下:
======================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 97 0 Grand Totals
======================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 97 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
======================================================================
编译的结果完全符合推测,只有ZI数据相差了1个字节。这个字节正是未初始化的一个字符型变量“a”所引起的。
注意:如果一个变量被初始化为0,则该变量的处理方法与未初始化华变量一样放在ZI区域。
即:ARM C程序中,所有的未初始化变量都会被自动初始化为0。
总结:
1.C中的指令以及常量被编译后是RO类型数据。
2.C中的未被初始化或初始化为0的变量编译后是ZI类型数据。
3.C中的已被初始化成非0值的变量编译后市RW类型数据。
附:
程序的编译命令(假定C程序名为tst.c):
armcc -c -o tst.o tst.c
armlink -noremove -elf -nodebug -info totals -info sizes -map -list aa.map -o tst.elf tst.o
编译后的信息就在aa.map文件中。
ROM主要指:NAND Flash,Nor Flash
RAM主要指:PSRAM,SDRAM,SRAM,DDRAM
Image$$??$$Limit 的含义
对于刚学习ARM的人来说,如果分析它的启动代码,往往不明白下面几个变量的含义:|Image$$RO$$Limit|、|Image$$RW$$Base|、|Image$$ZI$$Base|。
首先申明我使用的调试软件为ADS1.2,当我们把程序编写好以后,就要进行编译和链接了,在ADS1.2中选择MAKE按钮,会出现一个Errors and Warnings 的对话框,在该栏中显示编译和链接的结果,如果没有错误,在文件的最后应该能看到Image component sizes,后面紧跟的依次是Code,RO Data ,RW Data ,ZI Data ,Debug 各个项目的字节数,最后会有他们的一个统计数据:
Code 163632 ,RO Data 20939 ,RW Data 53 ,ZI Data 17028
Tatal RO size (Code+ RO Data) 184571 (180.25kB)
Tatal RW size(RW Data+ ZI Data) 17081(16.68 kB)
Tatal ROM size(Code+ RO Data+ RW Data) 184624(180.30 kB)
后面的字节数是根据用户不同的程序而来的,下面就以上面的数据为例来介绍那几个变量的计算。
在ADS的Debug Settings中有一栏是Linker/ARM Linker,在output选项中有一个RO base选项,下面应该有一个地址,我这里是0x0c100000,后面的RW base 地址是0x0c200000,然后在Options选项中有Image entry point ,是一个初始程序的入口地址,我这里是0x0c100000 。
有了上面这些信息我们就可以完全知道这几个变量是怎么来的了:
|Image$$RO$$Base| = Image entry point = 0x0c100000 ;表示程序代码存放的起始地址
|Image$$RO$$Limit|=程序代码起始地址+代码长度+1=0x0c100000+Tatal RO size+1
= 0x0c100000 + 184571 + 1 = 0x0c100000 +0x2D0FB + 1
= 0x0c12d0fc
|Image$$RW$$Base| = 0x0c200000 ;由RW base 地址指定
|Image$$RW$$Limit| =|Image$$RW$$Base|+ RW Data 53 = 0x0c200000+0x37(4的倍数,0到55,共56个单元)
=0x0c200037
|Image$$ZI$$Base| = |Image$$RW$$Limit| + 1 =0x0c200038
|Image$$ZI$$Limit| = |Image$$ZI$$Base| + ZI Data 17028
=0x0c200038 + 0x4284
=0x0c2042bc
也可以由此计算:
|Image$$ZI$$Limit| = |Image$$RW$$Base| +TatalRWsize(RWData+ZIData) 17081
=0x0c200000+0x42b9+3(要满足4的倍数)
=0x0c2042bc
*******************************************************************************
Scatter file (分散加载描述文件)用于armlink的输入参数,他指定映像文件内部各区域的download与运行时位置。Armlink将会根据scatter file生成一些区域相关的符号,他们是全局的供用户建立运行时环境时使用。
(注意:当使用了scatter file 时将不会生成以下符号:
Image$$RW$$Base,
Image$$RW$$Limit,
Image$$RO$$Base,
Image$$RO$$Limit,
Image$$ZI$$Base,
Image$$ZI$$Limit)
二.什么时候使用scatter file
当然首要的条件是你在利用ADS进行项目开发,下面我们看看更具体的一些情况。
1 存在复杂的地址映射:例如代码和数据需要分开放在在多个区域。
2 存在多种存储器类型:例如包含 Flash,ROM,SDRAM,快速SRAM。我们根据代码与数据的特性把他们放在不同的存储器中,比如中断处理部分放在快速SRAM内部来提高响应速度,而把不常用到的代码放到速度比较慢的Flash内。
3 函数的地址固定定位:可以利用Scatter file实现把某个函数放在固定地址,而不管其应用程序是否已经改变或重新编译。
4 利用符号确定堆与堆栈:
5 内存映射的IO:采用scatter file可以实现把某个数据段放在精确的地指处。
因此对于嵌入式系统来说scatter file是必不可少的,因为嵌入式系统采用了ROM,RAM,和内存映射的IO。
三.scatter file 实例
1 简单的内存映射
LOAD_ROM 0x0000 0x8000
{
EXEC_ROM 0x0000 0x8000
{
*(+RO)
}
RAM 0x10000 0x6000
{
*(+RW, +ZI)
}
}
LOAD_ROM(下载区域名称) 0x0000(下载区域起始地址) 0x8000(下载区域最大字节数)
{
EXEC_ROM(第一执行区域名称) 0x0000(第一执行区域起始地址) 0x8000(第一执行区域最大字节数)
{
*(+RO (代码与只读数据) )
}
RAM(第二执行区域名称) 0x10000(第二执行区域起始地址) 0x6000(第二执行区域最大字节数)
{
*(+RW(读写变量), +ZI(未初始化变量))
}
}
2 复杂内存映射
LOAD_ROM_1 0x0000
{
EXEC_ROM_1 0x0000
{
program1.o(+RO)
}
DRAM 0x18000 0x8000
{
program1.o (+RW, +ZI)
}
}
LOAD_ROM_2 0x4000
{
EXEC_ROM_2 0x4000
{
program2.o(+RO)
}
SRAM 0x8000 0x8000
{
program2.o (+RW, +ZI)
}
}
LOAD_ROM_1 0x0000(下载区域一起始地址)
{
EXEC_ROM_1 0x0000(第一执行区域开始地址)
{
program1.o(+RO) (program1.o内的Code与RO data 放在第一执行区域)
}
DRAM 0x18000(第二执行区域开始地址) 0x8000(第二执行区域最大字节数)
{
program1.o (+RW, +ZI) (program1.o内的RW data与 ZI data 放在第二执行区域)
}
}
LOAD_ROM_2 0x4000(下载区域二起始地址)
{
EXEC_ROM_2 0x4000
{
program2.o(+RO) (program2.o内的Code与RO data 放在第一执行区域)
}
SRAM 0x8000 0x8000
{
program2.o (+RW, +ZI) (program2.o内的RW data与 ZI data 放在第二执行区域)
}
2.1 BNF 符号与语法
”:由引号来表示的符号保持其字面原意,如A”+”B标示A+B。
A ::= B :定义A为B。
[A]:标示可选部分,如A[B]C用来标示ABC或AC。
A+:用来标示A可以重复任意次,如A+可标示A,AA,AAA, …
A*:同A+。
A | B:用来标示选择其一,不能全选。如A|B用来标示A或者B。
(A B):标示一个整体,当和|符号或复杂符号的多次重复一起使用时尤其强大,如(AB)+(C|D)标示ABC,ABD,ABABC,ABABD, …
2.2分散加载文件各部分描述
如图2.1所示为一个完整的分散加载脚本描述结构图。下面我们对图示中各个部分进行讲述。
2.2.1 加载区描述
每个加载区有:
名称:供连接器确定不同下载区域
基地址:相对或绝对地址
属性:可选
最大字节数:可选
执行区域列:确定执行时各执行区域的类型与位置
load_region_name (base_address | ("+" offset)) [attribute_list] [ max_size ]
"{"
execution_region_description+
"}"
load_region_name:下载区域名称,最大有效字符数31。(并不像执行区域段名用于Load$$region_name,而是仅仅用于标示下载区域)。
base_address:本区域内部目标被连接到的地址(按字对齐)。
+offset:相对前一个下载区域的偏移量(4的整数倍,如果为第一个区域)。
2.2.2 执行区描述
每个执行区有:
名称:供连接器确定不同下载区域
基地址:相对或绝对地址
属性:确定执行区域的属性
最大字节数:可选
输入段:确定放在该执行区域的模块
exec_region_name (base_address | "+" offset) [attribute_list] [max_size]
"{"
input_section_description+
"}"
exec_region_name:执行区域名称,最大有效字符数31。
base_address:本执行区域目标要被联接到的位置,按字对齐。
+offset:相对于前一个执行区域结束地址的偏移量,4的整数倍;如果之前没有执行区域(本执行区域为该下载区域的第一个执行区域),则该偏移量是相对于该下载区域的基址偏移量。
attribute_list:PI,OVERLAY,ABSOLUTE,FIXED,UNINIT。
PI: 位置无关。
OVERLAY: 覆盖。
ABSOLUTE: 绝对地址。
FIXED: 固定地址,下载地址与执行地址具有该地址指示确定。
UNINIT: 未初始化数据。
RELOC:无法明确指定执行区域具有该属性,而只能通过继承前一个执行区或父区域获得。
对于PI,OVERLAY,ABSOLUTE,FIXED,我们只能选择一个,缺省属性为ABSOLUTE。一个执行区域要么直接继承其前面的执行区域的属性或者具有属性为ABSOLUTE。
具有PI,OVERLAY,RELOC属性的执行区域允许其地址空间重叠,对于ABSOLUTE,FIXED 属性执行区域地址空间重叠Armlink会报错。
max_size:可选,他用于指使Armlink在实际分配空间大于指定值时报错。
input_section_description:指示输入段的内容。
基本语法2
2.2.3 输入段描述
输入段:
模块名:目标文件名,库成员名,库文件名。名称可以使用通配符。
输入段名,或输入段属性(READ-ONLY,CODE)。
module_select_pattern
["("
("+" input_section_attr | input_section_pattern)
([","] "+" input_section_attr | "," input_section_pattern))*
")"]
2.2.3.1
module_select_pattern:选择的模块名称(目标文件,库文件成员,库文件),模块名可以使用通配符(*匹配任意多个字符,?匹配任意一个字符),名称不区分字母大小写,它是供选择的样本。
例1:*libtx.a (+RO)
libtx.a为threadX库文件。
例2:tx_ill.o (INIT)
tx_ill.o为threadX中断向量目标文件。
2.2.3.2
input_section_attr:输入段属性选择子,每个选择子以”+”开头,选择子不区分大小写字符。
选择子可选:
RO-CODE,
RO-DATA,
RO(selects both RO-CODE and RO-DATA),
RW-DATA,
RW-CODE,
RW(selects both RW-CODE and RW-DATA),
ZI,
ENTRY( that is a section containing an ENTRY point)
以下同义词可以选择:
CODE (for RO-CODE),
CONST( for RO-DATA),
TEXT (for RO),
DATA (for RW),
BSS (for ZI)。
还有两个伪属性:FIRST,LAST。如果各段的先后顺序比较重要时,可以使用FIRST,LAST标示一个执行区域的第一个和最后一个段。
例1:os_main_init.o (INIT ,+FIRST)
FIRST表示放于本执行区域的开始处。
例2:*libtx.a (+RO)
RO 表示*libtx.a的只读部分。
2.2.3.3
input_section_pattern:输入段名。
例1:os_main_init.o (INIT ,+FIRST)
INIT 为os_main_init.o的一个段。
例2:os_stackheap.o (heap)
heap 为os_stackheap.o的一个段。
例3:os_stackheap.o (stack)
stack为os_stackheap.o的一个段。
提高篇
3.1 在scatter file中指定胶合段
胶合段用于实现ARM代码到Thumb代码的切换或者实现代码的长转移。使用scatter file可以指定怎样放置胶合输入段。通常,在scatter file中一个执行区域可以拥有胶合段选择*(Venner$$Code)。
Armlink把胶合输入段放到拥有段选择符*(Veneer$$Code)的执行区域中,这样做是安全的。
可能由于地址范围问题或者受执行区域大小限制无法完成把胶合段分配个某个执行区域。如果当发生胶合段无法加到指定区域时,他将会被加到那些包含了生成胶合段的可重载输入段的执行区域。
3.2 创建根执行区域
根执行区域就是指那些执行与加载时地址相同的区域。
当你为映像文件指定初始化入口点或者由于你只使用一个ENTRY导向符而使得连接器创建初始化入口位置时,你就必须确保该入口点位于根执行区域。如果入口点不在根执行区域,连接将会失败,连接器会报错。
如:ENTRY point (0x00000000) lies within non-root region ER_ROM
可以通过以下方式实现在scatter file中指定根执行区域。
① 显示或缺省的指定执行区的属性为ABSOLUTE,同时使得加载区域与第一个执行区域具有相同的地址。
② 使用FIXED属性使得执行区域的加载地址与其执行时地址保持不变。
3.3 创建根执行区域
可以通过在scatter file中为某个执行区域指定FIXED属性来实现该区域加载于运行时地址保持不变。
FIXED可以用来在一个加载区中创建多个根执行区域。因此我们可以通过它实现把某个函数或一段数据放到目标地址,从而可以通过指针方便地访问该地址。比如,我们可以实现把常量表和checksum放到ROM上的固定地址处。
注意:
① 为了使得代码更加易于维护和调试,请尽量少使用scatter file来指定放置位置,而是应该尽可能多地让连接器来确定函数和数据的位置。
② 3.3.1 怎样把函数或数据放到指定地址
通常,编译器处理来自单个源文件的RO,RW,和ZI段。这些区域包括源文件的代码与数据。如果打算把单个函数或数据项放到某个固定地址,我们就必须让编译器单独处理这些函数和数据。
我么可以通过以下方式处理单个目标:
① 把函数和数据放到其源文件。
② 使用编译选项 –zo为每个函数单独生成一个目标文件。(参看ARM Compiler Guide)
③ 在C,C++源文件内利用 #pragma arm section 来生成多命名段。
④ 在汇编源文件内利用AREA 导向符来生成可重载段。
3.3.2 怎样放置单个目标文件的内容
3.3.3 怎样使用ARM的 section pragma
通常把函数和数据放到其源代码文件,然后放到其目标文件的相应段中。然而,我们也可以#pragma 和scatter file实现单独处理某个命名段。
// file adder.c
int x1 = 5; // in.data
int y1[100]; // in.bss
int const z1[3] = {1,2,3}; // in.constdata
int sub1(int x) // in.text
{
return x-1;
}
#pragma arm section rwdata = "foo", code ="foo"
int x2 = 5; // in foo (data part of region)
char *s3 = "abc"; // s3 in foo, "abc" in .constdata
int add1(int x)
{
return x+1;
} // in foo (.text part of region)
#pragma arm section code, rwdata // return to default placement
FLASH 0x24000000 0x4000000
{
FLASH 0x24000000 0x4000000
{
init.o (Init, +First) ;//place code from init.o first
* (+RO) ;//sub1(), z1[]
}
32bitRAM 0x0000
{
vectors.o (Vect, +First)
* (+RW,+ZI) ;//x1, y1
}
ADDER 0x08000000
{
adder.o (foo) ;//x2, string s3, and add1()
}