解释与编译

文章目录

  • 1.解释与编译的基本概念
  • 2.解释与编译的特性
  • 3. 静态语言,脚本语言
  • 4.`C/C++` 的编译和运行过程详解
    • 4.1 编译过程
      • 4.1.1 编译
        • 4.1.1.1 编译预处理
        • 4.1.1.2 编译,优化阶段
      • 4.1.2 汇编
    • 4.2 链接过程
    • 4.3 举例:linux系统下GCC的编译链接
      • 4.3.1 实例hello.c->hello.exe
      • 4.3.2 objdump命令

解释与编译_第1张图片

计算机不能直接理解高级语言,只能直接理解机器语言(低级语言)所以必须把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。

1.解释与编译的基本概念

翻译的方式有两种,一是编译,而是解释。

  1. 解释性语言: 解释性语言编写的程序不进行预先编译,以文本方式存储程序代码,在发布程序的时候,看起来省了道编译的工序,但是在运行程序的时候,解释性语言必须先解释再运行。 将源代码逐条转换成目标代码 同时 逐条运行的过程,类似英语中的 同声传译

解释与编译_第2张图片

  1. **编译性语言:**需要一个专门的编译过程,把程序编译成为机器语言的文件,比如exe文件,以后要运行的话就不用重新翻译了,直接使用编译的结果就OK了。譬如牛腩系统是asp.net开发的,它所使用的是c#语言。C#属于编译性的语言,所以第一次我们在运行的时候会感觉很慢,但是之后每次运行相比第一次就快很多了。 将源代码一次性转换成目标代码的过程,类似英语中的全文翻译

    解释与编译_第3张图片

举例:读书大比拼

小红和小强两个人看书,看得都是日语原文的小说。在看的时候,小红有电子词典,一边看书一边翻译。然后第一遍就很开看完了。

小强呢,一边看书,一边查字典,然后将每一句的解释都写在了纸上,这样第一次看书他看得时间相对来说比较慢。
过一个月又进行一次读书大赛,和上次读的书一样,这次呢小红还是老方法,但是小强就相对很快,因为他把解释都写下来了,所以读起来就很快了。
如果还进行一次比赛,结果和第二次的一样。。。
小红做的工作就是计算机中的解释性语言所具有的特征,小强呢就是编译性语言所具有的特征。

2.解释与编译的特性

解释性语言与编译性语言的不同

  1. 解释性语言每个语句用到的时候才会编译,而且不会被保存起来,所以他的执行效率要低,而且不能生成可独立执行的可执行文件,应用程序不能脱离其解释器。但是这中方式比较灵活可以动态地调节,修改应用程序很方便
  • 解释则在每次程序运行时都需要解释器和源代码,不能集成太多优化技术,因为代码优化技术会消耗运行时间,使整个程序的执行速度受到影响。
  • 只要存在解释器,源代码可以在任何操作系统上运行可移植性好
  1. 编译性语言:把程序编译成机器语言的文件后,然后保存在电脑中,当电脑看到同样的语言后就回去查找以前编译的,这样他的执行效率会很高。但是应用程序一旦需要修改,必须先修改源代码再重新生成新的目标文件才能执行,只有目标文件而没有源代码,修改很不方便
  • 编译过程只进行一次,所以,编译过程的速度并不是关键,目标代码的运行速度是关键。因此,编译器一般都集成尽可能多的优化技术,使生成的目标代码具备更好的执行效率
  • 目标代码不需要编译器就可以运行,在同类操作系统上使用灵活

3. 静态语言,脚本语言

根据执行方式的不同,编程语言分为2类:静态语言(编译),脚本语言(解释)。

  1. 脚本语言:
  • 执行程序时需要源代码,源代码维护灵活跨多个操作系统平台

    • Python, JavaScript, PHPPython语言是一种被广泛使用的高级通用脚本编程语言,虽然采用解释执行方式,但是它的解释器也保留了编译器的部分功能,随程序运行,解释器也会生成一个完整的目标代码。这种将解释器和编译器结合的新解释器是现代脚本语言为了提升计算机性能的一种有益演进。
  1. 静态语言:

    • 编译器一次性生成目标代码,优化更充分,程序运行速度更快。

    • C/C++

4.C/C++ 的编译和运行过程详解

解释与编译_第4张图片
解释与编译_第5张图片

C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。

编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。

链接是把目标文件、操作系统的启动代码和用到的库文件进行组织,形成最终生成可执行代码的过程。过程图解如下:。

4.1 编译过程

编译分为:编译和汇编

4.1.1 编译

编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段:

4.1.1.1 编译预处理

读取c源程序,对其中的伪指令(以# 开头的指令)和特殊符号进行处理,并且删除注释。 预编译程序所完成的基本上是对源程序的**“替代”工作**。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输入而被翻译成为机器指令。

伪指令主要包括以下四个方面:

  1. 宏定义指令: 如 #define Name TokenString , #undef Name 对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。

    #include 
    using namespace std;
    int main(int argc, char* argv[])
    {
    // xxx is defined
    bool b = true;
    #define xxx
    #ifdef xxx
            cout << "xxx is defined #1" << endl; // this line is printed
    #endif
    // undefine xxx
    #undef xxx
    #ifdef xxx
            cout << "xxx is defined #2" << endl; // not printed
    #endif
    // define xxx again
    if(b)
    {
    #define xxx
    }
    #ifdef xxx
            cout << "xxx is defined #3" << endl; // printed
    #endif
    return 0;
    }
    
    //结果输出:
    //xxx is defined #1
    //xxx is defined #3
    
  2. 条件编译指令,如#ifdef,#ifndef,#endif等 。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

  3. 头文件包含指令,如# include "FileName" 或者# include < FileName> 等。 在头文件中一般用伪指令# define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。

  • 采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条# include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。

  • 包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/ usr/ include目录下。在程序中# include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在# include中要用双引号("")。

  1. 特殊符号: 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
4.1.1.2 编译,优化阶段

经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。 编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码

优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。

  • 优化一部分是对中间代码的优化,这种优化不依赖于具体的计算机。 主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。
  • 另一种优化则主要针对目标代码的生成而进行的。 同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

4.1.2 汇编

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码

目标文件由段组成。通常一个目标文件中至少有两个段:

  1. 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
  2. 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

汇编程序生成的实际上是目标文件:可重定位文件(其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据)。

4.2 链接过程

由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

  1. 静态链接:在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
  2. 动态链接:在此种方式下,函数的代码放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。**使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。**但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

4.3 举例:linux系统下GCC的编译链接

我们在linux使用的gcc编译器便是把以上的几个过程进行捆绑,使用户只使用一次命令就把编译工作完成,这的确方便了编译工作,但对于初学者了解编译过程就很不利了,下图便是gcc代理的编译过程:

解释与编译_第6张图片

命令:

  1. 预编译:.c文件->.i文件,使用的gcc命令是gcc -E,对应的预处理命令是cpp
  2. 编译:.c/.h文件->.s文件,使用的gcc命令是gcc -S,对应的编译命令是cc -S
  3. 汇编:.s文件->.o文件,使用的gcc命令是gcc -c,对应的汇编命令是as
  4. 链接:.o文件->可执行文件,使用的gcc命令是gcc,对应的链接命令是ld

总结起来编译过程就上面的四个过程:预编译处理(.c) --> 编译、优化程序(.s、.asm)--> 汇编程序(.obj、.o、.a、.ko) --> 链接程序(.exe、.elf、.axf等)。

一般情况下,我们只需要知道分成编译和链接两个阶段,编译阶段将源程序(*.c) 转换成为目标代码(一般是obj文件,至于具体过程就是上面说的那些阶段),链接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件(exe文件)就可以了,其他的都需要在实践中多多体会才能有更深的理解。

4.3.1 实例hello.c->hello.exe

  1. 编写hello.c文件

解释与编译_第7张图片

  1. 预处理:gcc -E hello.c -o hello.i 其中-o作用是指定输出文件的名字,若不加,自动生成其他名字的文件。

    解释与编译_第8张图片

  2. 编译命令:gcc -S hello.i -o hello.s 对代码进行语法、语义分析和错误判断,生成汇编代码文件。

    解释与编译_第9张图片

  3. 汇编命令: 把汇编代码转换成计算机可认识的二进制文件 。gcc -c hello.s -o hello.o查看.o文件格式:objdump -h hello.o ,另外:打开elf类目标文件的命令readelf -all hello.o 但是linux下可以,windows下目标文件不能由readelf打开。

    解释与编译_第10张图片

    解释与编译_第11张图片

  4. 链接命令: 通俗的讲就是把多个*.o文件合并成一个可执行文件,即二进制指令文件。 gcc hello.o -o hello,查看到底链接了哪些库:ldd hello.exe

    解释与编译_第12张图片

    解释与编译_第13张图片

4.3.2 objdump命令

objdump命令是Linux下的反汇编目标文件.o或者可执行文件.exe的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。

  1. 反汇编objdump -d hello.o

    解释与编译_第14张图片

  2. 显示目标文件各个section的头部摘要信息objdump -h hello.o

    解释与编译_第15张图片

  3. 显示文件头信息:objdump -f hello.o

    解释与编译_第16张图片

关于各个section含义介绍:

  • .text:已编译程序的机器代码。
  • .rodata:只读数据,比如printf语句中的格式串和开关(switch)语句的跳转表。
  • .data:已初始化的全局C变量。局部C变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss节中。
  • .bss:未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率在:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
  • .symtab:一个符号表(symbol table),它存放在程序中被定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的表目。
  • .rel.text:当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。
  • .rel.data:被模块定义或引用的任何全局变量的信息。一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改。
  • .debug:一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源文件。只有以-g选项调用编译驱动程序时,才会得到这张表。
  • .line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译驱动程序时,才会得到这张表。
  • .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
--archive-headers 
-a 
显示档案库的成员信息,类似ls -l将lib*.a的信息列出。 
 
-b bfdname 
--target=bfdname 
指定目标码格式。这不是必须的,objdump能自动识别许多格式,比如: 
 
objdump -b oasys -m vax -h fu.o 
显示fu.o的头部摘要信息,明确指出该文件是Vax系统下用Oasys编译器生成的目标文件。objdump -i将给出这里可以指定的目标码格式列表。 
 
-C 
--demangle 
将底层的符号名解码成用户级名字,除了去掉所开头的下划线之外,还使得C++函数名以可理解的方式显示出来。 
 
--debugging 
-g 
显示调试信息。企图解析保存在文件中的调试信息并以C语言的语法显示出来。仅仅支持某些类型的调试信息。有些其他的格式被readelf -w支持。 
 
-e 
--debugging-tags 
类似-g选项,但是生成的信息是和ctags工具相兼容的格式。 
 
--disassemble 
-d 
从objfile中反汇编那些特定指令机器码的section。 
 
-D 
--disassemble-all 
与 -d 类似,但反汇编所有section. 
 
--prefix-addresses 
反汇编的时候,显示每一行的完整地址。这是一种比较老的反汇编格式。 
 
-EB 
-EL 
--endian={big|little} 
指定目标文件的小端。这个项将影响反汇编出来的指令。在反汇编的文件没描述小端信息的时候用。例如S-records. 
 
-f 
--file-headers 
显示objfile中每个文件的整体头部摘要信息。 
 
-h 
--section-headers 
--headers 
显示目标文件各个section的头部摘要信息。 
 
-H 
--help 
简短的帮助信息。 
 
-i 
--info 
显示对于 -b 或者 -m 选项可用的架构和目标格式列表。 
 
-j name
--section=name 
仅仅显示指定名称为name的section的信息 
 
-l
--line-numbers 
用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。 
 
-m machine 
--architecture=machine 
指定反汇编目标文件时使用的架构,当待反汇编文件本身没描述架构信息的时候(比如S-records),这个选项很有用。可以用-i选项列出这里能够指定的架构. 
 
--reloc 
-r 
显示文件的重定位入口。如果和-d或者-D一起使用,重定位部分以反汇编后的格式显示出来。 
 
--dynamic-reloc 
-R 
显示文件的动态重定位入口,仅仅对于动态目标文件意义,比如某些共享库。 
 
-s 
--full-contents 
显示指定section的完整内容。默认所有的非空section都会被显示。 
 
-S 
--source 
尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。 
 
--show-raw-insn 
反汇编的时候,显示每条汇编指令对应的机器码,如不指定--prefix-addresses,这将是缺省选项。 
 
--no-show-raw-insn 
反汇编时,不显示汇编指令的机器码,如不指定--prefix-addresses,这将是缺省选项。 
 
--start-address=address 
从指定地址开始显示数据,该选项影响-d、-r和-s选项的输出。 
 
--stop-address=address 
显示数据直到指定地址为止,该项影响-d、-r和-s选项的输出。 
 
-t 
--syms 
显示文件的符号表入口。类似于nm -s提供的信息 
 
-T 
--dynamic-syms 
显示文件的动态符号表入口,仅仅对动态目标文件意义,比如某些共享库。它显示的信息类似于 nm -D|--dynamic 显示的信息。 
 
-V 
--version 
版本信息 
 
--all-headers 
-x 
显示所可用的头信息,包括符号表、重定位入口。-x 等价于-a -f -h -r -t 同时指定。 
 
-z 
--disassemble-zeroes 
一般反汇编输出将省略大块的零,该选项使得这些零块也被反汇编。 
 
@file 
可以将选项集中到一个文件中,然后使用这个@file选项载入。

你可能感兴趣的:(编译原理,编译器,gcc/gdb编译调试,解释器模式)