CrossCore Embedded Studio 中修改 LDF 文件
引言
当遇到链接器错误(如本文档"常见错误"部分所述)时,解决方案通常需要掌握修改 LDF 文件来控制链接过程的技巧。虽然本文档内容并非详尽无遗,但将详细说明修改 LDF 文件的基本要素,以及如何使用项目选项和编译器编译指令辅助链接器。
Blackfin 与系统构建器
在 CrossCore Embedded Studio 中,Blackfin 和 SHARC 处理器都使用生成的 LDF 文件。通过新建项目向导或通过系统配置对话框(可通过在项目资源管理器窗口中双击 *.svc 文件访问)添加"启动代码/LDF"插件来添加 LDF。在尝试修改 LDF 时,需要注意该插件的某些特性。
首要考虑的是"启动代码/LDF"插件的 LDF 部分提供了多个选项,可用于控制系统构建器自动生成 LDF 的方式。此选项的优点是无需深入了解 LDF 命令,但明显缺点是相比手动修改 LDF,这些项目选项提供的灵活性较低。
修改 MEMORY{...} 映射
LDF 包含 MEMORY{...} 块,用于定义处理器上的各种内存范围,以便链接器知道如何组合可执行文件。CrossCore Embedded Studio 包含的默认 LDF 以注释形式列出了处理器的所有有效内存范围,然后在 MEMORY{...} 块中定义每个内存范围。例如:
LDF
MEMORY
{
MEM_L1_SCRATCH { START(0xFFB00000) END(0xFFB00FFF) TYPE(RAM) WIDTH(8) }
MEM_L1_CODE_CACHE { START(0xFFA10000) END(0xFFA13FFF) TYPE(RAM) WIDTH(8) }
MEM_L1_CODE { START(0xFFA00000) END(0xFFA0BFFF) TYPE(RAM) WIDTH(8) }
...
}
对于 Blackfin,内存映射较为简单,因为其宽度固定为 8。而 SHARC 处理器的内存段可以有多种宽度:8、16、32、48 或 64 位。在修改或定义新段时,需确保内存范围(START 到 END)对指定的 WIDTH 有效。还需注意,连接到外部内存接口的内存宽度必须与该内存或总线的物理宽度一致。例如,21369 的外部内存接口为 32 位,因此宽度必须设为 32。
对于系统构建器生成的 LDF,需注意其重新生成时会覆盖对内存映射的修改。可以通过在适当的 VDSG 部分插入新定义来添加新段。此外,对于 SDRAM 内存范围,可通过系统配置对话框中 LDF 部分的"启动代码/LDF"插件启用"自定义分区"选项,这将用VDSG部分插入新定义来添加新段。此外,对于SDRAM内存范围,可通过系统配置对话框中LDF部分的"启动代码/LDF"插件启用"自定义分区"选项,这将用VDSG 标签包裹 MEMORY{...} 块中的 SDRAM 定义,从而允许自定义内存范围。
宏
默认 LDF 使用宏来方便地引用链接中的对象组,并作为整体控制其放置,而无需单独引用每个对象。最基本的宏(根据目标不同可能有变体)是 OBJECTS 和OBJECTS和LIBRARIES。
LIBRARIES 宏提供对链接到项目中的 .dlb 库文件内所有对象的集体引用,而LIBRARIES宏提供对链接到项目中的.dlb库文件内所有对象的集体引用,而OBJECTS 宏提供对项目中源文件经编译/汇编处理生成的 .doj 目标文件的集体引用。
大多数情况下,这两个宏已足够。但有时可能需要将某些对象/库单独列为更高优先级,或与默认的 OBJECTS 和OBJECTS和LIBRARIES 集合区分开。此时可自定义宏来包含所需对象/库。例如:
LDF
$MY_OBJS_AND_LIBS = main.doj, afunction.doj, mylibrary.dlb;
随后可在 SECTIONS{...} 块中使用 INPUT_SECTIONS 命令引用该宏,将其放置在特定内存段:
LDF
aSection
{
INPUT_SECTIONS($MY_OBJS_AND_LIBS(program))
} > MEM_L1_CODE
修改 SECTIONS{...} 块
控制对象的最终放置位置取决于 LDF 配置。LDF 中包含输出段命令,用于指示链接器将哪些对象(及其部分)组合后放置在内存段中。
需牢记 LDF 中的所有命令按出现顺序处理。输出段命令基本形式如下:
LDF
output_section_name
{
section_commands
// 例如:
INPUT_SECTIONS($OBJECTS(input_section_name))
} > memory_segment
还可使用其他选项控制内存类型和初始化(零初始化、非初始化等),但本文档不涉及这些内容。输出段命令的详细信息请参考《链接器和实用程序手册》。
output_section_name 是帮助链接器根据后续的 section_commands 创建不同对象组的名称,以便为其分配空间并放置到 MEMORY{...} 块定义的内存段中。该名称可任意指定,但在其所属的 PROCESSOR{...} 块中必须唯一。
完整输出段命令的基本示例如下:
LDF
an_output_section
{
INPUT_SECTION_ALIGN(4)
INPUT_SECTIONS($OBJECTS(program) $LIBRARIES(program))
} > MEM_L1_CODE
在此示例中,输出段名称 "an_output_section" 映射到 MEMORY{...} 映射中定义的 "MEM_L1_CODE"。段命令包括:
- INPUT_SECTION_ALIGN(4):指示链接器将所有对象按 4 字边界对齐。例如,若对象为 3 字,则跳过下一字,下一对象从其后开始,确保所有内容占用 4 字节的倍数。
- INPUT_SECTIONS:从 OBJECTS 和OBJECTS和LIBRARIES 宏包含的对象中提取 'program' 段,并将其链接到指定的 MEM_L1_CODE 内存段。
INPUT_SECTIONS 命令与 section/default_section 编译指令
所有目标文件 (.doj) 都包含"段"。不同数据类型有默认段名,例如数据放在 'data1',代码放在 'program',零初始化数据放在 'bsz' 等。也可自定义段名,这对控制对象位置非常有用。
推荐使用 C/C++ 源文件中的 "section" 和 "default_section" 编译指令,以及汇编文件中的 ".section" 关键字来实现。例如,若源文件包含多个函数,并希望将某一函数与默认 "program" 段中的其他函数分开以便更精细地控制其位置:
C
#pragma section("MyData")
int myFirstLargeArray[10000];
int mySecondLargeArray[10000];
#pragma section("MyCode")
void myFunction
{
...
}
在此示例中,"myFirstLargeArray" 将被放置在 "MyData" 段,而 "mySecondLargeArray" 因编译指令作用域结束,将被放置在默认段(如 "data1")。函数 myFunction 由下一个 section 编译指令放置在 "MyCode" 段。
'default_section' 编译指令可用于在更大范围内控制段名,其最大作用域到当前源文件结束,之后编译器将恢复默认段。例如:
C
#pragma default_section(DATA, "MyData")
int myFirstLargeArray[10000];
int mySecondLargeArray[10000];
#pragma default_section(DATA, "data1")
int myThirdLargeArray[10000];
在此示例中,将所有 "DATA" 的段名改为 "MyData",因此前两个数组使用该段名。将 DATA 的默认段重置为 "data1" 后,第三个数组将使用该段名,直至再次更改或文件结束。
了解编译器/汇编器如何为对象分配段,以及链接器如何处理输出段命令后,下一步是将这些知识结合以控制对象的放置。
最基本的方法是通过 section 编译指令/关键字,利用 LDF 中的现有命令放置对象。例如,若要在 BF537 目标的 SDRAM 中放置数组,可查看 BF537 LDF 中的如下命令:
LDF
sdram0_bank1
{
INPUT_SECTION_ALIGN(4)
INPUT_SECTIONS( $OBJECTS(sdram0_data) $LIBRARIES(sdram0_data))
...
} > MEM_SDRAM0_BANK1
将数组放置在此段的方法:
C
#pragma section("sdram0_data")
int myLargeArrayInSDRAM[10000];
也可通过 section 编译指令定义自定义段名,然后在 LDF 中添加 INPUT_SECTIONS 命令:
C
#pragma section("myCustomData")
int myLargeArrayInSDRAM[10000];
在 LDF 中修改:
LDF
sdram0_bank1
{
INPUT_SECTION_ALIGN(4)
INPUT_SECTIONS( $OBJECTS(sdram0_data) $LIBRARIES(sdram0_data))
INPUT_SECTIONS( $OBJECTS(myCustomData))
...
} > MEM_SDRAM0_BANK1
在更复杂的场景中,若需将对象与其他对象分开映射以控制其优先级,可使用自定义段名并添加输出段命令:
LDF
my_sdram_data
{
INPUT_SECTION_ALIGN(4)
INPUT_SECTIONS( $OBJECTS(myCustomData))
...
} > MEM_SDRAM0_BANK1
INPUT_SECTIONS 命令的直接引用
INPUT_SECTIONS 命令可直接引用链接中的对象以控制其段放置。例如,假设 "main.c" 文件包含多个 "program" 和 "data1" 段,生成 main.doj 对象,可通过以下命令放置其段:
LDF
L1_code
{
INPUT_SECTIONS(main.doj(program))
} >MEM_L1_CODE
L1_data_a
{
INPUT_SECTIONS(main.doj(data1))
}
类似地,可通过仅引用库来放置库中所有对象的 "program" 段:
LDF
INPUT_SECTIONS(libc532y.dlb(program))
更高级的用法可定位库中的特定对象:
LDF
INPUT_SECTIONS( libc532y.dlb [ sprintf32.doj (program) ] )
注意:括号的间距非常重要,错误使用会导致链接器错误。
输出段命令的优先级
链接器按 LDF 中的命令顺序处理。若 INPUT_SECTIONS 命令同时引用 OBJECTS 和OBJECTS和LIBRARIES 宏,则 $OBJECTS 优先:
LDF
INPUT_SECTIONS($OBJECTS(program) $LIBRARIES(program))
修改默认 LDF 时,需注意可能将对象映射到不希望的位置,并确保自定义命令位于默认命令之前。默认流程是 LDF 先填充 L1 内存,再 L2,最后 SDRAM。所有对象都包含在 $OBJECTS 宏中,因此需注意默认命令可能将对象放置在 L1 内存中,而您可能希望其位于 SDRAM。
为避免此问题,可在 LDF 的 SECTIONS{...} 块开头使用自定义输出段命令,确保其优先处理。对于生成的 LDF,使用 SECTIONS{...} 块中的第一组 $VDSG 标签。
RESOLVE 命令
LDF 提供 RESOLVE() 命令用于控制符号位置。例如,默认 Blackfin LDF 使用以下命令将 'start' 符号放置在 L1 代码内存的首地址:
LDF
RESOLVE(start, 0xFFA00000)
此原则可用于将特定函数等符号固定在指定地址。需注意对齐要求,避免因未对齐导致异常。
多核目标的多个 DXE/输出文件
生成多个 DXE 文件本质上是修改 LDF 以控制每个 DXE 链接的对象。需为每个 DXE 定义 PROCESSOR{...} 块,并在其中使用 OUTPUT(filename.dxe) 指定输出文件名。最后,在 SECTIONS{...} 块中使用自定义宏和 INPUT_SECTIONS 命令控制每个 DXE 链接的对象。BF561 和 BF609 EZ-KIT Lite 项目是很好的参考示例,它们是 CrossCore Embedded Studio 中目前仅有的多处理器目标。