映像文件分析,ADS 中startup.s 文件启动分析,学嵌入式开发ADS 必看
2010-04-17 10:21
声明: 我也是转来的,不是原创,由于别人是网易的日志,不能直接转,所以…… 感谢原
创!让我明白了startup.s 文件中的一些代码。
1、什么是arm 的映像文件,
arm 映像文件其实就是可执行文件,包括bin 或hex 两种格式,可以直接烧到ROM 里执行。
在axd 调试过程中,我们调试的是axf 文件,其实这也是一种映像文件,它只是在bin 文件中
加了一个文件头和一些调试信息。
映像文件一般由域组成,域最多由三个输出段组成(RO,RW,ZI),输出段又由输入段组成。所
谓域,指的就是整个bin 映像文件所处在的区域,它又分为加载域和运行域。对于嵌入式系
统而言,程序映象都是存储在Flash 存储器等一些非易失性器件中的,而在运行时,程序中
的RW 段必须重新装载到可读写的RAM 中。简单来说,程序的加载时域就是指程序烧入Flash
中的状态,运行时域是指程序执行时的状态。一般来说flash 里的整个bin 文件所在的地址
空间就是加载域,当然在程序一般都不会放在flash 里执行,一般都会搬到sdram 里运行工作,
它们在被搬到sdram 里工作所处的地址空间就是运行域。我们输入的代码,一般有代码部分
和数据部分,这就是所谓的输入段,经过编译后就变成了bin 文件中ro 段和rw 段,还有所
谓的zi 段,这就是输出段。在ARM 的集成开发环境中,只读的代码段和常量被称作RO 段
(ReadOnly);可读写的全局变量和静态变量被称作RW 段(ReadWrite);RW 段中要被初始化为
零的变量被称为ZI 段(ZeroInit)。对于加载域中的输出段,一般来说RO 段后面紧跟着RW 段,
RW 段后面紧跟着ZI 段。在运行域中这些输出段并不连续,但RW 和ZI 一定是连着的。ZI
段和RW 段中的数据其实可以是RW 属性。
2、简单地址映射
对于比较简单的情况,可以在ADS 集成开发环境的ARM LINKER 选项output 中指定RO Base
和RW Base,即在simple 模式下,告知连接器RO 和RW 的连接基地址。
这种模式下,ARM Linker 会输出以下符号,它们指示了在运行域中各个输出段所处的地址空
间,在使用的时候可以用IMPORT 引入:
| Image$$RO$$Base|: 表示RO 段在运行域中的起始地址
|Image$$RO$$Limit|:表示RO 区末地址后面的地址,即RW 数据源的起始地址
|Image$$RW$$Base|:RW 区在RAM 里的执行区起始地址,也就是编译器选项RW_Base 指定
的地址
|Image$$ZI$$Base|:ZI 区在RAM 里面的起始地址
|Image$$ZI$$Limit|:ZI 区在RAM 里面的结束地址后面的一个地址
RO Base 对应的就是| Image$$RO$$Base|,RW Base 对应的是|Image$$RW$$Base|,由于ZI 段
是包含在RW 段里的,所以|Image$$RW$$Limit| 就等于|Image$$ZI$$limit| 。
下面给出一个例子,假设RO Base 设为0x00000000,后面的RW Base 地址是0x30000000,
然后在Options 选项中有Image entry point ,是一个初始程序的入口地址,设为0x00000000(程
序的入口地址都是从代码段(RO)开始的)。现在要做的就是将RWsection 移到以0x30000000
开始的地方,并且创造一个ZI section。
首先比较Image$$RO$$Limit 和Image$$RW$$Base,如果相等,说明execution view 下RW
section 的地址和load view 下RW section 的地址相同,这样,不需要移动RW section;如果不等,
说明需要移动RW section 到它在execution view 中的地方,把ROM 里|Image$$RO$$Limt|开
始的RW 初始数据拷贝到RAM 里面|Image$$RW$$Base|开始的地址,当RAM 这边的目标地址
到达|Image$$ZI$$Base|后就表示RW 区的结束和ZI 区的开始,接下去就对这片ZI 区进行清零
操作,直到遇到结束地址|Image$$ZI$$Limit|。
ARM 映像文件及其地址映射(二)
示例代码如下:
IMPORT |Image$$RO$$Limit|
IMPORT |Image$$RW$$Base|
IMPORT |Image$$ZI$$Base|
IMPORT |Image$$ZI$$Limit|
IMPORT main ; 声明C 程序中的Main()函数
AREA Start,CODE,READONLY ; 声明代码段Start
ENTRY ; 标识程序入口
CODE32 ; 声明32 位ARM 指令
Reset LDR SP,=0x40003F00
; 初始化C 程序的运行环境
LDR R0,=|Image$$RO$$Limit| ;得到RW 数据源的起始地址
LDR R1,=|Image$$RW$$Base| ;RW 区在RAM 里的执行区起始地址
LDR R3,=|Image$$ZI$$Base| ;ZI 区在RAM 里面的起始地址
CMP R0,R1 ;检查RWsection 的地址在load view 和execution view 下是否相等
BEQ LOOP1 ;如果相等就不移动RWsection,直接建立ZI scetion
LOOP0 ;否则就copy RWsection 到execution view 下指定的地址
CMP R1,R3
LDRCC R2,[R0],#4 ;它把从R0 中的地址开始的section copy 到R1 中的地址开
始的section
STRCC R2,[R1],#4
BCC LOOP0
LOOP1
LDR R1,=|Image$$ZI$$Limit| ;ZI section 末地址
MOV R2,#0 ;将ZI section 需要的初始化量装入R2
LOOP2
CMP R3,R1 ;建立并初始化ZI section
STRCC R2,[R3],#4
BCC LOOP2
B main ; 跳转到C 程序代码Main()函数
END
注:LDRCC R2,[R0],#4 ;将地址为R0 的内存单元数据读取到R2 中,然后R0=R0+4
CC(小于),EQ(相等)为条件码。
当我们把程序编写好以后,就要进行编译和链接了,在ADS1.2 中选择MAKE 按钮,会出现
一个Errors and Warnings 的对话框,在该栏中显示编译和链接的结果,如果没有错误,在文
件的最后应该能看到Image component sizes,后面紧跟的依次是Code,RO Data ,RW Data ,
ZI Data ,Debug 各个项目的字节数,最后会有他们的一个统计数据,后面的字节数是根据
用户不同的程序而来的。
Image component sizes
Code RO Data RWData ZI Data Debug
17256 158096 8 184 112580 Object
Totals
1064 299 0 0 796
Library Totals
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Code RO Data RWData ZI Data Debug
18320 158395 8 184 113376
Grand Totals
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Total RO Size(Code+RO Data) 176715(172.57KB)
Total RWSize(RWData+ZI Data) 192 ( 0.19KB)
Total ROM Size(Code+RO Data+RWData) 176723(172.58KB)
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Code :显示代码占用了多少字节。
RO Data 显示只读数据占用了多少字节。
RW Data 显示读写数据占用了多少字节。
ZI Data 显示零初始化的数据占用了多少字节。
Debug 显示调试数据占用了多少字节。
Object Totals 显示链接到一起以后生成映像的对象占用了多少字节。
Library Totals 显示已提取并作为单个对象添加到映像中的库成员占用了多少字节。
Grand Totals 显示映像的真实大小。Grand Totals=Library Totals+Object Totals
下面就以上面的数据为例来介绍几个变量的计算:
|Image$$RO$$Base|=Image entry point=0x00000000;表示程序代码存放的起始地址
|Image$$RO$$Limit|=|Image$$RO$$Base|+Total RO Size ( Code+Ro Data )
=0x0+176715+1=0x0002B24C(因为要满足4 的倍数,所以+1)
|Image$$RW$$Base|=0x30000000;由RW Base 指定
|Image$$RW$$Limit|=|Image$$RW$$Base|+Total RW Size ( RW Data+ZI Data )
=0x30000000+192=0x300000C0
|Image$$ZI$$Base|=|Image$$RW$$Base|+RWData=0x30000000+8=0x30000008
|Image$$ZI$$Limit|=|Image$$RW$$Limit|
3、复杂地址映射
对于复杂情况,如RO段被分成几部分并映射到存储空间的多个地方时,需要创建一个称为“分
布装载描述文件”的文本文件,通知连接器把程序的某一部分连接在存储器的某个地址空间。
需要指出的是,分布装载描述文件中的定义要按照系统重定向后的存储器分布情况进行。在
引导程序完成初始化的任务后,应该把主程序转移到RAM 中去运行,以加快系统的运行速
度。
如下图,为了解决复杂memory map 的问题需要用到scatter load 机制。
__main() 和main()之不同:
当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序,即呼叫主应用程序。
最简单的一种情况是:
IMPORT main
B main
直接从启动代码跳转到应用程序的主函数入口,当然主函数名字可以由用户随便定义。
在ARM ADS 环境中,还另外提供了一套系统级的呼叫机制。
IMPORT _main
B _main
_main()是编译系统提供的一个函数,负责完成库函数的初始化和初始化应用程序执行环境,
最后自动跳转到main()。所以说,前者(_main)是库函数,后者就是我们自己编写的main()主
函数;
因此我们用的B _main 其实是执行库函数,然后该库函数再调用我们的main() 函数,因此在单
步调试时会看到先要跑一段程序(其实是库函数),然后再单步到我们自己的main 函数(这个同
时也说明如果有B _main 则就对应必须有main 函数,否则编译出错),如果我们用B main 来进
入我们的主函数的话,那在单步调试时就看到直接进入到我们自己的main 函数了,中间不会看
到其他程序;
那么用B _main 和用B main 这两这进入我们的main 函数方式有什么不同呢?
如果采用前者则会由编译器加入一段"段拷贝"程序,即我们说的从加载域到执行域转化程序;
而采用后者就没有这个了,因此如果要进行"段拷贝"只能自己动手编写程序来实现了,完成段
拷贝后就可以进入我们的主函数了,当然这个主函数不一定是叫做main(),可以起个其他好听
的名字,这个有别于使用B __main 方式;不管采用哪种方式进入我们的程序,都要有一段"段拷
贝"程序,跑完了段拷贝后才能可以进入我们主程序了!(顺便提一下:startup.s 这个文件并没有
所谓的"段拷贝"功能,再看也无益!)
对含有启动程序来说,"执行地址与加载地址相同"不容易实现:如果执行地址与加载地址相同
哪当然不需要做"段拷贝",但是个人理解编译器还会加入"段拷贝"程序(如果用B __main 的话),
只是因为条件不满足而不执行而已;但是对含有启动程序来说,"执行地址与加载地址相同"就
不容易了.因为启动程序是要烧到非易失存储器里,用来在上电执行的,而这个程序必定会有
RW 段,如果RW 放在非易失存储器,如FLASH,那就不好实现RW 功能了,因此要给RW 移动到
能够实现RW 功能的存储器,如SRAM 等.因此,对含有启动程序来说,"执行地址与加载地址相
同"就不容易实现;程序的入口点在C 库中的__main 处,在该点,库代码执行以下操作:
1. 将非零(只读和读写)运行区域从其载入地址复制到运行地址。
2. 清零ZI 区域。
3. 跳转到__rt_entry。