参考链接

DARM-30 (iar.com)

嵌入式IDE(1):IAR中ICF链接文件详解和实例分析_icf文件-CSDN博客

概述

链接文件主要用于划分存储区域,不同芯片的存储配置不同,数据的放置需要根据芯片的存储来配置,标准段为

Section Kind Description Memory Location Example
.text ro code Holds the program code ROM void Function(void)
.rodata const Holds constant data ROM const uint32 variable = 0
.textrw inited Holds _ramfunc declared program code RAM _ramfunc void Function(void)
.textrw_int Holds initializers for the .textrw declared section ROM
.intvec ro code Holds the reset vector table and exception vectors ROM
.noinit uninit Holds _no_init static and global variables. RAM __no_init uint32 variable
.bss zero Holds zero-initilialized static and global variables RAM uint32 variable = 0
.data inited Holds static and global initialized variables RAM uint32 variable = 1
.data_init Holds initial values for data sections ROM
CSTACK uninit Holds the stack RAM auto variables and parameters
HEAP Holds the heap used for dynamically allocated data RAM standard libry function malloc

而芯片存储区域可能如下

存储分区

[!tip]

如果在代码中想要将某些变量或者某个函数放置到指定的位置,那么首先需要先确定变量类型,变量类型将决定这个变量可以放在哪些位置,比如普通的变量就要放在可读可写的位置(SRAM),而一些常量则要放在只读位置(.text)中,如果变量类型的操作权限和存储地址支持的操作权限一致,则可以放置,否则就放不了。

LD

LD 文件是 GNU 工具链(如 GCC)使用的链接器脚本,用于指定如何将程序的各个部分(如代码段、数据段等)放置到目标设备的内存中。LD 脚本的语法非常灵活,支持详细的内存区域分配、段定义以及符号管理。

基本结构

LD 链接脚本主要由以下几部分组成:

  1. 全局声明和变量:设置内存区域和段的分配。
  2. 段定义(SECTIONS):定义程序的各个段(如 .text, .data, .bss 等)在内存中的位置。
  3. 符号管理:可以设置一些符号(如 __start__end)的地址。

内存区域定义

使用 MEMORY 关键字来定义目标设备的内存区域。每个内存区域指定起始地址(ORIGIN)和长度(LENGTH)。

1
2
3
4
5
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M
RAM (rw) : ORIGIN = 0x20000000, LENGTH = 128K
}
  • FLASH 区域指定为可读 (r) 和可执行 (x)。
  • RAM 区域指定为可读 (r) 和可写 (w)。

段定义

SECTIONS 用来定义程序的各个段及其如何映射到内存中。每个段由其名称(如 .text.data 等)定义,并可以指定要放置的内存区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SECTIONS
{
.text :
{
*(.text) // 所有 .text 段的内容
} > FLASH // 放置到 FLASH 区域

.data :
{
*(.data) // 所有 .data 段的内容
} > RAM // 放置到 RAM 区域

.bss :
{
*(.bss) // 所有 .bss 段的内容
} > RAM // 放置到 RAM 区域

.stack :
{
*(.stack) // 堆栈区域
} > RAM // 放置到 RAM 区域
}
  • *() 用于包含所有具有相应段名的输入部分。例如,*(.text) 包含所有 .text 段的内容。
  • > MEMORY_REGION 用于将段放置到指定的内存区域。

特殊符号定义

可以在链接器脚本中定义特殊符号,来标记某些特定的位置(如段的开始和结束)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SECTIONS
{
.text :
{
__text_start = .; // 定义一个符号,表示 .text 段的开始
*(.text)
__text_end = .; // 定义一个符号,表示 .text 段的结束
} > FLASH

.data :
{
__data_start = .; // 定义一个符号,表示 .data 段的开始
*(.data)
__data_end = .; // 定义一个符号,表示 .data 段的结束
} > RAM
}
  • . 是当前地址指针,它会随着段内容的放置而移动。

栈和堆定义

通常会为栈和堆分配特殊的内存区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SECTIONS
{
.stack :
{
__stack_start = .;
*(.stack)
__stack_end = .;
} > RAM

.heap :
{
__heap_start = .;
*(.heap)
__heap_end = .;
} > RAM
}

输入文件与输出文件

可以指定输入的目标文件和输出文件的格式。

1
2
OUTPUT_FORMAT("elf32-littlearm")
INPUT("main.o", "startup.o")
  • OUTPUT_FORMAT 用于指定生成的输出文件格式。
  • INPUT 用于指定链接器输入的目标文件。

常用符号

LD 链接器脚本中常用的符号通常用于标记程序段的位置、内存地址或控制程序的执行。以下是常见的符号及其用途:

.

.表示当前的内存地址。在脚本中使用来指示段的起始或结束位置。

1
2
__text_start = .;  // 当前地址是 .text 段开始
__text_end = .; // 当前地址是 .text 段结束

__start___end_

标记段的起始和结束地址。

1
2
__start_text = .;  // .text 段开始
__end_text = .; // .text 段结束

_start

程序的入口地址。

1
_start = 0x08000000;  // 程序入口

__heap_start__heap_end

标记堆内存的起始和结束地址。

1
2
__heap_start = .;   // 堆开始
__heap_end = .; // 堆结束

__stack_start__stack_end

标记栈的起始和结束地址。

1
2
__stack_start = .;  // 栈开始
__stack_end = .; // 栈结束

ENTRY

指定程序的入口点。

1
ENTRY(_start)

_etext_edata_end

标记

1
.text
1
.data

和程序结束位置。

1
2
3
_etext = .;  // .text 段结束
_edata = .; // .data 段结束
_end = .; // 程序结束

_stack

指定栈的起始地址。

1
_stack = 0x20020000;  // 栈起始地址

_heap_size

设置堆的大小。

1
_heap_size = 0x1000;  // 堆大小为 4KB

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* 定义内存区域 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M
RAM (rw) : ORIGIN = 0x20000000, LENGTH = 128K
}

/* 定义各个段的放置 */
SECTIONS
{
/* 代码段 */
.text :
{
__text_start = .; /* .text 段开始位置 */
*(.text) /* 所有 .text 段内容 */
__text_end = .; /* .text 段结束位置 */
} > FLASH

/* 已初始化数据段 */
.data :
{
__data_start = .; /* .data 段开始位置 */
*(.data) /* 所有 .data 段内容 */
__data_end = .; /* .data 段结束位置 */
} > RAM

/* 未初始化数据段 */
.bss :
{
__bss_start = .; /* .bss 段开始位置 */
*(.bss) /* 所有 .bss 段内容 */
__bss_end = .; /* .bss 段结束位置 */
} > RAM

/* 堆栈段 */
.stack :
{
__stack_start = .;
*(.stack)
__stack_end = .;
} > RAM

/* 堆段 */
.heap :
{
__heap_start = .;
*(.heap)
__heap_end = .;
} > RAM
}

SCT

在Keil中,.sct 文件是一个链接控制文件,用于定义如何链接目标文件、库和其他资源,以及如何分配内存空间。它主要用于Keil MDK(Microcontroller Development Kit)中的ARM编译器(armcc)和链接器(armlink)。

基本结构

.sct 文件通常包含以下几个部分:

  1. 内存区域定义:定义程序使用的内存区域(例如 Flash、RAM)。
  2. 输入文件:列出输入文件(例如目标文件、库文件等)。
  3. 分配符号:定义全局变量或函数的符号分配。
  4. 段定义:定义如何将各个代码段(例如 .text.data)映射到内存区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Keil .sct 文件示例

// 内存区域定义
define memory
{
ROM (rx) : ORIGIN = 0x00000000, LENGTH = 128K
RAM (rw) : ORIGIN = 0x20000000, LENGTH = 20K
}

// 输入文件
entry Reset_Handler
include "startup.s"

select
{
// 确定哪些文件需要链接
input "main.o"
input "startup.o"
input "myLib.a"
}

// 定义段的分配
define symbol __StackTop = 0x20005000
define symbol __HeapLimit = 0x20006000

// 段映射到内存区域
place in ROM
{
sections .text, .rodata
}

place in RAM
{
sections .data, .bss
}

// 保留内存区域(例如栈和堆)
reserve RAM (0x20000000, 0x20005000)

常见命令为:

  • define memory:定义内存区域及其大小和属性。
  • place in:将段放置到指定的内存区域中。
  • input:指定要链接的输入文件(目标文件或库文件)。
  • define symbol:定义符号(如栈顶、堆限制等)。
  • reserve:保留特定的内存区域,用于栈、堆等。
  1. 内存区域定义

    1
    2
    3
    4
    5
    define memory
    {
    ROM (rx) : ORIGIN = 0x00000000, LENGTH = 128K
    RAM (rw) : ORIGIN = 0x20000000, LENGTH = 20K
    }

    这部分定义了两个内存区域:ROMRAMROM 区域具有读取和执行权限(rx),RAM 区域具有读取和写入权限(rw)。同时,ORIGIN 表示该区域的起始地址,LENGTH 表示该区域的大小。

  2. 输入文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    entry Reset_Handler
    include "startup.s"

    select
    {
    input "main.o"
    input "startup.o"
    input "myLib.a"
    }

    这部分定义了要链接的输入文件,entry Reset_Handler 表示程序的入口点,通常是复位处理程序。includeselect 用于引入文件或库。

  3. 符号定义

    1
    2
    define symbol __StackTop = 0x20005000
    define symbol __HeapLimit = 0x20006000

    这部分定义了栈顶(__StackTop)和堆的边界(__HeapLimit)符号,通常用来标记栈和堆在内存中的位置。

  4. 段映射到内存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    place in ROM
    {
    sections .text, .rodata
    }

    place in RAM
    {
    sections .data, .bss
    }

    这里,.text.rodata(只读数据段)被放置在 ROM 中,.data.bss(未初始化数据段)被放置在 RAM 中。

  5. 内存保留

    1
    reserve RAM (0x20000000, 0x20005000)

    这部分保留了 RAM 区域中的特定内存区域,通常用于栈和堆。

常用符号

在Keil的.sct链接控制文件中,使用了一些字母和符号来表示不同的内存区域、段、属性等。这些符号通常用于描述内存布局和链接器如何处理各个段。

内存区域和段的标识符

  • ROM:通常表示“只读存储器”(Read-Only Memory)。在嵌入式系统中,ROM区域通常存放程序代码和常量数据(如常量字符串、初始化数据等)。
    • ROM 区域的权限:通常是 rx(读取和执行权限),即代码被加载到ROM中后会被执行。
  • RAM:表示“随机访问存储器”(Random Access Memory)。在嵌入式系统中,RAM区域用于存储程序的运行时数据、栈、堆等。
    • RAM 区域的权限:通常是 rw(读取和写入权限),即数据可以被读取和修改。
  • ER_RO:指只读区域(Read-Only)。通常用于存放不需要修改的代码和数据(如程序代码、只读常量等)。在该区域中的数据是不可更改的。
  • ER_RW:指读写区域(Read/Write)。用于存放程序中需要修改的数据(如全局变量、静态变量等)。
  • ER_RW_1:可能是特定应用中的第二个读写区域,特别用于某些特殊用途的数据(如未初始化的全局变量)。
  • PATCH_TABLE:指补丁表区域,通常用于存放运行时需要的补丁或配置数据。
  • HOST_DATA:指主机数据区域,通常用于存放一些动态链接的库文件中的数据(如 .lib 文件)。

属性标识符

  • +FIRST:表示该文件或符号被放置在指定段的开头。这个标识符通常用于指定入口点(如 RESET 处理程序)所在的位置,确保它是该段的第一个元素。
  • +RO:表示只读数据。用于指定该段中的数据是只读的,例如代码段或常量数据段。
  • +ZI:表示“零初始化”数据(Zero Initialized)。这些变量在程序启动时会被初始化为零。它们通常存放在 .bss 段(未初始化数据段)。
  • +RW:表示“读写”数据。这些变量会在程序运行时被读写,通常存放在 .data 段中。
  • NOINIT:表示“不初始化”的数据,通常是未初始化的数据,如某些保留内存。它们不会被链接器初始化,因此在启动时会保持未定义的值。
  • UNINIT:表示未初始化数据的区域,这些数据不会在程序启动时进行初始化。通常与 NOINIT 类似,但也可能指定特定的内存区域用于存放这类数据。

其他关键字和函数

  • ImageLength:这是一个用于计算内存区域大小的宏或函数。它返回指定段的字节长度。比如,ImageLength(PATCH_TABLE) 会返回 PATCH_TABLE 区域的大小。
  • ScatterAssert:这是一个用于断言内存布局是否符合要求的命令。如果通过 ImageLength 计算得出的内存区域总大小超过了某个限制,ScatterAssert 会抛出一个错误。这有助于确保程序的内存使用不会超出目标设备的内存限制。

常见的符号解释

  • *:表示“匹配所有”。例如,*(+RO) 表示匹配所有只读数据,*(ram_code) 表示匹配所有名为 ram_code 的段或符号。
  • +:表示包含特定属性的数据或段。例如,+RW 表示数据段包含读写权限。

文件参数

在 Keil .sct 链接控制文件中,文件后面的括号部分(如 startup.o (RESET, +FIRST)input "main.o" (symbol))用于指定额外的属性、符号和链接器行为。这些括号内的内容通常有特定的含义,用来控制文件、段或符号在内存中的位置、优先级或其他特性。

符号标识符(Symbols)

这类参数指定特定的符号在文件中的处理方式。最常见的符号标识符是 RESET,它用于标记程序的复位入口点。

  • RESET:指定该文件包含复位处理程序,并将其作为程序启动时的入口点。这个符号通常指向复位处理代码的起始位置。

    1
    startup.o (RESET)

    这表示 startup.o 文件包含复位处理程序,并且该符号会作为程序的入口点。

段位置控制符(Position Control)

这些参数用于控制段在内存中的位置,影响文件的排列顺序,通常用于控制文件或段的顺序。

  • +FIRST:指示将该文件或符号放在当前段的最前面,通常用于复位程序或初始化代码。

    1
    startup.o (RESET, +FIRST)

    这表示将 startup.o 文件中的 RESET 符号放置在段的开头位置。

  • +LAST:指示将该文件或符号放在当前段的末尾位置。这个符号较少使用,但有时可以用来将特定文件或符号放置到段的末尾。

段类型属性(Section Attributes)

这些属性指定段的访问权限或其他属性。常见的段属性有:

  • +RO:表示该段是只读的,通常用于放置代码或只读数据(如 .text, .rodata)。

    1
    main.o (+RO)

    这表示将 main.o 中的段标记为只读,并放置在只读内存区域。

  • +RW:表示该段是可读写的,通常用于放置变量、堆栈等数据。

    1
    main.o (+RW)

    这表示将 main.o 中的段标记为读写数据,并放置在读写内存区域。

  • +ZI:表示零初始化数据,这些数据会在程序启动时被初始化为零,通常用于 .bss 段。

    1
    data.o (+ZI)

    这表示将 data.o 中的段标记为零初始化数据。

自定义符号与特性

  • NOINIT:指示该段的数据不会被初始化,通常用于 .bss 段之外的特殊区域。

    1
    data.o (NOINIT)

    这表示 data.o 中的数据不会进行初始化。

  • UNINIT:类似于 NOINIT,表示数据未初始化。

    1
    main.o (UNINIT)

    这表示 main.o 中的数据段未初始化。

内存区域标识符(Memory Region Identifiers)

这些参数用于将符号或段与特定的内存区域关联,例如 ROMRAM 等。

  • ROMRAM:指示文件或符号属于某个内存区域。通常在 .sct 文件的定义部分会定义这些内存区域。

    1
    main.o (ROM)

    这表示将 main.o 中的段放置在 ROM 区域。

示例

以以下段落为例:

1
2
3
4
5
ER_RO 0x10002000
{
startup_fr1010.o (RESET, +FIRST)
*(+RO)
}
  • ER_RO:表示一个只读区域(Read-Only Region),将数据放置在该区域。
  • 0x10002000:该区域的起始地址。
  • startup_fr1010.o (RESET, +FIRST):将 startup_fr1010.o 文件中的 RESET 启动代码放置在该段的开头,并指定它为程序的第一个代码段。
  • *(+RO):将所有的只读数据(如 .rodata 段)放置在这个段中。

[!important]

文件的排列顺序并不是简单地“文件顺序”,而是由 (sections)和 内存区域(memory regions)决定的。链接器会根据每个文件中的段类型来决定它们在内存中的布局。

  1. 文件内部的段排序:每个文件(如 main.ostartup.omyLib.a)都会包含若干段(如 .text.data.rodata.bss 等)。链接器会根据这些段的类型,将每个段放置在合适的内存区域(如 ROM、RAM 等)中。
  2. 文件的顺序:如果两个文件包含相同类型的段(如 .text.data),则它们在内存中的顺序==会遵循 .sct 文件中 inputselect 语句中文件的顺序==。这意味着,链接器会按照文件在 select 块中的顺序处理这些文件,但文件内部的段仍然会按照它们的类型和目标区域被放置。
  3. 静态库 myLib.a 的处理:静态库(如 myLib.a)包含了多个目标文件。链接器会按照静态库中的文件顺序,将其中的段与其他输入文件的段合并,通常按照静态库中的顺序依次处理。
  4. 段的合并:如果多个文件的相同类型段(如多个文件的 .text 段)被加载到同一个内存区域(如 ROM 区域),则这些段的内容会被合并在一起。在这种情况下,文件的顺序会影响段的最终顺序。

ICF

ICF(IAR Configurable File)是IAR专有的配置文件格式,主要用来描述内存布局、段的分配、堆栈的定义等内容。ICF文件通过对内存的精确控制来优化应用程序的内存使用。

基本结构

ICF文件通常由几个主要部分组成:

  1. 内存定义(Memory)
  2. 区域划分(Region)
  3. 数据分配(Placement)

内存定义

可以使用define memory声明芯片的物理内存范围。

1
define memory <名称> with size = <大小>;

示例:

1
define memory mem with size = 4G;  // 定义4GB地址空间(覆盖所有可能地址)

可以使用define block定义特殊内存块(如堆、栈)。

1
define block <名称> with alignment = <对齐>, size = <大小> { <段或符号> };

示例:

1
2
define block CSTACK with alignment = 8, size = 0x1000 { }; // 栈(8字节对齐,4KB)
define block HEAP with size = 0x800 { }; // 堆(2KB)

区域划分

可以使用define region定义一个具体的物理存储区域(如 FLASH、RAM)。

1
define region <名称> = mem:[from <起始地址> to <结束地址>] [attrs = <属性>];
  • 属性(attrs)
    • readonly:只读(代码、常量)
    • readwrite:可读写(变量)
    • execute:可执行(代码)
1
2
define region ROM_region = mem:[from 0x10000 to 0x3FFFF] attrs = readonly;
define region CUSTOM_region = mem:[from 0x2F000 to 0x2F100]; // 自定义区域

说明:

  • mem: 表示在 define memory 中定义的空间;
  • fromto 指明区域的起止地址;
  • 也可使用 sizedefine region RAM = mem:[from 0x20000000 size 0x4000];

在定义内存之前,可以使用define symbol定义一个符号常量(仅链接器使用)来使代码通俗易懂。

1
2
define symbol Start = 0x400;
define symbol Size = 0x200

使用 define exported symbol定义并导出符号(可在 C 中 extern 使用)。

1
2
define exported symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define exported symbol __ICFEDIT_region_RAM_end__ = 0x20003FFF;

导出的符号常用于:

  • 启动文件;
  • 堆栈初始化;
  • 应用与 bootloader 通信。

示例

1
define region FLASH = mem:[from Start size Size];

数据分配

使用place可以将代码/数据分配到指定区域。

1
2
place at address mem:<绝对地址> { <段或块> };  // 固定地址
place in <region名称> { <段或块> }; // 放入某区域
  • 标准段(section)(由编译器自动生成):

    段名 用途 典型内容
    .text 代码段(可执行指令) 函数代码、中断向量表
    .rodata 只读数据(常量) const 全局变量、字符串常量
    .data 已初始化的全局/静态变量(需从Flash加载到RAM) const 的初始化变量
    .bss 未初始化的全局/静态变量(RAM清零) static int x;(未显式初始化)
    .noinit 不初始化的变量(RAM保留原始值) __no_init 修饰的变量
    .stack 栈空间(由链接器分配) 局部变量、函数调用帧
    .heap 堆空间(动态内存) malloc() 分配的变量
  • 自定义段(如 .my_section

常见语法形式:

语法 说明
place in <region> 将段放入指定区域(位置由链接器决定)
place at start of <region> 放在区域起始处
place at end of <region> 放在区域末尾
place at address mem:<addr> 放在绝对地址
1
2
place in ROM_region { readonly };          // 所有只读数据放入ROM
place at address mem:0x2F000 { section .my_array }; // 固定地址

段类型与匹配方式为:

类型 说明
readonly 只读段(代码、常量)
readwrite 可读写段(全局变量)
section .name 指定具体段名
section .ANY (+RO, +RW, +ZI) 按属性匹配所有符合条件的段
UNINIT / noinit 未初始化的段(不会在启动时清零或复制)

示例:

1
2
readwrite section .bss, section .noinit;
readonly section .text, section .rodata;

有时候避免不了初始化控制:

1
2
initialize by copy { readwrite };  // 初始化变量(从Flash拷贝到RAM)
do not initialize { section .noinit }; // 不初始化(如.noinit段)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 将代码段放入 Flash */
"CODE": place at start of FLASH {
readonly section .intvec,
section .text,
section .rodata;
}

/* 将可写数据放入 RAM */
"DATA": place in RAM {
readwrite section .data,
section .bss,
section .my_ram_section;
}

/* 将特定段放到固定地址 */
".boot_info": place at address mem:0x20001000 {
section .boot_info;
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*###ICF### Section handled by ICF editor, don't touch! ****/
/*-Editor annotation file-*/
/* IcfEditorFile="$TOOLKIT_DIR$\config\ide\IcfEditor\cortex_v1_0.xml" */
/*-Specials-*/
define symbol __ICFEDIT_intvec_start__ = 0x10000;
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x10000;
define symbol __ICFEDIT_region_ROM_end__ = 0x0002EFFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x20007FFF;

/*-Sizes-*/
define symbol __ICFEDIT_size_cstack__ = 0x1000;
define symbol __ICFEDIT_size_heap__ = 0x0;
/**** End of ICF editor section. ###ICF###*/

define memory mem with size = 4G;
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];

define block CSTACK with alignment = 8, size = __ICFEDIT_size_cstack__ { };
define block HEAP with alignment = 8, size = __ICFEDIT_size_heap__ { };

initialize by copy { readwrite };
do not initialize { section .noinit };

place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };

place in ROM_region { readonly };
place in RAM_region { readwrite,
block CSTACK, block HEAP };

export symbol __ICFEDIT_region_RAM_start__;
export symbol __ICFEDIT_region_RAM_end__;

常见错误

错误信息 原因 解决办法
section placement failed 区域太小或重叠 调整 region 大小或顺序
undefined symbol 未定义导出符号 添加 define exported symbol
数据未初始化 段属性错误 检查是否应为 readwrite
拷贝错误 .data 的 load 区未定义 确认 Flash 与 RAM 之间的复制关系