ARM Cortex-M0+机器码系列文件分析及应用研究
2019-01-10蔡伯峰王宜怀
蔡伯峰, 王宜怀
(1. 苏州大学计算机科学与技术学院, 江苏 苏州 215006; 2. 泰州职业技术学院信息技术学院, 江苏 泰州 225300)
0 引言
MCU应用工程经编译链接后会生成机器码文件等系列文件, 其中机器码文件须下载到开发板MCU的Flash存储区中投入运行. 与通用计算机系统相比, 嵌入式系统的存储资源较匮乏且速度低, 但对实时性、 可靠性的要求却较高[1-2], 所以在MCU程序数据占用空间和执行时间上要精心设计、 尽量优化, 尤其对功能复杂耗费较多资源的MCU应用工程更是如此. 为此就要充分了解程序和数据存储机制与空间分配情况、 MCU启动和程序执行过程、 程序执行时间等信息, 并有比较方便实用的计算空间占用和执行时间的方法, 以便修改或优化相关配置文件、 程序和数据等. 但目前有关机器码系列文件分析及应用的系统性研究较少. 本研究全面地分析机器码系列文件并综合运用所得的信息来分析MCU启动过程, 提出计算程序数据占用空间和执行时间的简明快捷的方法.
1 MCU应用工程编译链接后的系列生成文件
任一MCU应用工程往往都包含.c、 .S等多个源代码文件, 它们由KDS(kinetis design studio)开发环境中集成的Cross ARM GCC编译器, 依次通过预编译、 编译、 汇编等一系列过程, 生成相应的多个中间文件——可重定位目标文件.o. 再由集成的链接器根据开发人员编写的链接脚本文件.ld中的相关规则, 将这些.o文件和所调用的函数或变量所在的静态链接库一起链接组合成针对ARM 内核的格式为.elf的可执行目标文件, 以及机器码文件.hex、 列表文件.lst、 映像文件.map. 其中格式为.elf的文件可通过USBDM等方式下载到目标MCU中运行.
2 机器码系列文件分析
本研究所要分析的机器码系列文件包括.hex、 .lst、 .map文件. .elf文件是二进制文件且包含大量调试和说明信息, 需要专门工具软件打开, 可读性差、 分析困难. .hex文件是在KDS下由.elf文件经格式转换而成的文件, 它是十六进制文本组成的ASCII码文件, 并包含.elf文件中的主要内容如程序入口地址和各个输出段等, 结构较清晰且可读性好, 分析方便, 而.elf文件中的其他内容已包含在.lst和.map文件中, 因此本研究所称的机器码文件指.hex文件. 为了能对机器码系列文件进行方便、 准确的分析, 还须借助于链接脚本文件.
2.1 链接脚本文件
控制链接过程用的链接脚本文件.ld主要用于链接时重定位代码和提供数据在内存中的存储位置, 它规定如何将来自中间文件的输入段放入最终.elf文件的匹配输出段中, 并控制各部分地址分配. 以NXP公司的KL25Z128为例, 它包含FLASH128KB、 SRAM16KB, 其链接脚本文件intflash.ld包含的与程序、 数据安排相关内容及分析列于表1.
表1 链接脚本文件简明分析
MEMORY命令定义并划分Flash和RAM存储空间可用资源区, 设置其读写、 执行等属性. SECTIONS命令包括符号量赋值语句和输出段描述语句, 前者定义供各个文件使用的符号量, 后者定义包含在各个区中的所有输出段, 并确定每个输出段在最终目标文件中应该由哪些中间文件的哪些输入段构成.
.ld文件可由开发人员根据实际MCU应用工程程序和数据对空间的需求情况、 链接的具体要求等进行编写或先由KDS开发环境生成后再行修改.
2.2 映像文件分析
映像文件.map提供了查看代码、 常量、 静态变量、 全局变量、 堆栈等存放的起始地址和空间占用信息, 但指定的地址是动态分配的, 随应用工程改动而发生相应变化.
文件内容主要包括: 1)应用工程未使用的各中间文件中的输入段和函数、 常量、 静态变量、 全局变量的空间占用信息, 形如“.group 0x00000000 0x8 gpio.o”. 2)Flash和RAM存储空间可用资源区配置信息, 包括m_interrupts、 m_flash_config、 m_text、 m_data及默认存储区起始地址、 空间大小和读写属性, 形如“m_text 0x00000800 0x0001f7ff xr”. 3)链接脚本, 包括链接的各中间文件和静态库文件名, 形如“LOAD main.o”. 4)应用工程实际使用的各中间文件中的输入段和函数、 常量、 静态变量、 全局变量的存储空间分配信息, 包括在链接脚本文件各SECTIONS命令所定义的符号量的值和输出段描述语句所涉及到的各中间文件中各输入段、 函数、 常量、 静态变量、 全局变量等占用的存储空间起始地址及长度, 形如“.text.UART0_IRQHandler 0x0000084c 0x8 isr.o”. 对于数据段、 堆和栈, 存储空间起始地址指运行时的虚拟内存地址VMA在装载时, 数据段、 堆和栈被装载到Flash存放代码段之后的空间中, 装载地址是LMA. 5)输出到.elf中来自各中间文件的调试段存放的起始地址和占用空间信息, 形如“.debug_info 0x000000b1 0xf6 main.o”.
2.3 列表文件分析
列表文件.lst提供了存储空间分配情况、 所有符号信息及机器码、 汇编代码、 源代码之间的对应关系, 用于程序分析.
文件内容主要包括: 1)存储在Flash中的MCU程序执行开始地址, 形如“start address 0x00000801”; 2)程序头(形如“LOAD off 0x000000b4 vaddr 0x00000000 paddr 0x00000000 align 2**2”和“filesz 0x000000c0 memsz 0x000000c0 flags r--”)及输出段信息(形如“interrupts 000000c0 00000000 00000000 000000b4 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA”), 包括各输出段在最终目标文件中存放的偏移地址和占用空间、 输出段VMA和LMA、 输出段占用的存储器空间和读写等属性、 地址对齐方式; 3)符号表, 包含目标文件中所有符号信息: 符号名、 使用范围(指全局、 局部、 弱函数)、 符号类型(指段名d、 文件名df、 函数名F、 常量/静态变量/全局变量O、 符号量)、 所属段、 链接地址(指链接器给符号编排的VMA地址)和占用空间, 形如“1ffff000 g .text 00000000 __data_start__”; 4)MCU程序实际使用或调用的各函数源代码对应的汇编代码、 机器码及在m_text区中的存储地址, 形如“86c:2314 movs r3, #20”; 5)常量存储地址及值, 形如“d40: 40049000 4004a000 4004b000 4004c000”.
2.4 机器码文件分析
机器码文件.hex提供了中断向量表、 机器码及常量, 以及初始化了的静态变量和全局变量, 通过SWD等方式下载到目标MCU中运行, 用于代替通过USBDM方式下载的.elf文件. .hex是十六进制文本文件, 由多行符合Intel HEX文件格式的文本构成, 其中每一行为一条HEX记录. 记录分成6种类型, 但记录格式相同, 每个记录格式均包括1B的开始标记“:”、 1B的数据区长度、 2B的偏移量(装载到存储空间的偏移地址, 对非数据记录为0000)、 1B的记录类型(00-数据记录, 01-文件结束记录, 02-扩展段地址, 03-开始段地址, 04-扩展线性地址, 05-链接开始地址)、nB的数据/信息区(机器码、 常量、 静态变量和全局变量初始值、 地址等)、 1B的校验和.
图1 KL25_UART工程生成的.hex文件部分记录Fig.1 Partial records of KL25_UART.hex file
现以KL25_UART应用工程生成的.hex文件为例分析, 该应用工程用于测试KL25 开发板串口与PC机间通信情况, 其.hex文件的部分内容见图1.
根据链接脚本文件对存储区的划分, 中断向量表存放在Flash存储区的开始区域, 起始地址为“00000000”, 每个向量占4字节. 根据芯片启动文件startup_MKL25Z4.S对中断向量表的定义, 序号0中断向量是__StackTop(即栈顶地址, 符号量), 序号1中断向量是Reset_Handler(即芯片复位处理函数), 序号29中断向量是UART1_IRQHandler(即UART1模块中断处理函数)等等. 在Flash存储区的中断向量区中存储的是这些符号量或函数的链接地址即起始地址, 从.ld和.map文件中可以查到__StackTop、 Reset_Handler、 UART1_IRQHandler的值或函数的链接地址, 在本研究中分别为0x20003000、 0x00000800、 0x000008EC.
图1从第1行到12行是中断向量表记录, 数据/信息区中每4字节代表一个中断向量. 第1行记录中的“:10”表示记录以“:”开始, 长度为“0x10”; 接下来的“0000”是偏移地址, 表示数据在装载到Flash中时将从偏移地址为“0000”处开始存放; 紧接着的“00”代表该记录为“数据记录”类型; 接着是具体机器码数据“0030002001080000 4508000045080000”, 该数据装载到Flash中以小端方式(KL25 MCU内部采用的存储方式, 即字的低字节存储在低地址中)依次存放, 记录中只有这些数据才会被装载到Flash存储区中. 其中, 第1个4字节组“00 30 00 20”实际表示的数据是“20 00 30 00”, 即__StackTop. 第2个“01 08 00 00”→“00 00 08 01”, 其内容减1的值就是Reset_Handler链接地址, 也是程序开始执行地址. 在MCU启动时会被装入PC寄存器中, 内容减1是因为KL25的Cortex-M0+处理器的指令地址为半字对齐, 即PC寄存器的最低位必须始终为0, 但程序在跳转时, PC的最低位必须被置为1, 以表明内核仍处于thumb状态, 而非ARM状态. 最后的“FD”为校验和. 其余4字节组减1后的值为对应中断向量号的中断处理函数的链接地址.
第13行是Flash配置域. 从第14行到第208行是程序对应的机器码(含常量), 因为根据.lst文件, .text段的LMA=0x00000800, 再根据.ld文件对.text段的定义, 在.text中将依次存放非函数代码、 函数代码、 字符串和数组常量等. 根据.lst文件中的.data段的LMA=0x00001428可知, 第209行装载的是初始化过的静态变量及全局变量值. 第210行是程序开始执行的地址“00 00 08 00”(减1后). 第211行(最后一行)为文件结束记录, 记录类型为“0x01”.
由上述分析可知, 常量、 已初始化的静态变量和全局变量的值集中存放在机器码的后面, 这样将会与机器码一起装载到MCU的Flash中, 掉电后不会丢失. 而根据链接脚本文件.ld, 未初始化的静态变量和全局变量将存放在RAM中.data段后的.bss区域, 但.bss段不占用机器码文件和目标Flash空间, 只占用运行时的RAM. 当上电启动后, 在Reset_Handler函数(见后文分析)中将静态变量和全局变量的值拷贝到RAM的VMA处, 而在RAM中操作数据读写速度很快, 同时清零.bss段. 这也使得程序和数据占用的空间不能超过Flash容量, 数据和堆栈占用的空间不能超过RAM容量, 堆栈用于中断处理函数及其他函数调用时的现场保存、 局部变量存放、 程序中间结果存放等, 在设置堆栈大小时要予以考虑.
3 机器码系列文件分析的实际应用
通过综合运用.ld、 .lst、 .map及 .hex等机器码系列文件, 除可了解程序数据链接方法与过程、 函数的机器码及存放地址、 常变量的装载内存地址、 虚拟内存地址、 符号的处理方法外, 还可分析芯片上电启动过程并理解程序的执行过程、 计算程序占用空间和执行时间等.
3.1 在分析MCU上电或复位过程中的应用
根据机器码系列文件内容和MCU内部工作机制, 可以分析出从MCU复位到main函数之前的程序执行过程.
1) MCU上电或复位后, 其内部机制会从Flash的0x00000000地址处, 取出中断向量表第一表项内容赋给堆栈指针SP, 完成堆栈指针初始化工作. 第一表项内容正是.hex文件中第1行的第1个中断向量, 为栈顶地址.
2) MCU内部机制将第二表项内容, 赋给程序计数器PC. 第二表项内容正是.hex文件中第1行的第2个中断向量, 为Reset_Handler函数的链接地址, 本研究是0x00000800, 因而执行.hex的第14行中的机器码, 机器码存储在Flash的偏移地址是0x0800而实际地址是0x00000000+0x0800开始的存储区, 通过查看.lst可知. 该机器码对应于startup_MKL25Z4.S文件中的Reset_Handler函数: 关总中断、 调用SystemInit()函数关闭看门狗并初始化系统时钟、 开总中断、 将ROM中的已初始化的静态变量和全局变量的值拷贝到RAM中、 清零未初始化.bss段、 进入用户主函数main.
弄清启动执行过程后, 实际应用中, 就可根据是否要开启看门狗、 复制中断向量表至RAM、 清零未初始化BSS数据段等要求修改优化相关代码, 以进一步提高系统的扩展性、 稳定性、 可靠性. 例如, 可在main函数执行前添加监控代码以判断是否有命令要执行, 以便将机器码写入到Flash中, 若有则进行写入, 用这种方法可以快捷地更新用户程序.
3.2 在计算程序和数据占用空间上的应用
因.hex文件中的程序和数据直接装载到Flash中, 其相关文件也提供了程序、 数据分配和占用空间的辅助信息, 可据此计算出程序、 数据占用的存储空间, 以便决定是否要优化程序和数据.
要计算常量、 静态和全局变量以及单个函数占用的空间, 可根据.lst、 .map文件中提供的关于函数、 常量、 静态变量和全局变量等占用的空间信息来进行. 要计算特定的一行或多行程序占用的空间, 可根据.lst文件提供的源代码、 汇编代码、 机器码的对应关系进行, 即计算程序语句对应的所有机器码占用的总字节数. 要计算整个MCU应用工程占用的空间, 可直接对.hex文件中除最后2行入口地址记录和结束记录外每条记录的数据区字节数求和, 或根据.lst文件中“程序头及输出段的信息”中的size字段值计算.
例如, 通过计算后决定要修改某个函数或功能段, 可以先根据.lst文件中提供的关于其前几个机器码及存储地址, 在.hex中定位到该函数或功能段. 然后进行修改, 也可在main永久循环中添加监控代码, 以动态在线更新Flash中相应代码. 如要修改常量、 静态和全局变量, 先根据.lst文件提供的LMA在.hex中进行定位, 再根据需要修改, 或用监控程序在线更新Flash中相应值.
3.3 在计算程序执行时间上的应用
在嵌入式系统设计中, 当延时时间短且重复次数较少时可以用完全软件方式实现计数或定时, 即利用计算机执行指令的耗时实现. 对于用C语言编写的延时程序, 关键在于要精确测算出C语句的执行耗时, 为此, 可根据.lst文件提供的C语句生成的汇编指令和机器码及内核参考手册上提供的相关汇编指令的指令周期进行精确计算, 进而方便地设计循环次数或选择循环体语句.
4 结语
开发人员可以根据本研究, 在开发MCU应用工程时自行编写或修改链接脚本文件、 修改启动程序、 优化程序和数据等, 以便充分利用有限资源, 提高系统可靠性、 稳定性. 在后续研究中, 将开展对机器码相关文件中有关调试信息的研究, 提升其应用性.