GCC驱动程序读取源文件.c比如hello.c,并把它翻译成一个可执行目标文件hello,需要经历一下四个阶段,如下图示:
C语言编译过程分成四个步骤: 1,由.c文件到.i文件,这个过程叫预处理 2,由.i文件到.s文件,这个过程叫编译 3,由.s文件到.o文件,这个过程叫汇编 4,由.o文件到可执行文件,这个过程叫链接
用gcc查看预处理过程(假设源文件叫hello.c)gcc -o hello.i hello.c -E 然后用 vi hello.i 即可查看生成的预处理文件
按ESC 输入:$ 跳到预处理文件 可看到hello.c源码
宏的本质:预处理阶段的单纯的字符串替换
预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令。
汇编阶段。汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符,如果我们在文本文件中打开hello.o文件,看到的将是一堆乱码。
链接阶段。链接器(ld)负责处理合并目标代码,生成一个可执行目标文件,可以被加载到内存中,由系统执行。
GCC和G++的区别和联系:
gcc和g++都是GNU(一个组织)的编译器。
1、对于.c后缀的文件,gcc把它当做是C程序;g++当做是C++程序;
2、对于.cpp后缀的文件,gcc和g++都会当做c++程序。
3、编译阶段,g++会调用gcc;
4、连接阶段,通常会用g++来完成,这是因为gcc命令不能自动和c++程序使用的库连接。
过程图解如下:
预处理器:将.c 文件转化成 .i文件,使用的gcc命令是:gcc –E,对应于预处理命令cpp;编译器:将.c/.h文件转换成.s文件,使用的gcc命令是:gcc –S,对应于编译命令 cc –S;汇编器:将.s 文件转化成 .o文件,使用的gcc 命令是:gcc –c,对应于汇编命令是 as;链接器:将.o文件转化成可执行程序,使用的gcc 命令是: gcc,对应于链接命令是 ld;加载器:将可执行程序加载到内存并进行执行,loader和ld-linux.so。
编译过程又可以分成两个阶段:编译和汇编。
编译是指编译器读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码。
源文件的编译过程包含两个主要阶段:
第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。
主要是以下几方面的处理:
宏定义指令,如 #define a b 对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉头文件包含指令,如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。特殊符号,预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用,这涉及到头文件的定位即搜索路径问题。头文件搜索规则如下:
所有header file的搜寻会从-I开始然后找环境变量 C_INCLUDE_PATH,CPLUS_INCLUDE_PATH,OBJC_INCLUDE_PATH指定的路径再找默认目录(/usr/include、/usr/local/include、/usr/lib/gcc-lib/i386-linux/2.95.2/include......)
第二个阶段编译、优化阶段,编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
汇编实际上指汇编器(as)把汇编语言代码翻译成目标机器指令的过程。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
静态库(static library)就是将相关的目标模块打包形成的单独的文件。使用ar命令。
静态库的优点在于:
程序员不需要显式的指定所有需要链接的目标模块,因为指定是一个耗时且容易出错的过程;链接时,连接程序只从静态库中拷贝被程序引用的目标模块,这样就减小了可执行文件在磁盘和内存中的大小。动态库(dynamic library)是一种特殊的目标模块,它可以在运行时被加载到任意的内存地址,或者是与任意的程序进行链接。
动态库的优点在于:
更新动态库,无需重新链接;对于大系统,重新链接是一个非常耗时的过程;运行中可供多个程序使用,内存中只需要有一份,节省内存。
链接器主要是将有关的目标文件彼此相连接生成可加载、可执行的目标文件。链接器的核心工作就是符号表解析和重定位。
链接器将函数的代码从其所在地(目标文件或静态链接库中)拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
为创建可执行文件,链接器必须要完成的主要任务:
符号解析:把目标文件中符号的定义和引用联系起来;重定位:把符号定义和内存地址对应起来,然后修改所有对符号的引用。关于符号表和符号解析以及重定位的分析后续学习。
在此种方式下,函数的定义在动态链接库或共享对象的目标文件中。在编译的链接阶段,动态链接库只提供符号表和其他少量信息用于保证所有符号引用都有定义,保证编译顺利通过。动态链接器(ld-linux.so)链接程序在运行过程中根据记录的共享对象的符号定义来动态加载共享库,然后完成重定位。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
加载器把可执行文件从外存加载到内存并进行执行。 Linux中进程运行时的内存映像如下:
加载过程如下:
加载器首先创建如上图所示的内存映像,然后根据段头部表,把目标文件拷贝到内存的数据和代码段中。然后,加载器跳转到程序入口点(即符号_start 的地址),执行启动代码(startup code),启动代码的调用顺序如所示:
UNIX系统提供了一系列工具帮助理解和处理目标文件。GNUbinutils 包也提供了很多帮助。这些工具包括:
AR :创建静态库,插入、删除、列出和提取成员;STRINGS :列出目标文件中所有可以打印的字符串;STRIP :从目标文件中删除符号表信息;NM :列出目标文件符号表中定义的符号;SIZE :列出目标文件中节的名字和大小;READELF :显示一个目标文件的完整结构,包括ELF 头中编码的所有信息。OBJDUMP :显示目标文件的所有信息,最有用的功能是反汇编.text节中的二进制指令。LDD :列出可执行文件在运行时需要的共享库。