目录
- 一、程序的存储与运行
- 1、存储
- 2、加载、运行
- 二、sct 分散加载文件
- 1、简介
- 2、文件格式
- 2.1 加载域
- 2.2 执行域
- 2.3 输入节区描述
- 3、配置 sct 文件
一、程序的存储与运行
1、存储
程序编译后,应用程序中所有具有同一性质的数据(包括代码)被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状态,这些域的意义如下:
Code
:即代码域,它指的是编译器生成的机器指令,这些内容被存储到 ROM 区。RO-data
:Read Only data
,即只读数据域,它指程序中用到的只读数据,这些数据被存储在 ROM 区,因而程序不能修改其内容。- 例如 C 语言中 const 关键字定义的变量就是典型的 RO-data。
RW-data
:Read Write data,即可读写数据域,它指初始化为"非0值"的可读写数据,程序刚运行时,这些数据具有非0的初始值,且运行的时候它们会常驻在RAM区,因而应用程序可以修改其内容。- 例如 C 语言中使用定义的全局变量,且定义时赋予"非 0 值"给该变量进行初始化。
ZI-data
:Zero Initialie data
,即 0 初始化数据,它指初始化为"0 值"的可读写数据域,它与RW-data
的区别是程序刚运行时这些数据初始值全都为 0,而后续运行过程与RW-data
的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改其内容。- 例如 C 语言中使用定义的全局变量,且定义时赋予"0 值"给该变量进行初始化
- 若定义该变量时没有赋予初始值,编译器会把它当
ZI-data
来对待,初始化为 0;
ZI-data
的栈空间(Stack
)及堆空间(Heap
):- 在 C 语言中,函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。
- 使用 malloc 动态分配的变量属于堆空间。
- 在程序中的栈空间和堆空间都是属于
ZI-data
区域的,这些空间都会被初始值化为 0 值。编译器给出的ZI-data
占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用 malloc 动态申请堆空间,编译器会优化,不把堆空间计算在内)。
详细内容可以参考如下文章:
STM32 map 文件浅析 、单片机内存区域划分
总结如下:
程序组件 | 所属类别 |
---|---|
机器代码指令 | Code |
常量 | RO-data |
初值非0的全局变量 | RW-data |
初值为0的全局变量 | ZI-data |
局部变量 | ZI-data 栈空间 |
使用malloc动态分配的空间 | ZI-data 堆空间 |
2、加载、运行
RW-data
和 ZI-data
它们仅仅是初始值不一样而已,为什么编译器非要把它们区分开?原因如下:
应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如 STM32 的内部 FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的,见下图。
程序在存储状态时,RO section
及 RW Section
都被保存在 ROM 区。当程序开始运行时,内核直接从 ROM 中读取代码,并且在执行主体代码前,会先执行一段加载代码,它把 RW Section
数据从 ROM 复制到 RAM,并且在 RAM 加入 ZI Section
,ZI Section
的数据都被初始化为 0。加载完后 RAM 区准备完毕,正式开始执行主体程序。
编译生成的 RW-data
的数据属于图中的 RW Section
,ZI-data
的数据属于图中的 ZI Section
。是否需要掉电保存,这就是把 RW-data
与 ZI-data
区别开来的原因:
- 因为在 RAM 创建数据的时候,默认值为 0,
- 但如果有的数据要求初值非 0,那就需要使用 ROM 记录该初始值,运行时再复制到 RAM。
STM32 的 RO 区域不需要加载到 SRAM,内核直接从 FLASH 读取指令运行。计算机系统的应用程序运行过程很类似,不过计算机系统的程序在存储状态时位于硬盘,执行的时候甚至会把上述的 RO 区域(代码、只读数据)加载到内存,加快运行速度,还有虚拟内存管理单元(MMU)辅助加载数据,使得可以运行比物理内存还大的应用程序。而 STM32 没有 MMU,所以无法支持 Linux 系统。
当程序存储到 STM32 芯片的内部 FLASH 时(即 ROM 区),它占用的空间是 Code
、RO-data
及 RW-data
的总和,所以如果这些内容比STM32 芯片的 FLASH 空间大,程序就无法被正常保存了。当程序在执行的时候,需要占用内部 SRAM 空间(即 RAM 区),占用的空间包括RW-data
和 ZI-data
。应用程序在各个状态时各区域的组成见下表。
程序状态与区域 | 组成 |
---|---|
程序执行时的只读区域(RO) | Code + RO data |
程序执行时的可读写区域(RW) | RW data + ZI data |
程序存储时占用的ROM区 | Code + RO data + RW data |
而这些区域的起始地址和大小,以及各个函数变量应该放在哪个存储器区域中就是由本文要讲的 sct 文件定义的。
二、sct 分散加载文件
1、简介
当工程按默认配置构建时,MDK 会根据我们选择的芯片型号,获知芯片的内部 FLASH 及内部 SRAM 存储器概况,生成一个以工程名命名的后缀为 *.sct
的分散加载文件(Linker Control File,scatter loading),链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。
- 可以设置源文件中定义的所有变量自动按地址分配到外部 SDRAM,这样就不需要再使用关键字
__attribute__
按具体地址来指定了; - 利用它还可以控制代码的加载区与执行区的位置,例如可以把程序代码存储到单位容量价格便宜的 NAND-FLASH 中,但在 NAND-FLASH 中的代码是不能像内部 FLASH 的代码那样直接提供给内核运行的,这时可通过修改分散加载文件,把代码加载区设定为 NAND-FLASH 的程序位置,而程序的执行区设定为 SDRAM 中的位置,这样链接器就会生成一个配套的分散加载代码,该代码会把 NAND-FLASH 中的代码加载到 SDRAM 中,内核再从 SDRAM 中运行主体代码,大部分运行 Linux 系统的代码都是这样加载的。
2、文件格式
下面是一个由 MDK 默认生成的 sct 文件:
我使用的是 STM32F407,不同的芯片型号内存不一样
LR_IROM1 0x08000000 0x00080000 { ; load region size_region(加载域,基地址空间大小)
ER_IROM1 0x08000000 0x00040000 { ; load address = execution address(加载地址 = 执行地址)
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00020000 { ; RW data(可读写数据)
.ANY (+RW +ZI)
}
}
在默认的 sct 文件配置中仅分配了 Code
、RO-data
、RW-data
及 ZI-data
这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。
sct 文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号"{}“分隔开,最外层的是加载域,第二层”{}"内的是执行域,其整体结构见下图。
2.1 加载域
sct 文件的加载域格式如下:
加载域名 (基地址 | ("+"地址偏移)) [属性列表] [最大容量]
"{"
执行区域描述+
"}"
本例中为:
LR_IROM1 0x08000000 0x00080000 {
...
}
- 加载域名: 在 map 文件中的描述会使用名称
LR_IROM1
来标识空间。 - 基地址 + 地址偏移:基地址为 STM32 内部 FLASH 的基地址 0x08000000,地址偏移可选
- 属性列表: 属性列表说明了加载域的是否为绝对地址 N 字节对齐等属性。本例中没有描述加载域的属性。
- 最大容量: 最大容量说明了这个加载域可使用的最大空间,该配置也是可选的,如果加上这个配置后,当链接器发现工程要分配到该区域的空间比容量还大,它会在工程构建过程给出提示。STM32 内部 FLASH 的大小 0x00080000(512KB)
2.2 执行域
执行域名 (基地址 | "+"地址偏移) [属性列表] [最大容量 ]
"{"
输入节区描述
"}"
本例中为:
ER_IROM1 0x08000000 0x00040000 { ; load address = execution address(加载地址 = 执行地址)
...
}
RW_IRAM1 0x20000000 0x00020000 { ; RW data(可读写数据)
...
}
执行域的格式与加载域是类似的,区别只是输入节区的描述有所不同。
本例中包含了 ER_IROM1
及 RW_IRAM1
两个执行域,它们分别对应描述了 STM32 的内部 FLASH 及内部 SRAM 的基地址及空间大小。而它们内部的“输入节区描述”说明了哪些节区要存储到这些空间,链接器会根据它来处理编排这些节区。
2.3 输入节区描述
模块选择样式 “(“输入节区样式”,” “+“输入节区属性”)”
模块选择样式 “(“输入节区样式”,” “+“节区特性”)”
模块选择样式 “(“输入符号样式”,” “+“输入节区属性”)”
模块选择样式 “(“输入符号样式”,” “+“节区特性”)”
本例中为:
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
.ANY (+RW +ZI)
- 模块选择样式: 模块选择样式可用于选择 o 及 lib 目标文件作为输入节区,它可以直接使用目标文件名或“
*
”通配符,也可以使用“.ANY
”。- 使用语句“
.o
”可以选择所有 o 文件,使用“.lib
”可以选择所有 lib 文件,使用“*
”或“.ANY
”可以选择所有的 o 文件及 lib 文件。 - 其中“
.ANY
”选择语句的优先级是最低的,所有其它选择语句选择完剩下的数据才会被“.ANY
”语句选中。
- 使用语句“
- 输入节区样式: 通过输入节区样式可以选择要控制的节区。“
(RESET, +First)
” 语句的 RESET 就是输入节区样式,它选择 RESET 的节区,并使用后面介绍的节区特性控制字“+First
”表示它要存储到本区域的第一个地址。 - “
(InRoot$$Sections)
” 是一个链接器支持的特殊选择符号,它可以选择所有标准库里要求存储到 root 区域的节区。 - 输入符号样式: 可以选择要控制的符号,符号样式需要使用“
:gdef:
”来修饰。例如可以使用“*(:gdef:Value_Test)
”来控制选择符号“Value_Test
”。 - 输入节区属性: 通过在模块选择样式后面加入输入节区属性,可以选择样式中不同的内容,每个节区属性描述符前要写一个“
+
”号,使用空格或“,
”号分隔开,可以使用的节区属性描述符见下表。
节区属性描述符 | 说明 |
---|---|
RO-CODE、CODE | 只读代码段 |
RO-DATA、CONST | 只读数据段 |
RO及TEXT | 包括 RO-CODE 和 RO-DATA |
RW-DATA | 可读写数据段 |
RW-CODE | 可读写代码段 |
RW、DATA | 包括 RW-DATA 和 RW-CODE |
ZI及BSS | 初始化为 0 的可读写数据段 |
XO | 只可执行的区域 |
ENTRY | 节区的入口点 |
例如,示例文件中使用"
.ANY(+RO)
“选择剩余所有节区 RO 属性的内容都分配到执行域 ER_IROM1 中,使用”.ANY(+RW +ZI)
"选择剩余所有节区 RW 及 ZI 属性的内容都分配到执行域 RW_IRAM1中。
- 节区特性:节区特性可以使用"
+FIRST
"或"+LAST
"选项配置它要存储到的位置,FIRST
存储到区域的头部,LAST
存储到尾部。通常重要的节区会放在头部,而 CheckSum(校验和)之类的数据会放在尾部。- 例如示例文件中使用"
(RESET,+First)
"选择了 RESET 节区,并要求把它放置到本区域第一个位置,而 RESET 是工程启动代码中定义的向量表,该向量表中定义的堆栈顶和复位向量指针必须要存储在内部 FLASH 的前两个地址,这样 STM32 才能正常启动(详见 STM32 芯片启动过程),所以必须使用 FIRST 控制它们存储到首地址。
- 例如示例文件中使用"
总的来说,我们的 sct 示例文件配置如下:
- 程序的加载域为内部 FLASH 的 0x08000000,最大空间为 0x00100000;程
- 序的执行基地址与加载基地址相同,其中 RESET 节区定义的向量表要存储在内部 FLASH 的首地址,且所有 o 文件及 lib 文件的 RO 属性内容都存储在内部 FLASH 中;
- 程序执行时 RW 及 ZI 区域都存储在以 0x20000000 为基地址,大小为 0x00020000 的空间。
链接器根据 sct 文件链接,链接后各个节区、符号的具体地址信息可以在 map 文件中查看。
3、配置 sct 文件
通过 Use Memory Layout from Target Dialog
选项可以选择是使用 MDK 生成还是使用用户自定义的 sct 文件。
取消选择后,即可自己设置 sct 文件,点击下面的 Edit 即可编辑 sct 文件:
可以看到,其地址、大小和如下设置是对应的:
现在尝试分配一个变量到 RAM 中:
uint32_t gTest __attribute__((section(".my_data")));
int main(void)
{
...
gTest = 10;
printf("Value gTest == %d is in: %p\r\n", gTest, &gTest);
...
}
sct 文件修改如下:
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00040000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
MY_DATA 0x20000000 0x00005000 {
.ANY(my_section)
}
RW_IRAM1 0x20010000 0x00010000 { ; RW data
.ANY (+RW +ZI)
}
}
最终打印出来的结果为:
map 文件: