1.1 什么时候使用分散加载文件
因此对于嵌入式系统来说 scatter file 是必不可少的,因为嵌入式系统采用了 ROM, RAM,和内存映射的 IO。通过使用分散加载机制,可以使用文本文件中的描述为链接器指定映像的内存映射。分散加载为您提供了对映像组件分组和位置的全面控制。分散加载可以用于简单映像,但它通常仅用于具有复杂内存映射的映像,即多个区在加载和执行时分散在内存映射中。
1.2 适用范围
有时候用户希望将不同代码放在不同存储空间,也就是通过编译器生成的映像文件需要包含多个域,每个域在加载和运行时可以有不同的地址。要生成这样的映像文件,必须通过某种方式告知编译器相关的地址映射关系。
Scattter loading 是 ARM 连接器(armlink)提供的一种机制,该机制让你能够把可执行映像文件分成多个区域,然后分别为它们指定在存储器上的存储位置。 Scatter loading 机制用于定位装载区域和运行区域在分离的存储映像中的位置。
1.3 再谈 ARM Image(镜像文件)
ARM 中的源文件经过编译器编译生成的目标文件.obj(Object file)和相应的 C/C 运行时库(Runtime Library)经过连接器的处理后,生成 ELF 格式的映像文件(image)——它可以被写入目标设备的 ROM 中直接运行或加载后运行。镜像文件的组成如图所示。
图 1.1 镜像文件组成示意图
可执行文件由映像、区、输出节和输入节的层次结构构成:
关于 RO、 RW 以及 ZI 数据段的详细内容在前文中已有叙述,在此就不再详述。
ARM 映像文件各组成部分在存储系统中的地址有两种:
1. 装载区域
程序在装载之后、运行之前,所占有的存储区域能够被分成多个装载区域,每个装载区域就是一个连续的字节块。
2. 运行区域
程序在运行时所占有的存储区域能够被分成多个装载区域,每个装载区域也是一个连续的字节块。
注意:一个装载区域可能包含多个运行区域;一个运行区域只能属于一个装载区域。
1.4 节放置
链接器根据输入节的属性在一个区内对它们进行排序。具有相同属性的输入节在区内形成相邻块。每个输入节的基址由链接器定义的排序顺序确定,并且在包含它的输出节中正确对齐。通常,生成映像时链接器按以下顺序对输入节进行排序:
注意:此排序顺序不受分散加载描述文件或对象文件名中的排序的影响。如果执行区包含4MB 的 Thumb 代码、 16M 的 Thumb-2 代码或超过 32MB 的 ARM 代码,则链接器可能会更改排序顺序,从而将长跳转中间代码的数量减至最小。在缺省情况下,链接器创建由 RO、 RW 和可选的 ZI 输出节组成的映像。 RO 输出节在具有内存管理硬件的系统上运行时可以受到保护。RO 节也可以放在目标的 ROM 中。
部分映像集合在一起,形成最小数量的相邻区。 armlink 按如下属性对输入节排序:
1. 只读代码
2. 只读数据
3. 读写代码
4. 读写数据
5. 零初始化数据
具有相同属性的输入节按名称排序。名称是区分大小写的,并且使用 ASCII 字符排列顺序,对名称按字母顺序进行比较。属性和名称相同的输入节根据它们在输入文件中的相对位置进行排序。这些规则意味着库中所含属性和名称相同的输入节的位置不可预判。如果需要更精确的定位,可以显式指定各个模块并将这些模块包含在输入列表中。
1.5 一个简单的加载过程
在一个简单的嵌入式计算机系统中,存储器一般被分成 ROM 和 RAM。连接器生成的映像被分成“Read-Only”段和“Read-Write”段(包含已初始数据和未初始化数据,未初始化数据也叫 ZI 数据)。通常,在程序下载(烧入)的时候,它们会被一块下载到 ROM 上;而在程序开始执行时, Read-Write 段会从 ROM 被 Copy 到 RAM。
如下图所示:
图 1.2 加载过程描述示意图
以上是一个简单的例子,但在复杂的嵌入式系统中,其存储器往往包括 ROM,SRAM,DRAM, FLASH 等等,加载过程比如上实例要复杂一些。
1.6 分散加载文件语法
分散加载文件主要由一个加载时域(区)和多个运行时域(区)组成,其大致结构如下图所示。
图 1.3 分散加载文件的语法结构
1.6.1 加载时域(区)描述
加载时域语法格式如程序清单 1.1 所示,其每项含义解释如下。
程序清单 1.1 加载时域描述语法描述
load_region_name(base_address|(" "offset))[attribute_list][max_size]
{
execution_region_description
}
注:程序清单 4.1 中’|’、‘ []’符号的使用请参考 BNF 语法。
1.6.2 运行时域(区)描述
运行时域语法格式如程序清单 1.2 所示,其每项含义解释如下。
程序清单1.2 运行时域语法描述
exec_region_name(base_address|" "offset)[attribute_list][max_size|" "length]
{
input_section_description*
}
1.6.3 输入段描述
输入段语法描述如程序清单 1.3 所示。
程序清单 1.3 输入段语法描述
module_select_pattern [ "(" input_section_selector ( "," input_section_selector )* ")" ]
(" " input_section_attr | input_section_pattern | input_symbol_pattern)
● module_select_pattern:目标文件滤波器,支持使用通配符“*”与“?”。其中符号“*”代表零个或多个字符,符号“?”代表单个字符。进行匹配时所有字符不区分大小写。当 module_select_pattern 与以下内容之一相匹配时,输入段将与模块选择器模式相匹配:
1.包含段和目标文件的名称;
2.库成员名称(不带前导路径名);
3.库的完整名称(包括路径名)。如果名称包含空格,则可以使用通配符简化搜索。例如,使用*libname.lib 匹配 C:\lib dir\libname.lib。
● nput_section_attr:属性选择器与输入段属性相匹配。每个 input_section_attr 的前面有一个“ ”号。如果指定一个模式以匹配输入段名称,名称前面必须有一个“ ”号。可以省略紧靠“ ”号前面的任何逗号。选择器不区分大小写。可以识别以下选择器:
◆ RO-Code;
◆ RO-DATA;
◆ RO,同时选择 RO-CODE 和 RO-DATA;
◆ RW-DATA;
◆ RW-CODE;
◆ RW,同时选择 RW-CODE 和 RW-DATA;
◆ ZI;
◆ ENTRY:即包含 ENTRY 点的段。
● 可以识别以下同义词:
◆ CODE 表示 RO-CODE;
◆ CONST 表示 RO-DATA;
◆ TEXT 表示 RO;
◆ DATA 表示 RW;
◆ BSS 表示 ZI。
● 可以识别以下伪属性:
◆ FIRST;
◆ LAST。
通过使用特殊模块选择器模式.ANY 可以将输入段分配给执行区,而无需考虑其父模块。可以使用一个或多个.ANY 模式以任意分配方式填充运行时域。在大多数情况下,使用单个.ANY等效于使用*模块选择器。
1.7 实战测试(一个简单的例程)
程序清单 1.4 Keil 默认的分散加载描述文件代码
; *************************************************************
; *** Scatter-Loading Description File generated by uVision
; *************************************************************
LR_IROM1 0x00000000 0x00040000 { ; load region size_region
ER_IROM1 0x00000000 0x00040000 { ; load address = execution address
*.o (RESET, First)
*(InRoot$$Sections)
.ANY ( RO)
}
RW_IRAM1 0x10000000 0x00008000 { ; RW data
.ANY ( RW ZI)
}
}
程序清单 1.4 所示的即为 keil 默认的分散加载文件,加载结果如下图所示。
图 1.4 加载结果图
这仅仅只是一个片段,不过可以看出其与上文的分散加载代码的描述是相吻合的。现在按照代码清单 1.5 修改代码,看看是不是 RO 段的加载区域会改变。
程序清单 1.5 测试代码
LR_IROM1 0x00010000 0x00040000 { ; load region size_region
ER_IROM1 0x00010000 0x00040000 { ; load address = execution address
*.o (RESET, First)
*(InRoot$$Sections)
.ANY ( RO)
}
RW_IRAM1 0x10000000 0x00008000 { ; RW data
.ANY ( RW ZI)
}
}
按照上面对分散加载文件的介绍可知,这段代码运行后, RO 段的加载地址将由 0x00000000变为 0x00010000,下面让我们实际操作一下,看看我们预想的是否正确。要想直接执行这段代码是行不通的,正常的 Keil 编译器默认是不会自动执行.sct(分散加载描述)文件的,需要按下图所示修改相应的链接器配置。
图 1.5 需要修改的 keil 配置示意图
这样我们编写的分散加载文件就可以运行了。重新链接工程,再次查看 LPC1700.map 文件,如下图所示。
图 1.6 再次加载结果示意图
与图 1.4 对比,从图中我们可以明显看出程序加载的基址变为了 0x00010000,这与我们编写的分散加载代码的内容一致,说明代码成功运行,结果和我们预想的一样。
1.8 实战分散加载
假设,一个 Cortex-M3 内核的 LPC17xx 微控制器有 Flash、 RAM 的资源如下:
那这一个分散加载文件应该怎样描述呢?可参考如下所示的配置。
程序清单 1.6 一个普通的分散加载文件配置
LR_IROM1 0x00000000 0x00040000 { ; 定义一个加载时域,域基址: 0x00000000,域大
; 小为 0x00040000,对应实际 Flash 的大小
ER_IROM1 0x00000000 0x00040000 { ; 定义一个运行时域,第一个运行时域必须和加载
*.o (RESET, First) ; 时域起始地址相同,否则库不能加载到该时域的
.ANY ( RO) ; 错误,其域大小一般也和加载时域大小相同
; 将 RESET 段最先加载到本域的起始地址外,即
; RESET 的起始地址为 0, RESET 存储的是向量表
; 加载所有匹配目标文件的只读属性数据,包含:
; Code、 RW-Code、 RO-Data
}
RW_IRAM1 0x10000000 0x00008000 {
* ( RW ZI) ; 定义一个运行时域,域基址: 0x10000000,域大
; 小为 0x00008000,对应实际 RAM 大小
; 加载所有区配目标文件的 RW-Data、 ZI-Data
; 这里也可以用.ANY 替代*号
}
}
1.8.1 多块 RAM 的分散加载文件配置
还是上述的 MCU,假设其增加了另外一块 RAM,其资源如下:
如果我想将这两块不连续的 RAM 都使用起来(可使用 64KB RAM)?分散加载文件应怎样描述呢?可参考如下所示配置。
程序清单 1.7 双 RAM 的加载文件配置
LR_IROM1 0x00000000 0x00040000 {
ER_IROM1 0x00000000 0x00040000 {
*.o (RESET, First)
.ANY ( RO)
}
RW_IRAM1 0x10000000 0x00008000 { ; 定义 RAM1 的运行时域
; 使用.ANY 进行随意分配变量,这里不能使用*号; 替代, *表示匹配有所有的目标文件,这样变量就
; 无法分配到第二块 RAM 空间了
.ANY ( RW ZI)
}
RW_IRAM2 0x2007C000 0x00008000 { ; 定义第 RAM2 的运行时域
; 同样使用.ANY 随意分配变量的方式
.ANY ( RW ZI)
}
; 如果还有另多的 RAM 块,在这里增加新的运行
; 时域即可,格式和 RAM2 的定义相同
}
如上所示的分散加载文件配置,确实可将两块 RAM 都使用起来,即有 64KB 的 RAM 可以使用,但其并不能完全等价于一个 64KB 的 RAM,实际应用可能会碰到如下的问题。如我在 main.c 文件中声明了 1 个 40KB 的数组,如程序清单1.8所示,程序中红色部分已注释,可暂不理会。
程序清单 1.8 定义大数组出错
// main.c 文件
…
unsigned char GucTest0[40 * 1024]; // 定义一个 40KB 的数组
//unsigned char GucTest1[20 * 1024]; // 定义第一个 20KB 数组
//unsigned char GucTest2[20 * 1024]; // 定义第二个 20KB 数组
…
// end of file
如程序清单 1.8 所示的程序在编译时会出现错误,并提示没有足够的空间,为什么呢?原因为数组是一个整体,其内部元素的地址是连续的,不能分割的,但是在两个不连续的 32KB空间中,是没办法分配出一个连续的 40KB 地址空间,所以编译会提示空间不足,分配 40KB数组失败。
还是程序清单 1.8 所示的程序,去掉 40KB 的数据,换成 2 个 20KB 的数组(即红色注释代码部分),编译结果又会如何呢?编译结果还是提示空间不足,这又是为什么呢?这里出错的原因其实和上面的原因是相同的,首先,重温一下,.ANY 的作用, .ANY 是一个通配符,当其与以下内容之一相匹配时将进行选择。
后面两个和本次讨论的话题无关,再仔细的看第一个匹配项为:包含段和目标文件,段这里先不用理会,因为这里没有用到段,所以只剩下目标文件。要注意是“目标文件”,不是其它,即是说一个 C 文件编译后,其所有的变量、代码都会作为一个整体。所以定义两个 20KB 和定义了一个 40KB,在编译器看来都是一样,就是这个 C 文件总共定义了 40KB 的空间,我要用40KB 的空间来分配它,因此会出现同样的错误。
关于大数组分配的解决方法,有两种,分别如下:
将数组分开在不同的 C 文件中定义,避免在同一个 C 文件定义的数据大小总量超过其中最大的分区;
将一个 C 数组,使用段定义,使其从该 C 文件中独立出来,这样编译器就不会将它们作为一个整体来划分空间了,其示例如程序清单 1.9 所示。
程序清单 4.9 使用段的方式分配多数组
#pragma arm section zidata = "SRAM" // 在 C 文件中定义新的段
unsigned char GucTest1[20 * 1024]; // 定义第一个 20KB 数组
#pragma arm section // 恢复原有的段
unsigned char GucTest2[20 * 1024]; // 定义第二个 20KB 数组,这 20KB 数组不会和
// GucTest1 作为一个整体来划分空间
1.8.2 多块 Flash 的分散加载文件配置
再一下上述的 MCU,假其增加多了一块 Flash,不是 RAM,其资源如下。
注意这里多增加的一块的不是 RAM,而是 Flash,其情况会如何呢?假设其相同,那写法应该就是如程序清单 1.10 所示的样子。
程序清单 1.10 双 Flash 的错误配置示例 1
LR_IROM1 0x00000000 0x00040000 {
ER_IROM1 0x00000000 0x00040000 { ; 定义 Flash1 运行时域
*.o (RESET, First) ; 先加载向量表
.ANY ( RO) ; 随意分配只读数据
}
ER_IROM2 0x20000000 0x00200000 { ; 定义 Flash2 运行时域
.ANY ( RO) ; 随意分配只读数据
}
RW_IRAM1 0x10000000 0x00008000 {
.ANY ( RW ZI)
}
}
如程序清单 1.10 的分散加载配置,进行编译却出错了,错误提示如下所示。
..Error: L6202E: __main.o(!!!main) cannot be assigned to non-root region 'ER_IROM2'
..Error: L6202E: __scatter.o(!!!scatter) cannot be assigned to non-root region 'ER_IROM2'
..Error: L6202E: __scatter_copy.o(!!handler_copy) cannot be assigned to non-root region 'ER_IROM2'
..Error: L6202E: __scatter_zi.o(!!handler_zi) cannot be assigned to non-root region 'ER_IROM2'
..Error: L6202E: anon$$obj.o(Region$$Table) cannot be assigned to non-root region 'ER_IROM2'
..Error: L6203E: Entry point (0x20000001) lies within non-root region ER_IROM2.
该错误的意思是说, __main.o、 __scatter.o、 __scatter_copy.o 等不能被加载到第二块 Flash的运行时域 ER_IROM2,也就是说这几项目数据只能加载到 ER_IROM1 的运行时域。为什么这些数据不能放到第二个运行时域呢?后面再进行解释。
无论如何,但总的来说是使用.ANY 引起的错误, .ANY 是让编译器随意分配数据,所以数据有可能被分配到 ER_IROM2。如果手动这出错的几项到 ER_IROM1,结果又会如何呢?请看
如程序清单 1.11 所示的配置。
程序清单 1.11 双 Flash 的错误配置示例 2
LR_IROM1 0x00000000 0x00040000 {
ER_IROM1 0x00000000 0x00040000 {
*.o (RESET, First) ; 先加载向量表
__main.o ; 手动加载到 ER_IROM1,避免自动分配引起错误
__scatter.o ; 避免自动分配
__scatter_copy.o ; 避免自动分配
__scatter_zi.o ; 避免自动分配
* (Region$$Table) ; 避免自动分配
.ANY ( RO)
}
ER_IROM2 0x20000000 0x00200000 {
.ANY ( RO)
}
RW_IRAM1 0x10000000 0x00008000 {
.ANY ( RW ZI)
}
}
终于,编译没有错误了,但在软件仿真下,现象明显不正确, main()函数都跑不到,这?这个问题得从第一个时域与加载时域的关系来进行说明,其关系如下所示。
1. 第一个运行时域存放的代码不会进行额外拷贝
因为分散加载文件有一项很强大的功能,就是可以将 Flash 的代码拷贝到 RAM 中运行,这一段拷贝代码就存在于__main()函数中,但拷贝代码不能拷贝自身,所以必须规定有一个运行时域中存放的代码是不会被拷贝的,这个指的就是第一个运行时域。
拷贝代码为什么不能拷贝自身呢?举个例子,假设 A 代码是要被拷贝到 RAM 执行的代码,那 A 代码必先存储于 Flash 中,然后被拷贝到 RAM 中。这样 A 代码不就存在两段了么,但是只能有一段是有效的,就像定义了两个相同名字的函数,最终只能留下一个。所以最终被认定为拷贝后的代码才是有效的。那就是说 A 代码从 Flash 中拷贝到了 RAM, RAM 中代码才是有效, Flash 中的代码是无效的,其它程序调用 A 代码也是调用 RAM 中的代码而不是 Flash 的。那如果 A 是一段拷贝代码,那就会发生如下的现象,一段程序调用 RAM 中的 A 代码, RAM中的 A 代码再将自己从 Flash 中拷贝到 RAM。
结论,一段代码必须先完成拷贝,才能被执行。换句理解就是拷贝代码前包括自身的所有代码都不能拷贝,也就是说这些代码全部都必须放在第一个运行时域中。
2. 规定其余运行时域中存放的代码均会被拷贝
一个加载时域,只需要一个不拷贝的运行时域即可。所以规定其余所有的运行时域中的代码均会被拷贝。
3. 第一个运行时域的基址必须与加载域基址相同
为了保证第一个运行时域的代码能够被正确存储和执行,因此要求第一个运行时域的基址必须和加载时域的基址相同。看完了第一个运行时域与加载时域的关系,那程序清单 1.10、程序清单 1.11 出现错误的原因就很明了了。
程序清单 1.12 程序清单 1.7 出错的原因
ER_IROM2 0x20000000 0x00200000 {
.ANY ( RO) ; 定义 Flash1 运行时域
; 随意分配只读数据,但代码存放在第二个运行时
; 域中,所以该代码是运行时才被拷贝到这里,
; 那就是说要往 Flash1 直接写数据,当然会导致
; 程序出错了
}
程序清单 1.13 给出了双 Flash 的分散加载文件正确配置示例,如下所示。
程序清单 1.13 双 Flash 的正确配置示例
LR_IROM1 0x00000000 0x00040000 { ; 定义 Flash1 的加载域
ER_IROM1 0x00000000 0x00040000 {
*.o (RESET, First)
.ANY ( RO) ; 随机分配只读数据
}
RW_IRAM1 0x10000000 0x00008000 {
.ANY ( RW ZI)
}
}
LR_IROM2 0x20000000 0x00200000 { ; 定义 Flash2 的加载域
ER_IROM2 0x20000000 0x00200000 {
.ANY ( RO) ; 随机分配只读数据,代码不会进行拷贝
}
}
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved