本文是学习野火的指南针开发板过程的学习笔记,可能有误,详细请看B站野火官方配套视频教程
(这个教程真的讲的很详细,给官方三连吧)
引言:
我们用MDK编写源代码,然后编译生成机器码,再把机器码下载到STM32芯片上运行,但是这个编译、下载的过程MDK究竟做了什么工作?它编译后生成的各种文件又有什么作用?
01编译过程
编译流程:
1.编译:
MDK软件使用的编译器是armcc和armasm,它们根据每个c/c++和汇编源文件编译成对应的以“.o”为后缀名的对象文件(Object Code,也称目标文件)
其内容主要是从源文件编译得到的机器码,包含了代码、数据以及调试使用的信息
链接:
链接器armlink把各个.o文件及库文件链接成一个映像文件“.axf”或“.elf”
格式转换:
一般来说Windows或Linux系统使用链接器直接生成可执行映像文件elf后,内核根据该文件的信息加载后,就可以运行程序了,但在单片机平台上,需要把该文件的内容加载到芯片上,所以还需要对链接器生成的elf映像文件利用格式转换器fromelf转换成“.bin”或“.hex”文件,交给下载器下载到芯片的FLASH或ROM中
具体工程中的编译过程:
打开工程后,点击MDK的“rebuild”按钮,它会重新构建整个工程,构建的过程会在MDK下方的“Build Output”窗口输出提示信息。
- 说明的是构建过程调用的编译器。图中的编译器名字是“V5.06(build 20)”,后面附带了该编译器所在的文件夹。在电脑上打开该路径,可看到该编译器包含下图中的各个编译工具,如armar (用于把.o文件打包成lib文件的)、armasm、armcc、armlink及fromelf
- 使用armasm编译汇编文件。图中列出了编译startup启动文件时的提示,编译后每个汇编源文件都对应有一个独立的.o文件。
- 使用armcc编译c/c++文件,同时编译后每个c/c++源文件都对应有一个独立的.o文件。
- 使用armlink链接对象文件,根据程序的调用把各个.o文件的内容链接起来,最后生成程序的axf映像文件,并附带程序各个域大小的说明,包括Code、RO-data、RW-data及ZI-data的大小。
- 使用fromelf生成下载格式文件,它根据axf映像文件转化成hex文件,并列出编译过程出现的错误(Error)和警告(Warning)数量。
- 提示给出整个构建过程消耗的时间。
构建完成后,可在工程的“Output”及“Listing”目录下找到由以上过程生成的各种文件
02程序的组成、存储与运行
程序的组成
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的栈空间(Stack):
函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。
ZI-data的堆空间(Heap):
使用malloc动态分配的变量属于堆空间。
而在程序中的栈空间和堆空间都是属于ZI-data区域的,这些空间都会被初始值化为0值。编译器给出的ZI-data占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用malloc动态申请堆空间,编译器会优化,不把堆空间计算在内)。
存储与运行
引言:RW-data和ZI-data它们仅仅是初始值不一样而已,为什么编译器非要把它们区分开?
原因分析:
应用程序具有静止状态和运行状态,静止态的程序存储在非易失存储器中(内部FLASH),运行状态程序存放在内存中
要知道:程序在存储状态时,RO节(RO section)及RW节都被保存在ROM区。当程序开始运行时,内核直接从ROM中读取代码,并且在执行主体代码前,会先执行一段加载代码,它把RW节数据从ROM复制到RAM, 并且在RAM加入ZI节,ZI节的数据都被初始化为0。加载完后RAM区准备完毕,正式开始执行主体程序。
把RW-data与ZI-data区别开来的原因是——是否需要掉电保存,因为在RAM创建数据的时候,默认值为0,但如果有的数据要求初值非0,那就需要使用ROM记录该初始值,运行时再复制到RAM。
程序运行过程:STM32的RO区域不需要加载到SRAM,内核直接从FLASH读取指令运行。计算机系统的应用程序运行过程很类似,不过计算机系统的程序在存储状态时位于硬盘,执行的时候甚至会把上述的RO区域(代码、只读数据)加载到内存,加快运行速度,还有虚拟内存管理单元(MMU)辅助加载数据,使得可以运行比物理内存还大的应用程序。而STM32没有MMU,所以无法支持Linux和Windows系统。
程序存储到STM32芯片的内部FLASH时(即ROM区),它占用的空间是Code、RO-data及RW-data的总和,所以如果这些内容比STM32芯片的FLASH空间大,程序就无法被正常保存了。
程序执行的时候,需要占用内部SRAM空间(即RAM区),占用的空间包括RW-data和ZI-data。
在MDK中,我们建立的工程一般会选择芯片型号,选择后就有确定的FLASH及SRAM大小,若代码超出了芯片的存储器的极限,编译器会提示错误,这时就需要裁剪程序了,裁剪时可针对超出的区域来优化。
03编译工具链
即MDK调用的各种编译工具(具体请看 MDK的帮助手册《ARM Development Tools》)
调用这些编译工具,需要用到Windows的“命令行提示符工具”,为了让命令行方便地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。
设置环境变量
1. 添加路径到PATH环境变量
(Win11为例)使用资源管理器+右击“此电脑”,或者直接快捷键WIN+U =打开设置
打开Windows的命令行——快捷键Win+R,输入cmd
在弹出的命令行窗口中输入“fromelf”回车
若窗口打印出formelf的帮助说明,那么路径正常,反之若提示“不是内部名外部命令,也不是可运行的程序…”信息,说明路径不对,请重新配置环境变量,并确认该工作目录下有编译工具链
fromelf
这个添加环境变量的过程本质就是让命令行通过“PATH”路径找到“fromelf.exe”程序运行,默认运行“fromelf.exe”时它会输出自己的帮助信息,这就是工具链的调用过程,MDK本质上也是如此调用工具链的,只是它集成为GUI,相对于命令行对用户更友好
armcc、armasm及armlink工具链的具体用法
1. armcc
armcc用于把c/c++文件编译成ARM指令代码,编译后会输出ELF格式的O文件(对象、目标文件),在命令行中输入“armcc”回车可调用该工具,它会打印帮助说明:
armcc
armcc --cpu list
在命令行中输入“armcc --cpu list”,可查看cpu列表
当我们修改MDK的编译配置时,可看到该控制命令也会有相应的变化。然而我们无法在该编译选项框中输入命令,只能通过MDK提供的选项修改。
2. armlink
armlink是链接器,它把各个O文件链接组合在一起生成ELF格式的AXF文件,AXF文件是可执行的,下载器把该文件中的指令代码下载到芯片后,该芯片就能运行程序了;利用armlink还可以控制程序存储到指定的ROM或RAM地址。在MDK中可在“Option for Target->Linker”页面配置armlink选项:
armlink
链接器默认是根据芯片类型的存储器分布来生成程序的,该存储器分布被记录在工程里的sct后缀的文件中,有特殊需要的话可自行编辑该文件,改变链接器的链接方式。
3.armar、fromelf及用户指令
armar 工具用于把工程打包成库文件, fromelf 可根据 axf 文件生成 hex 、 bin 文件, hex 和 bin 文件 是大多数下载器支持的下载文件格式 在 MDK 中,针对 armar 和 fromelf 工具的选项几乎没有,仅集成了生成 HEX 或 Lib 的选项:利用fromelf生成bin文件,可在MDK的“Option for Target->User”页中添加调用fromelf的指令:
在User配置页面中,提供了三种类型的用户指令输入框,在不同组的框输入指令,可控制指令的执行时间,分别是编译前(Before Compile c/c++ file)、构建前(Before Build/Rebuild)及构建后(After Build/Rebuild)执行。这些指令并没有限制必须是arm的编译工具链,例如如果您自己编写了python脚本,也可以在这里输入用户指令执行该脚本。 图中的生成bin文件指令调用了fromelf工具,紧跟后面的是工具的选项及输出文件名、输入文件名。由于fromelf是根据axf文件生成bin的,而axf文件又是构建(build)工程后才生成,所以我们把该指令放到“After Build/Rebuild”一栏。
04MDK工程的文件类型
MDK工程中包含了各种各样的文件,且主要分为MDK相关文件、源文件以及编译、链接器生成的文件:
Project目录下的工程文件:
1.uvprojx文件
uvprojx文件就是我们平时双击打开的工程文件,它记录了整个工程的结构,如芯片类型、工程包含了哪些源文件等内容:
2.uvprojx文件
uvoptx文件记录了工程的配置选项,如下载器的类型、变量跟踪配置、断点位置以及当前已打开的文件等等:
3. uvguix文件
uvguix文件记录了MDK软件的GUI布局,如代码编辑区窗口的大小、编译输出提示窗口的位置等等。
uvprojx、uvoptx及uvguix都是使用XML格式记录的文件,若使用记事本打开可以看到XML代码。 而当使用MDK软件打开时,它根据这些文件的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中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件(Listings、Objects),在工程的:
1.lib库文件
在某些场合下可能不希望提供给第三方一个可用的代码库,但不希望对方看到源码,这个时候我们就可以把工程生成lib文件(Library file,即可执行文件,下载到芯片上直接运行的机器码)提供给对方,如:
得到生成的 *.lib 文件后,可把它像 C 文件一样添加到其它工程中,并在该工程调用 lib 提供的函数接口,除了不能看到 *.lib 文件的源码,在应用方面它跟 C 源文件没有区别
2.dep、d依赖文件
*.dep和*.d文件(Dependency file)记录的是工程或其它文件的依赖,主要记录了引用的头文件路径,其中*.dep是整个工程的依赖,它以工程名命名,而*.d是单个源文件的依赖,它们以对应的源文件名命名。这些记录使用文本格式存储,我们可直接使用记事本打开:
3.crf交叉引用文件
*.crf是交叉引用文件(Cross-Reference file),它主要包含了浏览信息(browse information),即源代码中的宏定义、变量及函数的定义和声明的位置。 我们在代码编辑器中点击“Go To Definition Of ‘xxxx’”可实现浏览跳转,跳转的时候,MDK就是通过*.crf文件查找出跳转位置的。
通过配置MDK中的“Option for Target->Output->Browse Information”选项可以设置编译时是否生成浏览信息,只有勾选该选项并编译后,才能实现上面的浏览跳转功能。如下图:
*.crf 文件使用了特定的格式表示,直接用文本编辑器打开会看到大部分乱码
4. o、axf及elf文件
*.o、*.elf、*.axf、*.bin及*.hex文件都存储了编译器根据源代码生成的机器码,根据应用场合的不同,故有所区别。
*.o、*.elf、*.axf以及前面提到的lib文件都是属于目标文件,它们都是使用ELF格式来存储的
ELF是Executable and Linking Format的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。在Linux及Windows系统下都有使用该格式的文件(或类似格式)用于记录应用程序的内容,告诉操作系统如何链接、加载及执行该应用程序。Linux下的ELF格式,与MDK使用的格式有小区别,但大致相同。
目标文件主要有如下三种类型:
可重定位的文件(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文件。它们的关系如下:
可以看到,由于都使用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函数,而它内部调用了“stm32f10x_gpio.c”的GPIO_Init函数,经过armcc编译后,LED_GPIO_Config及GPIO_Iint函数都成了指令代码,分别存储在bsp_led.o及stm32f10x_gpio.o文件中,这些指令在*.o文件都没有指定地址,仅包含了内容、大小以及调用的链接信息,而经过链接器后,链接器给它们都分配了特定的地址,并且把地址根据调用指向链接起来。
具体文件的内容,使用fromelf文件可以查看*.o、*.axf及*.lib文件的ELF信息。
ELF文件头
使用命令行,切换到文件所在的目录,输入“fromelf –text –v bsp_led.o”命令,可控制输出bsp_led.o的详细信息,利用“-c、-z”等选项还可输出反汇编指令文件、代码及数据文件等信息
cd xxx
fromelf --text -v bsp_led.o
为了便于阅读,已使用fromelf指令生成了“多彩流水灯.axf”、“bsp_led”及“多彩流水灯.lib”的ELF信息,并已把这些信息保存在配套工程文件
至此后面的内容比较水,没有加上自己的思考和总结(24.11.15待复习待更新)
程序头
对比“elf信息输出”目录下的bsp_led_o_elfInfo_v.txt文件和“多彩流水灯_axf_elfInfo_v.txt”文件可发现*.axf文件的ELF文件头对程序头的大小说明为非0值,且给出了它在文件的偏移地址,在输出信息之中,包含了程序头的详细信息。可看到,程序头的“Physical Addr”描述了本程序要加载到的内存地址“0x0800 0000”,正好是STM32内部FLASH的首地址;“size in file”描述了本程序占据的空间大小为“3176 bytes”,它正是程序烧录到FLASH中需要占据的空间
节区头
在ELF的原文件中,紧接着程序头的一般是节区的主体信息,在节区主体信息之后是描述节区主体信息的节区头,先来看看节区头中的信息了解概况。通过对比*.o文件及*.axf文件的节区头部信息,可以清楚地看出这两种文件的区别
这个节区头描述的是该函数被编译后的节区信息,其中包含了节区的类型(指令类型SHT_PROGBITS)、节区应存储到的地址(0x00000000)、它主体信息在文件位置中的偏移(52)以及节区的大小(96 bytes)。
由于*.o文件是可重定位文件,所以它的地址并没有被分配,是0x00000000,当链接器链接时,根据这个节区头信息,在文件中找到它的主体内容,并根据它的类型,把它加入到主程序中,并分配实际地址,链接后生成的*.axf文件。
在*.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节区的某地址。
节区主体及反汇编代码
使用fromelf的-c选项可以查看部分节区的主体信息,对于指令节区,可根据其内容查看相应的反汇编代码,打开“bsp_led_o_elfInfo_c.txt”文件可查看这些信息:
可看到,由于这是*.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的内容:
可看到,除了基地址以及跳转地址不同之外,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文件的反汇编信息)中则可找到它们的具体信息,且它们也有具体的地址空间。
在*.axf文件中,跳转到RCC_APB2PeriphClockCmd及GPIO_Init标号的这两个指令后都有注释,分别是“; 0x8000980”及“; 0x8000408”,它们是这两个标号所在的具体地址,而且这两个跳转语句的跟*.o中的也有区别,内容分别为“f7fffefd”及“f7fffc34”(*.o中的均为f7fffffe)。这就是链接器链接的含义,它把不同*.o中的内容链接起来了。
分散加载代码
引言:前面提到程序有存储态及运行态,它们之间应有一个转化过程,把存储在FLASH中的RW-data数据拷贝至SRAM。然而我们的工程中并没有编写这样的代码,在汇编文件中也查不到该过程,芯片是如何知道FLASH的哪些数据应拷贝到SRAM的哪些区域呢?
分析:通过查看“多彩流水灯_axf_elfInfo_c.txt”的反汇编信息,了解到程序中具有一段名为“__scatterload”的分散加载代码,它是由armlink链接器自动生成的。
这段分散加载代码包含了拷贝过程(LDM复制指令),而LDM指令的操作数中包含了加载的源地址,这些地址中包含了内部FLASH存储的RW-data数据。而 “__scatterload ”的代码会被“__main”函数调用,__main在启动文件中的“Reset_Handler”会被调用,因而,在主体程序执行前,已经完成了分散加载过程。
5.hex文件及bin文件
若编译过程无误,即可把工程生成前面对应的*.axf文件,而在MDK中使用下载器(DAP/JLINK/ULINK等)下载程序或仿真的时候,MDK调用的就是*.axf文件,它解释该文件,然后控制下载器把*.axf中的代码内容下载到STM32芯片对应的存储空间,然后复位后芯片就开始执行代码了。
然而,脱离了MDK或IAR等工具,下载器就无法直接使用*.axf文件下载代码了,它们一般仅支持hex和bin格式的代码数据文件。默认情况下MDK都不会生成hex及bin文件,需要配置工程选项或使用fromelf命令。
生成hex文件
配置操作如下,中勾选该选项,然后编译工程即可:
生成bin文件
使用MDK生成bin文件需要使用fromelf命令,在MDK的“Options For Target->Users”中加入命令:
fromelf --bin --output ..\..\Output\多彩流水灯.bin ..\..\Output\多彩流水灯.axf
其中…\…\…指自己工程的目录
fromelf需要根据工程的*.axf文件输入来转换得到bin文件,所以在命令的输入文件参数中要选择本工程对应的*.axf文件,在MDK命令输入栏中,我们把fromelf指令放置在“After Build/Rebuild”(工程构建完成后执行)一栏也是基于这个考虑,这样设置后,工程构建完成生成了最新的*.axf文件,MDK再执行fromelf指令,从而得到最新的bin文件。
hex文件格式
hex是Intel公司制定的一种使用ASCII文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到ROM中的数据,绝大多数下载器支持该格式。 一个hex文件由多条记录组成,而每条记录由五个部分组成,格式形如“:llaaaatt[dd…]cc”,例如本“多彩流水灯”工程生成的hex文件前几条记录:
记录的各个部分介绍如下:
- “:” :每条记录的开头都使用冒号来表示一条记录的开始;
- ll :以16进制数表示这条记录的主体数据区的长度(即后面[dd…]的长度);
- aaaa:表示这条记录中的内容应存放到FLASH中的起始地址;
- tt:表示这条记录的类型,它包含中的各种类型;
- dd:表示一个字节的数据,一条记录中可以有多个字节数据,ll区表示了它有多少个字节的数据;
- cc:表示本条记录的校验和,它是前面所有16进制数据 (除冒号外,两个为一组)的和对256取模运算的结果的补码。
tt的值 | 代表的类型 |
00 | 数据记录 |
01 | 本文件结束记录 |
02 | 扩展地址记录 |
04 | 扩展线性地址记录(表示后面的记录按个这地址递增) |
05 | 表示一个线性地址记录的起始(只适用于ARM) |
:020000040800F2
- 02:表示这条记录数据区的长度为2字节;
- 0000:表示这条记录要存储到的地址;
- 04:表示这是一条扩展线性地址记录;
- 0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高16位,与前面的“0000”结合在一起,表示要扩展的线性地址为“0x0800 0000”,这正好是STM32内部FLASH的首地址;
- F2:表示校验和,它的值为(0x02+0x00+0x00+0x04+0x08+0x00)%256的值再取补码。
再来看第二条记录: :10000000000400204501000829030008BF02000881
- 10:表示这条记录数据区的长度为16字节;
- 0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的FLASH首地址为(0x0800 0000+0x0000);
- 00:表示这是一条数据记录,数据区的是地址;
- 000400204501000829030008BF020008:这是要按地址存储的数据;
- 81:校验和
hex、bin及axf文件的区别与联系
bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。
- bin文件是最直接的代码映像,它记录的内容就是要存储到FLASH的二进制数据(机器码本质上就是二进制数据),在FLASH中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片FLASH平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);
- hex文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到FLASH的哪个地址,下载器可以根据这些信息辅助下载;
- axf文件在前文已经解释,它不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。
在“多彩流水灯_axf_elfInfo_c.txt”文件中不仅可以看到代码数据,还有具体的标号、地址以及反汇编得到的代码,虽然它不是*.axf文件的原始内容,但因为它是通过*.axf文件fromelf工具生成的,我们可认为*.axf文件本身记录了大量这些信息,它的内容非常丰富,熟悉汇编语言的人可轻松阅读。 在hex文件中包含了地址信息以及地址中的内容,而在bin文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex及axf文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三都之间的区别与联系。
根据axf文件的GPIO_Init函数的机器码,在bin及hex中找到的对应位置。所以经验丰富的人是有可能从bin或hex文件中恢复出汇编代码的,只是成本较高,但不是不可能。
如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在FLASH中的数据,从而得到bin映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件PCB,抄出一样的板子,再把bin映像下载芯片,直接山寨出目标产品,所以在实际的生产中,一定要注意做好加密措施。 由于axf文件中含有大量的信息,且直接使用fromelf即可反汇编代码,所以更不要随便泄露axf文件。 lib文件也能反使用fromelf文件反汇编代码,不过它不能还原出C代码,由于lib文件的主要目的是为了保护C源代码,也算是达到了它的要求。
6.htm静态调用图文件
在Output目录下,有一个以工程文件命名的后缀为*.bulid_log.htm及*.htm文件,如“多彩流水灯.bulid_log.htm”及“多彩流水灯.htm”,它们都可以使用浏览器打开。其中*.build_log.htm是工程的构建过程日志,而*.htm是链接器生成的静态调用图文件。 在静态调用图文件中包含了整个工程各种函数之间互相调用的关系图,而且它还给出了静态占用最深的栈空间数量以及它对应的调用关系链。
Listing目录下的文件
在Listing目录下包含了*.map及*.lst文件,它们都是文本格式的,可使用Windows的记事本软件打开。其中lst文件仅包含了一些汇编符号的链接信息,我们重点分析map文件。
1.map文件说明
map文件是由链接器生成的,它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的Code、RO-data、RW-data以及ZI-data的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”。
打开“多彩流水灯.map”文件,可看到它的第一部分——
节区的跨文件引用(Section Cross References):
可以了解到,这些跨文件引用的符号其实就是源文件中的函数名、变量名。有时在构建工程的时候,编译器会输出 “Undefined symbol xxx (referred from xxx.o)” 这样的提示,该提示的原因就是在链接过程中,某个文件无法在外部找到它引用的标号,因而产生链接错误。
例如:把bsp_led.c文件中定义的函数LED_GPIO_Config改名为LED_GPIO_ConfigABCD,而不修改main.c文件中的调用,就会出现main文件无法找到LED_GPIO_Config符号的提示。
删除无用节区
map文件的第二部分是删除无用节区的说明(Removing Unused input sections from the image.):
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到*.axf文件,不是指在*.o文件删除),这样可以防止这些无用数据占用程序空间。 例如,上面的信息中说明startup_stm32f10x.o中的HEAP(在启动文件中定义的用于动态分配的“堆”区)以及 stm32f10x_adc.o的各个节区都被删除了,因为在我们这个工程中没有使用动态内存分配,也没有引用任何stm32f10x_adc.c中的内容。由此也可以知道,虽然我们把STM32标准库的各个外设对应的c库文件都添加到了工程,但不必担心这会使工程变得臃肿,因为未被引用的节区内容不会被加入到最终的机器码文件中
符号映像表
map文件的第三部分是符号映像表(Image Symbol Table):
这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到LED_GPIO_Config符号存储在0x080002c4地址,它属于Thumb Code类型,大小为90字节,它所在的节区为bsp_led.o文件的i.LED_GPIO_Config节区。
存储器映像索引
map文件的第四部分是存储器映像索引(Memory Map of the image):
该工程的存储器映像索引分为ER_IROM1及RW_IRAM1部分,它们分别对应STM32内部FLASH及SRAM的空间。相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型及属性,由此可以区分Code、RO-data、RW-data及ZI-data。
例如,从上面的表中我们可以看到i.LED_GPIO_Config节区存储在内部FLASH的0x080002c4地址,大小为0x00000060,类型为Code,属性为RO。而程序的STACK节区(栈空间)存储在SRAM的0x20000000地址,大小为0x00000400,类型为Zero,属性为RW(即RW-data) 。
映像组件大小
map文件的最后一部分是包含映像组件大小的信息(Image component sizes),这也是最常查询的内容:
这部分包含了各个使用到的*.o文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,它们分类描述了具体占据的Code、RO-data、RW-data及ZI-data的大小,并根据这些大小统计出占据的ROM总空间。
此处最后两部分信息,如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字节) 。
综合整个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.分散加载文件的格式
打开MDK默认使用的sct文件,在Output目录下可找到“多彩流水灯.sct”,该文件记录的内容:
在默认的sct文件配置中仅分配了Code、RO-data、RW-data及ZI-data这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。 sct文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号“{}”分隔开,最外层的是加载域,第二层“{}”内的是执行域,其整体结构如下(分散加载文件的整体结构):
sct文件的加载域
加载域格式如下:
- 加载域名:名称,在map文件中的描述会使用该名称来标识空间。如本例中只有一个加载域,该域名为LR_IROM1
- 基地址+地址偏移:这部分说明了本加载域的基地址,可以使用+号连接一个地址偏移,算进基地址中,整个加载域以它们的结果为基地址。如本例中的加载域基地址为0x08000000,刚好是STM32内部FLASH的基地址。
- 属性列表:属性列表说明了加载域的是否为绝对地址、N字节对齐等属性,该配置是可选的。本例中没有描述加载域的属性。
- 最大容量:最大容量说明了这个加载域可使用的最大空间,该配置也是可选的,如果加上这个配置后,当链接器发现工程要分配到该区域的空间比容量还大,它会在工程构建过程给出提示。本例中的加载域最大容量为0x00080000,即512KB,正是本型号STM32内部FLASH的空间大小。
- 模块选择样式:模块选择样式可用于选择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”。
- 输入节区属性:通过在模块选择样式后面加入输入节区属性,可以选择样式中不同的内容,每个节区属性描述符前要写一个“+”号,使用空格或“,”号分隔开,可以使用的节区属性描述符:
例如,示例文件中使用“.ANY(+RO)”选择剩余所有节区RO属性的内容都分配到执行域ER_IROM1中,使用“.ANY(+RW +ZI)”选择剩余所有节区RW及ZI属性的内容都分配到执行域RW_IRAM1中。
- 节区特性:节区特性可以使用“+FIRST”或“+LAST”选项配置它要存储到的位置,FIRST存储到区域的头部,LAST存储到尾部。通常重要的节区会放在头部,而CheckSum(校验和)之类的数据会放在尾部。
例如示例文件中使用“(RESET,+First)”选择了RESET节区,并要求把它放置到本区域第一个位置,而RESET是工程启动代码中定义的向量表,该向量表中定义的堆栈顶和复位向量指针必须要存储在内部FLASH的前两个地址,这样STM32才能正常启动,所以必须使用FIRST控制它们存储到首地址。
总的来说,我们的sct示例文件配置如下:程序的加载域为内部FLASH的0x08000000,最大空间为0x00080000;程序的执行基地址与加载基地址相同,其中RESET节区定义的向量表要存储在内部FLASH的首地址,且所有o文件及lib文件的RO属性内容都存储在内部FLASH中;程序执行时RW及ZI区域都存储在以0x20000000为基地址,大小为0x00010000的空间(64KB),这部分正好是STM32内部主SRAM的大小。
链接器根据sct文件链接,链接后各个节区、符号的具体地址信息可以在map文件中查看。
3.通过MDK配置选项来修改sct文件 (这个比较重要)
了解sct文件的格式后,可以手动编辑该文件控制整个工程的分散加载配置,但sct文件格式比较复杂,所以MDK提供了相应的配置选项可以方便地修改该文件,这些选项配置能满足基本的使用需求
使用MDK生成还是使用用户自定义的sct文件。在MDK的“Options for Target->Linker->Use Memory Layout from Target Dialog”选项即可配置该选择:
选择使用MDK生成的sct文件
使用指定的sct文件构建工程
通过Target对话框控制存储器分配
若我们在 Linker 中勾选了“使用 Target 对话框的存储器布局”选项,那么“ Options for Target ”对 话框中的存储器配置就生效了。主要配置是在 Device 标签页中选择芯片的类型,设定芯片基本 的内部存储器信息以及在 Target 标签页中细化具体的存储器配置 ( 包括外部存储器 ) 见图选择芯片类型 及图 Target 对话框中的存储器分配 。图中 Device 标签页中选定了芯片的型号为 STM32F103ZE ,选中后,在 Target 标签页中的存储器信息会根据芯片更新。
MDK修改存储器分配示例
下面我们尝试修改 Target 标签页中的这些存储信息,例如,把 STM32 内部的 SRAM 分成两等份,按照图修改 IRAM1 的基地址及仅使用 IRAM2 的配置 中的 1 配置,把 IRAM1 的基地址设置为 0x20000000 ,大小改为 0x8000 ,把 IRAM2 的基地址设置为 0x20008000 ,大小为 0x800 然后编译工程,查看到工程的 sct 文件如 代码清单 :MDK-20 所示。虽然修改后 IRAM1 和 IRAM2 加 起来还是原来的内部 SRAM 空间,但它演示了对 Target 选项的修改是如何影响 sct 文件的,可以尝试其它配置,观察 sct 文件,以学习 sct 文件的语法。 编译后可见,sct 文件根据 Target 标签页做出了相应的改变,除了这种修改外,在 Target 标签页上还控制同时使用 IRAM1 和 IRAM2 、加入外部 RAM( 如外接的 SRAM) ,外部 FLASH 等控制文件分配到指定的存储空间
设定好存储器的信息后,可以控制各个源文件定制到哪个部分存储器,在MDK的工程文件栏中,选中要配置的文件,右键,并在弹出的菜单中选择“Options for File xxxx”即可弹出一个文件配置对话框,在该对话框中进行存储器定制.
虽然MDK的这些存储器配置选项很方便,但有很多高级的配置还是需要手动编写sct文件实现的,例如MDK选项中的内部ROM选项最多只可以填充两个选项位置,若想把内部ROM分成多片地址管理就无法实现了;另外MDK配置可控的最小粒度为文件,若想控制特定的节区也需要直接编辑sct文件。