LV16-06-MDK-02-工程文件类型

本文主要是STM32开发——MDK工程文件类型相关的一些相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

本篇笔记主要是参考的野火的文档。使用的工程呢可以去这里下载:ebf_stm32f103_badao_std_code: 野火STM32F103 霸道开发板 标准库教程配套代码 (gitee.com)

一、MDK工程文件类型

1. 文件汇总

1.1 Project目录下的工程文件

后缀说明
*.uvguixMDK5工程的窗口布局文件,在MDK4中*.UVGUI后缀的文件功能相同
*.uvprojxMDK5的工程文件,它使用了XML格式记录了工程结构,双击它可以打开整个工程,在MDK4中*.UVPROJ后缀的文件功能相同
*.uvoptxMDK5的工程配置选项,包含debugger、trace configuration、breakpooints以及当前打开的文件,在MDK4中*.UVOPT后缀的文件功能相同
*.ini某些下载器的配置记录文件

1.2 源文件

后缀说明
*.cC语言源文件
*.cppC++语言源文件
*.hC/C++的头文件
*.s汇编语言的源文件
*.inc汇编语言的头文件(使用“$include”来包含)

1.3 Output目录

后缀说明
*.dep整个工程的依赖文件
*.d描述了对应.o的依赖的文件
*.crf交叉引用文件,包含了浏览信息(定义、引用及标识符)
*.o可重定位的对象文件(目标文件)
*.bin二进制格式的映像文件,是纯粹的FLASH映像,不含任何额外信息
*.hexIntel Hex格式的映像文件,可理解为带存储地址描述格式的bin文件
*.elf由GCC编译生成的文件,功能跟axf文件一样,该文件不可重定位
*.axf由ARMCC编译生成的可执行对象文件,可用于调试,该文件不可重定位
*.sct链接器控制文件(分散加载)
*.scr链接器产生的分散加载文件
*.lnpMDK生成的链接输入文件,用于调用链接器时的命令输入
*.htm链接器生成的静态调用图文件
*.build_log.htm构建工程的日志记录文件

1.4 Listing目录

后缀说明
*.lstC及汇编编译器产生的列表文件
*.map链接器生成的列表文件,包含存储器映像分布
*.ini仿真、下载器的脚本文件
其它

上边就是一些相关文件的后缀名和大概的说明,这里就一一说明了,挑几个可能会用到的说明一下。

2. Project工程文件

uvprojx、 uvoptx 及 uvguix 都是使用 XML 格式记录的文件,若使用记事本打开可以看到 XML 代码。而当使用 MDK 软件打开时,它根据这些文件的 XML 记录加载工程的各种参数,使得我们每次重新打开工程时,都能恢复上一次的工作环境。

这些工程参数都是当 MDK 正常退出时才会被写入保存,所以若 MDK 错误退出时 (如使用 Windows 的任务管理器强制关闭),工程配置参数的最新更改是不会被记录的,重新打开工程时要再次配置。其中 uvprojx 文件是最重要的,删掉它我们就无法再正常打开工程了,而 uvoptx 及 uvguix 文件并不是必须的,可以删除,重新使用 MDK 打开 uvprojx工程文件后,会以默认参数重新创建 uvoptx 及 uvguix 文件。 (所以当使用 Git/SVN 等代码管理的时候,往往只保留 uvprojx 文件)

2.1 uvprojx 文件

uvprojx 文件就是我们平时双击打开的工程文件,它记录了整个工程的结构,如芯片类型、工程包含了哪些源文件等内容 。

image-20230529195524330

2.2 uvoptx 文件

uvoptx 文件记录了工程的配置选项,如下载器的类型、变量跟踪配置、断点位置以及当前已打开的文件等等 。

image-20230529195633353

2.3 uvguix 文件

uvguix 文件记录了 MDK 软件的 GUI 布局,如代码编辑区窗口的大小、编译输出提示窗口的位置等等。

image-20230529195721667

3. Output目录下文件

3.1 Output目录位置选择

Output目录是存放输出文件以及一些中间文件的目录,这个目录是可以配置的:

image-20230529200416215

点击 MDK 中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件,在工程的“【Options for Targe】→【Output】→【Select Folder for Objects】”中配置输出目录的位置。

3.2 lib库文件

上边好像没有列出,但是可能会用到。在某些情况,我们希望提供给第三方一个可用的代码库,但不希望对方看到源码,这个时候我们就可以把工程生成lib文件(Library file)提供给对方,在MDK中可配置【Options for Target】→【Create Library】选项把工程编译成库文件,

image-20230414222218504

工程中生成可执行文件或库文件只能二选一,默认编译是生成可执行文件的,可执行文件即我们下载到芯片上直接运行的机器码。得到生成的*.lib文件后,可把它像C文件一样添加到其它工程中,并在该工程调用lib提供的函数接口,除了不能看到*.lib文件的源码,在应用方面它跟C源文件没有区别。

3.2 dep、d 依赖文件

*.dep 和 *.d 文件 (Dependency file) 记录的是工程或其它文件的依赖,主要记录了引用的头文件路径,其中 *.dep 是整个工程的依赖,它以工程名命名,而*.d 是单个源文件的依赖,它们以对应的源文件名命名。这些记录使用文本格式存储,我们可直接使用记事本打开,见图工程的 dep 文件内容:

image-20230529200949898 image-20230529201003158

3.3 crf 交叉引用文件

*.crf是交叉引用文件(Cross-Reference file),它主要包含了浏览信息(browse information),即源代码中的宏定义、变量及函数的定义和声明的位置。我们在代码编辑器中点击【Go To Definition Of ‘xxxx’】可实现浏览跳转,跳转的时候,MDK就是通过 *.crf 文件查找出跳转位置的。

通过配置MDK中的【Option for Target】→【Output】→【Browse Information】选项可以设置编译时是否生成浏览信息。注意只有勾选该选项并编译后,才能实现函数定义,宏定义这些浏览跳转功能。

image-20230414222447760

跳转操作如下图:

image-20230529201102127

*.crf 文件使用了特定的格式表示,直接用文本编辑器打开会看到大部分乱码 ,文件内容的话,这里就不再深究了。

3.4 o、axf文件

*.o、*.elf、*.axf、*.bin及*.hex文件都存储了编译器根据源代码生成的机器码,根据应用场合的不同,它们又有所区别。但是我看到MDK的输出目录中其实并没有*.elf文件,这里也放在一起学习一下吧。

3.4.1 目标文件说明

*.o、*.elf、*.axf以及前面提到的lib文件都是属于目标文件,它们都是使用ELF格式来存储的,关于ELF格式的详细内容,我还没有深究,后边需要学习的话再补充吧,这里简单说明一下,详细的可以看这里(本地文档:ELF文件格式.pdf)。

ELF是Executable and Linking Format的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。在Linux及Windows系统下都有使用该格式的文件(或类似格式)用于记录应用程序的内容,告诉操作系统如何链接、加载及执行该应用程序。

目标文件主要有如下三种类型:

(1)可重定位的文件(Relocatable File),包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。例如MDK的armcc和armasm生成的*.o文件就是这一类,另外还有Linux的*.o 文件,Windows的 *.obj文件。

(2)可执行文件(Executable File) ,它包含适合于执行的程序,它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。

例如MDK的armlink生成的*.elf及*.axf文件,(使用gcc编译工具可生成*.elf文件,用armlink生成的是*.axf文件,*.axf文件在*.elf之外,增加了调试使用的信息,其余区别不大,后面我们仅讲解*.axf文件),另外还有Linux的/bin/bash文件,Windows的*.exe文件。

(3)共享目标文件(Shared Object File), 它的定义比较难理解,我们直接举例,MDK生成的*.lib文件就属于共享目标文件,它可以继续参与链接,加入到可执行文件之中。另外,Linux的 .so ,如/lib/ glibc-2.5.so,Windows的DLL都属于这一类。

3.4.2 o文件与axf文件的关系

*.axf文件是由多个*.o文件链接而成的,而*.o文件由相应的源文件编译而成,一个源文件对应一个*.o文件。

image311.jpeg

图中的中间代表的是 armlink 链接器,在它的右侧是输入链接器的*.o文件,左侧是它输出的*.axf文件。可以看到,由于都使用ELF文件格式,*.o与*.axf文件的结构是类似的,它们包含ELF文件头、程序头、节区(section)以及节区头部表。各个部分的功能说明如下:

ELF文件头用来描述整个文件的组织,例如数据的大小端格式,程序头、节区头在文件中的位置等。
程序头 告诉系统如何加载程序,例如程序主体存储在本文件的哪个位置,程序的大小,程序要加载到内存什么地址等等。 MDK的可重定位文件*.o不包含这部分内容,因为它还不是可执行文件,而armlink输出的*.axf文件就包含该内容了。
节区 是*.o文件的独立数据区域,它包含提供给链接视图使用的大量信息,如指令(Code)、数据(RO、RW、ZI-data)、 符号表(函数、变量名等)、重定位信息等,例如每个由C语言定义的函数在*.o文件中都会有一个独立的节区。
节区头 存储在最后,包含了本文件节区的信息,如节区名称、大小等等。

总的来说,链接器把各个*.o文件的节区归类、排列,根据目标器件的情况编排地址生成输出,汇总到*.axf文件。例如,“流水灯”工程中在“bsp_led.c”文件中有一个LED_GPIO_Config函数,而它内部调用了“stm32f1xx_hal_gpio.c”的GPIO_Init函数,经过 armcc 编译后,LED_GPIO_Config 及 GPIO_Iint 函数都成了指令代码,分别存储在 bsp_led.o 及 stm32f1xx_hal_gpio.o 文件中,这些指令在*.o文件都没有指定地址,仅包含了内容、大小以及调用的链接信息,而经过链接器后,链接器给它们都分配了特定的地址,并且把地址根据调用指向链接起来。

image321.jpeg

3.5 ELF文件各部分说明

前边知道*.o和*.axf文件都是ELF格式的,它包含ELF文件头、程序头、节区(section)以及节区头部表,这一部分我们来看一下各部分的说明。

3.5.1 ELF文件头

3.5.1.1 生成elf信息

接下来我们看看具体文件的内容,使用fromelf文件可以查看*.o、*.axf及*.lib文件的ELF信息。使用命令行,切换到文件所在的目录,输入“fromelf –text –v file_name.o”命令,可控制输出file_name.o的详细信息。这一部分会用到野火的相关例程,可以去这里下载:ebf_stm32f103_badao_std_code: 野火STM32F103 霸道开发板 标准库教程配套代码 (gitee.com)

image-20230414225429664

然后我们打开文件然后解压,进入程序目录,我么可以这样(原来win11下多了一个叫终端的东西,可以通过终端选择是powershell还是命令提示符):

image-20230414232030451

然后就会打开powershell,并且处于这个目录下:

image-20230414232403045

fromelf –text命令还有一些参数,我们使用这些选项来生成对应的文件,我们来了解一下:

fromelf选项 可查看的信息 生成到相应的文件
-v 详细信息 bsp_led_o_elfInfo_v.txt
-a 数据的地址 bsp_led_o_elfInfo_a.txt
-c 反汇编代码 bsp_led_o_elfInfo_c.txt
-d data section的内容 bsp_led_o_elfInfo_d.txt
-e 异常表 bsp_led_o_elfInfo_e.txt
-g 调试表 bsp_led_o_elfInfo_g.txt
-r 重定位信息 bsp_led_o_elfInfo_r.txt
-s 符号表 bsp_led_o_elfInfo_s.txt
-t 字符串表 bsp_led_o_elfInfo_t.txt
-y 动态段内容 bsp_led_o_elfInfo_y.txt
-z 代码及数据的大小信息 bsp_led_o_elfInfo_z.txt
1
2
3
4
5
6
7
8
9
10
11
12
# 放在 bsp_led.o 目录下,命名为1.ps1
fromelf --text -v bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_v.txt
fromelf --text -a bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_a.txt
fromelf --text -c bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_c.txt
fromelf --text -d bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_d.txt
fromelf --text -e bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_e.txt
fromelf --text -g bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_g.txt
fromelf --text -r bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_r.txt
fromelf --text -s bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_s.txt
fromelf --text -t bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_t.txt
fromelf --text -y bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_y.txt
fromelf --text -z bsp_led.o --output ../elfInfo/bsp_led_o_elfInfo_z.txt

我们运行这个脚本:

1
PS D:\MyLinux\Ubuntu\Sharedfiles\3Linux\16-LV16\ebf_stm32f103_badao_std_code-master\46-MDK编译过程及文件全解\程序\MDK文 件详解-GPIO输出—多彩流水灯\Output> .\1.ps1

然后就可以在上一级目录下自动创建elfInfo文件夹,并在此文件夹下生成上边那些文件。

image-20230414234303798
无法运行脚本?

我们像linux一样运行脚本时,报这个?

image-20230414233729717

计算机上启动 Windows PowerShell 时,执行策略很可能是 Restricted(默认设置)。Restricted 执行策略不允许任何脚本运行。 AllSigned 和 RemoteSigned 执行策略可防止 Windows PowerShell 运行没有数字签名的脚本。我们使用下边的命令确认:

1
get-executionpolicy
image-20230414233833843

发现果然是这样,然后我们以管理员身份运行powershell并输入以下命令,并输入Y即可:

1
set-executionpolicy remotesigned
image-20230414234055475
3.5.1.2 内容分析

我们打开 bsp_led_o_elfInfo_v.txt文件,可以看到文件开头内容如下:

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
========================================================================

** ELF Header Information

File Name: bsp_led.o // bsp_led.o文件

Machine class: ELFCLASS32 (32-bit) // 32位机器
Data encoding: ELFDATA2LSB (Little endian) // 小端格式
Header version: EV_CURRENT (Current version)
Operating System ABI: none
ABI Version: 0
File Type: ET_REL (Relocatable object) (1) // 可重定位类型
Machine: EM_ARM (ARM)

Entry offset (in SHF_ENTRYSECT section): 0x00000000
Flags: None (0x05000000)

ARM ELF revision: 5 (ABI version 2)

Header size: 52 bytes (0x34)
Program header entry size: 0 bytes (0x0) // 程序头大小
Section header entry size: 40 bytes (0x28)

Program header entries: 0
Section header entries: 178

Program header offset: 0 (0x00000000) // 程序头在文件中的位置(没有程序头)
Section header offset: 379672 (0x0005cb18) // 节区头在文件中的位置

Section header string table index: 175

========================================================================

在这个*.o文件中,它的ELF文件头中告诉我们它的程序头(Program header)大小为“0 bytes”,且程序头所在的文件位置偏移也为“0”,这说明它是没有程序头的。

3.5.2 程序头

接下来打开“ 流水灯_axf_elfInfo_v.txt”文件,查看工程的 流水灯.axf 文件的详细信息:

image-20230415220024144

按照相同的方法,我们执行以下命令:

1
2
PS D:\MyLinux\Ubuntu\Sharedfiles\3Linux\16-LV16\ebf_stm32f103_badao_std_code-master\46-MDK编译过程及文件全解\程序\MDK文
件详解-GPIO输出—多彩流水灯\Output> fromelf --text -v .\流水灯.axf --output ../elfInfo/流水灯_axf_elfInfo_v.txt

这样就可以在上一级目录中生成“流水灯_axf_elfInfo_v.txt”文件,我们打开这个文件,文件头部内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
========================================================================

** ELF Header Information

File Name: .\流水灯.axf

# 中间部分省略......

========================================================================

** Program header #0

Type : PT_LOAD (1)
File Offset : 52 (0x34)
Virtual Addr : 0x08000000
Physical Addr : 0x08000000
Size in file : 1492 bytes (0x5d4)
Size in memory: 2516 bytes (0x9d4)
Flags : PF_X + PF_W + PF_R + PF_ARM_ENTRY (0x80000007)
Alignment : 8


========================================================================

对比之下,可发现*.axf文件的ELF文件头对程序头的大小说明为非0值,且给出了它在文件的偏移地址,在输出信息之中,包含了程序头的详细信息。

程序头的“Physical Addr”描述了本程序要加载到的内存地址“0x0800 0000”,正好是STM32内部FLASH的首地址。“size in file”描述了本程序占据的空间大小为“1492 bytes”,它正是程序烧录到FLASH中需要占据的空间。这个大小等于在keil中编译时输出信息中的 Code + RO-data 的大小。

image-20230415220615175

3.5.2 节区头

在ELF的原文件中,紧接着程序头的一般是节区的主体信息,在节区主体信息之后是描述节区主体信息的节区头,我们来看看节区头中的信息了解概况。 通过对比*.o文件及*.axf文件的节区头部信息,可以清楚地看出这两种文件的区别:

  • *.o文件的节区信息(“bsp_led_o_elfInfo_v.txt”文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
** Section #1

Name : i.LED_GPIO_Config // 节区名
// 此节区包含程序定义的信息,其格式和含义都由程序来解释。
Type : SHT_PROGBITS (0x00000001)
// 此节区在进程执行过程中占用内存。 节区包含可执行的机器指令。
Flags : SHF_ALLOC + SHF_EXECINSTR (0x00000006)
Addr : 0x00000000 // 地址
File Offset : 52 (0x34) // 在文件中的偏移
Size : 96 bytes (0x60) // 大小
Link : SHN_UNDEF
Info : 0
Alignment : 4 // 字节对齐
Entry Size : 0
====================================

这个节区的名称为LED_GPIO_Config,它正好是我们在bsp_led.c文件中定义的函数名。注意:编译时要勾选“【Options for Target 】→ 【C/C++】→ 【One ELF Section per Function】”中的选项,生成的*.o文件内部的代码区域才会与C文件中定义的函数名一致,否则它会把多个函数合成一个代码段, 名字一般跟C文件中的函数名不同。

image-20230415221049396

这个节区头描述的是该函数被编译后的节区信息,其中包含了节区的类型(指令类型SHT_PROGBITS)、节区应存储到的地址(0x00000000)、它主体信息在文件位置中的偏移(52)以及节区的大小(96 bytes)。

由于*.o文件是可重定位文件,所以它的地址并没有被分配,是0x00000000(假如文件中还有其它函数,该函数生成的节区中,对应的地址描述也都是0)。 当链接器链接时,根据这个节区头信息,在文件中找到它的主体内容,并根据它的类型,把它加入到主程序中,并分配实际地址,链接后生成的*.axf文件。

  • *.axf文件的节区信息(“流水灯_axf_elfInfo_v.txt”)
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
** Section #1

Name : ER_IROM1 // 节区名
// 此节区包含程序定义的信息,其格式和含义都由程序来解释。
Type : SHT_PROGBITS (0x00000001)
// 此节区在进程执行过程中占用内存。 节区包含可执行的机器指令
Flags : SHF_ALLOC + SHF_EXECINSTR (0x00000006)
Addr : 0x08000000 // 地址
File Offset : 52 (0x34)
Size : 1492 bytes (0x5d4) // 大小
Link : SHN_UNDEF
Info : 0
Alignment : 4
Entry Size : 0
====================================
** Section #2

Name : RW_IRAM1 // 节区名
// 包含将出现在程序的内存映像中的为初始化数据。 根据定义,当程序开始执行,系统
// 将把这些数据初始化为 0。
Type : SHT_NOBITS (0x00000008)
// 此节区在进程执行过程中占用内存。 节区包含进程执行过程中将可写的数据。
Flags : SHF_ALLOC + SHF_WRITE (0x00000003)
Addr : 0x20000000 // 地址
File Offset : 1544 (0x608)
Size : 1024 bytes (0x400) // 大小
Link : SHN_UNDEF
Info : 0
Alignment : 8
Entry Size : 0
====================================

在*.axf文件中,主要包含了两个节区,一个名为ER_IROM1,一个名为RW_IRAM1,这些节区头信息中除了具有*.o文件中节区头描述的节区类型、文件位置偏移、大小之外,更重要的是它们都有具体的地址描述,其中 ER_IROM1的地址为0x08000000,而RW_IRAM1的地址为0x20000000,它们正好是STM32内部FLASH及SRAM的首地址,对应节区的大小就是程序需要占用FLASH及SRAM空间的实际大小。

也就是说,经过链接器后,它生成的*.axf文件已经汇总了其它*.o文件的所有内容,生成的ER_IROM1节区内容可直接写入到STM32内部FLASH的具体位置。例如,前面*.o文件中的i.LED_GPIO_Config节区已经被加入到*.axf文件的ER_IROM1节区的某地址。

3.5.3 节区主体及反汇编代码

使用fromelf的 -c 选项可以查看部分节区的主体信息,对于指令节区,可根据其内容查看相应的反汇编代码,我们打开“bsp_led_o_elfInfo_c.txt”文件可查看 这些信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// *.o文件的LED_GPIO_Config节区及反汇编代码(bsp_led_o_elfInfo_c.txt文件)
** Section #1 'i.LED_GPIO_Config' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
Size : 96 bytes (alignment 4)
Address: 0x00000000

$t
i.LED_GPIO_Config
LED_GPIO_Config
// 地址 内容 [ASCII码(无意义)] 内容对应的代码
0x00000000: b508 .. PUSH {r3,lr}
0x00000002: 2101 .! MOVS r1,#1
0x00000004: 2008 . MOVS r0,#8
0x00000006: f7fffffe .... BL RCC_APB2PeriphClockCmd
0x0000000a: 2020 MOVS r0,#0x20
0x0000000c: f8ad0000 .... STRH r0,[sp,#0]
0x00000010: 2010 . MOVS r0,#0x10
0x00000012: f88d0003 .... STRB r0,[sp,#3]
0x00000016: 2003 . MOVS r0,#3
0x00000018: f88d0002 .... STRB r0,[sp,#2]
0x0000001c: 4669 iF MOV r1,sp
0x0000001e: 480f .H LDR r0,[pc,#60] ; [0x5c] = 0x40010c00
0x00000020: f7fffffe .... BL GPIO_Init
0x00000024: 2001 . MOVS r0,#1
// 下边的内容省略

可看到,由于这是*.o文件,它的节区地址还是没有分配的,基地址为0x00000000,接着在LED_GPIO_Config标号之后,列出了一个表,表中包含了地址偏移、相应地址中的内容以及根据内容反汇编得到的指令。细看汇编指令,还可看到它包含了跳转到RCC_APB2PeriphClockCmd及GPIO_Init标号的语句,而且这两个跳转语句原来的内容都是“f7fffffe”,这是因为在*.o文件中并没有RCC_APB2PeriphClockCmd及GPIO_Init标号的具体地址索引,在*.axf文件中,这是不一样的。

接下来我们打开“流水灯_axf_elfInfo_c.txt”文件,查看*.axf文件中,ER_IROM1节区中对应LED_GPIO_Config的内容,我们执行以下命令生成这个文件:

1
2
PS D:\MyLinux\Ubuntu\Sharedfiles\3Linux\16-LV16\ebf_stm32f103_badao_std_code-master\46-MDK编译过程及文件全解\程序\MDK文
件详解-GPIO输出—多彩流水灯\Output> fromelf --text -c .\流水灯.axf --output ../elfInfo/流水灯_axf_elfInfo_c.txt

然后我们打开这个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// *.axf文件的LED_GPIO_Config反汇编代码(流水灯_axf_elfInfo_c.txt文件)
i.LED_GPIO_Config
LED_GPIO_Config
0x080002c4: b508 .. PUSH {r3,lr}
0x080002c6: 2101 .! MOVS r1,#1
0x080002c8: 2008 . MOVS r0,#8
0x080002ca: f000f82f ../. BL RCC_APB2PeriphClockCmd ; 0x800032c
0x080002ce: 2020 MOVS r0,#0x20
0x080002d0: f8ad0000 .... STRH r0,[sp,#0]
0x080002d4: 2010 . MOVS r0,#0x10
0x080002d6: f88d0003 .... STRB r0,[sp,#3]
0x080002da: 2003 . MOVS r0,#3
0x080002dc: f88d0002 .... STRB r0,[sp,#2]
0x080002e0: 4669 iF MOV r1,sp
0x080002e2: 480f .H LDR r0,[pc,#60] ; [0x8000320] = 0x40010c00
0x080002e4: f7ffff5e ..^. BL GPIO_Init ; 0x80001a4
0x080002e8: 2001 . MOVS r0,#1
// 下边的内容省略

可看到,除了基地址以及跳转地址不同之外,LED_GPIO_Config中的内容跟*.o文件中的一样。另外,由于*.o是独立的文件,而*.axf是整个工程汇总的文件,所以在*.axf中包含了所有调用到*.o文件节区的内容。例如,在“bsp_led_o_elfInfo_c.txt”(bsp_led.o文件的反汇编信息)中不包含RCC_APB2PeriphClockCmd及GPIO_Init的内容,而在“流水灯_axf_elfInfo_c.txt” (流水灯.axf文件的反汇编信息)中则可找到它们的具体信息,且它们也有具体的地址空间。

image-20230529204130641

在*.axf文件中,跳转到RCC_APB2PeriphClockCmd及GPIO_Init标号的这两个指令后都有注释,分别是“; 0x800032c”及“; 0x80001a4”,它们是这两个标号所在的具体地址,而且这两个跳转语句的跟*.o中的也有区别,内容分别为“f000f82f”及“f7ffff5e”(*.o中的均为f7fffffe)。这就是链接器链接的含义,它把不同*.o中的内容链接起来了。

3.5.4 分散加载代码

前面提到程序有存储态及运行态,它们之间应有一个转化过程,把存储在内部FLASH中的RW-data数据拷贝至内部SRAM。然而我们的工程中并没有编写这样的代码,在汇编文件中也查不到该过程,芯片是如何知道FLASH的哪些数据应拷贝到SRAM的哪些区域呢?

通过查看“流水灯_axf_elfInfo_c.txt”的反汇编信息,可以看到到程序中具有一段名为“__scatterload”的分散加载代码,它是由 armlink 链接器自动生成的

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
// 分散加载代码(流水灯_axf_elfInfo_c.txt文件)
.text
__scatterload
__scatterload_rt2
0x08000168: 4c06 .L LDR r4,[pc,#24] ; [0x8000184] = 0x80005c4
0x0800016a: 4d07 .M LDR r5,[pc,#28] ; [0x8000188] = 0x80005d4
0x0800016c: e006 .. B 0x800017c ; __scatterload + 20
0x0800016e: 68e0 .h LDR r0,[r4,#0xc]
0x08000170: f0400301 @... ORR r3,r0,#1
0x08000174: e8940007 .... LDM r4,{r0-r2}
0x08000178: 4798 .G BLX r3
0x0800017a: 3410 .4 ADDS r4,r4,#0x10
0x0800017c: 42ac .B CMP r4,r5
0x0800017e: d3f6 .. BCC 0x800016e ; __scatterload + 6
0x08000180: f7ffffda .... BL __main_after_scatterload ; 0x8000138
$d
0x08000184: 080005c4 .... DCD 134219204
0x08000188: 080005d4 .... DCD 134219220
$t
// ... ...
i.__scatterload_copy
__scatterload_copy
0x080004a0: e002 .. B 0x80004a8 ; __scatterload_copy + 8
0x080004a2: c808 .. LDM r0!,{r3}
0x080004a4: 1f12 .. SUBS r2,r2,#4
0x080004a6: c108 .. STM r1!,{r3}
0x080004a8: 2a00 .* CMP r2,#0
0x080004aa: d1fa .. BNE 0x80004a2 ; __scatterload_copy + 2
0x080004ac: 4770 pG BX lr
i.__scatterload_null
__scatterload_null
0x080004ae: 4770 pG BX lr
i.__scatterload_zeroinit
__scatterload_zeroinit
0x080004b0: 2000 . MOVS r0,#0
0x080004b2: e001 .. B 0x80004b8 ; __scatterload_zeroinit + 8
0x080004b4: c101 .. STM r1!,{r0}
0x080004b6: 1f12 .. SUBS r2,r2,#4
0x080004b8: 2a00 .* CMP r2,#0
0x080004ba: d1fb .. BNE 0x80004b4 ; __scatterload_zeroinit + 4
0x080004bc: 4770 pG BX lr
0x080004be: 0000 .. MOVS r0,r0

这段分散加载代码包含了拷贝过程(主要使用LDM复制指令),如:

1
0x08000174:    e8940007    ....    LDM      r4,{r0-r2}

而LDM指令的操作数中包含了加载的源地址,这些地址中包含了内部FLASH存储的RW-data数据,执行这些指令后数据就会从FLASH地址加载到内部SRAM的地址。我们再来看一下这个文件的__main的反汇编代码部分:

1
2
3
4
5
6
7
8
9
10
//__main的反汇编代码部分(流水灯_axf_elfInfo_c.txt文件)
.ARM.Collect$$$$00000000
.ARM.Collect$$$$00000001
__Vectors_End
__main
_main_stk
0x08000130: f8dfd00c .... LDR sp,__lit__00000000 ; [0x8000140] = 0x20000400
.ARM.Collect$$$$00000004
_main_scatterload
0x08000134: f000f818 .... BL __scatterload ; 0x8000168

可以看最后一行, “__scatterload ”的代码会被“__main”函数调用,__main在启动文件中的“Reset_Handler”会被调用,因而,在主体程序执行前,已经完成了分散加载过程。

3.6 hex文件及bin文件

若编译过程无误,即可把工程生成前面对应的*.axf文件,而在MDK中使用下载器(DAP/JLINK/ULINK等)下载程序或仿真的时候,MDK调用的就是*.axf文件,它解释该文件,然后控制下载器把*.axf中的代码内容下载到STM32芯片对应的存储空间,然后复位后芯片就开始执行代码了。

然而,脱离了MDK或IAR等工具,下载器就无法直接使用*.axf文件下载代码了,它们一般仅支持hex和bin格式的代码数据文件。默认情况下MDK都不会生成hex及bin文件,需要配置工程选项或使用fromelf命令。

3.6.1 生成hex文件

生成hex文件的配置比较简单,在“【Options for Target】→【Output】→【Create Hex File】”中勾选该选项,然后编译工程即可。

image-20230415223432834

3.6.2 生成bin文件

使用MDK生成bin文件需要使用fromelf命令,在MDK的“【Options For Target】→【Users】”中加入:

1
fromelf --bin --output ..\..\Output\流水灯.bin ..\..\Output\流水灯.axf
image-20230410231410382

该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它, 才能为自己的工程定制指令,首先看看fromelf的帮助(前提是已经将 fromelf 加入环境变量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS C:\Users\20380> fromelf
Product: MDK Plus 5.29
Component: ARM Compiler 5.06 update 6 (build 750)
Tool: fromelf [4d35e3]
For support see http://www.arm.com/support
Software supplied by: ARM Limited

ARM image conversion utility
fromelf [options] input_file

Options:
--help display this help screen
# 中间部分省略.....
Binary Output Formats:
--bin Plain Binary
# 中间部分省略.....
Output Formats Requiring Debug Information
--fieldoffsets Assembly Language Description of Structures/Classes
--expandarrays Arrays inside and outside structures are expanded

Other Output Formats:
--elf ELF
--text Text Information
# 中间部分省略.....

我们在MDK输入的指令格式是遵守fromelf帮助里的指令格式说明的,其格式为:

1
fromelf [options] input_file

其中optinos是指令选项,一个指令支持输入多个选项,每个选项之间使用空格隔开,我们的实例中使用“–bin”选项设置输出bin文件,使用“–output file”选项设置输出文件的名字为“..\..\Output流水灯.bin”,这个名字是一个相对路径格式,一个“..”表示当前目录的上一层,两个“..”表示上两层 目录(当前目录是指uvprojx工程文件所在的位置)。如果不了解如何使用“..”表示路径,可使用MDK命令输入框后面的文件夹图标打开文件浏览器选择 文件,加入绝对路径,在命令的最后使用“..\..\Output流水灯.axf”作为命令的输入文件。

图 40‑38 fromelf命令格式分解

fromelf需要根据工程的*.axf文件输入来转换得到bin文件,所以在命令的输入文件参数中要选择本工程对应的*.axf文件,于是在MDK命令输入栏中,我们把fromelf指令放置在“After Build/Rebuild”(工程构建完成后执行)一栏,这样设置后,工程构建完成生成了最新的*.axf文件,MDK再执行fromelf指令,从而得到最新的bin文件。

设置完成生成hex的选项或添加了生成bin的用户指令后,点击工程的编译(build)按钮,重新编译工程。就会有bin文件生成啦:

image-20230529205311123

3.6.3 hex文件格式

hex是Intel公司制定的一种使用ASCII文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到ROM中的数据,绝大多数下载器支持该格式。一个hex文件由多条记录组成,而每条记录由五个部分组成,格式形如“:llaaaatt[dd…]cc”记录的各个部分介绍如下:

  • “:” :每条记录的开头都使用冒号来表示一条记录的开始;
  • ll :以16进制数表示这条记录的主体数据区的长度(即后面[dd…]的长度);
  • aaaa:表示这条记录中的内容应存放到FLASH中的起始地址;
  • tt:表示这条记录的类型,它包含中的各种类型;
添加说明
tt的值代表的类型
00数据记录
01本文件结束记录
02扩展地址记录
04扩展线性地址记录(表示后面的记录按个这地址递增)
05表示一个线性地址记录的起始(只适用于ARM)
  • dd :表示一个字节的数据,一条记录中可以有多个字节数据,ll区表示了它有多少个字节的数据;
  • cc :表示本条记录的校验和,它是前面所有16进制数据 (除冒号外,两个为一组)的和对256取模运算的结果的补码。

例如,“流水灯”工程生成的hex文件前几条记录如下:

1
2
3
4
5
6
7
// Hex文件实例(流水灯.hex文件,可直接用记事本打开)
:020000040800F2
:10000000000400204501000829030008BF02000881
:10001000250300088D0100089D0400080000000071
:100020000000000000000000000000004D03000878
:1000300091010008000000002B03000839040008AB
:100040005F0100085F0100085F0100085F01000810
  • 第一条记录解释如下:

(1)02:表示这条记录数据区的长度为2字节;

(2)0000:表示这条记录要存储到的地址;

(3)04:表示这是一条扩展线性地址记录;

(4)0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高16位,与前面的“0000”结合在一起,表示要扩展的线性地址为“0x0800 0000”,这正好是STM32内部FLASH的首地址;

(5)F2:表示校验和,它的值为(0x02+0x00+0x00+0x04+0x08+0x00)%256的值再取补码。

  • 再来看第二条记录:

(1)10:表示这条记录数据区的长度为2字节;

(2)0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的FLASH首地址为(0x0800 0000+0x0000);

(3)00:表示这是一条数据记录,数据区的是地址;

(4)000400204501000829030008BF020008:这是要按地址存储的数据;

(5)81:校验和

为了更清楚地对比bin、hex及axf文件的差异,我们来查看这些文件内部记录的信息来进行对比。

3.6.4 hex、bin及axf文件的区别与联系

bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。

  • bin文件是最直接的代码映像,它记录的内容就是要存储到FLASH的二进制数据(机器码本质上就是二进制数据), 在FLASH中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片FLASH平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);
  • hex文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到FLASH的哪个地址,下载器可以根据这些信息辅助下载;
  • axf文件,不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。

同一个工程生成的bin、hex及axf文件的大小如下图:

image-20230416140839500

实际上,这个工程要烧写到FLASH的内容总大小为1492字节,然而在Windows中查看的bin文件却比它大( bin文件是FLASH的代码映像,大小应一致),这是因为Windows文件显示单位的原因,使用右键查看文件的属性,可以查看它实际记录内容的大小

image-20230416140927118

接下来我们打开本工程的“流水灯.bin”、“流水灯.hex”及由“流水灯.axf”使用fromelf工具输出的反汇编文件“流水灯_axf_elfInfo_c.txt” 文件:

image412

在“流水灯_axf_elfInfo_c.txt”文件中不仅可以看到代码数据,还有具体的标号、地址以及反汇编得到的代码,虽然它不是*.axf文件的原始内容,但因为它是通过*.axf文件fromelf工具生成的,我们可认为*.axf文件本身记录了大量这些信息,它的内容非常丰富,熟悉汇编语言的人可轻松阅读。

在hex文件中包含了地址信息以及地址中的内容,而在bin文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex及axf文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三都之间的区别与联系。

由于文件中存储的都是机器码,该图是根据axf文件的GPIO_Init函数的机器码,在bin及hex中找到的对应位置。 所以经验丰富的人是有可能从bin或hex文件中恢复出汇编代码的,只是成本较高,但不是不可能。

GPIO_Init函数的代码数据在三个文件中的表示

如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在FLASH中的数据,从而得到bin映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件PCB,抄出一样的板子,再把bin映像下载芯片,直接山寨出目标产品,所以在实际的生产中,一定要注意做好加密措施。由于axf文件中含有大量的信息,且直接使用fromelf即可反汇编代码,所以更不要随便泄露axf文件。lib文件也能反使用fromelf文件反汇编代码,不过它不能还原出C代码,由于lib文件的主要目的是为了保护C源代码,也算是达到了它的要求。

3.7 总结

总结一下bin、hex、axf、elf文件格式有什么区别吧:从存储数据的信息量上看:elf > axf > hex > bin,所以这也就确定了只能将大信息量的文件格式向小信息量的文件格式转换,如只能将 hex 文件转换为 bin 文件,当然如果指定了下载地址,也可以将 bin 转换为 hex 文件。

文件类型 说明
BIN文件 bin文件是纯粹的机器码,没有地址信息,不能使用记事本直接打开,要使用bin文件阅读器才能打开,如果使用bin文件烧录程序时,需要指定下载地址。一些下载器只能使用BIN文件进行下载,在进行OTA远程升级时必须使用bin文件。
HEX文件 一般是指Intel标准的hex文件,可以使用记事本直接打开,是十六进制数据,包含了基地址、偏移量、校验和、文件开始和结束标志等信息,与bin文件最大的不同就是包含了下载地址。由于hex文件是十六进制数据,而bin文件是二进制数据,如十六进制0xFF,用二进制表示为1111 1111,所以hex文件要比bin文件大得多。与axf文件相比,不含调试信息,不能用于调试。
AXF文件 包含了调试信息,如进行在Keil环境使用Debug功能时,就是先将axf文件下载到芯片内,才能进行调试。如使用J-Link的J-Scope功能时,必须使用axf文件。
ELF文件 是由GCC编译器生成的。elf文件可以直接转换为hex和bin。

4. Listing目录下文件

在Listing目录下包含了*.map及*.lst文件,它们都是文本格式的,可使用Windows的记事本软件打开。其中lst文件仅包含了一些汇编符号的链接信息,这些文件中重点是map文件

4.1 Listing目录位置选择

这里存放的都是链接器链接过程中用到的或者生成的文件,我们也是可以自行配置目录位置的:

image-20230529200721976

【Options for Targe】→【Listing】→【SelectFolder for Listings】”选项配置这些文件的输出路径 。

4.2 打开map文件

map文件存在于Listing目录中,我们怎么在MDK中打开?我们直接双击这里:

image-20230529205801781

双击之后没打开?这个我也不清楚,但是重新配置一下listing目录的位置就可以双击打开啦,我也不是很明白为什么,可能是需要map文件在某个子目录吧。

4.3 map文件说明

map文件是由链接器生成的,它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的Code、RO-data、RW-data以及ZI-data的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”,接下来是各部分介绍。

4.3.1 节区的跨文件引用

打开“流水灯.map”文件,可看到它的第一部分——节区的跨文件引用(Section Cross References):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 节区的跨文件引用部分(流水灯.map文件)
Component: ARM Compiler 5.06 update 6 (build 750) Tool: armlink [4d35ed]

==============================================================================

Section Cross References

startup_stm32f10x_hd.o(RESET) refers to startup_stm32f10x_hd.o(STACK) for __initial_sp
startup_stm32f10x_hd.o(RESET) refers to startup_stm32f10x_hd.o(.text) for Reset_Handler
// 中间省略......
startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.SysTick_Handler) for SysTick_Handler
// 中间省略......
main.o(i.main) refers to bsp_led.o(i.LED_GPIO_Config) for LED_GPIO_Config
main.o(i.main) refers to main.o(i.Delay) for Delay
bsp_led.o(i.LED_GPIO_Config) refers to stm32f10x_rcc.o(i.RCC_APB2PeriphClockCmd) for RCC_APB2PeriphClockCmd
bsp_led.o(i.LED_GPIO_Config) refers to stm32f10x_gpio.o(i.GPIO_Init) for GPIO_Init
bsp_led.o(i.LED_GPIO_Config) refers to stm32f10x_gpio.o(i.GPIO_SetBits) for GPIO_SetBits
// 后边省略......

在这部分中,详细列出了各个*.o文件之间的符号引用。由于*.o文件是由asm或c/c++源文件编译后生成的,各个文件及文件内的节区间互相独立,链接器根据它们之间的互相引用链接起来,链接的详细信息在这个“Section Cross References”一一列出。

例如,开头部分说明的是startup_stm32f10x.o文件中的“RESET”节区分为它使用的“__initial_sp” 符号引用了同文件“STACK”节区。

我们继续浏览,可看到main.o文件的引用说明,如说明main.o文件的i.main节区为它使用的 LED_GPIO_Config 符号引用了 bsp_led.o 文件 i.LED_GPIO_Config 节区。

同样地,下面还有bsp_led.o文件的引用说明,如说明了bsp_led.o文件的i.LED_GPIO_Config节区为它使用的GPIO_Init符号引用了stm32f10x_gpio.o文件的i.GPIO_Init节区。

可以了解到,这些跨文件引用的符号其实就是源文件中的函数名、变量名。有时在构建工程的时候,编译器会输出 “Undefined symbol xxx (referred from xxx.o)” 这样的提示,该提示的原因就是在链接过程中,某个文件无法在外部找到它引用的标号,因而产生链接错误。例如,我们把bsp_led.c文件中定义的函数LED_GPIO_Config改名为LED_GPIO_ConfigA,而不修改main.c文件中的调用,就会出现main文件无法找到LED_GPIO_Config符号的提示(Undefined symbol xxxx from xxx.o)。

image-20230416143210100

4.3.2 删除无用节区

map文件的第二部分是删除无用节区的说明(Removing Unused input sections from the image):

1
2
3
4
5
6
7
8
9
10
11
//删除无用节区部分(流水灯.map文件)
Removing Unused input sections from the image.

Removing startup_stm32f10x_hd.o(HEAP), (512 bytes).
Removing core_cm3.o(.emb_text), (32 bytes).
Removing system_stm32f10x.o(i.SystemCoreClockUpdate), (164 bytes).
Removing system_stm32f10x.o(.data), (20 bytes).
Removing misc.o(i.NVIC_Init), (112 bytes).
Removing misc.o(i.NVIC_PriorityGroupConfig), (20 bytes).
Removing misc.o(i.NVIC_SetVectorTable), (20 bytes).
// 后边的省略

这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到*.axf文件,不是指在*.o文件删除),这样可以防止这些无用数据占用程序空间。

例如,上面的信息中说明startup_stm32f10x.o中的HEAP(在启动文件中定义的用于动态分配的“堆”区)以及 stm32f10x_adc.o的各个节区都被删除了,因为在我们这个工程中没有使用动态内存分配,也没有引用任何stm32f10x_adc.c中的内容。由此也可以知道,虽然我们把STM32HAL库的各个外设对应的c库文件都添加到了工程,但不必担心这会使工程变得臃肿,因为未被引用的节区内容不会被加入到最终的机器码文件中。

4.3.3 符号映像表

map文件的第三部分是符号映像表(Image Symbol Table):

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
51
52
//符号映像表部分(流水灯.map文件)
Image Symbol Table

Local Symbols

Symbol Name Value Ov Type Size Object(Section)
// 中间部分省略......
i.DebugMon_Handler 0x08000190 Section 0 stm32f10x_it.o(i.DebugMon_Handler)
i.Delay 0x08000192 Section 0 main.o(i.Delay)
i.GPIO_Init 0x080001a4 Section 0 stm32f10x_gpio.o(i.GPIO_Init)
i.GPIO_SetBits 0x080002ba Section 0 stm32f10x_gpio.o(i.GPIO_SetBits)
i.HardFault_Handler 0x080002be Section 0 stm32f10x_it.o(i.HardFault_Handler)
i.LED_GPIO_Config 0x080002c4 Section 0 bsp_led.o(i.LED_GPIO_Config)
i.MemManage_Handler 0x08000324 Section 0 stm32f10x_it.o(i.MemManage_Handler)
i.NMI_Handler 0x08000328 Section 0 stm32f10x_it.o(i.NMI_Handler)
i.PendSV_Handler 0x0800032a Section 0 stm32f10x_it.o(i.PendSV_Handler)
i.RCC_APB2PeriphClockCmd 0x0800032c Section 0 stm32f10x_rcc.o(i.RCC_APB2PeriphClockCmd)
i.SVC_Handler 0x0800034c Section 0 stm32f10x_it.o(i.SVC_Handler)
i.SetSysClock 0x0800034e Section 0 system_stm32f10x.o(i.SetSysClock)
SetSysClock 0x0800034f Thumb Code 8 system_stm32f10x.o(i.SetSysClock)
i.SetSysClockTo72 0x08000358 Section 0 system_stm32f10x.o(i.SetSysClockTo72)
SetSysClockTo72 0x08000359 Thumb Code 214 system_stm32f10x.o(i.SetSysClockTo72)
i.SysTick_Handler 0x08000438 Section 0 stm32f10x_it.o(i.SysTick_Handler)
i.SystemInit 0x0800043c Section 0 system_stm32f10x.o(i.SystemInit)
i.UsageFault_Handler 0x0800049c Section 0 stm32f10x_it.o(i.UsageFault_Handler)
i.__scatterload_copy 0x080004a0 Section 14 handlers.o(i.__scatterload_copy)
i.__scatterload_null 0x080004ae Section 2 handlers.o(i.__scatterload_null)
i.__scatterload_zeroinit 0x080004b0 Section 14 handlers.o(i.__scatterload_zeroinit)
i.main 0x080004c0 Section 0 main.o(i.main)
STACK 0x20000000 Section 1024 startup_stm32f10x_hd.o(STACK)

Global Symbols

Symbol Name Value Ov Type Size Object(Section)
// 中间部分省略
LED_GPIO_Config 0x080002c5 Thumb Code 90 bsp_led.o(i.LED_GPIO_Config)
MemManage_Handler 0x08000325 Thumb Code 4 stm32f10x_it.o(i.MemManage_Handler)
NMI_Handler 0x08000329 Thumb Code 2 stm32f10x_it.o(i.NMI_Handler)
PendSV_Handler 0x0800032b Thumb Code 2 stm32f10x_it.o(i.PendSV_Handler)
RCC_APB2PeriphClockCmd 0x0800032d Thumb Code 26 stm32f10x_rcc.o(i.RCC_APB2PeriphClockCmd)
SVC_Handler 0x0800034d Thumb Code 2 stm32f10x_it.o(i.SVC_Handler)
SysTick_Handler 0x08000439 Thumb Code 2 stm32f10x_it.o(i.SysTick_Handler)
SystemInit 0x0800043d Thumb Code 78 system_stm32f10x.o(i.SystemInit)
UsageFault_Handler 0x0800049d Thumb Code 4 stm32f10x_it.o(i.UsageFault_Handler)
__scatterload_copy 0x080004a1 Thumb Code 14 handlers.o(i.__scatterload_copy)
__scatterload_null 0x080004af Thumb Code 2 handlers.o(i.__scatterload_null)
__scatterload_zeroinit 0x080004b1 Thumb Code 14 handlers.o(i.__scatterload_zeroinit)
main 0x080004c1 Thumb Code 252 main.o(i.main)
Region$$Table$$Base 0x080005c4 Number 0 anon$$obj.o(Region$$Table)
Region$$Table$$Limit 0x080005d4 Number 0 anon$$obj.o(Region$$Table)
__initial_sp 0x20000400 Data 0 startup_stm32f10x_hd.o(STACK)
// 后边的省略

这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到LED_GPIO_Config符号存储在0x080002c4地址,它属于Thumb Code类型,大小为90字节,它所在的节区为bsp_led.o文件的i.LED_GPIO_Config节区。

4.3.4 存储器映像索引

map文件的第四部分是存储器映像索引(Memory Map of the image):

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
51
52
53
54
55
56
57
58
59
60
// 存储器映像索引部分(流水灯.map文件)
Memory Map of the image

Image Entry point : 0x08000131

Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000005d4, Max: 0x00080000, ABSOLUTE)

Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x000005d4, Max: 0x00080000, ABSOLUTE)

Exec Addr Load Addr Size Type Attr Idx E Section Name Object

0x08000000 0x08000000 0x00000130 Data RO 3 RESET startup_stm32f10x_hd.o
0x08000130 0x08000130 0x00000000 Code RO 3207 * .ARM.Collect$$$$00000000 mc_w.l(entry.o)
0x08000130 0x08000130 0x00000004 Code RO 3210 .ARM.Collect$$$$00000001 mc_w.l(entry2.o)
0x08000134 0x08000134 0x00000004 Code RO 3213 .ARM.Collect$$$$00000004 mc_w.l(entry5.o)
0x08000138 0x08000138 0x00000000 Code RO 3215 .ARM.Collect$$$$00000008 mc_w.l(entry7b.o)
0x08000138 0x08000138 0x00000000 Code RO 3217 .ARM.Collect$$$$0000000A mc_w.l(entry8b.o)
0x08000138 0x08000138 0x00000008 Code RO 3218 .ARM.Collect$$$$0000000B mc_w.l(entry9a.o)
0x08000140 0x08000140 0x00000000 Code RO 3220 .ARM.Collect$$$$0000000D mc_w.l(entry10a.o)
0x08000140 0x08000140 0x00000000 Code RO 3222 .ARM.Collect$$$$0000000F mc_w.l(entry11a.o)
0x08000140 0x08000140 0x00000004 Code RO 3211 .ARM.Collect$$$$00002712 mc_w.l(entry2.o)
0x08000144 0x08000144 0x00000024 Code RO 4 .text startup_stm32f10x_hd.o
0x08000168 0x08000168 0x00000024 Code RO 3224 .text mc_w.l(init.o)
0x0800018c 0x0800018c 0x00000004 Code RO 3129 i.BusFault_Handler stm32f10x_it.o
0x08000190 0x08000190 0x00000002 Code RO 3130 i.DebugMon_Handler stm32f10x_it.o
0x08000192 0x08000192 0x00000012 Code RO 3108 i.Delay main.o
0x080001a4 0x080001a4 0x00000116 Code RO 1292 i.GPIO_Init stm32f10x_gpio.o
0x080002ba 0x080002ba 0x00000004 Code RO 1300 i.GPIO_SetBits stm32f10x_gpio.o
0x080002be 0x080002be 0x00000004 Code RO 3131 i.HardFault_Handler stm32f10x_it.o
0x080002c2 0x080002c2 0x00000002 PAD
0x080002c4 0x080002c4 0x00000060 Code RO 3192 i.LED_GPIO_Config bsp_led.o
0x08000324 0x08000324 0x00000004 Code RO 3132 i.MemManage_Handler stm32f10x_it.o
0x08000328 0x08000328 0x00000002 Code RO 3133 i.NMI_Handler stm32f10x_it.o
0x0800032a 0x0800032a 0x00000002 Code RO 3134 i.PendSV_Handler stm32f10x_it.o
0x0800032c 0x0800032c 0x00000020 Code RO 1710 i.RCC_APB2PeriphClockCmd stm32f10x_rcc.o
0x0800034c 0x0800034c 0x00000002 Code RO 3135 i.SVC_Handler stm32f10x_it.o
0x0800034e 0x0800034e 0x00000008 Code RO 24 i.SetSysClock system_stm32f10x.o
0x08000356 0x08000356 0x00000002 PAD
0x08000358 0x08000358 0x000000e0 Code RO 25 i.SetSysClockTo72 system_stm32f10x.o
0x08000438 0x08000438 0x00000002 Code RO 3136 i.SysTick_Handler stm32f10x_it.o
0x0800043a 0x0800043a 0x00000002 PAD
0x0800043c 0x0800043c 0x00000060 Code RO 27 i.SystemInit system_stm32f10x.o
0x0800049c 0x0800049c 0x00000004 Code RO 3137 i.UsageFault_Handler stm32f10x_it.o
0x080004a0 0x080004a0 0x0000000e Code RO 3228 i.__scatterload_copy mc_w.l(handlers.o)
0x080004ae 0x080004ae 0x00000002 Code RO 3229 i.__scatterload_null mc_w.l(handlers.o)
0x080004b0 0x080004b0 0x0000000e Code RO 3230 i.__scatterload_zeroinit mc_w.l(handlers.o)
0x080004be 0x080004be 0x00000002 PAD
0x080004c0 0x080004c0 0x00000104 Code RO 3109 i.main main.o
0x080005c4 0x080005c4 0x00000010 Data RO 3226 Region$$Table anon$$obj.o


Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x080005d4, Size: 0x00000400, Max: 0x00010000, ABSOLUTE)

Exec Addr Load Addr Size Type Attr Idx E Section Name Object

0x20000000 - 0x00000400 Zero RW 1 STACK startup_stm32f10x_hd.o


==============================================================================

工程的存储器映像索引分为 ER_IROM1 及 RW_IRAM1 部分,它们分别对应STM32内部FLASH及SRAM的空间。相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型及属性,由此可以区分Code、RO-data、RW-data及ZI-data。

  • (1)加载域和执行域

我们先来看一下这两个部分:

image-20230529211803575

框中圈出来的,翻译过来就是加载区域和执行区域,加载区域基地址是0x08000000,最大大小为0x0008000,换算一下就是512KB,这就是我们整个内部FLASH,执行区域的基地址为0x08000000,最大也是0x0008000,中间的Size就是两者实际的大小,这两个一般是不一样大的,一般来讲,执行区域的大小是要小于加载区域的大小的,因为RW section在运行的时候会被复制到内部SRAM中去,比如说全局变量就属于这一部分,我们可以定义一个较大的数组让,后再main.c中更改一下其中的数据然后重新编译,应该就会发现执行区域和加载区域大小发生了变化,必去我们定义一个大小为10字节的全局数组:

image-20230529213119774

然后我们用执行域大小减去加载域大小,会发现值为0xc,其实已经不仅仅是10字节了,这里就是12个字节,具体还有什么就触及知识盲区了,后边知道了再补充吧。但是我们发现在MDK输出信息窗口的RW-data的大小为12字节,其实这里执行域大小-加载域大小应该就是等于RW-data的大小,因为这一部分会被复制到SRAM运行。

image-20230529213416563
  • (2)最后两部分

例如,从上面的表中我们可以看到i.LED_GPIO_Config节区存储在内部FLASH的0x080002c4地址,大小为0x00000060,类型为Code,属性为RO。

image-20230529210432888

而程序的STACK节区(栈空间)存储在SRAM的0x20000000地址,大小为0x00000400,类型为Zero,属性为RW(即RW-data):

image-20230529210513865

4.3.5 映像组件大小

map文件的最后一部分是包含映像组件大小的信息(Image component sizes),这也是最常查询的内容:

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
// 映像组件大小部分(流水灯.map文件)
Image component sizes

Code (inc. data) RO Data RW Data ZI Data Debug Object Name

96 6 0 0 0 658 bsp_led.o
0 0 0 0 0 4524 core_cm3.o
278 8 0 0 0 1779 main.o
36 8 304 0 1024 972 startup_stm32f10x_hd.o
282 0 0 0 0 2887 stm32f10x_gpio.o
26 0 0 0 0 5038 stm32f10x_it.o
32 6 0 0 0 701 stm32f10x_rcc.o
328 28 0 0 0 214261 system_stm32f10x.o
----------------------------------------------------------------------
1084 56 320 0 1024 230820 Object Totals
0 0 16 0 0 0 (incl. Generated)
6 0 0 0 0 0 (incl. Padding)
// 中间部分省略
==============================================================================
Code (inc. data) RO Data RW Data ZI Data Debug

1172 72 320 0 1024 230312 Grand Totals
1172 72 320 0 1024 230312 ELF Image Totals
1172 72 320 0 0 0 ROM Totals
==============================================================================
Total RO Size (Code + RO Data) 1492 ( 1.46kB)
Total RW Size (RW Data + ZI Data) 1024 ( 1.00kB)
Total ROM Size (Code + RO Data + RW Data) 1492 ( 1.46kB)
==============================================================================

这部分包含了各个使用到的*.o文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,它们分类描述了具体占据的Code、RO-data、RW-data及ZI-data的大小,并根据这些大小统计出占据的ROM总空间。我们仅来看一看分析最后两部分信息:

image-20230529210631552

如Grand Totals一项,它表示整个代码占据的所有空间信息,其中Code类型的数据大小为1172字节,这部分包含了72字节的指令数据(inc .data)已算在内,另外RO-data占320字节,RW-data占0字节,ZI-data占1024字节。在它的下面两行有一项ROM Totals信息,它列出了各个段所占据的ROM空间,除了ZI-data不占ROM空间外,其余项都与Grand Totals中相等(RW-data也占据ROM空间,只是本工程中没有RW-data类型的数据而已)。

最后一部分列出了只读数据(RO)、可读写数据(RW)及占据的ROM大小。其中只读数据大小为1492字节,它包含Code段及RO-data段; 可读写数据大小为1024字节,它包含RW-data及ZI-data段;占据的ROM大小为1492字节,它除了Code段和RO-data段,还包含了运行时需要从ROM加载到RAM的RW-data数据(本工程中RW-data数据为0字节)。

4.3.6 总结

综合整个map文件的信息,可以分析出,当程序下载到STM32的内部FLASH时,需要使用的内部FLASH是从0x0800 0000地址开始的大小为1492字节的空间;当程序运行时,需要使用的内部SRAM是从0x20000000地址开始的大小为1024字节的空间。

粗略一看,发现这个小程序竟然需要1024字节的SRAM,但仔细分析map文件后,可了解到这1024字节都是STACK节区的空间(即栈空间),栈空间大小是在启动文件中定义的,这1024字节是默认值(0x00000400)。它是提供给C语言程序局部变量申请使用的空间,若我们确认自己的应用程序不需要这么大的栈,完全可以修改启动文件,把它改小一点,查看前面讲解的htm静态调用图文件可了解静态的栈调用情况,可以用它作为参考。

二、SCT分散加载文件

1. sct分散加载文件简介

当工程按默认配置构建时,MDK会根据我们选择的芯片型号,获知芯片的内部FLASH及内部SRAM存储器概况,生成一个以工程名命名的后缀为*.sct的分散加载文件(Linker Control File,scatter loading),链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。例如,我们可以设置源文件中定义的所有变量自动按地址分配到外部SRAM,这样就不需要再使用关键字“__attribute__”按具体地址来指定了。

利用它还可以控制代码的加载区与执行区的位置,例如可以把程序代码存储到单位容量价格便宜的NAND-FLASH中,但在NAND-FLASH中的代码是不能像内部FLASH的代码那样直接提供给内核运行的,这时可通过修改分散加载文件,把代码加载区设定为NAND-FLASH的程序位置,而程序的执行区设定为SRAM中的位置,这样链接器就会生成一个配套的分散加载代码,该代码会把NAND-FLASH中的代码加载到SRAM中,内核再从SRAM中运行主体代码,大部分运行Linux系统的代码都是这样加载的。

2. 分散加载文件的格式

2.1 格式说明

下面先来看看MDK默认使用的sct文件,在Output目录下可找到“流水灯.sct”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 默认的分散加载文件内容(“流水灯.sct”)
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00080000 { ; load region size_region 加载域,基地址 空间大小
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address 载地址 = 执行地址
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00010000 { ; RW data 可读写数据
.ANY (+RW +ZI)
}
}

在默认的sct文件配置中仅分配了Code、RO-data、RW-data及ZI-data这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。

sct文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同 等级的域之间使用花括号“{ }”分隔开,最外层的是加载域,第二层“{ }”内的是执行域,其整体结构如下:

分散加载文件的整体结构

2.2 加载域

sct文件的加载域格式如下:

1
2
3
4
5
//方括号中的为选填内容
加载域名 (基地址 | ("+" 地址偏移)) [属性列表] [最大容量]
"{"
执行区域描述+
"}"

配合前面的分散加载文件内容,各部分介绍如下:

  • 加载域名:名称,在map文件中的描述会使用该名称来标识空间。如本例中只有一个加载域,该域名为LR_IROM1。
  • 基地址+地址偏移:这部分说明了本加载域的基地址,可以使用+号连接一个地址偏移,算进基地址中, 整个加载域以它们的结果为基地址。如本例中的加载域基地址为0x08000000,刚好是STM32内部FLASH的基地址。
  • 属性列表:属性列表说明了加载域的是否为绝对地址、N字节对齐等属性,该配置是可选的。本例中没有描述加载域的属性。
  • 最大容量:最大容量说明了这个加载域可使用的最大空间,该配置也是可选的,如果加上这个配置后, 当链接器发现工程要分配到该区域的空间比容量还大,它会在工程构建过程给出提示。本例中的加载域最大容量为0x00080000,即512KB,正是本型号STM32内部FLASH的空间大小,比如我们要是定义一个全局变量,一下子超过了512KB,空间不足的时候,编译器就会报错。

2.3 执行域

sct文件的执行域格式:

1
2
3
4
5
//方括号中的为选填内容
执行域名 (基地址 | "+" 地址偏移) [属性列表] [最大容量 ]
"{"
输入节区描述
"}"

执行域的格式与加载域是类似的,区别只是输入节区的描述有所不同,在上边流水灯.sct例子中包含了ER_IROM1及RW_IRAM两个执行域,它们分别对应描述了STM32的 内部FLASH及内部SRAM的基地址及空间大小。而它们内部的“输入节区描述”说明了哪些节区要存储到这些空间,链接器会根据它来处理编排这些节区。

2.4 输入节区描述

配合加载域及执行域的配置,在相应的域配置“输入节区描述”即可控制该节区存储到域中,其格式如下:

1
2
3
4
5
6
//除模块选择样式部分外,其余部分都可选选填
模块选择样式"("输入节区样式",""+"输入节区属性")"
模块选择样式"("输入节区样式",""+"节区特性")"

模块选择样式"("输入符号样式",""+"节区特性")"
模块选择样式"("输入符号样式",""+"输入节区属性")"

配合前面流水灯.sct中的分散加载文件内容,各部分介绍如下:

  • 模块选择样式:模块选择样式可用于选择o及lib目标文件作为输入节区,它可以直接使用目标文件名或“*”通配符, 也可以使用“.ANY”。例如,使用语句“bsp_led.o”可以选择bsp_led.o文件,使用语句“*.o”可以选择所有o文件,使用“*.lib”可以选择所有lib文件,使用“*”或“.ANY”可以选择所有的o文件及lib文件。其中“.ANY”选择语句的优先级是最低的,所有其它选择语句选择完剩下的数据才会被“.ANY”语句选中。

  • 输入节区样式:我们知道在目标文件中会包含多个节区或符号,通过输入节区样式可以选择要控制的节区。示例文件中“(RESET,+First)”语句的RESET就是输入节区样式,它选择了名为RESET的节区,并使用后面介绍的节区特性控制字“+First”表示它要存储到本区域的第一个地址。示例文件中的“*(InRoot$$Sections)”是一个链接器支持的特殊选择符号,它可以选择所有HAL库里要求存储到root区域的节区,如__main.o、__scatter*.o等内容。

  • 输入符号样式:同样地,使用输入符号样式可以选择要控制的符号,符号样式需要使用“: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(校验和)之类的数据会放在尾部。例如流水灯.sct文件中使用“(RESET,+First)”选择了RESET节区,并要求把它放置到本区域第一个位置,而RESET是工程启动代码中定义的向量表,该向量表中定义的堆栈顶和复位向量指针必须要存储在内部FLASH的前两个地址,这样STM32才能正常启动,所以必须使用FIRST控制它们存储到首地址。
1
2
3
4
5
6
7
8
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size

__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler

总的来说,我们的sct示例文件配置如下:程序的加载域为内部FLASH的0x08000000,最大空间为0x00080000;程序的执行基地址与加载基地址相同,其中RESET节区定义的向量表要存储在内部FLASH的首地址,且所有o文件及lib文件的RO属性内容都存储在内部FLASH中;程序执行时RW及ZI区域都存储在以0x20000000为基地址,大小为0x00010000的空间(64KB),这部分正好是STM32内部主SRAM的大小。

链接器根据sct文件链接,链接后各个节区、符号的具体地址信息可以在map文件中查看。需要注意,当有多个执行域的时候,STM32在分配变量的时候会优先选择容量较大的执行域进行分配,除非我们直接指定了某些文件必须在某个执行域。

3. 通过MDK配置选项来修改sct文件

了解sct文件的格式后,可以手动编辑该文件控制整个工程的分散加载配置,但sct文件格式比较复杂,所以MDK提供了相应的配置选项可以方便地修改该文件,这些选项配置能满足基本的使用需求。

3.1 选择sct文件的产生方式

首先需要选择sct文件产生的方式,选择使用MDK生成还是使用用户自定义的sct文件。在MDK的“【Options for Target】→【Linker】→【Use Memory Layout from Target Dialog】”选项即可配置该选择:

image-20230416150518915

该选项为“是否使用Target对话框中的存储器分布配置”,勾选后,它会根据“Options for Target”对话框中的选项生成sct文件,这种情况下,即使我们手动打开它生成的sct文件编辑也是无效的,因为每次构建工程的时候,MDK都会生成新的sct文件覆盖旧文件。该选项在MDK中是默认勾选的,若希望MDK使用我们手动编辑的sct文件构建工程,需要取消勾选,并通过Scatter File框中指定sct文件的路径:

image-20230416150619026

3.2 通过Target对话框控制存储器分配

若我们在Linker中勾选了“使用Target对话框的存储器布局”选项,那么“Options for Target”对话框中的存储器配置就生效了。主要配置是在Device标签页中选择芯片的类型,设定芯片基本的内部存储器信息以及在Target标签页中细化具体的 存储器配置(包括外部存储器):

image-20230416150742032

图中Device标签页中选定了芯片的型号为STM32F103ZE,选中后,在Target标签页中的存储器信息会根据芯片更新。

Target对话框中的存储器分配

在Target标签页中存储器信息分成只读存储器(Read/Only Memory Areas)和可读写存储器(Read/Write Memory Areas)两类,即ROM和RAM,而且它们又细分成了片外存储器(off-chip)和片内存储器(on-chip)两类。

例如,由于我们已经选定了芯片的型号,MDK会自动根据芯片型号填充片内的ROM及RAM信息,其中的IROM1起始地址为0x80000000,大小为0x80000,正是该STM32型号的内部FLASH地址及大小;而IRAM1起始地址为0x20000000,大小为0x10000,正是该STM32内部主SRAM的地址及大小。图中的IROM1及IRAM1前面都打上了勾,表示这个配置信息会被采用,若取消勾选,则该存储配置信息是不会被使用的。

在某些芯片,会有多个内部SRAM空间,如STM32F429系列。它会在标签页中的IRAM2一栏默认也填写了配置信息,设置STM32F4系列特有的内部高速SRAM(被称为CCM)。

而如果希望设置外部SRAM空间,可以把外部SRAM的信息写到对话框里“off-chip”的“RAM1”配置中。

下面我们尝试修改Target标签页中的这些存储信息,例如,把STM32内部的SRAM分成两等份,按照下图中的配置,把IRAM1的基地址设置为0x20000000,大小改为0x8000,把IRAM2的基地址设置为0x20008000,大小为0x8000:

image-20230416151336450

然后编译工程, 查看到工程的sct文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00008000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x20008000 0x00008000 {
.ANY (+RW +ZI)
}
}

虽然修改后IRAM1和IRAM2加起来还是原来的内部SRAM空间,但它演示了对Target选项的修改是如何影响sct文件的,我们也可以尝试其它配置,观察sct文件,以学习sct文件的语法。可以发现,sct文件根据Target标签页做出了相应的改变,除了这种修改外,在Target标签页上还控制同时使用IRAM1和IRAM2、加入外部RAM(如外接的SRAM),外部FLASH等。当RW_IRAM1空间用完后,会自动使用RW_IRAM2。

3.3 控制文件分配到指定的存储空间

3.3.1 操作步骤

设定好存储器的信息后,可以控制各个源文件定制到哪个部分存储器,在MDK的工程文件栏中,选中要配置的文件,右键,并在弹出的菜单中选择“Options for File xxxx”即可弹出一个文件配置对话框,在该对话框中进行存储器定制,如下图,使用右键打开文件配置并把它的RW区配置成使用IRAM2:

图 40‑52 使用右键打开文件配置并把它的RW区配置成使用IRAM2

在弹出的对话框中有一个“Memory Assignment”区域(存储器分配),在该区域中可以针对文件的各种属性内容进行分配,如Code/Const内容(RO)、Zero Initialized Data内容(ZI-data)以及Other Data内容(RW-data),点击下拉菜单可以找到在前面Target页面配置的IROM1、IRAM1、IRAM2等存储器。例如图中我们把这个bsp_led.c文件的Other Data属性的内容分配到了IRAM2存储器(在Target标签页中我们勾选了IRAM1及IRAM2),当在bsp_led.c文件定义了一些RW-data内容时(如初值非0的全局变量), 该变量将会被分配到IRAM2空间,配置完成后点击OK,然后编译工程,查看到的sct文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00008000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x20008000 0x00008000 {
bsp_led.o (+RW)
.ANY (+RW +ZI)
}
}

可以看到在sct文件中的RW_IRAM2执行域中增加了一个选择bsp_led.o中RW内容的语句。类似地,我们还可以设置某些文件的代码段被存储到特定的ROM中,或者设置某些文件使用的ZI-data或RW-data存储到外部SRAM中(控制ZI-data到SDRAM时注意还需要修改启动文件设置堆栈对应的地址,原启动文件中的地址是指向内部SRAM的)。

虽然MDK的这些存储器配置选项很方便,但有很多高级的配置还是需要手动编写sct文件实现的,例如MDK选项中的内部ROM选项最多只可以填充两个选项位置,若想把内部ROM分成多片地址管理就无法实现了;另外MDK配置可控的最小粒度为文件,若想控制特定的节区也需要直接编辑sct文件。

不过这个并不能体现出我们定义的一些东西确实被分配到了RW_TRAM2。

【注意】这里我们要是修改了某一个c文件中数据在执行域的位置的话,会有这样的变化,文件左下角会有一个雪花的符号。

image-20230530093355921

3.3.2 一个实例

  • (1)我们在main.c中定一个全局数组,并在main.c中使用一下,防止被编译器优化
image-20230529223111082
  • (2)我们编译一下,查看map文件中这个全局数组的地址(在Image Symbol Table部分)
image-20230529223325533

可以看到这个数组是存储在0x20008000这个位置的,也就是说这个变量被定义在了RW_IRAM1区域。

1
2
3
RW_IRAM1 0x20000000 0x00008000  {  ; RW data
.ANY (+RW +ZI)
}
  • (3)我们通过MDK的Target面板修改sct文件如下:
image-20230529223538076

我们重新编译后,生成的sct文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00008000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x20008000 0x00008000 {
.ANY (+RW +ZI)
}
}
  • (4)通过【main.c】→【右键】→【Options for File main.c】→【OtherData】→【IRAM2 [0x20008000-0x2000FFFF]】
image-20230529223843189

然后我们重新编译工程,生成的sct文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00008000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x20008000 0x00008000 {
main.o (+RW)
.ANY (+RW +ZI)
}
}

我们会发现RW_IRAM2多了一个 main.o (+RW)。

  • (5)查看map文件,找到test数组的定义地址:
image-20230529224055149

我们发现,这个数组已经被定义到0x20008000为起始的区域中了。