ARM 编译连接原理入门

来源:互联网 发布:招聘新媒体美工的要求 编辑:程序博客网 时间:2024/06/05 03:21
ARM 编译,连接 和调试原理
ARM RealView 编译工具已经发展了16年,一直致力于为客户提供最好的编译器。RVDS 是ARM公司继SDT与ADS1.2之后主推的新一代开发工具。目前最高版本是3.1。它由下面三部分组成:
(1):RealView编译器(RVCT)
(2):RealView汇编器(armasm)
(3):RealView连接器(armlinker)
(4):RealView调试器(RVDebugger)

1:特点
  RVDS对代码密度的提升、代码执行速度的提高,都可以由ARM开 发工具自动实现,而不需要软件开发人员花费过多的时间手动优化高级语言代码。这是RVDS的优势所在。先前版本中的编译器armcc,tcc,armcpp,tcpp 已经整合成一个编译器armcc,可以将标准的C或C++语言源程序编译成32位ARM指令代码或者16位Thumb指令代码或者Thumb-2指令代码。编译器输出的ELF格式的目标文件,包含调试信息。除此之外,编译器可以输出所生成的汇编语言列表文件。RVDS的编译器根据最新的ARM架构进行特别的优化,针对每个ARM架构都提供最好的代码执行性能,最优的代码密度。可以根据需要选择调试信息级别,以及不同的代码优化方向和优化级别。

2  RVCT中C和RogueWaveC++库 
2.1:完整ISO标准C语言库 
    标准C语言函数集,C语言库需要的支持函数以及在Semihosted执行环境中需要的目标相关的函数。
    ARM C语言库结构使用户很容易定义目标相关函数,以适应特定的目标环境。 
2.2:浮点函数库使用ARM 在IEEE754标准(二进制浮点算法)上实现的  浮点环境。 
2.3:RogueWaveC++库 
    RogueWaveC++库包含标准C++函数,编译器需要的支持函数

3 目标代码加载
各种源文件经过ARM编译器编译后生成ELF 格式的目标文件。这些目标文件和相应的C/C++运行时库经过ARM连接器处理后,生成ELF格式映像文件。ARM连接器可以去除使用不到的代码段和函数,这样可以减少内存的使用。ARM连接器可以将不同的指令代码和数据代码放置到不同的内存地址范围。通常在嵌入式系统中,指令和数据代码会固化在非易失性存储器中(ROM或Flash),可以从这些地方上电启动。从运行速度方面考虑,部分指令和数据代码会在启动后搬运到易失性存储器(RAM)中,因此连接器可以使用一些方法机制来配置调度。 
   这种分散装载(scatterloading)的机制可以让把不同的指令和数据分散的放到不同的地址,而且这些地址在系统启动和系统运行可以是不同的映射。 详细的地址分配可以是用参数来指定,或者用一个描述文件来作为连接器的参数。使用描述文件会使维护起来非常简单,而且如果要改变地址分配,不需要把整个项目完全重新来做,只要把项目中需要的目标重新连接即可。 
  一个scatterloading文件的示例: 
LOAD_FLASH 0x04000000 0x80000            ; 启动地址和长度 

    EXE_FLASH 0x04000000 0x80000 
        { 
                init.o (Init, +First)            ;   
                * (+RO)                          ;   
        } 
        32bitRAM 0x0000 0x2000 
        { 
                vectors.o (Vect, +First)      ;   
                int_handler.o (+RO) 
        } 
        16bitRAM 0x2000 0x80000 
        { 
                * (+RW,+ZI)                    ;   
        } 

   scatterloading文件的解释说明
  本文件定义了启动区域和三个执行区域。在大括号外面定义了启动区域(LOAD_FLASH),里面三个定义了执行区域:
    EXEC_FLASH
    32bitRAM
    16bitRAM) 
(1):为了提高运行速度,异常向量(在vectors.s)和异常处理函数
   (在int_handler.c)被重新放置到32bitRAM的零地址开始的地方。 
(2):可以读写的变量被复制到16bitRAM的0x2000地址开始的地方。 
(3):零初始化的数据和可读写数据放在16bitRAM内。 
(4):其他不需要搬运的代码只需要还放在Flash里就好。 

4  RVCT 的优化级别与优化方向 
  4.1:armcc的四个优化级别
      -O1、-O2、-O3、-O4,
  4.2:两个编译选项
      -Otime、-Ospace。 
-Ospace与-Otime负责给编译器提供代码优化的大方向,告知编译器编译任务的主要目标是
代码密度(-Ospace)还是代码性能(-Otime)。而-O1、-O2、-O3、-O4则分别代表4种逐次递进的不同优化级别。 

  OSpace 还是OTime? 
   显然代码密度与代码执行速度在很多情况下是一对矛盾。以下面的代码为例。例1中左右两段代码可以完成相同的任务,但是左边的有较高的代码密度,右边的则有较高的执行速度。因为当expr = 0时,标志循环结束时,右边的代码可以顺序执行下去;而左边代码必须先跳转至循环体首部判断expr的值,随后再跳转道循环体尾,继续执行下一条指令。 
例1 代码速度与尺寸的对比 
例1:
while (expr)         ||        if (expr) do  
{                    ||       {  do 
    body;            ||              { body; } 
                     ||          while (expr); 
}                    ||          } 

  那么我们什么时候使用Otime 什么时候使用Ospace呢?Otime与Ospace
需要开发人员根据系统实际需求来决定,最好的情况是在两者之间找到一个合适的平衡点,而不是单纯的追求速度或者代码尺寸的缩小。即,将不同的代码模块根据其特性分别使用不同的编译选项。
此外,RVCT编译器支持很多非常有用的编译选项。
--no_inline(取消所有代码的内联函数)
--split_ldm(限制LDM/STM指令的最大操作寄存器数目)、
--split_sections(将每个函数,而不是源文件,作为一个编译单元进行操作) 
编译器的所有这一切都可以严格根据开发者的要求,帮助开发人员得到系统真正需要的优化过了的代码。 

O3 还是O2? 
老的开发工具,如ADS1.2中,只有3种递进的代码优化级别,对应3种编译选项,即-O0(Minimum optimization)、-O1(Restricted optimization)、-O2(High optimization )。 
  使用-O0编译选项时,RVCT编译器只对代码作最基本的优化操作,编译结束后用户得到的代码与用户手写源代码之间的差距很小,这种特性的主要作用是为了方便用户在程序开发阶段的调试工作,避免由于优化而产生的调试屏障。此外,很多资深软件工程师偏向于手写优化代码,在这种情况下,由于代码已经被优化过,可以使用-O0编译选项减少RVCT的工作量,节省编译链接的时间。 
  -O1与-O2则分别是相对于-O0更加高级别的编译优化选项,前者提供有限的优化;后者则会对代码进行较大程度的优化改进操作。 
RVDS中新增加了-O3(Maximum optimization)编译选项,它可以最大程度的发挥RVCT编译器的优势,将代码编译成最优。O3与O2都是较高级别的编译优化选项,但-O3相比较于-O2,主要优势有以下几点。当用户使用-O3选项时: 
(1): 编译器会自动对代码进行髙阶标量优化。所谓的高阶标量优化就是编译器对根据代码特点,针对循环、指针等进行髙阶优化。 
(2): 编译器会把尽可能多函数的编译为内联(inline)函数; 
(3): Multifile compilation功能被自动使能。 

对于循环与指针的髙阶优化(High-level scalar optimizations) 
当编译选项为-O3 和–Otime时,RVCT会根据代码的具体情况,针对循环、指针等部分作髙阶优化工作:循环解开(Loop unrolling)、融合(fusion)、位置调整(interchange)、指针优化等等。以例2的函数为例。例2是一段简单的C循环函数,在循环中含有数组指针调用。 
例2     
CodeA 
void increment(int *restrict b,int *restrict c)   
{       int i; 
        for (i = 0; i < 100; i++) 
        { 
            c = b + 1; 
        }       

CodeB 
void increment(int *b, int *c) 

int i; 
int *pb, *pc;   
int b3, b4; 
pb = b - 1;   
pc = c - 1; 
b3 = pb[1]; 

for (i = (100 / 2); i != 0; i--)   
        { 
            b4 = *(pb += 2); 
            pc[1] = b3 + 1;   
            b3 = pb[1];   
            *(pc += 2) = b4 + 1; 
        }   

仔细观察可以发现,CodeA与CodeB可以完成同样的功能,即将数组b的每个成员加1赋值给数组c对应成员。但是CodeB与CodeA相比,有较高的执行速度。主要体现在以下几点: 
(1):循环100次变成了循环50次(loop unrolling),减少了跳转次数; 
(2):数组变成了指针,减少每次计算数组偏移量的指令; 
(3):微调了不同代码操作的执行顺序,减少了流水线stall的情况; 
(4):循环从++循环变成了――循环。这样可以使用ARM指令的条件位,为每次循环减少了一条判断指令。 
很多程序员就是这样,通过这种手写不同的C 代码,再实现相同任务的情况下,提高了代码执行效率。 
在RVDS中,使用-O3 和–Otime编译选项,RVCT会自动帮助程序员进行这些髙阶标量优化,即,RVCT会直接将CodeA 优化成以前由CodeB才能得到的汇编代码。虽然优化之后函数的代码尺寸大于原先的函数,但是执行速度却有大大的提高,经过统计,使用EEMBC benchmarking,-O3编译选项编译得到的最终代码平均性能相对于-O1可以有10%的提升,而总体代码尺寸只增加了1%。 
5  Multifile compilation 
  按照传统的编译方式,我们先把各个C或C++文件单独编译成.obj文件,再将这些目标文件链接在一起。考虑到虽然在编译单独的C 或C++文件时,编译器会充分发挥它的优化特性;但此时,编译器无法关注到大量的C 或C++文件接口之间可以优化的部分。所以在传统的编译结果里,还有许多优化的余地。如何才能让编译器同时关注和编译所有的源代码呢? 
Multifile compilation是RVDS一个较新的特性,它可以帮助开发人员将所有的源文件作为一个compilation unit进行编译,并最终生成一个大的目标文件(如图3中的file1.o)。Mutifile compilation给软件开发人员带来的直接优势有以下几点: 
(1):增加inline的可能性。由于inline只能发生在一个compilation unit中,所以在没有使用mutifile compilation时,inline只能发生在一个源文件范围内。
Multifile Compilation将一个compilation unit扩大到了所有源文件的范围上,所以直接增加了inline发生的几率。 
(2):增加了基地址与函数间优化的可能性。同inline一样,所有的基地址与函数间的优化也必须在一个compilation unit中,随着conpilation unit的扩大这种优化的可能性也增加了。
(3):减少了scatter file的复杂性。

原创粉丝点击