MDK工程的文件类型
除了上述编译过程生成的文件,MDK工程中还包含了各种各样的文件,下面我们统一介绍,MDK工程的常见文件类型见表 13。
表 13 MDK常见的文件类型(不分大小写)
后缀 | 说明 |
---|---|
Project目录下的工程文件 | |
*.uvguix | MDK5工程的窗口布局文件,在MDK4中*.UVGUI后缀的文件功能相同 |
*.uvprojx | MDK5的工程文件,它使用了XML格式记录了工程结构,双击它可以打开整个工程,在MDK4中*.UVPROJ后缀的文件功能相同 |
*.uvoptx | MDK5的工程配置选项,包含debugger、trace configuration、breakpooints以及当前打开的文件,在MDK4中*.UVOPT后缀的文件功能相同 |
*.ini | 某些下载器的配置记录文件 |
源文件 | |
*.c | C语言源文件 |
*.cpp | C++语言源文件 |
*.h | C/C++的头文件 |
*.s | 汇编语言的源文件 |
*.inc | 汇编语言的头文件(使用“$include”来包含) |
Output目录下的文件 | |
*.lib | 库文件 |
*.dep | 整个工程的依赖文件 |
*.d | 描述了对应.o的依赖的文件 |
*.crf | 交叉引用文件,包含了浏览信息(定义、引用及标识符) |
*.o | 可重定位的对象文件(目标文件) |
*.bin | 二进制格式的映像文件,是纯粹的FLASH映像,不含任何额外信息 |
*.hex | Intel Hex格式的映像文件,可理解为带存储地址描述格式的bin文件 |
*.elf | 由GCC编译生成的文件,功能跟axf文件一样,该文件不可重定位 |
*.axf | 由ARMCC编译生成的可执行对象文件,可用于调试,该文件不可重定位 |
*.sct | 链接器控制文件(分散加载) |
*.scr | 链接器产生的分散加载文件 |
*.lnp | MDK生成的链接输入文件,用于调用链接器时的命令输入 |
*.htm | 链接器生成的静态调用图文件 |
*.build_log.htm | 构建工程的日志记录文件 |
Listing目录下的文件 | |
*.lst | C及汇编编译器产生的列表文件 |
*.map | 链接器生成的列表文件,包含存储器映像分布 |
其它 | |
*.ini | 仿真、下载器的脚本文件 |
这些文件主要分为MDK相关文件、源文件以及编译、链接器生成的文件。我们以“多彩流水灯”工程为例讲解各种文件的功能。
uvprojx、uvoptx、uvguix及ini工程文件
在工程的“Project”目录下主要是MDK工程相关的文件,见图 117。
图 117 Project目录下的uvprojx、uvoptx、uvguix及ini文件
uvprojx文件
uvprojx文件就是我们平时双击打开的工程文件,它记录了整个工程的结构,如芯片类型、工程包含了哪些源文件等内容,见图 118。
图 118 工程包含的文件、芯片类型等内容
uvoptx文件
uvoptx文件记录了工程的配置选项,如下载器的类型、变量跟踪配置、断点位置以及当前已打开的文件等等,见图 119。
图 119 代码编辑器中已打开的文件
uvprojx文件
uvguix文件记录了MDK软件的GUI布局,如代码编辑区窗口的大小、编译输出提示窗口的位置等等。
图 120 记录MDK工作环境中各个窗口的大小
uvprojx、uvoptx及uvguix都是使用XML格式记录的文件,若使用记事本打开可以看到XML代码,见图 117。而当使用MDK软件打开时,它根据这些文件的XML记录加载工程的各种参数,使得我们每次重新打开工程时,都能恢复上一次的工作环境。
图 121 使用记事本打开uvprojx、uvoptx及uvguix文件可看到XML格式的记录
这些工程参数都是当MDK正常退出时才会被写入保存,所以若MDK错误退出时(如使用Windows的任务管理器强制关闭),工程配置参数的最新更改是不会被记录的,重新打开工程时要再次配置。根据这几个文件的记录类型,可以知道uvprojx文件是最重要的,删掉它我们就无法再正常打开工程了,而uvoptx及uvguix文件并不是必须的,可以删除,重新使用MDK打开uvprojx工程文件后,会以默认参数重新创建uvoptx及uvguix文件。(所以当使用Git/SVN等代码管理的时候,往往只保留uvprojx文件)
源文件
源文件是工程中我们最熟悉的内容了,它们就是我们编写的各种源代码,MDK支持c、cpp、h、s、inc类型的源代码文件,其中c、cpp分别是c/c++语言的源代码,h是它们的头文件,s是汇编文件,inc是汇编文件的头文件,可使用“$include”语法包含。编译器根据工程中的源文件最终生成机器码。
Output目录下生成的文件
点击MDK中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件,在工程的“Options for Targe->Output->Select Folder for Objects”和“Options for Targe->Listing->Select Folder for Listings”选项配置它们的输出路径,见图 122和图 123。
图 122 设置Output输出路径
图 123设置Listing输出路径
编译后Output和Listing目录下生成的文件见图 124。
图 124 编译后Output及Listing文件夹中的内容
接下来我们讲解Output路径下的文件。
lib库文件
在某些场合下我们不希望提供给第三方一个可用的代码库,但不希望对方看到源码,这个时候我们就可以把工程生成lib文件(Library file)提供给对方,在MDK中可配置“Options for Target->Create Library”选项把工程编译成库文件,见图 125。
图 125 生成库文件或可执行文件
工程中生成可执行文件或库文件只能二选一,默认编译是生成可执行文件的,可执行文件即我们下载到芯片上直接运行的机器码。
得到生成的.lib文件后,可把它像C文件一样添加到其它工程中,并在该工程调用lib提供的函数接口,除了不能看到.lib文件的源码,在应用方面它跟C源文件没有区别。
dep、d依赖文件
.dep和.d文件(Dependency file)记录的是工程或其它文件的依赖,主要记录了引用的头文件路径,其中.dep是整个工程的依赖,它以工程名命名,而.d是单个源文件的依赖,它们以对应的源文件名命名。这些记录使用文本格式存储,我们可直接使用记事本打开,见图 126和图 127。
图 126 工程的dep文件内容
图 127 bsp_led.d文件的内容
crf交叉引用文件
*.crf是交叉引用文件(Cross-Reference file),它主要包含了浏览信息(browse information),即源代码中的宏定义、变量及函数的定义和声明的位置。
我们在代码编辑器中点击“Go To Definition Of ‘xxxx’”可实现浏览跳转,见图 128,跳转的时候,MDK就是通过*.crf文件查找出跳转位置的。
图 128 浏览信息
通过配置MDK中的“Option for Target->Output->Browse Information”选项可以设置编译时是否生成浏览信息,见图 129。只有勾选该选项并编译后,才能实现上面的浏览跳转功能。
图 129 在Options forTarget中设置是否生成浏览信息
*.crf文件使用了特定的格式表示,直接用文本编辑器打开会看到大部分乱码,见图 130,我们不作深入研究。
图 130 crf文件内容
o、axf及elf文件
.o、.elf、.axf、.bin及*.hex文件都存储了编译器根据源代码生成的机器码,根据应用场合的不同,它们又有所区别。
ELF文件说明
.o、.elf、*.axf以及前面提到的lib文件都是属于目标文件,它们都是使用ELF格式来存储的,关于ELF格式的详细内容请参考配套资料里的《ELF文件格式》文档了解,它讲解的是Linux下的ELF格式,与MDK使用的格式有小区别,但大致相同。在本教程中,仅讲解ELF文件的核心概念。
ELF是Executable and Linking Format的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。在Linux及Windows系统下都有使用该格式的文件(或类似格式)用于记录应用程序的内容,告诉操作系统如何链接、加载及执行该应用程序。
目标文件主要有如下三种类型:
- 可重定位的文件(Relocatable File),包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。
例如MDK的armcc和armasm生成的.o文件就是这一类,另外还有Linux的.o 文件,Windows的 *.obj文件。
- 可执行文件(Executable File) ,它包含适合于执行的程序,它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。
例如MDK的armlink生成的.elf及.axf文件,(使用gcc编译工具可生成.elf文件,用armlink生成的是.axf文件,.axf文件在.elf之外,增加了调试使用的信息,其余区别不大,后面我们仅讲解.axf文件),另外还有Linux的/bin/bash文件,Windows的.exe文件。
- 共享目标文件(Shared Object File), 它的定义比较难理解,我们直接举例,MDK生成的*.lib文件就属于共享目标文件,它可以继续参与链接,加入到可执行文件之中。另外,Linux的.so,如/lib/ glibc-2.5.so,Windows的DLL都属于这一类。
o文件与axf文件的关系
根据上面的分类,我们了解到,.axf文件是由多个.o文件链接而成的,而.o文件由相应的源文件编译而成,一个源文件对应一个.o文件。它们的关系见图 131。
图 131.axf文件与.o文件的关系
图中的中间代表的是armlink链接器,在它的右侧是输入链接器的.o文件,左侧是它输出的axf文件。
可以看到,由于都使用ELF文件格式,.o与.axf文件的结构是类似的,它们包含ELF文件头、程序头、节区(section)以及节区头部表。各个部分的功能说明如下:
- ELF文件头用来描述整个文件的组织,例如数据的大小端格式,程序头、节区头在文件中的位置等。
- 程序头告诉系统如何加载程序,例如程序主体存储在本文件的哪个位置,程序的大小,程序要加载到内存什么地址等等。MDK的可重定位文件.o不包含这部分内容,因为它还不是可执行文件,而armlink输出的.axf文件就包含该内容了。
- 节区是.o文件的独立数据区域,它包含提供给链接视图使用的大量信息,如指令(Code)、数据(RO、RW、ZI-data)、符号表(函数、变量名等)、重定位信息等,例如每个由C语言定义的函数在.o文件中都会有一个独立的节区;
- 存储在最后的节区头则包含了本文件节区的信息,如节区名称、大小等等。
总的来说,链接器把各个.o文件的节区归类、排列,根据目标器件的情况编排地址生成输出,汇总到.axf文件。例如,见图 132,“多彩流水灯”工程中在“bsp_led.c”文件中有一个LED_GPIO_Config函数,而它内部调用了“stm32f4xx_gpio.c”的GPIO_Init函数,经过armcc编译后,LED_GPIO_Config及GPIO_Iint函数都成了指令代码,分别存储在bsp_led.o及stm32f4xx_gpio.o文件中,这些指令在*.o文件都没有指定地址,仅包含了内容、大小以及调用的链接信息,而经过链接器后,链接器给它们都分配了特定的地址,并且把地址根据调用指向链接起来。
图 132 具体的链接过程
ELF文件头
接下来我们看看具体文件的内容,使用fromelf文件可以查看.o、.axf及*.lib文件的ELF信息。
使用命令行,切换到文件所在的目录,输入“fromelf –text –v bsp_led.o”命令,可控制输出bsp_led.o的详细信息,见图 133。 利用“-c、-z”等选项还可输出反汇编指令文件、代码及数据文件等信息,请亲手尝试一下。
图 133 使用fromelf查看o文件信息
为了便于阅读,我已使用fromelf指令生成了“多彩流水灯.axf”、“bsp_led”及“多彩流水灯.lib”的ELF信息,并已把这些信息保存在独立的文件中,在配套资料的“elf信息输出”文件夹下可查看,见表 14。
表 14 配套资料里使用fromelf生成的文件
fromelf选项 | 可查看的信息 | 生成到配套资料里相应的文件 |
---|---|---|
-v | 详细信息 | bsp_led_o_elfInfo_v.txt/多彩流水灯_axf_elfInfo_v.txt |
-a | 数据的地址 | bsp_led_o_elfInfo_a.txt/多彩流水灯_axf_elfInfo_a.txt |
-c | 反汇编代码 | bsp_led_o_elfInfo_c.txt/多彩流水灯_axf_elfInfo_c.txt |
-d | data section的内容 | bsp_led_o_elfInfo_d.txt/多彩流水灯_axf_elfInfo_d.txt |
-e | 异常表 | bsp_led_o_elfInfo_e.txt/多彩流水灯_axf_elfInfo_e.txt |
-g | 调试表 | bsp_led_o_elfInfo_g.txt/多彩流水灯_axf_elfInfo_g.txt |
-r | 重定位信息 | bsp_led_o_elfInfo_r.txt/多彩流水灯_axf_elfInfo_r.txt |
-s | 符号表 | bsp_led_o_elfInfo_s.txt/多彩流水灯_axf_elfInfo_s.txt |
-t | 字符串表 | bsp_led_o_elfInfo_t.txt/多彩流水灯_axf_elfInfo_t.txt |
-y | 动态段内容 | bsp_led_o_elfInfo_y.txt/多彩流水灯_axf_elfInfo_y.txt |
-z | 代码及数据的大小信息 | bsp_led_o_elfInfo_z.txt/多彩流水灯_axf_elfInfo_z.txt |
直接打开“elf信息输出”目录下的bsp_led_o_elfInfo_v.txt文件,可看到代码清单 11中的内容。
代码清单 11 bsp_led.o文件的ELF文件头(可到“bsp_led_o_elfInfo_v.txt”文件查看)
1 ========================================================================
2
3 ** ELF Header Information
4
5 File Name:
6 .\bsp_led.o //bsp_led.o文件
7
8 Machine class: ELFCLASS32 (32-bit) //32位机
9 Data encoding: ELFDATA2LSB (Little endian) //小端格式
10 Header version: EV_CURRENT (Current version)
11 Operating System ABI: none
12 ABI Version: 0
13 File Type: ET_REL (Relocatable object) (1) //可重定位文件类型
14 Machine: EM_ARM (ARM)
15
16 Entry offset (in SHF_ENTRYSECT section): 0x00000000
17 Flags: None (0x05000000)
18
19 ARM ELF revision: 5 (ABI version 2)
20
21 Built with
22 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]
23 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]
24
25 Header size: 52 bytes (0x34)
26 Program header entry size: 0 bytes (0x0) //程序头大小
27 Section header entry size: 40 bytes (0x28)
28
29 Program header entries: 0
30 Section header entries: 246
31
32 Program header offset: 0 (0x00000000) //程序头在文件中的位置(没有程序头)
33 Section header offset: 507224 (0x0007bd58) //节区头在文件中的位置
34
35 Section header string table index: 243
36
37 =====================================================================
在上述代码中已加入了部分注释,解释了相应项的意义,值得一提的是在这个*.o文件中,它的ELF文件头中告诉我们它的程序头(Program header)大小为“0 bytes”,且程序头所在的文件位置偏移也为“0”,这说明它是没有程序头的。
程序头
接下来打开“多彩流水灯_axf_elfInfo_v.txt”文件,查看工程的*.axf文件的详细信息,见代码清单 12。
代码清单 12 *.axf文件中的elf文件头及程序头(可到“多彩流水灯_axf_elfInfo_v.txt”文件查看)
1 ===================================================================
2
3 ** ELF Header Information
4
5 File Name:
6 .\多彩流水灯.axf //多彩流水灯.axf 文件
7
8 Machine class: ELFCLASS32 (32-bit) //32位机
9 Data encoding: ELFDATA2LSB (Little endian) //小端格式
10 Header version: EV_CURRENT (Current version)
11 Operating System ABI: none
12 ABI Version: 0
13 File Type: ET_EXEC (Executable) (2) //可执行文件类型
14 Machine: EM_ARM (ARM)
15
16 Image Entry point: 0x080001ad
17 Flags: EF_ARM_HASENTRY + EF_ARM_ABI_FLOAT_SOFT (0x05000202)
18
19 ARM ELF revision: 5 (ABI version 2)
20
21 Conforms to Soft float procedure-call standard
22
23 Built with
24 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]
25 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]
26
27 Header size: 52 bytes (0x34)
28 Program header entry size: 32 bytes (0x20)
29 Section header entry size: 40 bytes (0x28)
30
31 Program header entries: 1
32 Section header entries: 15
33
34 Program header offset: 335252 (0x00051d94) //程序头在文件中的位置
35 Section header offset: 335284 (0x00051db4) //节区头在文件中的位置
36
37 Section header string table index: 14
38
39 =================================================================
40
41 ** Program header #0
42
43 Type : PT_LOAD (1) //表示这是可加载的内容
44 File Offset : 52 (0x34) //在文件中的偏移
45 Virtual Addr : 0x08000000 //虚拟地址(此处等于物理地址)
46 Physical Addr : 0x08000000 //物理地址
47 Size in file : 1456 bytes (0x5b0) //程序在文件中占据的大小
48 Size in memory: 2480 bytes (0x9b0) //若程序加载到内存,占据的内存空间
49 Flags : PF_X + PF_W + PF_R + PF_ARM_ENTRY (0x80000007)
50 Alignment : 8 //地址对齐
51
52
53 ===============================================================
对比之下,可发现*.axf文件的ELF文件头对程序头的大小说明为非0值,且给出了它在文件的偏移地址,在输出信息之中,包含了程序头的详细信息。可看到,程序头的“Physical Addr”描述了本程序要加载到的内存地址“0x0800 0000”,正好是STM32内部FLASH的首地址;“size in file”描述了本程序占据的空间大小为“1456 bytes”,它正是程序烧录到FLASH中需要占据的空间。
节区头
在ELF的原文件中,紧接着程序头的一般是节区的主体信息,在节区主体信息之后是描述节区主体信息的节区头,我们先来看看节区头中的信息了解概况。通过对比.o文件及.axf文件的节区头部信息,可以清楚地看出这两种文件的区别,见代码清单 13。
代码清单 13 *.o文件的节区信息(“bsp_led_o_elfInfo_v.txt”文件)
1 ====================================
2 ** Section #4
3
4 Name : i.LED_GPIO_Config //节区名
6
7 //此节区包含程序定义的信息,其格式和含义都由程序来解释。
8 Type : SHT_PROGBITS (0x00000001)
10
11 //此节区在进程执行过程中占用内存。 节区包含可执行的机器指令。
12 Flags :SHF_ALLOC + SHF_EXECINSTR (0x00000006)
14 Addr : 0x00000000 //地址
15 File Offset : 68 (0x44) //在文件中的偏移
16 Size : 116 bytes (0x74) //大小
17 Link : SHN_UNDEF
19 Info : 0
20 Alignment : 4 //字节对齐
21 Entry Size : 0
22 ====================================
这个节区的名称为LED_GPIO_Config,它正好是我们在bsp_led.c文件中定义的函数名,这个节区头描述的是该函数被编译后的节区信息,其中包含了节区的类型(指令类型)、节区应存储到的地址(0x00000000)、它主体信息在文件位置中的偏移(68)以及节区的大小(116 bytes)。
由于.o文件是可重定位文件,所以它的地址并没有被分配,是0x00000000(假如文件中还有其它函数,该函数生成的节区中,对应的地址描述也都是0)。当链接器链接时,根据这个节区头信息,在文件中找到它的主体内容,并根据它的类型,把它加入到主程序中,并分配实际地址,链接后生成的.axf文件,我们再来看看它的内容,见代码清单 14。
代码清单 14 *.axf文件的节区信息(“多彩流水灯_axf_elfInfo_v.txt”文件)
1 ========================================================================
2 ** Section #1
3
4 Name : ER_IROM1 //节区名
5
6 //此节区包含程序定义的信息,其格式和含义都由程序来解释。
7 Type : SHT_PROGBITS (0x00000001)
8
9 //此节区在进程执行过程中占用内存。 节区包含可执行的机器指令
10 Flags : SHF_ALLOC + SHF_EXECINSTR (0x00000006)
11 Addr : 0x08000000 //地址
12 File Offset : 52 (0x34)
13 Size : 1456 bytes (0x5b0) //大小
14 Link : SHN_UNDEF
15 Info : 0
16 Alignment : 4
17 Entry Size : 0
18
19 ====================================
20 ** Section #2
21
22 Name : RW_IRAM1 //节区名
23
24 //包含将出现在程序的内存映像中的为初始
25 //化数据。 根据定义, 当程序开始执行, 系统
26 //将把这些数据初始化为 0。
27 Type : SHT_NOBITS (0x00000008)
28
29 //此节区在进程执行过程中占用内存。 节区包含进程执行过程中将可写的数据。
30 Flags : SHF_ALLOC + SHF_WRITE (0x00000003)
31 Addr : 0x20000000 //地址
32 File Offset : 1508 (0x5e4)
33 Size : 1024 bytes (0x400) //大小
34 Link : SHN_UNDEF
35 Info : 0
36 Alignment : 8
37 Entry Size : 0
38 ====================================
在.axf文件中,主要包含了两个节区,一个名为ER_IROM1,一个名为RW_IRAM1,这些节区头信息中除了具有.o文件中节区头描述的节区类型、文件位置偏移、大小之外,更重要的是它们都有具体的地址描述,其中 ER_IROM1的地址为0x08000000,而RW_IRAM1的地址为0x20000000,它们正好是内部FLASH及SRAM的首地址,对应节区的大小就是程序需要占用FLASH及SRAM空间的实际大小。
也就是说,经过链接器后,它生成的.axf文件已经汇总了其它.o文件的所有内容,生成的ER_IROM1节区内容可直接写入到STM32内部FLASH的具体位置。例如,前面.o文件中的i.LED_GPIO_Config节区已经被加入到.axf文件的ER_IROM1节区的某地址。
节区主体及反汇编代码
使用fromelf的-c选项可以查看部分节区的主体信息,对于指令节区,可根据其内容查看相应的反汇编代码,打开“bsp_led_o_elfInfo_c.txt”文件可查看这些信息,见代码清单 15。
代码清单 15 *.o文件的LED_GPIO_Config节区及反汇编代码(bsp_led_o_elfInfo_c.txt文件)
1 ** Section #4 'i.LED_GPIO_Config' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
2 Size : 116 bytes (alignment 4)
3 Address: 0x00000000
4
5 $t
6 i.LED_GPIO_Config
7 LED_GPIO_Config
8 // 地址 内容 (ASCII码) 内容对应的代码
9 // (无意义)
10 0x00000000: e92d41fc -..A PUSH {r2-r8,lr}
11 0x00000004: 2101 .! MOVS r1,#1
12 0x00000006: 2088 . MOVS r0,#0x88
13 0x00000008: f7fffffe .... BL RCC_AHB1PeriphClockCmd
14 0x0000000c: f44f6580 O..e MOV r5,#0x400
15 0x00000010: 9500 .. STR r5,[sp,#0]
16 0x00000012: 2101 .! MOVS r1,#1
17 0x00000014: f88d1004 .... STRB r1,[sp,#4]
18 0x00000018: 2000 . MOVS r0,#0
19 0x0000001a: f88d0006 .... STRB r0,[sp,#6]
20 0x0000001e: f88d1007 .... STRB r1,[sp,#7]
21 0x00000022: f88d0005 .... STRB r0,[sp,#5]
22 0x00000026: 4f11 .O LDR r7,[pc,#68] ;
23 0x00000028: 4669 iF MOV r1,sp
24 0x0000002a: 4638 8F MOV r0,r7
25 0x0000002c: f7fffffe .... BL GPIO_Init
26 0x00000030: 006c l. LSLS r4,r5,#1
27 /....以下省略*/
可看到,由于这是.o文件,它的节区地址还是没有分配的,基地址为0x00000000,接着在LED_GPIO_Config标号之后,列出了一个表,表中包含了地址偏移、相应地址中的内容以及根据内容反汇编得到的指令。细看汇编指令,还可看到它包含了跳转到RCC_AHB1PeriphClockCmd及GPIO_Init标号的语句,而且这两个跳转语句原来的内容都是“f7fffffe”,这是因为还.o文件中并没有RCC_AHB1PeriphClockCmd及GPIO_Init标号的具体地址索引,在*.axf文件中,这是不一样的。
接下来我们打开“多彩流水灯_axf_elfInfo_c.txt”文件,查看*.axf文件中,ER_IROM1节区中对应LED_GPIO_Config的内容,见代码清单 16。
代码清单 16*.axf文件的LED_GPIO_Config反汇编代码(多彩流水灯_axf_elfInfo_c.txt文件)
1 i.LED_GPIO_Config
2 LED_GPIO_Config
3 0x080002a4: e92d41fc -..A PUSH {r2-r8,lr}
4 0x080002a8: 2101 .! MOVS r1,#1
5 0x080002aa: 2088 . MOVS r0,#0x88
6 0x080002ac: f000f838 ..8. BL RCC_AHB1PeriphClockCmd ; 0x8000320
7 0x080002b0: f44f6580 O..e MOV r5,#0x400
8 0x080002b4: 9500 .. STR r5,[sp,#0]
9 0x080002b6: 2101 .! MOVS r1,#1
10 0x080002b8: f88d1004 .... STRB r1,[sp,#4]
11 0x080002bc: 2000 . MOVS r0,#0
12 0x080002be: f88d0006 .... STRB r0,[sp,#6]
13 0x080002c2: f88d1007 .... STRB r1,[sp,#7]
14 0x080002c6: f88d0005 .... STRB r0,[sp,#5]
15 0x080002ca: 4f11 .O LDR r7,[pc,#68] ; [0x8000310] = 0x40021c00
16 0x080002cc: 4669 iF MOV r1,sp
17 0x080002ce: 4638 8F MOV r0,r7
18 0x080002d0: f7ffffa5 .... BL GPIO_Init ; 0x800021e
19 0x080002d4: 006c l. LSLS r4,r5,#1
20 /....以下省略*/
可看到,除了基地址以及跳转地址不同之外,LED_GPIO_Config中的内容跟.o文件中的一样。另外,由于.o是独立的文件,而.axf是整个工程汇总的文件,所以在.axf中包含了所有调用到*.o文件节区的内容。例如,在“bsp_led_o_elfInfo_c.txt”(bsp_led.o文件的反汇编信息)中不包含RCC_AHB1PeriphClockCmd及GPIO_Init的内容,而在“多彩流水灯_axf_elfInfo_c.txt” (多彩流水灯.axf文件的反汇编信息)中则可找到它们的具体信息,且它们也有具体的地址空间。
在.axf文件中,跳转到RCC_AHB1PeriphClockCmd及GPIO_Init标号的这两个指令后都有注释,分别是“; 0x8000320”及“; 0x800021e”,它们是这两个标号所在的具体地址,而且这两个跳转语句的跟.o中的也有区别,内容分别为“f000f838e”及“f7ffffa5”(.o中的均为f7fffffe)。这就是链接器链接的含义,它把不同.o中的内容链接起来了。
分散加载代码
学习至此,还有一个疑问,前面提到程序有存储态及运行态,它们之间应有一个转化过程,把存储在FLASH中的RW-data数据拷贝至SRAM。然而我们的工程中并没有编写这样的代码,在汇编文件中也查不到该过程,芯片是如何知道FLASH的哪些数据应拷贝到SRAM的哪些区域呢?
通过查看“多彩流水灯_axf_elfInfo_c.txt”的反汇编信息,了解到程序中具有一段名为“__scatterload”的分散加载代码,见代码清单 17,它是由armlink链接器自动生成的。
代码清单 17 分散加载代码(多彩流水灯_axf_elfInfo_c.txt文件)
1
2 .text
3 __scatterload
4 __scatterload_rt2
5 0x080001e4: 4c06 .L LDR r4,[pc,#24] ; [0x8000200] = 0x80005a0
6 0x080001e6: 4d07 .M LDR r5,[pc,#28] ; [0x8000204] = 0x80005b0
7 0x080001e8: e006 .. B 0x80001f8 ; __scatterload + 20
8 0x080001ea: 68e0 .h LDR r0,[r4,#0xc]
9 0x080001ec: f0400301 @... ORR r3,r0,#1
10 0x080001f0: e8940007 .... LDM r4,{r0-r2}
11 0x080001f4: 4798 .G BLX r3
12 0x080001f6: 3410 .4 ADDS r4,r4,#0x10
13 0x080001f8: 42ac .B CMP r4,r5
14 0x080001fa: d3f6 .. BCC 0x80001ea ; __scatterload + 6
15 0x080001fc: f7ffffda .... BL __main_after_scatterload ; 0x80001b4
16 $d
17 0x08000200: 080005a0 .... DCD 134219168
18 0x08000204: 080005b0 .... DCD 134219184
这段分散加载代码包含了拷贝过程(LDM复制指令),而LDM指令的操作数中包含了加载的源地址,这些地址中包含了内部FLASH存储的RW-data数据。而 “scatterload ”的代码会被“main”函数调用,见代码清单 18,__main在启动文件中的“Reset_Handler”会被调用,因而,在主体程序执行前,已经完成了分散加载过程。
代码清单 18 __main的反汇编代码(部分,多彩流水灯_axf_elfInfo_c.txt文件)
1 __main
2 _main_stk
3 0x080001ac: f8dfd00c .... LDR sp,lit00000000 ; [0x80001bc] = 0x20000400
4 .ARM.Collect$$$$00000004
5 _main_scatterload
6 0x080001b0: f000f818 .... BL __scatterload ; 0x80001e4
hex文件及bin文件
若编译过程无误,即可把工程生成前面对应的.axf文件,而在MDK中使用下载器(DAP/JLINK/ULINK等)下载程序或仿真的时候,MDK调用的就是.axf文件,它解释该文件,然后控制下载器把*.axf中的代码内容下载到STM32芯片对应的存储空间,然后复位后芯片就开始执行代码了。
然而,脱离了MDK或IAR等工具,下载器就无法直接使用*.axf文件下载代码了,它们一般仅支持hex和bin格式的代码数据文件。默认情况下MDK都不会生成hex及bin文件,需要配置工程选项或使用fromelf命令。
生成hex文件
生成hex文件的配置比较简单,在“Options for Target->Output->Create Hex File”中勾选该选项,然后编译工程即可,见图 134。
图 134 生成hex文件的配置
生成bin文件
使用MDK生成bin文件需要使用fromelf命令,在MDK的“Options For Target->Users”中加入图 135中的命令。
图 135 使用fromelf指令生成bin文件
图中的指令内容为:
“fromelf --bin --output ....\Output\多彩流水灯.bin ....\Output\多彩流水灯.axf”
该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它,才能为自己的工程定制指令,首先看看fromelf的帮助,见图 136。
图 136 fromelf的帮助
我们在MDK输入的指令格式是遵守fromelf帮助里的指令格式说明的,其格式为:
“fromelf [options] input_file”
其中optinos是指令选项,一个指令支持输入多个选项,每个选项之间使用空格隔开,我们的实例中使用“--bin”选项设置输出bin文件,使用“--output file”选项设置输出文件的名字为“....\Output\多彩流水灯.bin”,这个名字是一个相对路径格式,如果不了解如何使用“..\”表示路径,可使用MDK命令输入框后面的文件夹图标打开文件浏览器选择文件,在命令的最后使用“....\Output\多彩流水灯.axf”作为命令的输入文件。具体的格式分解见图 137。
图 137 fromelf命令格式分解
fromelf需要根据工程的.axf文件输入来转换得到bin文件,所以在命令的输入文件参数中要选择本工程对应的.axf文件,在MDK命令输入栏中,我们把fromelf指令放置在“After Build/Rebuild”(工程构建完成后执行)一栏也是基于这个考虑,这样设置后,工程构建完成生成了最新的*.axf文件,MDK再执行fromelf指令,从而得到最新的bin文件。
设置完成生成hex的选项或添加了生成bin的用户指令后,点击工程的编译(build)按钮,重新编译工程,成功后可看到图 138中的输出。打开相应的目录即可找到文件,若找不到bin文件,请查看提示输出栏执行指令的信息,根据信息改正fromelf指令。
图 138 fromelf生成hxe及bin文件的提示
其中bin文件是纯二进制数据,无特殊格式,接下来我们了解一下hex文件格式。
hex文件格式
hex是Intel公司制定的一种使用ASCII文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到ROM中的数据,绝大多数下载器支持该格式。
一个hex文件由多条记录组成,而每条记录由五个部分组成,格式形如“:llaaaatt[dd…]cc”,例如本“多彩流水灯”工程生成的hex文件前几条记录见代码清单 19。
代码清单 19 Hex文件实例(多彩流水灯.hex文件,可直接用记事本打开)
1 :020000040800F2
2 :1000000000040020C10100081B030008A30200082F
3 :100010001903000809020008690400080000000034
4 :100020000000000000000000000000003D03000888
5 :100030000B020008000000001D0300081504000862
6 :10004000DB010008DB010008DB010008DB01000820
记录的各个部分介绍如下:
- “:” :每条记录的开头都使用冒号来表示一条记录的开始;
- ll :以16进制数表示这条记录的主体数据区的长度(即后面[dd…]的长度);
- aaaa:表示这条记录中的内容应存放到FLASH中的起始地址;
- tt:表示这条记录的类型,它包含中的各种类型;
表 15 tt值所代表的类型说明
tt的值 | 代表的类型 |
---|---|
00 | 数据记录 |
01 | 本文件结束记录 |
02 | 扩展地址记录 |
04 | 扩展线性地址记录(表示后面的记录按个这地址递增) |
05 | 表示一个线性地址记录的起始(只适用于ARM) |
- dd:表示一个字节的数据,一条记录中可以有多个字节数据,ll区表示了它有多少个字节的数据;
- cc:表示本条记录的校验和,它是前面所有16进制数据 (除冒号外,两个为一组)的和对256取模运算的结果的补码。
例如,代码清单 19中的第一条记录解释如下:
- 02:表示这条记录数据区的长度为2字节;
- 0000:表示这条记录要存储到的地址;
- 04:表示这是一条扩展线性地址记录;
- 0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高16位,与前面的“0000”结合在一起,表示要扩展的线性地址为“0x0800 0000”,这正好是STM32内部FLASH的首地址;
- F2:表示校验和,它的值为(0x02+0x00+0x00+0x04+0x08+0x00)%256的值再取补码。
再来看第二条记录:
- 10:表示这条记录数据区的长度为2字节;
- 0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的FLASH首地址为(0x0800 0000+0x0000);
- 00:表示这是一条数据记录,数据区的是地址;
- 00040020C10100081B030008A3020008:这是要按地址存储的数据;
- 2F:校验和
为了更清楚地对比bin、hex及axf文件的差异,我们来查看这些文件内部记录的信息来进行对比。
hex、bin及axf文件的区别与联系
bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。
- bin文件是最直接的代码映像,它记录的内容就是要存储到FLASH的二进制数据(机器码本质上就是二进制数据),在FLASH中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片FLASH平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);
- hex文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到FLASH的哪个地址,下载器可以根据这些信息辅助下载;
- axf文件在前文已经解释,它不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。
同一个工程生成的bin、hex及axf文件的大小见图 139。
图 139 同一个工程的bin、bex及axf文件大小
实际上,这个工程要烧写到FLASH的内容总大小为1456字节,然而在Windows中查看的bin文件却比它大( bin文件是FLASH的代码映像,大小应一致),这是因为Windows文件显示单位的原因,使用右键查看文件的属性,可以查看它实际记录内容的大小,见图 140。
图 140 bin文件大小
接下来我们打开本工程的“多彩流水灯.bin”、“多彩流水灯.hex”及由“多彩流水灯.axf”使用fromelf工具输出的反汇编文件“多彩流水灯_axf_elfInfo_c.txt” 文件,清晰地对比它们的差异,见图 141。如果您想要亲自阅读自己电脑上的bin文件,推荐使用sublime软件打开,它可以把二进制数以ASCII码呈现出来,便于阅读。
图 141 同一个工程的bin、hex及axf文件对代码的记录
在“多彩流水灯_axf_elfInfo_c.txt”文件中不仅可以看到代码数据,还有具体的标号、地址以及反汇编得到的代码,虽然它不是.axf文件的原始内容,但因为它是通过.axf文件fromelf工具生成的,我们可认为*.axf文件本身记录了大量这些信息,它的内容非常丰富,熟悉汇编语言的人可轻松阅读。
在hex文件中包含了地址信息以及地址中的内容,而在bin文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex及axf文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三都之间的区别与联系。
由于文件中存储的都是机器码,见图 142,该图是我根据axf文件的GPIO_Init函数的机器码,在bin及hex中找到的对应位置。所以经验丰富的人是有可能从bin或hex文件中恢复出汇编代码的,只是成本较高,但不是不可能。
图 142 GPIO_Init函数的代码数据在三个文件中的表示
如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在FLASH中的数据,从而得到bin映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件PCB,抄出一样的板子,再把bin映像下载芯片,直接山寨出目标产品,所以在实际的生产中,一定要注意做好加密措施。由于axf文件中含有大量的信息,且直接使用fromelf即可反汇编代码,所以更不要随便泄露axf文件。lib文件也能反使用fromelf文件反汇编代码,不过它不能还原出C代码,由于lib文件的主要目的是为了保护C源代码,也算是达到了它的要求。
htm静态调用图文件
在Output目录下,有一个以工程文件命名的后缀为.bulid_log.htm及.htm文件,如“多彩流水灯.bulid_log.htm”及“多彩流水灯.htm”,它们都可以使用浏览器打开。其中.build_log.htm是工程的构建过程日志,而.htm是链接器生成的静态调用图文件。
在静态调用图文件中包含了整个工程各种函数之间互相调用的关系图,而且它还给出了静态占用最深的栈空间数量以及它对应的调用关系链。
例如图 143是“多彩流水灯.htm”文件顶部的说明。
图 143“多彩流水灯.htm”中的静态占用最深的栈空间说明
该文件说明了本工程的静态栈空间最大占用56字节(Maximum Stack Usage:56bytes),这个占用最深的静态调用为“main->LED_GPIO_Config->GPIO_Init”。注意这里给出的空间只是静态的栈使用统计,链接器无法统计动态使用情况,例如链接器无法知道递归函数的递归深度。在本文件的后面还可查询到其它函数的调用情况及其它细节。
利用这些信息,我们可以大致了解工程中应该分配多少空间给栈,有空间余量的情况下,一般会设置比这个静态最深栈使用量大一倍,在STM32中可修改启动文件改变堆栈的大小;如果空间不足,可从本文件中了解到调用深度的信息,然后优化该代码。
注意:
查看了各个工程的静态调用图文件统计后,我们发现本书提供的一些比较大规模的工程例子,静态栈调用最大深度都已超出STM32启动文件默认的栈空间大小0x00000400,即1024字节,但在当时的调试过程中却没有发现错误,因此我们也没有修改栈的默认大小(有一些工程调试时已发现问题,它们的栈空间就已经被我们改大了),虽然这些工程实际运行并没有错误,但这可能只是因为它使用的栈溢出RAM空间恰好没被程序其它部分修改而已。所以,建议您在实际的大型工程应用中(特别是使用了各种外部库时,如Lwip/emWin/Fatfs等),要查看本静态调用图文件,了解程序的栈使用情况,给程序分配合适的栈空间。
Listing目录下的文件
在Listing目录下包含了.map及.lst文件,它们都是文本格式的,可使用Windows的记事本软件打开。其中lst文件仅包含了一些汇编符号的链接信息,我们重点分析map文件。
map文件说明
map文件是由链接器生成的,它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的Code、RO-data、RW-data以及ZI-data的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”,各部分介绍如下:
节区的跨文件引用
打开“多彩流水灯.map”文件,可看到它的第一部分——节区的跨文件引用(Section Cross References),见代码清单 110。
代码清单 110 节区的跨文件引用(部分,多彩流水灯.map文件)
1 ==============================================================================
2 Section Cross References
3
4 startup_stm32f429_439xx.o(RESET) refers to startup_stm32f429_439xx.o(STACK) for __initial_sp
5 startup_stm32f429_439xx.o(RESET) refers to startup_stm32f429_439xx.o(.text) for Reset_Handler
6 startup_stm32f429_439xx.o(RESET) refers to stm32f4xx_it.o(i.NMI_Handler) for NMI_Handler
7 startup_stm32f429_439xx.o(RESET) refers to stm32f4xx_it.o(i.HardFault_Handler) for HardFault_Handler
8 /...以下部分省略**/
9
10 main.o(i.main) refers to bsp_led.o(i.LED_GPIO_Config) for LED_GPIO_Config
11 main.o(i.main) refers to stm32f4xx_gpio.o(i.GPIO_ResetBits) for GPIO_ResetBits
12 main.o(i.main) refers to main.o(i.Delay) for Delay
13 main.o(i.main) refers to stm32f4xx_gpio.o(i.GPIO_SetBits) for GPIO_SetBits
14 bsp_led.o(i.LED_GPIO_Config) refers to stm32f4xx_rcc.o(i.RCC_AHB1PeriphClockCmd) for RCC_AHB1PeriphClockCmd
15 bsp_led.o(i.LED_GPIO_Config) refers to stm32f4xx_gpio.o(i.GPIO_Init) for GPIO_Init
16 bsp_led.o(i.LED_GPIO_Config) refers to stm32f4xx_gpio.o(i.GPIO_ResetBits) for GPIO_ResetBits
17 /...以下部分省略**/
18 ======================================================================
19
在这部分中,详细列出了各个.o文件之间的符号引用。由于.o文件是由asm或c/c++源文件编译后生成的,各个文件及文件内的节区间互相独立,链接器根据它们之间的互相引用链接起来,链接的详细信息在这个“Section Cross References”一一列出。
例如,开头部分说明的是startup_stm32f429_439xx.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符号引用了stm32f4xx_gpio.o文件的i.GPIO_Init节区。
可以了解到,这些跨文件引用的符号其实就是源文件中的函数名、变量名。有时在构建工程的时候,编译器会输出 “Undefined symbol xxx (referred from xxx.o)” 这样的提示,该提示的原因就是在链接过程中,某个文件无法在外部找到它引用的标号,因而产生链接错误。例如,见图 144,我们把bsp_led.c文件中定义的函数LED_GPIO_Config改名为LED_GPIO_ConfigABCD,而不修改main.c文件中的调用,就会出现main文件无法找到LED_GPIO_Config符号的提示。
图 144 找不到符号的错误提示
删除无用节区
map文件的第二部分是删除无用节区的说明(Removing Unused input sections from the image.),见代码清单 111。
代码清单 111 删除无用节区(部分,多彩流水灯.map文件)
1 =================================================================
2 Removing Unused input sections from the image.
3
4 Removing startup_stm32f429_439xx.o(HEAP), (512 bytes).
5 Removing system_stm32f4xx.o(.rev16_text), (4 bytes).
6 Removing system_stm32f4xx.o(.revsh_text), (4 bytes).
7 Removing system_stm32f4xx.o(.rrx_text), (6 bytes).
8 Removing system_stm32f4xx.o(i.SystemCoreClockUpdate), (136 bytes).
9 Removing system_stm32f4xx.o(.data), (20 bytes).
10 Removing misc.o(.rev16_text), (4 bytes).
11 Removing misc.o(.revsh_text), (4 bytes).
12 Removing misc.o(.rrx_text), (6 bytes).
13 Removing misc.o(i.NVIC_Init), (104 bytes).
14 Removing misc.o(i.NVIC_PriorityGroupConfig), (20 bytes).
15 Removing misc.o(i.NVIC_SetVectorTable), (20 bytes).
16 Removing misc.o(i.NVIC_SystemLPConfig), (28 bytes).
17 Removing misc.o(i.SysTick_CLKSourceConfig), (28 bytes).
18 Removing stm32f4xx_adc.o(.rev16_text), (4 bytes).
19 Removing stm32f4xx_adc.o(.revsh_text), (4 bytes).
20 Removing stm32f4xx_adc.o(.rrx_text), (6 bytes).
21 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogCmd), (16 bytes).
22 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogSingleChannelConfig), (12 bytes).
23 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogThresholdsConfig), (6 bytes).
24 Removing stm32f4xx_adc.o(i.ADC_AutoInjectedConvCmd), (24 bytes).
25 /...以下部分省略**/
26 ========================================================================
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到.axf文件,不是指在.o文件删除),这样可以防止这些无用数据占用程序空间。
例如,上面的信息中说明startup_stm32f429_439xx.o中的HEAP(在启动文件中定义的用于动态分配的“堆”区)以及 stm32f4xx_adc.o的各个节区都被删除了,因为在我们这个工程中没有使用动态内存分配,也没有引用任何stm32f4xx_adc.c中的内容。由此也可以知道,虽然我们把STM32标准库的各个外设对应的c库文件都添加到了工程,但不必担心这会使工程变得臃肿,因为未被引用的节区内容不会被加入到最终的机器码文件中。
符号映像表
map文件的第三部分是符号映像表(Image Symbol Table),见代码清单 112。
代码清单 112 符号映像表(部分,多彩流水灯.map文件)
1 ==============================================================================
2 Image Symbol Table
3
4 Local Symbols
5
6 Symbol Name Value Ov Type Size Object(Section)
7 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry.o ABSOLUTE
8 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry9a.o ABSOLUTE
9 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry9b.o ABSOLUTE
10 /...省略部分/
11 LED_GPIO_Config 0x080002a5 Thumb Code 106 bsp_led.o(i.LED_GPIO_Config)
12 MemManage_Handler 0x08000319 Thumb Code 2 stm32f4xx_it.o(i.MemManage_Handler)
13 NMI_Handler 0x0800031b Thumb Code 2 stm32f4xx_it.o(i.NMI_Handler)
14 PendSV_Handler 0x0800031d Thumb Code 2 stm32f4xx_it.o(i.PendSV_Handler)
15 RCC_AHB1PeriphClockCmd 0x08000321 Thumb Code 22 stm32f4xx_rcc.o(i.RCC_AHB1PeriphClockCmd)
16 SVC_Handler 0x0800033d Thumb Code 2 stm32f4xx_it.o(i.SVC_Handler)
17 SysTick_Handler 0x08000415 Thumb Code 2 stm32f4xx_it.o(i.SysTick_Handler)
18 SystemInit 0x08000419 Thumb Code 62 system_stm32f4xx.o(i.SystemInit)
19 UsageFault_Handler 0x08000469 Thumb Code 2 stm32f4xx_it.o(i.UsageFault_Handler)
20 scatterload_copy 0x0800046b Thumb Code 14 handlers.o(i.scatterload_copy)
21 scatterload_null 0x08000479 Thumb Code 2 handlers.o(i.scatterload_null)
22 scatterload_zeroinit 0x0800047b Thumb Code 14 handlers.o(i.scatterload_zeroinit)
23 main 0x08000489 Thumb Code 270 main.o(i.main)
24 /...省略部分/
25 ==============================================================================
这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到LED_GPIO_Config符号存储在0x080002a5地址,它属于Thumb Code类型,大小为106字节,它所在的节区为bsp_led.o文件的i.LED_GPIO_Config节区。
存储器映像索引
map文件的第四部分是存储器映像索引(Memory Map of the image),见代码清单 113。
代码清单 113 存储器映像索引(部分,多彩流水灯.map文件)
1 ==============================================================================
2 Memory Map of the image
3
4 Image Entry point : 0x080001ad
5 Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000005b0, Max: 0x00100000, ABSOLUTE)
6
7 Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x000005b0, Max: 0x00100000, ABSOLUTE)
8
9 Base Addr Size Type Attr Idx E Section Name Object
10
11 0x08000000 0x000001ac Data RO 3 RESET startup_stm32f429_439xx.o
12 /..省略部分/
13 0x0800020c 0x00000012 Code RO 5161 i.Delay main.o
14 0x0800021e 0x0000007c Code RO 2046 i.GPIO_Init stm32f4xx_gpio.o
15 0x0800029a 0x00000004 Code RO 2053 i.GPIO_ResetBits stm32f4xx_gpio.o
16 0x0800029e 0x00000004 Code RO 2054 i.GPIO_SetBits stm32f4xx_gpio.o
17 0x080002a2 0x00000002 Code RO 5196 i.HardFault_Handler stm32f4xx_it.o
18 0x080002a4 0x00000074 Code RO 5269 i.LED_GPIO_Config bsp_led.o
19 0x08000318 0x00000002 Code RO 5197 i.MemManage_Handler stm32f4xx_it.o
20 /..省略部分/
21 0x08000488 0x00000118 Code RO 5162 i.main main.o
22 0x080005a0 0x00000010 Data RO 5309 Region$$Table anon$$obj.o
23
24 Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000400, Max: 0x00030000, ABSOLUTE)
25
26 Base Addr Size Type Attr Idx E Section Name Object
27
28 0x20000000 0x00000400 Zero RW 1 STACK startup_stm32f429_439xx.o
29 ==============================================================================
本工程的存储器映像索引分为ER_IROM1及RW_IRAM1部分,它们分别对应STM32内部FLASH及SRAM的空间。相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型及属性,由此可以区分Code、RO-data、RW-data及ZI-data。
例如,从上面的表中我们可以看到i.LED_GPIO_Config节区存储在内部FLASH的0x080002a4地址,大小为0x00000074,类型为Code,属性为RO。而程序的STACK节区(栈空间)存储在SRAM的0x20000000地址,大小为0x00000400,类型为Zero,属性为RW(即RW-data)。
映像组件大小
map文件的最后一部分是包含映像组件大小的信息(Image component sizes),这也是最常查询的内容,见代码清单 114。
代码清单 114 映像组件大小(部分,多彩流水灯.map文件)
1 ==============================================================================
2 Image component sizes
3
4 Code (inc. data) RO Data RW Data ZI Data Debug Object Name
5
6 116 10 0 0 0 578 bsp_led.o
7 298 10 0 0 0 1459 main.o
8 36 8 428 0 1024 932 startup_stm32f429_439xx.o
9 132 0 0 0 0 2432 stm32f4xx_gpio.o
10 18 0 0 0 0 3946 stm32f4xx_it.o
11 28 6 0 0 0 645 stm32f4xx_rcc.o
12 292 34 0 0 0 253101 system_stm32f4xx.o
13
14 ----------------------------------------------------------------------
15 926 68 444 0 1024 263093 Object Totals
16 0 0 16 0 0 0 (incl. Generated)
17 6 0 0 0 0 0 (incl. Padding)
18
19 /...省略部分/
20 ==============================================================================
21 Code (inc. data) RO Data RW Data ZI Data Debug
22
23 1012 84 444 0 1024 262637 Grand Totals
241012 84 444 0 1024 262637 ELF Image Totals
25 1012 84 444 0 0 0 ROM Totals
26 ==============================================================================
27 Total RO Size (Code + RO Data) 1456 ( 1.42kB)
28 Total RW Size (RW Data + ZI Data) 1024 ( 1.00kB)
29 Total ROM Size (Code + RO Data + RW Data) 1456 ( 1.42kB)
30 ==============================================================================
这部分包含了各个使用到的*.o文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,它们分类描述了具体占据的Code、RO-data、RW-data及ZI-data的大小,并根据这些大小统计出占据的ROM总空间。
我们仅分析最后两部分信息,如Grand Totals一项,它表示整个代码占据的所有空间信息,其中Code类型的数据大小为1012字节,这部分包含了84字节的指令数据(inc .data)已算在内,另外RO-data占444字节,RW-data占0字节,ZI-data占1024字节。在它的下面两行有一项ROM Totals信息,它列出了各个段所占据的ROM空间,除了ZI-data不占ROM空间外,其余项都与Grand Totals中相等(RW-data也占据ROM空间,只是本工程中没有RW-data类型的数据而已)。
最后一部分列出了只读数据(RO)、可读写数据(RW)及占据的ROM大小。其中只读数据大小为1456字节,它包含Code段及RO-data段; 可读写数据大小为1024字节,它包含RW-data及ZI-data段;占据的ROM大小为1456字节,它除了Code段和RO-data段,还包含了运行时需要从ROM加载到RAM的RW-data数据。
综合整个map文件的信息,可以分析出,当程序下载到STM32的内部FLASH时,需要使用的内部FLASH是从0x0800 0000地址开始的大小为1456字节的空间;当程序运行时,需要使用的内部SRAM是从0x20000000地址开始的大小为1024字节的空间。
粗略一看,发现这个小程序竟然需要1024字节的SRAM,实在说不过去,但仔细分析map文件后,可了解到这1024字节都是STACK节区的空间(即栈空间),栈空间大小是在启动文件中定义的,这1024字节是默认值(0x00000400)。它是提供给C语言程序局部变量申请使用的空间,若我们确认自己的应用程序不需要这么大的栈,完全可以修改启动文件,把它改小一点,查看前面讲解的htm静态调用图文件可了解静态的栈调用情况,可以用它作为参考。
sct分散加载文件的格式与应用
sct分散加载文件简介
当工程按默认配置构建时,MDK会根据我们选择的芯片型号,获知芯片的内部FLASH及内部SRAM存储器概况,生成一个以工程名命名的后缀为*.sct的分散加载文件(Linker Control File,scatter loading),链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。
例如可以设置源文件中定义的所有变量自动按地址分配到外部SDRAM,这样就不需要再使用关键字“attribute”按具体地址来指定了;利用它还可以控制代码的加载区与执行区的位置,例如可以把程序代码存储到单位容量价格便宜的NAND-FLASH中,但在NAND-FLASH中的代码是不能像内部FLASH的代码那样直接提供给内核运行的,这时可通过修改分散加载文件,把代码加载区设定为NAND-FLASH的程序位置,而程序的执行区设定为SDRAM中的位置,这样链接器就会生成一个配套的分散加载代码,该代码会把NAND-FLASH中的代码加载到SDRAM中,内核再从SDRAM中运行主体代码,大部分运行Linux系统的代码都是这样加载的。
分散加载文件的格式
下面先来看看MDK默认使用的sct文件,在Output目录下可找到“多彩流水灯.sct”,该文件记录的内容见代码清单 115。
代码清单 115 默认的分散加载文件内容(“多彩流水灯.sct”)
1 ; *
2 ; Scatter-Loading Description File generated by uVision
3 ; *
4
5 LR_IROM1 0x08000000 0x00100000 { ; 注释:加载域,基地址 空间大小
6 ER_IROM1 0x08000000 0x00100000 { ; 注释:加载地址 = 执行地址
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11 RW_IRAM1 0x20000000 0x00030000 { ; 注释:可读写数据
12 .ANY (+RW +ZI)
13 }
14 }
15
在默认的sct文件配置中仅分配了Code、RO-data、RW-data及ZI-data这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。
sct文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号“{}”分隔开,最外层的是加载域,第二层“{}”内的是执行域,其整体结构见图 145。
图 145 分散加载文件的整体结构
加载域
sct文件的加载域格式见代码清单 116。
代码清单 116 加载域格式
1 //方括号中的为选填内容
2 加载域名 (基地址 | ("+" 地址偏移)) [属性列表] [最大容量]
3 "{"
4 执行区域描述+
5 "}"
配合前面代码清单 115中的分散加载文件内容,各部分介绍如下:
- 加载域名:名称,在map文件中的描述会使用该名称来标识空间。如本例中只有一个加载域,该域名为LR_IROM1。
- 基地址+地址偏移:这部分说明了本加载域的基地址,可以使用+号连接一个地址偏移,算进基地址中,整个加载域以它们的结果为基地址。如本例中的加载域基地址为0x08000000,刚好是STM32内部FLASH的基地址。
- 属性列表:属性列表说明了加载域的是否为绝对地址、N字节对齐等属性,该配置是可选的。本例中没有描述加载域的属性。
- 最大容量:最大容量说明了这个加载域可使用的最大空间,该配置也是可选的,如果加上这个配置后,当链接器发现工程要分配到该区域的空间比容量还大,它会在工程构建过程给出提示。本例中的加载域最大容量为0x00100000,即1MB,正是本型号STM32内部FLASH的空间大小。
执行域
sct文件的执行域格式见代码清单 117。
代码清单 117 执行域格式
1 //方括号中的为选填内容
2 执行域名 (基地址 | "+" 地址偏移) [属性列表] [最大容量 ]
3 "{"
4 输入节区描述
5 "}"
执行域的格式与加载域是类似的,区别只是输入节区的描述有所不同,在代码清单 115的例子中包含了ER_IROM1及RW_IRAM两个执行域,它们分别对应描述了STM32的内部FLASH及内部SRAM的基地址及空间大小。而它们内部的“输入节区描述”说明了哪些节区要存储到这些空间,链接器会根据它来处理编排这些节区。
输入节区描述
配合加载域及执行域的配置,在相应的域配置“输入节区描述”即可控制该节区存储到域中,其格式见代码清单 118。
代码清单 118 输入节区描述的几种格式
1 //除模块选择样式部分外,其余部分都可选选填
2 模块选择样式"("输入节区样式",""+"输入节区属性")"
3 模块选择样式"("输入节区样式",""+"节区特性")"
4
5 模块选择样式"("输入符号样式",""+"节区特性")"
6 模块选择样式"("输入符号样式",""+"输入节区属性")"
配合前面代码清单 115中的分散加载文件内容,各部分介绍如下:
- 模块选择样式:模块选择样式可用于选择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)”是一个链接器支持的特殊选择符号,它可以选择所有标准库里要求存储到root区域的节区,如main.o、scatter.o等内容。
- 输入符号样式:同样地,使用输入符号样式可以选择要控制的符号,符号样式需要使用“:gdef:”来修饰。例如可以使用“*(:gdef:Value_Test)”来控制选择符号“Value_Test”。
- 输入节区属性:通过在模块选择样式后面加入输入节区属性,可以选择样式中不同的内容,每个节区属性描述符前要写一个“+”号,使用空格或“,”号分隔开,可以使用的节区属性描述符见表 16。
表 16 属性描述符及其意义
节区属性描述符 | 说明 |
---|---|
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是工程启动代码中定义的向量表,见代码清单 119,该向量表中定义的堆栈顶和复位向量指针必须要存储在内部FLASH的前两个地址,这样STM32才能正常启动,所以必须使用FIRST控制它们存储到首地址。
代码清单 119 startup_stm32f429_439xx.s文件中定义的RESET区(部分)
1 ; Vector Table Mapped to Address 0 at Reset
2 AREA RESET, DATA, READONLY
3 EXPORT __Vectors
4 EXPORT __Vectors_End
5 EXPORT __Vectors_Size
6
7 Vectors **DCD initial_sp** ; Top of Stack
8 DCD Reset_Handler ; Reset Handler
9 DCD NMI_Handler ; NMI Handler
总的来说,我们的sct示例文件配置如下:程序的加载域为内部FLASH的0x08000000,最大空间为0x00100000;程序的执行基地址与加载基地址相同,其中RESET节区定义的向量表要存储在内部FLASH的首地址,且所有o文件及lib文件的RO属性内容都存储在内部FLASH中;程序执行时RW及ZI区域都存储在以0x20000000为基地址,大小为0x00030000的空间(192KB),这部分正好是STM32内部主SRAM的大小。
链接器根据sct文件链接,链接后各个节区、符号的具体地址信息可以在map文件中查看。
通过MDK配置选项来修改sct文件
了解sct文件的格式后,可以手动编辑该文件控制整个工程的分散加载配置,但sct文件格式比较复杂,所以MDK提供了相应的配置选项可以方便地修改该文件,这些选项配置能满足基本的使用需求,本小节将对这些选项进行说明。
选择sct文件的产生方式
首先需要选择sct文件产生的方式,选择使用MDK生成还是使用用户自定义的sct文件。在MDK的“Options for Target->Linker->Use Memory Layout from Target Dialog”选项即可配置该选择,见图 146。
图 146 选择使用MDK生成的sct文件
该选项的译文为“是否使用Target对话框中的存储器分布配置”,勾选后,它会根据“Options for Target”对话框中的选项生成sct文件,这种情况下,即使我们手动打开它生成的sct文件编辑也是无效的,因为每次构建工程的时候,MDK都会生成新的sct文件覆盖旧文件。该选项在MDK中是默认勾选的,若希望MDK使用我们手动编辑的sct文件构建工程,需要取消勾选,并通过Scatter File框中指定sct文件的路径,见图 147。
图 147 使用指定的sct文件构建工程
通过Target对话框控制存储器分配
若我们在Linker中勾选了“使用Target对话框的存储器布局”选项,那么“Options for Target”对话框中的存储器配置就生效了。主要配置是在Device标签页中选择芯片的类型,设定芯片基本的内部存储器信息以及在Target标签页中细化具体的存储器配置(包括外部存储器),见图 148及图 149。
图 148 选择芯片类型
图中Device标签页中选定了芯片的型号为STM32F429IGTx,选中后,在Target标签页中的存储器信息会根据芯片更新。
图 149 Target对话框中的存储器分配
在Target标签页中存储器信息分成只读存储器(Read/Only Memory Areas)和可读写存储器(Read/Write Memory Areas)两类,即ROM和RAM,而且它们又细分成了片外存储器(off-chip)和片内存储器(on-chip)两类。
例如,由于我们已经选定了芯片的型号,MDK会自动根据芯片型号填充片内的ROM及RAM信息,其中的IROM1起始地址为0x80000000,大小为0x100000,正是该STM32型号的内部FLASH地址及大小;而IRAM1起始地址为0x20000000,大小为0x30000,正是该STM32内部主SRAM的地址及大小。图中的IROM1及IRAM1前面都打上了勾,表示这个配置信息会被采用,若取消勾选,则该存储配置信息是不会被使用的。
在标签页中的IRAM2一栏默认也填写了配置信息,它的地址为0x10000000,大小为0x10000,这是STM32F4系列特有的内部64KB高速SRAM(被称为CCM)。当我们希望使用这部分存储空间的时候需要勾选该配置,另外要注意这部分高速SRAM仅支持CPU总线的访问,不能通过外设访问。
下面我们尝试修改Target标签页中的这些存储信息,例如,按照图 150中的1配置,把IRAM1的基地址改为0x20001000,然后编译工程,查看到工程的sct文件如代码清单 120所示;当按照图 150中的2配置时,同时使用IRAM1和IRAM2,然后编译工程,可查看到工程的sct文件如代码清单 121所示。
图 150 修改IRAM1的基地址及仅使用IRAM2的配置
代码清单 120 修改了IRAM1基地址后的sct文件内容
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20001000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 }
代码清单 121 仅使用IRAM2时的sct文件内容
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 RW_IRAM2 0x10000000 0x00010000 {
11 .ANY (+RW +ZI)
12 }
13 }
可以发现,sct文件都根据Target标签页做出了相应的改变,除了这种修改外,在Target标签页上还控制同时使用IRAM1和IRAM2、加入外部RAM(如外接的SDRAM),外部FLASH等。
控制文件分配到指定的存储空间
设定好存储器的信息后,可以控制各个源文件定制到哪个部分存储器,在MDK的工程文件栏中,选中要配置的文件,右键,并在弹出的菜单中选择“Options for File xxxx”即可弹出一个文件配置对话框,在该对话框中进行存储器定制,见图 151。
图 151 使用右键打开文件配置并把它的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文件内容见代码清单 122。
代码清单 122 修改bsp_led.c配置后的sct文件
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 RW_IRAM2 0x10000000 0x00010000 {
11 bsp_led.o (+RW)
12 .ANY (+RW +ZI)
13 }
14 }
可以看到在sct文件中的RW_IRAM2执行域中增加了一个选择bsp_led.o中RW内容的语句。
类似地,我们还可以设置某些文件的代码段被存储到特定的ROM中,或者设置某些文件使用的ZI-data或RW-data存储到外部SDRAM中(控制ZI-data到SDRAM时注意还需要修改启动文件设置堆栈对应的地址,原启动文件中的地址是指向内部SRAM的)。
虽然MDK的这些存储器配置选项很方便,但有很多高级的配置还是需要手动编写sct文件实现的,例如MDK选项中的内部ROM选项最多只可以填充两个选项位置,若想把内部ROM分成多片地址管理就无法实现了;另外MDK配置可控的最小粒度为文件,若想控制特定的节区也需要直接编辑sct文件。
接下来我们将讲解几个实验,通过编写sct文件定制存储空间。