您好,欢迎访问三七文档
1第十章编译系统和运行系统通常,除了编译器外,我们还需要一些其它工具的帮助,才能得到可执行的目标程序,这些工具包括预处理器、汇编器和连接器等。对于Fortran、Pascal和C来说,这些工具都较简单或明显。了解这些工具有助于我们掌握从源程序到可执行目标程序的实际处理过程,这些知识对于参与大型软件系统的开发是很有用的。本章介绍C语言编译系统。另外,目标代码运行时,还需要一些工具的支撑,如动态连接程序、无用单元收集程序等,这些工具的集合称为运行系统。本章还介绍Java语言的运行系统及其无用单元收集程序。10.1C语言的编译系统除了编译器外,我们还需要一些其它的工具来建立一个可执行的目标程序。本节以GNUC编译系统(简称GCC系统)为例,来说明程序设计语言编译系统的一般工作过程。一个C源程序可以分成若干个模块,存储在不同的文件中。C编译系统对这些源文件分别进行预处理、编译和汇编、形成可重定位的目标文件;然后再利用连接器将这些目标文件和必要的库文件连接成一个可执行的目标文件,即具有绝对地址的机器代码。这一过程可用图10.1描绘。大多数编译系统提供一个驱动程序来调用语言的预处理器、编译器、汇编器、连接器,以支持用户完成从源程序到可执行程序的翻译。在GCC系统中,驱动程序的名字是gcc(或cc)。下面我们结合一个C语言的程序实例来讨论GCC系统的工作步骤。图10.2中的程序由两个文件main.c和swap.c组成,为便于引用中间的语句,我们增加了行号。在Unix(还有Linux)环境下,键入如下命令可以得到该程序的可执行文件swap:gcc–v–oswapmain.cswap.c预处理器源程序修改后的源程序可重定位的目标程序可重定位的目标文件库图10.1一个语言编译系统编译器汇编器汇编程序连接器可执行的目标程序2这里,使用选项–v可以输出该编译系统各步骤执行的命令和执行结果,选项–o紧跟着的字符串指示生成的可执行文件的名字。10.1.1预处理器gcc首先调用预处理器cpp,将源程序文件翻译成一个ASCII中间文件,它是经修改后的源程序。图10.3是main.c经预处理后生成的中间文件main.i。预处理器产生编译器的输入,它实现以下功能:(1)文件包含预处理器可以把源程序文件中的包含声明(#include)扩展为程序正文。例如,当源程序文件中含有语句#includestdio.h时,预处理器会在系统标准路径下搜索stdio.h,再用文件stdio.h中的内容来代替这个语句。图10.2main.c和swap.c组成的程序main.c(1)#if1(2)intbuf[2];(3)#else(4)intbuf[2]={10,20};(5)#endif(6)voidswap();(7)#defineAbuf[0](8)intmain()(9){(10)scanf(%d,%d,buf,buf+1);(11)swap();(12)printf(%d,%d,A,buf[1]);(13)return0;(14)}swap.c(1)externintbuf[2];(2)int*bufp0=buf;(3)int*bufp1;(4)voidswap()(5){(6)inttemp;(7)bufp1=buf+1;(8)temp=*bufp0;(9)*bufp0=*bufp1;(10)*bufp1=temp;(11)}图10.3main.i的内容(1)#1“main.c”(2)(3)intbuf[2];(4)(5)(6)(7)voidswap();(8)(9)intmain()(10){(11)scanf(%d,%d,buf,buf+1);(12)swap();(13)printf(%d,%d,buf[0],buf[1]);(14)return0;(15)}3(2)宏展开C程序中可以使用#define来定义宏,一个宏定义给出一段C代码的缩写。预处理器将源程序文件中出现的、对宏的引用展开成相应的宏定义,这一过程称为宏展开。例如,main.c的第(7)行为宏A的定义,第(12)行中的A是对该宏的引用。在预处理后产生的main.i中,宏A的定义转换成一个空行,对宏A的引用则展开成buf[0]。(3)条件编译预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外。通常把排除在外的语句转换成空行。显然,实现一个这样的预处理器并不困难。有些语言的预处理器用于增强老的语言,使之包含现代的控制结构和数据类型。从增强语言到老语言的翻译由这样的预处理器完成。10.1.2汇编器GCC系统的编译器cc1产生汇编代码,main.i被编译成的ASCII汇编文件main.s见图10.4。这些汇编代码由汇编器进一步处理。最简单的汇编器对输入进行两遍扫描。在第一遍,汇编器扫描输入,将表示存储单元的所有标识符都存入符号表,并分配地址。在第二遍,汇编器再次扫描输入,把每个操作码翻译成机器语言中代表那个操作的位串,并把代表存储单元的每个标识符翻译成符号表中为这个标识符分配的地址。如果一个汇编代码文件中有外部符号的引用,如果汇编器生成的是可重定位的目标文件,那么汇编器的工作将变得略微复杂。在10.1.4节,当我们知道了目标文件的格式后,要实现一个汇编器并不是一件困难的事情。另外,一遍扫描完成汇编代码到可重定位目标代码的翻译也是完全可能的。在GCC编译系统中,要想得到源程序被编译成的汇编代码,只要加编译选项S就可以。例如,用gccSmain.c我们可以得到汇编文件main.s。使用汇编器asasomain.omain.s我们可以将main.s汇编成可重定位目标文件main.o。10.1.3连接器我们已经知道,汇编器或编译器输出的机器代码称为目标模块或目标文件,它有两种形式:(1)可重定位的目标文件它包含二进制代码和数据,可以和其它可重定位目标文件组装成一个可执行的目标文件。(2)可执行的目标文件它包含二进制代码和数据,可以直接被复制到内存并被执行。实际另外还有一种形式:(3)共享目标文件它是一种特殊的可重定位目标文件。我们可以在装入程序或运行程序时,动态地装入共享目标文件到内存并将它和程序连接。技术上,一个目标模块是一个字节序列,而一个目标文件则是一个以文件形式存储在外部存储器上的目标模块。不过,我们将不加区分地使用这两个术语。连接是一个收集、组织程序所需的不同代码和数据的过程(它们可能在不同的目标模块中),以便程序能被装入内存并被执行。连接可以在将源代码翻译成机器代码的编译时候完成、也可以在程序装入内存并执行的装入时完成,甚至可以在程序运行时完成。静态连接器负责将多个可重定位目标文件组成一个可执行目标文件(也可以组成一个可重定位目标文件);动态连接器则支持在内存中的可执行程序在执行时与共享目标文件4进行动态地连接。有些系统将装入可执行程序时与共享目标文件进行的连接也称为动态连接。如果这些目标文件是以有用的方式组在一起的,那么它们之间就会出现一些外部引用,即一个文件中的代码引用另一文件中的存储单元。这种引用可以是定义在一个文件而使用在另一个文件的数据单元,或者是入口点出现在一个文件而调用点出现在另一个文件的函数。在连接器ld的上下文中,一个重定位模块M定义和引用的符号通常分成三类:.filemain.c.version01.01gcc2_compiled.:.section.rodata.LC0:.string%d,%d——scanf和printf中使用的格式串.text.align4——按4字节对齐.globlmain.typemain,@function——本模块定义的函数mainmain:pushl%ebp——将老的基地址指针压栈movl%esp,%ebp——将当前栈顶指针作为基地址指针pushl$buf+4——实参buf+1入栈pushl$buf——实参buf入栈pushl$.LC0——格式串指针入栈callscanf——调用函数scanfaddl$12,%esp——栈顶指针恢复到参数压栈前的位置callswap——调用函数swapmovlbuf+4,%eax——取实参buf[1]到寄存器pushl%eax——实参buf[1]入栈movlbuf,%eax——取实参buf[0]到寄存器pushl%eax——实参buf[0]入栈pushl$.LC0——格式串指针入栈callprintf——调用函数printfaddl$12,%esp——栈顶指针恢复到参数压栈前的位置xorl%eax,%eaxjmp.L1.p2align4,,7.L1:leaveret.Lfe1:.sizemain,.Lfe1-main.commbuf,8,4——本模块定义的未初始化全局变量buf——占8字节,按4字节对齐.identGCC:(GNU)egcs-2.91.6619990314/Linux(egcs-1.1.2release)图10.4汇编文件main.s5(1)全局符号是指那些在模块M中定义,可以被其它模块引用的符号。它包括模块M中定义的非static属性的函数和全局变量。(2)局部符号是指那些在模块M中定义,且只能在本模块中引用的符号。它包括模块M中定义的有static属性的函数和全局变量。(3)外部符号是指那些由模块M引用并由其它模块定义符号。这样,连接器主要完成以下两个任务:(1)符号解析(symbolresolution)连接器识别各个目标模块中定义和引用的符号,为每一个符号引用确定它所关联的一个同名符号的定义。(2)重定位编译器和汇编器产生的代码节和数据节分别都是从零地址开始。连接器按如下方式来重定位这两节:将每一个符号定义关联到一个内存位置,然后修改所有对这些符号的引用,以使它们指向相关联的内存位置。在10.1.4节明白了目标文件的格式后,实现连接器不是一件困难的事情。10.1.4目标文件的格式目标文件格式随系统不同而不同。来自Bell实验室的第一个Unix系统使用a.out格式,至今Unix下的可执行文件仍被缺省地命名为a.out。SystemVUnix的早期版本使用COFF(CommonObjectFileFormat)格式。WindowsNT使用COFF的一个变体,称为PE(PortableExecutable)格式。现代Unix系统,如Linux、SystemVUnix的后期版本、BSDUnix变体和SunSolaris,都使用Unix的ELF(ExecutableandLinkableFormat)格式。这里仅讨论Unix使用的ELF文件格式。图10.5为典型的ELF可重定位目标文件格式。ELF头(header)开始于一个16字节的序列,它描述了字的大小和产生此文件的系统的字节次序。ELF头的其余部分包含的信息用于连接器分析和解释目标文件,其中包括:ELF头的大小、目标文件的类型(可重定位、可执行或共享等)、机器类型(如IA32)、节头表(sectionheadertable)在本目标文件中的偏移、节头表中条目的大小和数量。节头表描述目标文件中各节的位置和大小。在ELF头和节头表之间是节本身。典型的ELF可重定位目标文件包含以下各节。(1).text:被编译程序的机器代码。(2).rodata:诸如printf语句中的格式串和switch语句的跳转表(见7.4.4节)等只读数据。(3).data:已初始化的全局变量,如swap.c中的bufp0。(4).bss:未初始化的全局变量,如main.c中的buf及swap.c中的bufp1。该节在目标文ELF头.text.rodata.data.bss.symtab.rel.text.rel.data.debug.line.strtab节头表0描述目标文件的节节图10.5ELF可重定位文件格式6件中不占实际的空间,只是一个占位符。目标文件格式区分已初始化和未初始化变量是为提高空间的利用率:在目标文件中,未初始化变量不必占用实际外部存储器的任何空间(历史上,bss是bettersavespac
本文标题:编译系统和运行系统
链接地址:https://www.777doc.com/doc-6151940 .html