刘浩
(青岛职业技术学院信息学院,青岛 266555)
使用μ Vision进行ARM嵌入式软件开发的工程师,在阅读μ Vision自动生成的引导代码之后会发现它并没有直接调用main函数,而是调用了一个奇怪的__main函数,并且手册中没有相关的解释。虽然表面上看起来这并不是什么大问题,但是实际上这里面蕴含着一些重要的概念,例如映像结构、分散加载机制和重定位等。掌握了这些概念之后,可以更好地理解系统初始化过程。由于__main函数位于ARM C库中,没有提供源代码,为此本文通过反汇编的方法从源代码级别详细地阐述了__main函数的功能:复制重定位区、初始化ZI节,以及初始化堆栈和堆等。示例工程getting-started-project-at91sam9261-ek运行在Atmel公司的AT91SAM9261-EK评估板上,集成开发环境为 Keil公司的 μ Vision3.62c,配置使用 Real-View工具链。
C语言源文件至少经过预处理、编译和链接3个阶段,才能生成最终的可执行映像文件。在预处理阶段,预处理器对源文件进行头文件和宏替换。编译与链接过程如图1所示。在编译阶段,编译器分别对各个源文件进行编译生成对应的对象文件。在链接阶段,编译阶段生成的对象文件、其他库和分散加载文件一起作为链接器的输入,生成最终的可执行文件,同时生成的还有链接地址映射文件,它包含了关于映像的节、符号表和存储器映射等信息。
图1 编译与链接过程
在嵌入式系统中,为了最大限度地提高系统效率,通常需要将一些关键代码(例如异常处理器)和频繁访问的数据(例如异常向量)定位到访问速度较快的存储器中。RealView工具链使用分散加载机制来创建具有复杂存储器映射的映像。下面的分散加载文件sdram.sct是为了在外部SDRAM中调试程序而编写的。
分散加载文件用来描述代码和数据加载时和执行时应在的位置。以sdram.sct为例,它定义了1个加载区和4个执行区。应用程序的映像文件存储在起始地址为0x20000000的加载区Load_region中。加载视图和执行视图如图2所示。系统上电后,除了异常向量VECTOR需要重定位到起始地址为0x00300000的片内SRAM以外,其余的代码和数据的地址保持不变。也就是说,符号的加载地址和执行地址并不是完全匹配的,例如VECTOR的加载地址为0x20001da4而执行地址为0x00300000,而且在加载视图中根本不存在堆、堆栈和ZI节。但是从加载视图到执行视图的转换并不是自动完成的,这部分代码位于库函数__main中,它主要负责将VECTOR从Load_region区移动到Relocate_region区,初始化ZI区、堆和堆栈,最后调用main函数。
图2 加载视图和执行视图
链接器armlink在生成可执行文件的同时,还会生成一个链接地址映射文件at91sam9261-sdram.map。它包含了映像文件的各种信息,例如交叉引用、符号链接地址,以及加载区和执行区的基址、大小和内容等。表1和表2列出了一些__main函数中需要使用的重要信息,对照图2可以更加清楚地理解它们的含义。
表1 加载区和执行区信息
表2 重要符号信息
从表1可以看出,整个映像大小为7700(0x00001e14,即Load_region的大小)字节。映像由不需要移动的Fixed_region和需要重定位的 Relocate_region组成。Relocate_region的大小为0x70字节,堆栈和堆的大小均为0x1000字节。执行区Fixed_region包含大部分 RO节、全部RW节和ZI节。其中,前两部分的大小由表2中的Image$$-Fixed_region$$Limit给出,ZI节的大小则可以根据表3计算得到,即0x2140-(0x1e14-0x1e00)-0x1000-0x1000=0x12c(去除了堆和堆栈占据的空间)。
表3 映像信息
另外,也可以从Fixed_region的内容得到验证,RW节的大小为0x14(0x1da4~0x1d90,即 Section Name为.data的那些数据),ZI节的大小为0x12c(0x1ed0~0x1da4,即Section Name为.bss的那些数据)。
由于__main函数是一个库函数,没有源代码,这样只能通过单步调试的方式浏览它的汇编代码,或者使用反汇编工具(例如IDA,The Interactive Disassembler)分析映像文件获得源代码。
区表(Region Table)位于地址0x20001D70(符号Region$$Table$$Base,参见 at91sam9261-sdram.map 文件)和0x20001D90(符号 Region$$Table$$Limit)之间。它用于存储一些重要的数据,32个字节一组,分别表示需要处理的数据的源地址、目的地址、大小和处理函数。区表作为一个核心数据结构,数据块的拷贝和清零都在它的控制之下。
本例的区表如表4所列。它包含两个条目:第一个条目表示调用__scatterload_copy函数从地址0x20001DA4处拷贝0x70个字节到地址0x300000处,即重定位Relocate_region区,对应图2中的“①”;第二个条目表示调用__scatterload_zeroinit函数将自地址0x20001DA4开始的0x12C个字节清零,即清零ZI节,对应于图2中的“②”。
表4 区 表
__main函数的处理流程如图3所示。基本思想就是依次取出区表中的各个条目,以第4个整数作为处理函数,以前3个整数作为处理函数的参数,调用处理函数。在处理完区表中所有条目之后,跳转到__rt_entry函数;在初始化完堆栈、堆和库之后,最终将控制权转交给__main函数。
图3 __main函数流程
__scatterload_rt2 函数首先分别将 Region$$Table$$-Base和 Region$$Table$$Limit加载到寄存器 R10和 R11中。__scatterload_null函数取出区表中的条目并调用相应的处理函数,即调用__scatterload_copy拷贝Relocate_region到0x300000,以及调用__scatterload_zeroinit零初始化始于0x20001DA4大小为0x12C的ZI节。下面的汇编代码按照地址、机器码和指令的次序布局,例如“0x2000005C E08AA000 ADD R10,R10,R0”的含义为地址0x2000005C处有一条机器码为 E08AA000的指令“ADD R10,R10,R0”。
__main:
0x2000004C EB000000 BL__scatterload_rt2(0x20000054)0x20000050 EB00066E BL__rt_entry(0x20001A10)
__scatterload_rt2:
;构造 R0,加载 Region$$Table$$Base和 Region$$Table$$Limit
;如果R10=R11,则已处理完RegionTable中的所有条目,跳转;到__rt_entry
完成重定位和零初始化ZI节之后,R10等于R11,跳转到__rt_entry函数,为堆栈和堆开辟存储器空间(如图2中③所示)、初始化C运行时库,最后调用__main函数。
本文针对RealView工具链详细阐述了映像文件的内部结构、加载视图与执行视图之间的区别和转换等底层概念,解释了进入__main函数之前系统都发生了什么。虽然这些工作都不需要程序员编码实现,但是知道它们的存在和它们的默认实现,有助于加深对系统的了解和认识。
[1]Sloss Andrew N,Symes Dominic,Wright Chris.ARM嵌入式系统开发——软件设计与优化[M].沈建华,译.北京航空航天大学出版社,2005.
[2]Seal David.ARM Architecture Reference M anual[M].2nd Edition.London:Addison-Wesley,2000.
[3]ARM Limited.RealView编译工具2.0版—开发者指南.
[4]Bryant Randal E,O'Hallaron David.深入理解计算机系统(修订版)[M].龚奕利,雷迎春,译.北京:中国电力出版社,2004.