scatter基本点:
1. 编译后输出的映像文件中各段是首尾相连的,中间没有空闲的区域,它们的先后关系是根据链接时参数的先后次序决定的 armlinker -file1.o file2.o ……
2. scatter用于将编译后的映像文件中的特定段加载到多个分散的指定内存区域
3. 有2类域region:执行域(execution region,一般是ram区域)和加载域(load region,一般是rom区域)
4. 加载域:就是编译之后得到的二进制文件烧写到rom中的这一段区域,所有的代码RO、预定义变量RW、堆栈之类清不清空无关紧要的大片内存区域ZI,都包括在其中
5. 执行域:就是把加载域进行‘解压缩’后的样子。比如:RO没有变动还是在ROM中,RW被移到了SRAM中,而ZI被放置在SDRAM中
6. scatter本身并不能对映像实现‘解压缩’,编译器读入scatter文件之后会根据其中的各种地址生成启动代码,实现对映像的加载,而这一段代码就是* (InRoot$$Sections)它是__main()的一部分。这就是在汇编启动代码的最后跳转到__main() 而不是跳向main()的原因之一。
7. 起始地址与加载域重合的执行域成为root region,* (InRoot$$Sections)必须放在这个执行域中,否则链接的时候会报错。*(+RO)包含了* (InRoot$$Sections),所以如果在root region中用到了*(+RO)可以不再指定* (InRoot$$Sections),
scatter语法:
ROM_LOAD 0x00000000
{
ROM 0x00000000 0x003FFFFF
{
vectors.o (+RO,+FIRST)
* (InRoot$$Sections) ; All library sections that must be in a root region
*(+RO)
}
SRAM 0x00400000 0x003FFFFF
{
* (+RW,+ZI)
}
SDRAM1 0x41000000 UNINIT
{
stack.o (+ZI) ; stack.s中定义了top_of_stack为长度为1的space,指定栈顶地址
}
SDRAM2 +0 UNINIT
{
heap.o (+ZI)
}
}
注解:
1. ROM_LOAD是加载域。这里只有一个,也可以有多个(rom地址不连续的情况)
2. ROM、SRAM、SDRAM1、SDRAM2是执行域,有多个。第一个执行域必须和加载域地址重合,因为ARM的复位地址就是加载域的起始地址(有bootstrap的话加载域址就是bootstrap执行完后的跳转地址)
3. vectors.o (+RO, +FIRST) 中断向量表放在最开头
4. ROM 0x00000000 0x003FFFFF; 加载域名 起始地址 最大允许长度;‘最大允许长度’也可以省略,但缺点是编译器不会检查段是否溢出和别的段重叠了。‘起始地址’= +0表示紧接着上一段开始的连续地址。
5. * (InRoot$$Sections)是复制代码的代码
6. UNINT关键字表示不进行初始化清零
值得注意的是:在一个scatter文件中,同一个.o文件不能出现2次,即使是在2个不同的加载域中也不可以,否则会报错:Ambiguous selectors found for *.o,错误的例子:
LOAD1 0x00000000
{
EXE1
{
Init.o
}
}
LOAD2 0xFFFF0000
{
EXE2
{
Init.o
}
}
想起了中学里哲学课上老师让解释为什么人不能两次踏入同一条河流,当年稀里糊涂的写的答案,老师批了个大差,回去有没有补上,今天居然在这里遇到了老问题。。。推测是编译器自动生成的scatter载入代码InRoot$$Sections不支持把同一obj搬移2次。
这就带来一个问题:如果希望把同一段代码(如中断跳转表)载入2份拷贝到不同的地址,咋整?一个笨办法是自己写一段代码搬移程序来代替编译器自动生成的搬移代码,但前提是需要搞懂映像文件的组织,增加了工作量。投机一点的方法是在makefile中把一个.o文件复制并重新起一个名字,然后把它们传递给 armlink。另外,猜测scatter语法可能包含诸如+duplicate之类的关键字来允许同一段的多个副本
分散加载的实现:
由于现在的嵌入式技术发展比较快,各类存储器也层出不穷,但是它们在容量、成本和速度上有所差异,嵌入式系统又对成本比较敏感,那么合理的选择存储器和充分的利用存储器资源成为一个必要解决的问题。咋们工程师最喜欢的就是发掘问题,然后解决问题,基于嵌入式系统对存储器的敏感,那么要合理的利用存储器资源,就必须找到一种合理的方式。工程师们发现,可以把运行的程序放在不同成本的存储器中来寻找这个成本的支点,比如把没有运行的但是较为庞大的程序放在容量大、成本低、速度也较低的FLASH存储器中,要用的时候再去拿。但是,这里面又有一个问题,嵌入式本身就对信号的处理速度有较高的要求,这点在实时操作系统的定义上上有所体现。所以那些经常要用的程序段如果要保证其高速的运行那么就得放在一个在高速的存储器中,不过这是有代价的:较高成本,小容量。但是,相信由于技术的发展这个问题终将被解决,到时候寻找平衡点的问题也就不存在了。好了,说了多了点。切入正题。
程序总有两种状态:运行态和静止态。当系统掉电的时候程序需要被保存在非易失性的存储器中,且这个时候程序的排放是按照地址依次放的,换句话说:我才懒得管它怎么放,只要不掉就行。当系统上电后,CPU就要跑起来了,CPU属于高速器件,存储器总是不怎么能跟得上,既然跟不上那么我们就尽量缩短它们之间的差距,那留下一条路,那就是尽量提高存储器的读取速度,存储器类型决定其速度的水平,那么尽量放在速度高的存储器就成为首选解决方案。那么我们就把要执行的程序暂时拿到速度较快的RAM中。那么拿的过程就牵涉到程序的加载了。这就是要解决的问题。
一个映像文件由域(region)、输出段(output sections)和输入段(input sections)组成。不要想得太复杂,其实他们之间就是包含与被包含的关系,具体关系如下:
映像文件>域>输出段>输入段
输入段: 输入段就是我们写的应用程序代码+初始化的数据+应该被初始化为0的数据+没有初始化的数据。用英文表示就是:RO(READONLY),RW(READWRITE),ZI(ZEROInitialized),NOINIT(Not Initialized).ARM连接器根据各个输入段不同的属性把相同的再一起组合一下就成了输出段。(注 有关链接器的知识 补充。。。。。。。。)
输出端:为了简化编译过程和更容易取得各种段的地址,那么把多个同属性的输入段按照一定的规律组合在一起,当然这个输出段的属性就和它包含的输入段的属性一样。输入段的排放规律就是:最先排放RO属性的输入段,然后是RW属性段,最后是ZI 或NOINIT
域:为什么还要加一层域,我的理解是由于代码的功能不同,那么我们有必要把不同功能的代码分类放。我们可以把需要高速执行的代码放在一起、把执行效率高的放在一起 低的放在一起。
LOAD_ROM1 0X00000000 ; 从火车上取出来时的地址(如:成都站)
{
EXEC_ROM1 0x40000000
{
PROGRAM.O(+RO) ;把品牌RO的货物发给0x40000000去
RAM1 0x80000000
{
PROGRAM.O(+RW,+ZI);把品牌RW,ZI的货物依次发给0x80000000
}
}
......
}
其他的段也可以这样依葫芦画瓢。
scatter的原理就介绍这样,其中的语法和规则要多写多把代码的地址拖出来看才能体会。不过都是很简单的,生活中的小常识就能解决这些问题。为什么?因为设计这些规则的工程师的灵感就是源自生活。嘿嘿...享受把代码随处放的乐趣吧,...enjoy...