编译器基础

来源:百度文库 编辑:神马文学网 时间:2024/06/03 08:09:45
看了这个题目,请不要误会我要告诉您编译器是怎么实现的,我写这节的主要目的是告诉您通常编译器是怎样对待您所写的程序的。大家都知道,程序最终都要在CPU上运行,那么像C语言这样的高级语言来说,编译器就是联结C和CPU之间的桥梁了。在这一节里我要告诉您编译器是如何充当这个“桥梁”角色的。
       本节首先讲述一下程序员的层次问题,目的是让我们自己知道需要成为一名什么样的程序员;然后分别是编译器的相关介绍以及编译器是如何“对待”程序等内容。
3.1 软件和程序员的层次
       如果要了解程序员的层次,那就要先看看程序的层次了。看图3.1:
程序源代码
编译器
中央处理器(CPU)


图3.1 程序的层次
上图的语言描述就是程序源代码经过编译器生成可以在CPU运行的二进制代码。由此衍生出了程序员的两个层次——语言层次和二进制层次。
所谓的语言层次的程序员,就是那些可以使用某一个编程语言的程序开发人员。大多数的初学者都属于这个层次,能够遵照一定的规范完成编码工作,但是对下面的事情(编译器和CPU)就一知半解了。
所谓的二进制层次的程序员,就是那些熟练地掌握某一个编程语言并且知道这个语言的来龙去脉的程序开发人员。他们对程序有着深刻的认识,对于程序中每一部份发生的事情了如指掌。二进制层次的程序员要比语言层次的高,是软件中的高手,因此也比较难得。对于二进制层次的程序员来说,可以非常快速的掌握一门开发语言,因为对于他们来说在二进制层次上只有CPU指令的差别而没有语言的差别。不过要想成为一名这样的程序员是需要付出很多努力,并积累足够的编程经验才可以。
程序语言所要解决的问题就是如何更加高效的组织二进制代码。例如,汇编语言是对CPU指令的符号化,目的是为了容易使用CPU指令从而提高编码效率。由于汇编语言对二进制代码基本没什么组织,因此称它为“低级语言”。C/C++语言对二进制代码的组织有了很大的提高,制定了完全脱离了CPU指令的语法规则,大大提高了程序开发效率,因此我们称它为“高级语言”。从这个角度来讲,二进制始终是任何程序开发语言的归宿,因此在二进制层次理解程序是十分必要的。
然而,正是这些高级语言制定了脱离二进制指令的语法规则,将大部分二进制细节都隐藏在了编译器之内,因此加大了程序员与二进制之间的距离,使得成为一名二进制层次的程序员更加困难了。在这一节里我要向您揭示那些隐藏在编译器里的二进制细节,期望能够为您成为一名二进制层次的程序员提供帮助。
3.2 编译器的分类和作用
       从不同的角度可以进行不同的编译器分类,因此在这里列举一些典型的编译器并一一进行讲解,这样我们就可以更加直观的看到各个编译器之间的不同部分。这些典型的编译器分别是:汇编语言编译器、Borland C/C++编译器、ARM C/C++编译器、VC++编译器和典型的Java编译器。由于本书主要使用C语言相关的工具,因此这里主要列举的是C语言相关编译器。
       还有重要的一点是,在本书中大部分的编译器指的是编译器和链接器,除非它们两个摆在一起。
3.2.1汇编语言编译器
       我们知道,汇编语言只是简单的对CPU指令进行了封装(封装也就是包装的意思,通常计算机术语里这么称呼),因此汇编语言编译器是与CPU相关的。结果是使用汇编语言编写代码的可移植性很差,因为不同的CPU可能指令系统和寄存器都不一样。了解计算机历史的人应该知道,最初写的程序都是二进制的,就像是最原始的打孔机之类的东东,需要每个程序员记住每个指令的二进制值(也就是0和1的组合),编程效率可想而知(向那一代程序员致敬!)。不过汇编语言的出现使得编程效率大大的得到提升,程序员再也不需要记忆指令的二进制值了。也是从这里开始,程序效率提升的步伐大大加快了。紧接着,高级语言和计算机操作系统的飞跃发展更为提高编码效率创造了良好的条件。
3.2.2 Borland C/C++编译器
       这个编译器是在DOS操作系统时代非常流行的C语言编译器。从这句描述可以看出它是与操作系统相关的,当然要完成在CPU上运行是一定要和CPU相关的。通常对于一个编译器来说会根据不同的CPU类型生成不同的二进制代码,这通常通过为编译器传递参数来实现。
       现在可能有些人会问一个鸡生蛋还是蛋生鸡的问题:编译器是由谁来编译的?如果一定要追根溯源的话,我会告诉您,世界上第一个汇编语言编译器是用机器码写的,因此不需要编译;世界上第一个高级语言编译器是由汇编语言写的,操作系统也在这个过程中不断的发展。也正是由于不同CPU和操作系统平台以及编译器的交互发展,才产生了当今世界如此的软件规模。也可以想象的到英特尔和微软等欧美企业屹立不倒的原因了,因为他们伴随着计算机产业一同成长了那么多年。
3.2.3 ARM C/C++编译器
       ARM是当前嵌入式系统中最为常用的CPU内核芯片解决方案。由于ARM主要用于嵌入式系统,且嵌入式系统的操作系统千差万别,因此它的目标环境要求与操作系统无关,甚至于操作系统的源代码也是与应用程序代码一起编译的。因此从这个意义上来说,嵌入式系统对于二进制的表现是最完全的,从中我们可以一窥整个系统二进制层次的原貌。在以后的章节中我们也将主要围绕着ARM编译器进行讲解。
3.2.4 VC++编译器
       VC++编译器是微软的基于Windows操作系统的C/C++编译器,它是紧密的与Windows操作系统相关的。也正是由于它和Windows操作系统联系之紧密,导致它与跨平台没有了任何的关系。如果说对于普通的编译器,如ARM C/C++编译器、Borland C/C++,由于其需要实现的平台相关代码较少,我们可以比较容易的实现跨平台无关的代码开发,那么,VC++的编译器就十分的困难了,基本上不可能。
3.2.5 Java编译器
       Java是号称跨平台的,没错,确实是这样子。Java能够实现跨平台的前提条件是需要在这个Java平台上实现Java虚拟机,这个虚拟机可就不是跨平台的啦,而是与平台紧密相关的,每一种平台要实现各自的Java虚拟机。由此理解,Java编译器编译的目标代码不是针对CPU和操作系统的,而是真对Java虚拟机的。所以也可以称Java语言是一种半解释型的语言。从这里还可以看出,Java程序的执行效率比直接编译生成二进制的程序执行效率要低。
      
       接下来看看上面的各种编译器与CPU和操作系统的关系。看下表:
名称 与CPU相关性 与操作系统相关性
汇编语言编译器 强相关 无关
Borland C/C++编译器 强相关 相关
ARM C/C++编译器 强相关 无关
VC++编译器 强相关 强相关
Java编译器 无关 无关

       为了我们知识的完整性,我想还是有必要提及一下脚本(Script)语言。脚本语言广泛的应用于网页设计中,嵌入到网页内部实现动态或交互的Web应用。典型的是JavaScript和VBScript。脚本语言属于完全的解释型语言,不需要编译器,直接由脚本的处理程序生成结果。
3.3 编译器的数据处理方式
       编译器的作用是将源代码编译成可以在目标平台运行的程序,核心要处理的就是程序员所写的程序。因此,对于编译器要处理的主要程序元素有:代码、局部变量、全局变量、静态变量以及常量,概括起来就是代码和数据。如何在二进制层面组织代码和数据就是编译器需要完成的工作。通常在二进制层面存在以下三种类型的二进制数据:
       1、只读数据(RO Read Only):程序和常量
       2、可读可写数据(RW Read &Write):有初始化值的全局变量和静态变量
       3、零初始化数据(ZI Zero Initialize):无初始化值的全局变量和静态变量
       最终这些数据将会通过编译连接生成一个二进制的可执行文件,并将这些数据分别放在不同的可执行文件段(Section)中:只读数据放在text段中;可读可写数据放在data段中;另初始化数据放在bss(Block Started by Symbols)段中。接下来我们将看一看这些数据究竟是如何组织的。
3.3.1只读数据
       只读数据是我们的程序在执行的过程中不能修改的数据,开动脑筋,仔细地想一想在我们的程序中那些数据是不能修改的呢?
       第一个出现在我们脑海中的应该是那些常量。在C语言中,我们可以使用const关键字来声明一个常量。我们知道,一个变量按照其作用域可以分为全局变量和局部变量,对于一个全局的const变量来说,属于只读的数据是没有任何疑义的。但是对于一个使用const关键字的局部变量来说就有问题了,这些变量由于属于局部变量,因此在运行的时候仍然是放在堆栈中的。如果希望它变成无可争议的只读数据的话,那么我们需要同时声明这个变量为static类型的,因为在C语言中,静态变量采用了与全局变量一样的处理方式。
       除了常量之外,第二个属于只读数据的就是代码本身。代码是数据吗,有没有搞错?不要心存疑问,事实的确如此!在编译器看来,不论时变量还是代码,都属于数据。恰恰是程序中的代码是只读数据的主力军。因为在运行的时候,代码是不可改变的,所以对编译器来说,代码属于只读数据来说就不奇怪了。
3.3.2可读可写数据
       可读可写数据是指那些在代码中指定了初始化变量的全局变量和静态变量。在C语言中,全局变量属于固定分配内存的方式,需要在链接的时候就为其分配固定的存储空间。这些存储空间属于这个变量私有,从系统启动到关闭为止都只能由这个变量使用。如果一个非常量的全局变量含有初始值,那么我们就需要首先存储这些初始值,并把它们保存在我们生成的二进制可执行文件中,同时为它分配一个RAM中固定的存储空间。当我们开始执行这个可执行文件的时候,需要将这些初始值从可执行文件中复制到它所对应那段固定存储空间中。
       从某种程度上来说,可读可写的数据中的一步分需要存储在可执行文件的text区中,因为可读可写变量的初始值要存储在一个只读的空间中,在运行的时候才会复制到该变量对应的空间中。而这部分的初始值也就是只读数据的一步分了。这一点在不支持虚拟内存管理的嵌入式系统中表现得尤为突出。在采用这种方式的系统中是十分占用系统存储空间的,因为它既要占用代码段来存储初始值,而且也要占用同等的RAM空间来存储数据。
3.3.3零初始化数据
       零初始化数据就是在程序中定义的那些没有初始值的全局变量和静态变量。这些变量的特点就是固定分配存储空间,使用0做为初始化的值。这样,整个程序中零初始化变量就可以连接成为一个整片的内存区域,只需要知道地址区间,然后在初始化的时候都赋值为0就可以了。在执行程序的时候,展开ZI数据的信息,根据地址区间全部进行零初始化赋值操作。因此,零初始化数据之需要占用很少的空间来记录起始地址和结束地址就可以了。
       在嵌入式系统中,由于操作系统的代码和数据也包含在这三种类型的数据里面,因此对这三种数据的处理方式就有了很大的不同。从前面的章节中我们知道,嵌入式系统主要有CPU+ RAM+ Flash的结构,因此对于系统中的只读数据是存储在Flash中的,而RW和ZI数据则是存储在RAM中的。Flash芯片中存储的就相当于在Windows操作系统下的可执行文件(.exe),在系统启动的时候,将存储在Flash中的RW数据复制到对应的内存区域中,将ZI数据按照起始和结束地址零初始化相应的区域。
       到现在为止,细心的读者可能会发现还没有提及局部变量的处理过程,他们是存储在什么地方的?首先说明这里所说的局部变量不包含局部的静态变量,因为不管是全局静态变量还是局部静态变量全部按照全局变量方式处理。局部变量产生于函数的内部,因此它的产生和消亡也都在函数里面。在程序开始执行某一函数的时候,会为这个函数内声明的局部变量在栈内分配空间,在函数结束的时候将这些空间弹出。由此可以看出,在使用局部变量的时候要考虑栈的空间大小。由于系统中的栈主要由操作系统来管理,并且通常分配的空间是有限的(尤其是在嵌入式系统中),因此不要在函数体内使用较大的数组。
3.4 编译和链接
       当我们行进到这里的时候,我想我们不得不先澄清一下编译器和链接器的概念。在前面讨论的编译器中是包含了编译器和链接器两个部分的,用它来代表从代码到可执行程序的全部处理过程。在实际的编译生成可执行程序中实际上包含了两个步骤——编译和链接。编译的作用是执行预编译操作、检查源程序中的错误以及生成中间目标码,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File;链接的作用是将编译过程产生的中间目标码(Object File)组合生成最终的二进制可执行文件。这主要是考虑到有多个源文件的情况下可以单独的编译每一个源文件(或者可以称作模块),这样就为大工程的源代码编译管理带来了极大的方便,关于工程管理的知识我将在工程管理基础部分讲解。
       编译时,编译器需要的是语法的正确、函数与变量的声明的正确、编译器头文件的所在位置的正确,只要这些内容都正确,编译器就可以编译出中间目标文件。一般来说,每个源文件(.c/.cpp)都应该对应于一个中间目标文件(O文件或是OBJ文件)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File)。链接完成后就生成了可以运行的二进制可执行文件。
3.5 控制可执行程序的生成
       在完成了编译和链接生成可执行程序之后,我们或许要问怎么样控制这个可执行程序的生成呢?是编译器全部都安排好了吗?如果能够控制可执行程序的生成,那么采用什么方式控制呢?
       这一切的控制都是由链接器完成的。任何一个链接器都会有相应的方式控制程序的入口点(Entry)、程序基址(也就是程序在内存中的第一个位置)等内容。在VC等IDE开发环境中,已经通过“工程属性”的链接器设置选项来控制这些内容。典型的嵌入式系统中ARM编译器是通过一个叫做Scatter-Loading的.scl描述文件来控制整个二进制可执行程序的存储器安排。在我们前面的程序世界中我们都是使用main()函数做为C语言的程序入口,但实际上我们可以通过编译器的选项来控制这个入口函数。比如,我们可以在VC工程中的工程属性的编译器选项中指定入口点是mymain()函数,而不是main()函数。在ARM链接器中通过-first来指定入口函数的值。
       为了能够让我们更加深入的理解二进制可执行文件,在这里介绍一下ARM编译器使用的Scatter-Loading机制。首先我们先来看一个真实的Scatter-Loading描述文件的例子,这个例子的全部文件在Test4文件夹的system.scl文件内:
CODE_ROM 0 0xFFFFFF
{
       # Boot Block区域
    BB_ROM +0x0
    {
        bootblock.o (BOOTBLOCK_IVT, +FIRST)
        bootblock.o (BOOTBLOCK_CODE)
        bootblock.o (BOOTBLOCK_DATA)
    }
 
    # 主程序代码和常量
    MAIN_APP +0x0
    {
        * (+RO)
    }
 
    # RAM数据
    APP_RAM 0x10000000
    {
        * (+RW, +ZI)
    }
 
    # 用户自定义的保存区域
    RESERVED_RAM 0x10700000
    {
        mainapp.o (reserved)
    }
}

在这个文件中,CODE_ROM 0 0xFFFFFF指定了这个最终生成的二进制文件地址空间空间在0-0xFFFFFF内,也就是16M字节空间。在这个内部,依次规划了Boot Block区域的代码和常量数据段(RO数据)、主应用程序的代码和常量数据段(RO数据)、应用程序RAM数据(RW和ZI数据)以及用户特殊用途的保留区域的数据。
BB_ROM +0x0中的+0x0表示紧接着上面分配的空间,这里就是0。MAIN_APP +0x0就表示了接着BB_ROM之后的空间进行分配。APP_RAM 0x10000000表示RAM的地址空间从0x10000000地址开始,依次类推,这个内存影射的示意图如下:
Boot Block(ROM Code & Const)
应用程序(ROM Code & Const)
应用程序(RAM ZI& RW)
 
用户自定义保留区(RAM reserved)
0x00000000
Unknown Address
0x10000000
Unknown Address
0x10700000


图3.2存储器映射图
       用户保留区域是由用户根据需要通过#pragma arm section指定的数据区域类型,可以参看Test4中的mainapp.c的定义。
bootblock.o (BOOTBLOCK_IVT, +FIRST)是非常关键的一句,它描述了将bootblock.o中的BOOTBLOCK_IVT区域的数据做为最前端数据,+FIRST表示放在这个地址段的最前面,在这里也就是ARM CPU的中断向量表区域。
bootblock.o (BOOTBLOCK_CODE)和bootblock.o (BOOTBLOCK_DATA)中说明的是使用BOOTBLOCK_CODE 和BOOTBLOCK_DATA 说明的区域,可以参看bootblock.s中的相关内容。
* (+RO)表示将所有尚未匹配的只读数据放在这个区域,* (+RW, +ZI)表示将尚未匹配的RW和ZI数据放在这个空间中。
mainapp.o (reserved)表示将mainapp.o中的使用“reserved”包含的数据放在这个空间中。
       整个Test4工程展示了一个完整的ARM系统的构建要素,请认真地研究这个例子,它一定能够让您更加深刻的认识一个嵌入式系统。并且也有助于我们理解其他的系统,毕竟所有的系统原理都是相通的。在工程管理(Make File)一节中还将详细的展示怎样使用这个.scl文件。
3.6 可执行程序的启动过程
       对于一直生活在Windows世界的人们来说,最熟悉的可执行程序莫过于.exe文件了。每当打开这些文件的时候,都会启动一个应用程序。对于写程序的人来说,还知道程序中的main()函数或者由链接器指定的入口函数是首先被执行的。这种说法是正确的,只不过忽略了执行入口函数前的处理。最简单的,具有初始化值的全局变量是优先于main函数被赋值的。为了说明这个问题我们还是来看看Test4中的原文件吧。这个例子里面购建了一个相对完整的从ARM启动到执行main函数的过程,相信会对您理解其他的程序也会有所帮助。如果您有make运行环境和ADS1.2环境的话,这些代码是可以编译链接的。
与前面简短的例子不同,这个例子中包含了三个原文件和一个头文件,总共约300行程序。我相信,对于一个程序员来说,300行程序也不算什么。在这里我们占用了8页多的篇幅,相信这里是本书中最长的一段代码了,不过我觉得有必要讲解这段程序,好让我们看一看以前从未重点关心过的部分。
system.h – 系统头文件
#ifndef SYSTEM_H
#define SYSTEM_H
/*====================================================================
                        ARM 处理器常量定义
文件名:system.h
描述:
    这个文件内定义了ARM处理器的常量和栈尺寸,以及一个汇编的调用函数的宏
====================================================================*/
#ifndef _ARM_ASM_
typedef unsigned char     byte;         /* Unsigned 8 bit value type. */
typedef unsigned short    word;         /* Unsinged 16 bit value type. */
typedef unsigned long     dword;        /* Unsigned 32 bit value type. */
#endif //_ARM_ASM_
 
/* CPSR Control Masks         */
#define PSR_Fiq_Mask         0x40
#define PSR_Irq_Mask         0x80
 
/* Processor mode definitions */
#define PSR_Supervisor       0x13
#define PSR_System           0x1f
 
/* Stack sizes.               */
#define SVC_Stack_Size       0xd0
#define Sys_Stack_Size       0x400
 
/* 用户数据区大小             */
#define USER_HEAP_SIZE       0x1000
 
#if defined(_ARM_ASM_)
/*====================================================================
 名称: blatox
 描述:
      不必理会是在ARM或THUMB模式下调用函数的方法
 Arguments:
    destreg - 包含被叫函数地址的寄存器
 被修改的寄存器: lr
====================================================================*/
        MACRO
        blatox     $destreg
        ROUT
 
        tst     $destreg, #0x01         /* Test for thumb mode call. */
 
        ldrne   lr, =%1
        ldreq   lr, =%2
        bx      $destreg
1
        CODE16
        bx      pc
        ALIGN
        CODE32
2
        MEND
#endif // _ARM_ASM_
#endif //SYSTEM_H

 
bootblock.s – 系统入口文件
;*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
;                  System Boot Block
; 文件名:     bootblock.s
; 描述:  
; 此模块中定义了系统的中断向量表和Reset时的处理函数
;*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
;=====================================================================
;                           MODULE INCLUDE FILES
;=====================================================================
#include "system.h"
 
;=====================================================================
;                             MODULE IMPORTS
;=====================================================================
        IMPORT ram_init
        IMPORT __rt_entry
       
        ; 导入系统栈的地址
        IMPORT svc_stack
        IMPORT sys_stack
       
        ; 导入由链接器根据Scatter-Loading描述文件生成的数据区域RAM地址
        ; 和数据区大小
       
        ; 应用程序RAM
        IMPORT |Load$$APP_RAM$$Base|     
        IMPORT |Image$$APP_RAM$$Base|
        IMPORT |Image$$APP_RAM$$Length|    
        IMPORT |Image$$APP_RAM$$ZI$$Base|
        IMPORT |Image$$APP_RAM$$ZI$$Length|
       
        ; 用户保留区RAM
        IMPORT |Load$$RESERVED_RAM$$Base|
        IMPORT |Image$$RESERVED_RAM$$Base|
        IMPORT |Image$$RESERVED_RAM$$Length|
        IMPORT |Image$$RESERVED_RAM$$ZI$$Base|
        IMPORT |Image$$RESERVED_RAM$$ZI$$Length|
       
;=====================================================================
;                             MODULE EXPORTS
;=====================================================================
        ; 导出重新命名过的链接器符号,以便于其他模块使用
       
        ; 应用程序RAM
        EXPORT Load__APP_RAM__Base
        EXPORT Image__APP_RAM__Base
        EXPORT Image__APP_RAM__Length
        EXPORT Image__APP_RAM__ZI__Base
        EXPORT Image__APP_RAM__ZI__Length
       
        ; 用户保留区RAM
        EXPORT Load__RESERVED_RAM__Base
        EXPORT Image__RESERVED_RAM__Base
        EXPORT Image__RESERVED_RAM__Length
        EXPORT Image__RESERVED_RAM__ZI__Base
        EXPORT Image__RESERVED_RAM__ZI__Length
       
        ; 输出__main 和 _main 符号避免链接器包含标准C运行库的初始化处理
 
        EXPORT __main
        EXPORT _main
;=====================================================================
;                      处理器中断向量表
; ARM处理器的中断向量表开始于0x00000000H地址,这里也是系统重置时的入口点
;
; 除了系统重置以外,其余所有的中断都应该引入到异常处理程序中,但是这里没
; 有这样处理,仅提供了简单的示例,并统一调用boot_reset_handler进入重置程
; 序
;=====================================================================
        AREA    BOOTBLOCK_IVT, CODE, READONLY
        CODE32                     ; 32 bit ARM instruction set.
__main
_main
        ENTRY                      ; Entry point for boot image.
       
        ;ARM CPU中断向量表
        b       boot_reset_handler     ; ARM reset
        b       boot_reset_handler     ; ARM undefined instruction interrupt
        b       boot_reset_handler     ; ARM software interrupt
        b       boot_reset_handler     ; ARM prefetch abort interrupt
        b       boot_reset_handler     ; ARM data abort interrupt
        b       boot_reset_handler     ; Reserved by ARM Ltd.
        b       boot_reset_handler     ; ARM IRQ interrupt
        b       boot_reset_handler     ; ARM FIQ interrupt
       
;====================================================================
; 函数名:boot_reset_handler
; 描述:
; 系统初始化函数,进行如下处理:
;    1. 初始化系统栈
;    2. 初始化RAM
;    3. 调用__rt_entry初始化C语言库,并调用C语言的main函数
;=====================================================================
        AREA BOOTBLOCK_CODE, CODE
        CODE32                  ; 32 bit ARM instruction set
       
boot_reset_handler
        ; 进入超级用户模式并安装超级用户模式下的栈
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
        ldr     r13, =svc_stack+SVC_Stack_Size
       
        msr     CPSR_c, #PSR_System:OR:PSR_Fiq_Mask:OR:PSR_Irq_Mask
        ldr     r13, =sys_stack+Sys_Stack_Size
       
        // 返回超级用户模式
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
       
        ; 初始化RAM
        ldr     r4, =ram_init
        blatox r4
       
        ldr     a3, =__rt_entry
        bx      a3
       
        AREA    BOOTBLOCK_DATA, DATA, READONLY
 
;=====================================================================
;                       BOOT BLOCK 数据地址
; 根据导入的链接器RAM数据区地址,重新生成新的符号给Boot RAM初始化程序使用。
; 由于RAM初始化程序使用C语言完成,而C编译器需要-pcc参数才能使用$符号,因此
; 这里做一个符号的变换使得C语言可以直接使用
;
;=====================================================================
; 应用程序RAM
Load__APP_RAM__Base
        DCD |Load$$APP_RAM$$Base|
                   
Image__APP_RAM__Base
        DCD |Image$$APP_RAM$$Base|
              
Image__APP_RAM__Length
        DCD |Image$$APP_RAM$$Length|
                   
Image__APP_RAM__ZI__Base
        DCD |Image$$APP_RAM$$ZI$$Base|
                   
Image__APP_RAM__ZI__Length
        DCD |Image$$APP_RAM$$ZI$$Length|
       
; 用户保留区RAM
Load__RESERVED_RAM__Base
        DCD |Load$$RESERVED_RAM$$Base|
 
Image__RESERVED_RAM__Base
        DCD |Image$$RESERVED_RAM$$Base|
 
Image__RESERVED_RAM__Length
        DCD |Image$$RESERVED_RAM$$Length|
 
Image__RESERVED_RAM__ZI__Base
        DCD |Image$$RESERVED_RAM$$ZI$$Base|
 
Image__RESERVED_RAM__ZI__Length
        DCD |Image$$RESERVED_RAM$$ZI$$Length|
       
        END

 
raminit.c – RAM初始化和C运行库替换函数
/*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
                        RAM初始化文件
 
文件名:raminit.c
描述:
    此模块内包含了初始化RAM的函数以及链接时需要替换的C运行时库函数
 
*====*====*====*====*====*====*====*====*====*====*====*====*====*====*/
/*====================================================================
                           Include Files
====================================================================*/
#include "system.h"
 
/*====================================================================
                           Global Data
====================================================================*/
 
/* 由于在RAM初始化之前还不能够使用RAM,
    因此这里使用CPU寄存器做为变量的存储空间*/
__global_reg(1) dword *dst32;
__global_reg(2) dword *src32;
__global_reg(3) dword *stop_point;
 
/* 应用程序RAM */
extern byte * Load__APP_RAM__Base;
extern byte * Image__APP_RAM__Base;
extern byte * Image__APP_RAM__Length;
extern byte * Image__APP_RAM__ZI__Base;
extern byte * Image__APP_RAM__ZI__Length;
 
/* 用户保留区RAM */
extern byte * Load__RESERVED_RAM__Base;
extern byte * Image__RESERVED_RAM__Base;
extern byte * Image__RESERVED_RAM__Length;
extern byte * Image__RESERVED_RAM__ZI__Base;
extern byte * Image__RESERVED_RAM__ZI__Length;
 
/*====================================================================
                           Function Declare
====================================================================*/
/*CRT库函数替换*/
void __user_initial_stackheap ( ) { return; }
 
/*====================================================================
函数名:ram_init
 
描述:
 初始化RAM
依赖文件
 None
返回值
 None
====================================================================*/
void ram_init(void)
{
 /* 复制应用程序区的已初始化数据 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__Base +
                           (dword) Image__APP_RAM__Length);
 for( src32 = (dword *) Load__APP_RAM__Base,
         dst32 = (dword *) Image__APP_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
 
 /* 初始化应用程序区的零初始化数据 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__ZI__Base +
                           (dword) Image__APP_RAM__ZI__Length);
 for( dst32=(dword *) Image__APP_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
   
 /* 复制用户保留区的已初始化数据 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__Base +
                           (dword) Image__RESERVED_RAM__Length);
  for( src32 = (dword *) Load__RESERVED_RAM__Base,
         dst32 = (dword *) Image__RESERVED_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
   
 /* 初始化用户保留区的零初始化数据 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__ZI__Base +
                           (dword) Image__RESERVED_RAM__ZI__Length);
 for( dst32=(dword *) Image__RESERVED_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
}

 
mainapp.c – 程序主函数
/*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
                        主应用程序文件
 
文件名:mainapp.c
描述:
    此模块内包含了Main主函数
 
*====*====*====*====*====*====*====*====*====*====*====*====*====*====*/
/*====================================================================
                           Include Files
====================================================================*/
#include "system.h"
 
/*====================================================================
                           Global Data
====================================================================*/
#pragma arm section zidata = "reserved"
static byte gUserHeap[USER_HEAP_SIZE];
#pragma arm section zidata
 
/*----------------------------------------------------------------------------
 超级用户模式下的堆栈值
----------------------------------------------------------------------------*/
byte svc_stack[SVC_Stack_Size];
byte sys_stack[Sys_Stack_Size];
 
/*====================================================================
                           Function Declare
====================================================================*/
int mymain( void );
 
/*====================================================================
函数名:main
描述:
 程序运行的入口点
依赖文件
 None
返回值
 None, this routine does not return
====================================================================*/
int main( void )
{
    return mymain();
}
 
/*====================================================================
函数名:main
描述:
 程序运行的入口点
依赖文件
 None
返回值
 None, this routine does not return
====================================================================*/
int mymain( void )
{
    int i;
   
    // 初始化用户Heap区域
    for(i=0; i    {
        gUserHeap[i] = 0xFF;
    }
   
    for(;;){}// 一直在此处循环执行
}

在system.h文件中定义了一些常量,和一个汇编宏blatox,这个宏的用途是无缝的在汇编和C语言中进行调用。之所以有这个宏是因为C语言可以使用ARM的ARM指令集或THUMB精简指令集,前者是32位的指令长度,后者是16位指令长度。16位指令集比32位指令集更加节省二进制代码空间。由于在系统启动的时候CPU使用的全部是ARM指令,因此,如果当前C语言编译的代码是THUMB指令的话就需要不同的处理方式,函数blatox就可以通过判断寄存器的值来定位即将调用的代码是THUMB还是ARM的。
bootblock.s是一个汇编语言文件,我们从中可以看见它使用了#include来包含system.h文件。如果我们直接使用汇编编译器armasm是不能够识别的,因此要首先使用armcc的C语言编译器处理一下。指令如下:
armcc -ansic -E -cpu ARM7TDMI -apcs /noswst/interwork -D_ARM_ASM_ bootblock.s > bootblock..i
生成处理之后的bootblock.i纯汇编文件然后才能使用armasm编译。如果想了解其中每个参数的意义,可以查看ARM编译器的帮助文档。这里仅说明-D的作用与#define相同。
下面的代码是ARM CPU的中断向量表,根据上一节的Scatter-Loading描述文件可以知道,这个表的起始地址是0x00000000,也就是CPU一上电就会在此处取得指令执行。0x00000000地址是一个跳转指令,转到boot_reset_handler代码段执行。
AREA    BOOTBLOCK_IVT, CODE, READONLY
        CODE32                     ; 32 bit ARM instruction set.
__main
_main
        ENTRY                      ; Entry point for boot image.
       
        ;ARM CPU中断向量表
        b       boot_reset_handler     ; ARM reset
        b       boot_reset_handler     ; ARM undefined instruction interrupt
        b       boot_reset_handler     ; ARM software interrupt
        b       boot_reset_handler     ; ARM prefetch abort interrupt
        b       boot_reset_handler     ; ARM data abort interrupt
        b       boot_reset_handler     ; Reserved by ARM Ltd.
        b       boot_reset_handler     ; ARM IRQ interrupt
        b       boot_reset_handler     ; ARM FIQ interrupt

       这段代码中的__main和_main是为了替换掉链接时C运行库的相关内容的,各位读者可以试着在Test4种去掉这段代码,链接时将看到“Image does not have an entry point”的警告信息。
       紧接着程序跳转到boot_reset_handler代码段:
        AREA BOOTBLOCK_CODE, CODE
        CODE32                  ; 32 bit ARM instruction set
       
boot_reset_handler
        ; 进入超级用户模式并安装超级用户模式下的栈
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
        ldr     r13, =svc_stack+SVC_Stack_Size
       
        msr     CPSR_c, #PSR_System:OR:PSR_Fiq_Mask:OR:PSR_Irq_Mask
        ldr     r13, =sys_stack+Sys_Stack_Size
       
        // 返回超级用户模式
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
       
        ; 初始化RAM
        ldr     r4, =ram_init
        blatox r4
       
        ldr     a3, =__rt_entry
        bx      a3
        

       在这段代码中首先设置了系统的堆栈空间,然后是调用了ram_init函数执行RAM的初始化。再之后就调用C语言运行库的入口__rt_entry函数,从这里就正式进入了C的世界,当然在这里如果我们不想使用任何C语言库函数的话,我们可以指定一个这个值是mymain并且屏蔽掉mainapp.c中的main()函数。具体替换方法如下:
       1、注释掉mainapp.c中的main()函数
       2、注释掉bootblock.s文件24行的IMPORT __rt_entry语句,替换成IMPORT mymain语句
       3、将bootblock.s文件129行的__rt_entry替换成mymain,并使用blatox a3替换掉bx a3语句
       这个时候编译链接的语句就是没有任何C语言库函数影响的纯粹C语言版本的程序了。产生这种想法的动机是,在一个大型系统中希望能够自己实现相关的库函数或禁用编译器的库函数。例如,printf是输出一个语句,但是不同平台的输出方式是不同的,在DOS操作系统中是输出在显示器上,而在手机上可能显示在LCD屏幕上,或者进一步说,有些系统根本就没有输出。
       接下来看看ram_init中都作了些什么:
void ram_init(void)
{
 /* 复制应用程序区的已初始化数据 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__Base +
                           (dword) Image__APP_RAM__Length);
 for( src32 = (dword *) Load__APP_RAM__Base,
         dst32 = (dword *) Image__APP_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
 
 /* 初始化应用程序区的零初始化数据 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__ZI__Base +
                           (dword) Image__APP_RAM__ZI__Length);
 for( dst32=(dword *) Image__APP_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
   
 /* 复制用户保留区的已初始化数据 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__Base +
                           (dword) Image__RESERVED_RAM__Length);
 for( src32 = (dword *) Load__RESERVED_RAM__Base,
         dst32 = (dword *) Image__RESERVED_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
   
 /* 初始化用户保留区的零初始化数据 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__ZI__Base +
                           (dword) Image__RESERVED_RAM__ZI__Length);
 for( dst32=(dword *) Image__RESERVED_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
}

       根据我们上面的Scatter-Loading描述文件,有两段内存区域,一段是APP_RAM对应的链接器会给初始化程序提供如下几个变量:
Load$$APP_RAM$$Base 初始化数据存储的地址
Image$$APP_RAM$$Base 初始化数据RAM的基地址
Image$$APP_RAM$$Length 初始化数据的长度
Image$$APP_RAM$$ZI$$Base 零初始化数据RAM的基地址
Image$$APP_RAM$$ZI$$Length 零初始化数据的长度

再看这段程序初始化APP_RAM的过程,现在您应该可以看见Scatter-Loading描述文件与程序之间的关系了吧。这里要说明的是,由于在ARM C编译器中不能使用“$”符号,因此对于这些根据Scatter-Loading描述文件生成的链接符号我们在bootblock.s文件中进行了重新定义,使用“_”符号代替了“$”符号。
在这里所初始化的内容是程序运行时的空间,这些空间里的数据在关机之后是没有任何信息保存下来的,不管是ZI还是RW的。也就是说,在每一次开机的过程中,所执行的操作是完全相同的。或许有人会问,如果我需要记录一些变量的内容,让它能够在断电后仍旧能够保存下来,该怎么做呢?非常不幸的告诉您,这里不是实现这个功能的地方。这个功能需要文件系统或者其他非易失性(Non-Volatile NV)设备的支持,这通常就是一个系统存储参数的地方,要区分系统中不同设备的不同作用。对于RESERVED_RAM的处理就和APP_RAM一样了,就不再赘述了。这里就是一个完整的系统启动过程,进入了main函数之后,如何处理就是我们每个程序员自己的任务了。
在了解了ARM的启动过程之后,您应该也能够猜出在Windows下一个.exe可执行文件的执行过程了,虽然细节不同,但是思想是一样的,我在这里就不再赘述了。
3.7 函数的调用和返回
       在软件基础一节讲解函数的时候,我们知道了每个函数在运行的时候会给参数们留“位子”,以便于在使用函数的时候可以通过这些“位子”让函数输出不同的处理结果。但是我们还可能对这些“位子”的组织方式和返回存在着疑问。这一节我将结合Windows下的VC编译器和ARM编译器来进行讲解。当然不了解这些我们依然可以写出程序,但是,既然我们要追根溯源,那么还是了解一下吧。同时掌握这些东西还有利于优化程序。下表列出了几种典型的函数调用约定:
调用方式 参数传递方式
VC cdecl 参数经堆栈进行传递,由调用者负责清除堆栈
VC stdcall 参数经堆栈进行传递,由函数本身负责清除堆栈
VC fastcall 2个dword或更小的参数通过寄存器传递,其余经由堆栈传递。
ARM Call 4个dword或更小的参数通过寄存器传递,其余经由堆栈传递。

       从上面的约定可以看出,不同的编译器函数的调用约定细节是不一样的,这也决定了不同调用约定的函数行为的不同。
       cdecl调用约定由于由调用者负责堆栈的清除,因此可以实现可变参数的函数(如printf函数)。但是这样也会增加程序的代码空间,因为每一个调用函数的地方都需要相关的堆栈处理代码。
       stdcall调用约定是Windows操作系统API函数的默认调用方式,没有进行什么特别的处理。
       fastcall调用约定相当于stdcall约定的优化版本,它通过两个寄存器来实现参数的传递,这样就减少了堆栈的处理,因此可以加快函数的运行。同理可以推理出,如果fastcall调用约定的函数参数个数小于或等于2个的话,会取得最快的执行速度。
       ARM编译器的调用约定是使用4个寄存器来传递参数,因此对于ARM平台的程序来说,在小于等于4个参数的情况下可以得到最优化的函数调用效率。对于空间较大的参数(如一个大的结构体)ARM编译器将通过堆栈进行传递。
       对于函数的返回值,如果需要的传递空间比较小则通过寄存器传递,如果需要的空间较大,则通过内存进行间接的传递。上述几种方式的返回值传递都是一样的。
3.8 开发中与运行时
       我一直希望能够将您从C语言的世界中引入程序运行的二进制世界之中,并一直试图将二者的内在联系展示给您。虽然我并不能准确地知道是否达到了目的,但是我还是想在这里总结一下它们之间的关系。
       C语言与二进制分别对应了开发中与运行时两个程序的过程概念。对于很多的初学者会问一些诸如CPU怎么识别我定义的变量这类问题,回答这类问题是很棘手的。如果我告诉他CPU不认识我们定义的变量时,他一定会问CPU不认识那么程序怎么运行啊?这就像是一个在迷宫里的人,迷失在开发中与运行时两个世界组成的迷宫里。
变量是开发中的概念,经过编译器的处理,会将变量的外衣去掉,送进赤裸裸二进制数据的二进制世界中。编译器就是这两个世界的桥梁。在二进制世界里,CPU只认识二进制的数据和指令,没有数据类型的概念。例如,对于开发过程中的一个数组,到了二进制层面就变成了一个固定大小的二进制空间,不管这个数据类型是char的还是int的,CPU只是根据相应的操作指令来执行对这块空间的操作。因此,对于在C语言中的诸如变量、函数、结构体以及数组等等,统统是开发过程中的概念,只有编译器会接受这些概念。到了运行的时候CPU一概不认,CPU就是一个非常简单的家伙,只知道根据编译器生成的二进制指令执行,“只要符合规范,我就一言不发”。
只有透彻的理解了开发中与运行时之间的关系,才能从深层次理解程序的运行。因此在某种程度上来说,理解一下编译器本身的“内幕”是很有必要的。只有二进制世界,才是程序真正的世界。
3.9 小结
       这一章中主要介绍了编译器的分类以及编译器在整个系统中的作用,同时也包含了一些程序运行的知识,在这里着重说明了开发中和运行时两个程序的阶段,而编译器则成为了这两个阶段的桥梁。通过编译器对开发中的代码进行编译链接,才生成了可以运行的代码。编译器的作用就是将源代码转换成可以在目标机器上运行的机器码,这样可以通过高级语言的开发大大提高机器码的开发速度,可以说编译器是一个提高开发效率的工具。
思考题
       编译器将程序分成几种类型的数据?不同类型之间的区别是什么?