DSP学习(8)—— linker.cmd文件解析
前言
写工程的时候遇到报内存不够的错误,出现在linker.cmd的内存分配section,找到一篇英文文章详细说明了linker.cmd文件。记录下来以便日后查看。
英文原文链接:https://software-dl.ti.com/ccs/esd/documents/sdto_cgt_Linker-Command-File-Primer.html
问题陈述
您已经有一个linker.cmd,它适用于你开始的开发系统(套件,启动板,EVM等)。假设你需要更改linker.cmd,以对生产系统上的内存配置进行修改。本文通过解释您现在拥有的linker.cmd来帮助你做到这一点。
文章概述
Basic部分适合所有人。Basic部分中描述的代码出现在每个linker.cmd中。在Beyond Basic部分,选择性阅读,这里可以找到出现在某些(但不是全部)linker.cmd中的代码说明。
基本
Linker.cmd文件可能显示的任何内容:options, object file names, library names(选项、对象文件名、库名称。)Linker.cmd文件中可以创建全局符号,但本文没有描述这些。本部分重点介绍 MEMORY 指令,尤其是 SECTIONS 指令。这些指令出现在每个linker.cmd中。
MEMORY指令
MEMORY 指令的用途是为内存范围指定名称。这些内存范围名称在 SECTIONS 指令中使用。以下是来自典型MSP430系统的MEMORY指令的一部分...
MEMORY
{
SFR : origin = 0x0000, length = 0x0010
PERIPHERALS_8BIT : origin = 0x0010, length = 0x00F0
PERIPHERALS_16BIT : origin = 0x0100, length = 0x0100
RAM : origin = 0x1C00, length = 0x0FFE
INFOA : origin = 0x1980, length = 0x0080
INFOB : origin = 0x1900, length = 0x0080
INFOC : origin = 0x1880, length = 0x0080
INFOD : origin = 0x1800, length = 0x0080
FLASH : origin = 0x8000, length = 0x7F80
INT00 : origin = 0xFF80, length = 0x0002
/* ... and so on */
}
例如:以 RAM 开头的行定义了一个名为 RAM 的内存范围。它从地址0x1C00开始,具有0xFFE长度。
SECTIONS 指令
SECTIONS 指令同时执行两项操作。
- 它从输入部分形成输出部分
- 它将这些输出部分分配给内存
下面是显示 SECTIONS 指令如何工作的图形方式。
词汇表
描述 SECTIONS 指令需要了解这些术语。
- Object file 对象文件 - 对象文件是输入部分的集合。对象文件可以直接呈现给链接器(通过命令行或在命令文件中),也可以来自库。
- Input Section 输入节 - 一个对象文件中的一个节。输入部分可以初始化也可以不初始化。它可以包含代码或数据。
- Output Section 输出节 - 一个或多个输入节的集合。输出部分是根据 SECTIONS 指令形成的,但极少数例外情况在本文中未作说明。
- Memory Range 内存范围 - 在 MEMORY 指令中指定的系统内存范围
Section 命名约定
从理论上讲,仅凭名称,无法了解有关输入部分内容的任何信息。尽管如此,具有这些名称的输入部分通常具有以下内容:
名字 | 初始 化 | 笔记 |
---|---|---|
.text | 是的 | 可执行代码 |
.bss | 不 | 全局变量 |
.cinit | 是的 | 初始化全局变量的表 |
.data (EABI) | 是/否 | 初始化来自汇编程序 / 更改为未被链接器初始化 |
.data (COFF ABI) | 是的 | 初始化的数据 |
.stack | 不 | 系统堆栈 |
.heap / .sysmem | 不 | malloc 堆 |
.const | 是的 | 初始化的全局变量 |
.switch | 是的 | 某些开关语句的跳转表jump |
.init_array 或 .pinit | 是的 | 启动时调用的C++构造函数表 |
.cio | 不 | 用于 stdio 函数的缓冲区 |
节名称通常以“.”开头,但这不是必需的。
您可能会看到这些名称的变体,例如 .ebss 或 .fardata。这些部分与此表中描述的部分非常接近。
语法说明
本文这一部分中的所有示例都出现在 SECTIONS 指令中。
SECTIONS
{
/* all examples appear here */
}
表单输出部分
output_section_name /* Name the output section */
{
file1.obj(.text) /* List the input sections */
file2.obj(.text)
file3.obj(.text)
} > FLASH /* Allocate to FLASH memory range */
这将创建一个名为 output_section_name 的输出部分。它由 3 个输入部分组成:来自 file1.obj 的 .text、来自 file2.obj 的 .text 和来自 file3.obj 的 .text。它被分配给闪存范围。
显然,此语法不能很好地扩展到具有许多此类对象文件的系统。
output_section_name /* Name the output section */
{
/* Shortcut syntax for all input sections named .text */
*(.text)
} > FLASH /* Allocate to FLASH memory range */
这与前面的示例执行相同的操作,但有一个区别。前面的示例仅使用了 3 个输入节,并且为对象文件名和输入节名显式指定了这些输入节。此示例使用名为 .text 的所有输入节。确切地说,它使用所有名为 .text 的输入部分,这些部分不属于任何其他输出部分。
因为即使这样也不够短,所以此示例中的快捷方式建立在上一个示例的基础上。
.text > FLASH
此示例与上一个示例只有一个区别:输出部分的名称已从 output_section_name 更改为 .text。请注意输出节名称和输入节名称如何完全相同:.text。尽管有这种相似性,但重要的是不要忽视输入部分和包含它们的输出部分之间的区别。
下面是另一个快捷方式示例:
.text : {} > FLASH
此示例与上一个示例没有什么不同。之所以显示它,是因为该语法模式在许多linker.cmd中很常见。
您可以将这些快捷方式混合在一起。例如:
output_section_name
{
first.obj(.text) /* This code must be first */
*(.text)
} > FLASH
这将创建一个名为 output_section_name 的输出部分。第一个输入部分是 first.obj 中的 .text 部分。其余输入部分是来自所有其他对象文件的所有 .text 部分。它被分配给闪存范围。
将输出部分分配到内存
上述示例中的此语法...
... > FLASH
将输出部分分配到内存。闪存范围用于此特定情况。
您可能还会看到...
... > 0x20000000
对硬编码地址的分配始终在分配到命名内存范围之前完成。您的linker.cmd可能会利用这种排序差异。
另一种值得关注的技术...
#define BASE 0x20000000
/* many lines later */
... > BASE
这看起来与对命名内存范围的分配相同,但它实际上是对硬编码地址的分配。
勉强超越基础
从这一点开始,文章变得不那么全面,更具选择性。查看每个部分附带的示例。如果linker.cmd中出现此类代码,则该部分将对其进行描述。否则,您可以忽略它。
除非另有说明或说明,否则所有示例都发生在 SECTIONS 指令中。
添加和更改
如果您在linker.cmd中看到本文中未介绍的代码,请发布到 E2E 论坛。论坛回复通常会导致对本文进行添加或更改。
内存范围中的第一个输出部分
假设您看到类似于 ...
#define BASE 0x00200000
MEMORY
{
FLASH : origin = BASE, length = 0x0001FFD4
…
}
SECTIONS
{
.intvecs > BASE /* only section allocated to BASE */
.text > FLASH
.const > FLASH
…
}
此代码的净效应是 .intvecs 是闪存范围中的第一个输出部分。其余的输出部分也位于FLASH中,但可以按任何顺序分配。
#define BASE 是使用链接器的类似 C 的预处理器功能的一个示例。它用于建立闪存范围的开始。它还用于将 .intvecs 分配给该特定地址。对特定地址的分配始终在分配到命名内存范围之前完成。
分配到多个内存范围
考虑这个例子...
.text > FLASH0 | FLASH1
这意味着 .text 输出部分被分配到内存范围 FLASH0 或 FLASH1。首先尝试 FLASH0。如果它不能包含所有 .text,则尝试 FLASH1。注意:.text 不会拆分。整个输出部分放置在 FLASH0 或 FLASH1 中。
将输出部分拆分到多个内存范围
考虑这个例子...
.text : >> RAMM0 | RAML0 | RAML1
这意味着 .text 将跨这些内存范围进行拆分。请注意语法。如果所有 .text 都不适合 RAMM0,则将其拆分,其余部分进入剩余的内存范围。拆分发生在输入截面边界上。输入部分永远不会拆分。这意味着任何函数,数组,结构等都不能在中间拆分。内存范围按该顺序使用。>>
内存页
内存页仅在 C28xx 链linker.cmd中使用。
内存页在内存指令中指定,如下所示...
MEMORY
{
PAGE 0 :
RAMM0 : origin = ...
RAML0L1 : origin = ...
PAGE <span class="hljs-number">1</span> :
RAMM1 : origin = ...
RAML2 : origin = ...
}
每一页内存都是完全独立的。在页面之间,您可以重复使用内存范围名称和内存地址。下面的例子是完全合法的,但是一个非常糟糕的主意...
/* DO NOT DO THIS!!! */
MEMORY
{
PAGE 0 :
MEM_RANGE : origin = 0x100, length = 0x100
PAGE 1 :
MEM_RANGE : origin = 0x100, length = 0x100
}
C28xx 器件是从 20 世纪 80 年代开始的一长串 C2xxx 器件的后裔。这些早期的设备具有用于代码和数据的单独内存总线。这些总线连接到物理上独立的内存块。因此,PAGE 0 上的特定地址可能具有与 PAGE 1 上的相同地址不同的内容。从理论上讲,在C28xx设备上可以实现存储器总线的这种相同的单独连接,尽管它是一种很少(接近从未使用过)的功能。如果有丝毫疑问,请查看特定于设备的文档。
尽管几乎所有 C28xx 设备的所有内存总线都连接到所有内存,但这种使用内存页的传统仍然存在。如果linker.cmd使用 PAGE 0 和 PAGE 1,则最好继续以这种方式使用它。请注意上一个示例所说明的陷阱。
语法提示
在任何地方都可以写MEMORY_RANGE_NAME也可以写MEMORY_RANGE_NAME PAGE 0。从表面上看,这是因为可以在多个页面上使用相同的内存范围名称。因为这是糟糕的编程实践,所以编写页码实际上是为了保持所有内容清晰且易于维护。如果将内存范围名称和不存在的页面组合在一起,链接器将告诉您。
使输出部分无效
如果你看到这样的东西...
.reset : > RESET, PAGE = 0, TYPE = DSECT /* not used */
语法使此输出部分成为虚拟部分。虚拟部分不占用内存中的空间,并且不存在于输出文件中。最终效果是,所有名为 .reset 的输入部分都被静默地丢弃了。在这种特定情况下,这没关系。但并非所有情况都行。假设另一节中的代码调用 .reset 中的函数,或者另一节使用 .reset 中的数据。链接器以静默方式吞噬那些对 .reset 的引用。您可能更喜欢诊断。TYPE = DSECT
有关 DSECT 和其他类似特殊部分的详细信息,请参阅文章链接器特殊部分类型。
请参阅 ROM 代码或数据
此代码...
FPUmathTables : > FPUTABLES, PAGE = 0, TYPE = NOLOAD
是指向系统中已存在的部分。该部分通常以ROM或闪存形式提供。该语法赋予特殊的 noload 属性。noload 部分确实会占用内存中的空间,但它不存在于输出文件中。在实践中,noload部分内部没有对外部任何内容的引用。但是 noload 部分以外的其他部分可以引用 noload 部分内的代码和数据。在这种特定情况下,必须在ROM中提供一些浮点单位表,其他代码使用这些表。TYPE = NOLOAD
有关NOLOAD和其他类似特殊部分的更多信息,请参阅文章链接器特殊部分类型。
在一个地址加载,从另一个地址运行
请考虑以下示例:
.TI.ramfuncs : LOAD = FLASHD,
RUN = RAML0,
LOAD_START(_RamfuncsLoadStart),
LOAD_END(_RamfuncsLoadEnd),
RUN_START(_RamfuncsRunStart)
这将创建一个名为 的输出部分。它由所有输入部分组成,也命名为 。它有两种不同的分配。它被分配给FLASHD用于加载,RAML0用于运行。此输出部分放置在输出文件中,以便当加载程序时(可能通过将其编程到闪存中来实现),它位于FLASHD内存范围内。在系统执行期间的某个时候,在 中出现任何内容之前。使用TI.ramfuncs,应用程序将其从FLASHD复制到RAML0。请注意,此复制不会自动完成。此处未讨论的显式步骤必须在应用程序代码中执行。调用 中函数的任何其他部分。TI.ramfuncs 就像 .TI.ramfuncs已经在RAML0中。LOAD_START等运算符建立用于实现复制的符号。符号_RamfuncsLoadStart的值是起始加载地址。同样,_RamfuncsLoadEnd具有结束加载地址,_RamfuncsRunStart具有起始运行地址。
从库中分配单个输入节
请考虑以下示例:
IQmathTables3 : > IQTABLES3
{
IQmath.lib<IQNasinTable.obj> (IQmathTablesRam)
}
这形成了一个名为 IQmathTables3 的输出部分。它包含一个名为 IQmathTablesRam 的输入部分。该输入部分来自对象文件IQNasinTable.obj,它是IQmath.lib库的成员。此输出部分分配给 IQTABLES3 内存范围。
变体可用于分配库中的所有部分。
sinetext : > DDR2
{
--library=Sinewave_lib.lib(.text)
}
这将形成一个名为正弦文本的输出部分。它包含链接器从库Sinewave_lib.lib中使用的文件中的所有 .text 输入部分。它被分配给 DDR2 内存范围。请注意,链接器不会从 Sinewave_lib.lib 中引入所有文件。它只引入满足其他对象模块的开放引用所需的文件。这些其他模块的示例包括主应用程序代码中的文件,以及已包含的其他库中的对象文件。语法告诉链接器此文件不在当前目录中,并在库搜索路径中收集的目录中查找它。在前面的示例中不需要语法,因为尖括号的使用与 具有相同的效果。--library=
--library=
<>
--library=
将库中的输入部分分配到不同的加载和运行地址
此示例将前两个示例的技术组合在一起。
ipcConst
{
driverlib.lib<ipc.obj>(.const)
} LOAD = FLASH5, RUN = RAMLS0,
LOAD_START(constLoadStart),
LOAD_SIZE(constLoadSize),
RUN_START(constRunStart), ALIGN(8)
这将形成一个名为 ipcConst 的输出部分。它包含一个名为 .const 的输入部分。该输入部分来自对象文件 ipc.obj,它是库 driverlib.lib 的成员。它有两种不同的分配。它被分配给 FLASH5 用于加载,RAMLS0 用于运行。此输出部分放置在输出文件中,以便当加载程序时(可能通过将其编程到闪存中来实现),它位于FLASH5内存范围内。在系统执行期间的某个时候,在使用 ipcConst 中的任何内容之前,应用程序会将其从 FLASH5 复制到 RAMLS0。请注意,此复制不会自动完成。此处未讨论的显式步骤必须在应用程序代码中执行。引用 ipcConst 中数据的任何其他部分都表现得好像 ipcConst 始终位于 RAMLS0 中。LOAD_START、LOAD_SIZE等运算符建立用于实现复制的符号。有关这些运算符的详细信息,请在 CPU 系列的汇编语言工具手册中搜索标题为“地址和维度运算符”的子章节。ALIGN(8) 表示运行地址与 8 的倍数对齐。有关详细信息,请在同一汇编语言工具手册中搜索标题为“指定加载地址和运行地址”的子章节。
将输出部分组合在一起
假设您需要一些输出部分按顺序彼此相邻。你可能会写...
/* This does NOT work */
output_section_1 > RAM
output_section_2 > RAM
output_section_3 > RAM
链接器将所有这些输出部分放在 RAM 内存范围内。但它可以按任何顺序放置它们,但其他输出部分可以介于它们之间。使用 GROUP 指令按特定顺序将输出部分一起分配。这是一个实际的例子...
GROUP : > CTOMRAM
{
PUTBUFFER
PUTWRITEIDX
GETREADIDX
}
输出部分是PUTBUFFER,PUTWRITEIDX和GETREADIDX。它们以完全相同的顺序作为一个组分配给存储器范围CTOMRAM。请注意,各个输出部分没有任何内存分配规范。
这些输出部分名称全部采用大写字母。linker.cmd中的不成文约定是,只有像CTOMRAM这样的内存范围名称才用全大写字母书写。但是,你需要为违反该公约的行为做好准备。
内存属性
内存指令可能包含如下行...
MEMORY
{
...
FLASH1 (RX) : origin = 0x00204000, length = 0x1C000
FLASH2 (RX) : origin = 0x00260000, length = 0x1FFD0
CSM_RSVD_Z2 : origin = 0x0027FFD0, length = 0x000C
CSM_ECSL_Z2 : origin = 0x0027FFDC, length = 0x0024
C0 (RWX) : origin = 0x20000000, length = 0x2000
...
}
请注意两行和另一行上的。该语法指定这些内存范围的属性。每个字母表示一个内存属性属性。(RX)
(RWX)
- R:可以读取
- W:可以写
- X:可以包含可执行代码
- I:可以初始化
默认情况下,内存范围具有所有四个属性。
这些属性的记录目的是在 SECTIONS 指令中支持按内存属性进行节分配。这里没有这方面的例子,因为 TI 提供的linker.cmd都没有使用此功能。在这种情况下,此语法仅用作记录该内存范围内通常包含哪些类型的部分的方法。