编译_编译器 - CSDN
  • 编译的基本概念

    2019-07-10 14:33:39
    CPU执行程序的原理这篇文章中提到,“程序要想被CPU执行,首先要被编译成CPU可以执行的指令操作”,那编译成CPU可以执行的指令操作是什么意思呢?这篇文章就用来说明编译的实际意义是什么。 2、知识背景——CPU架构...

    1、本文目的

    CPU执行程序的原理这篇文章中提到,“程序要想被CPU执行,首先要被编译成CPU可以执行的指令操作”,那编译成CPU可以执行的指令操作是什么意思呢?这篇文章就用来说明编译的实际意义是什么。

    2、知识背景——CPU架构

    要谈编译,首先要说说CPU架构的概念。CPU架构也就是CPU指令集(指令就是汇编指令或者机器指令,比如Add是汇编指令,而对应的机器指令在MIPS下就是000000)架构,现有CPU架构包括鼎鼎有名的Intel的X86架构、ARM的ARM架构、MIPS的MIPS架构、DEC的Alpha架构。通俗来说,指令集就是指挥CPU如何运算的硬程序,没有这套指令的话,就没有办法指挥CPU运转,而计算机的所有运算都需要CPU参与。

    那编译呢,也就是将一段程序转换为指令集的过程。不同架构的指令集自然是不同的,带来的影响就是同一段代码,编译过后只能运行在对应的指令集上,比如一段C++代码,在X86下编译完了,只能在X86下运行,而不能运行在ARM架构下运行。

    而事实上,编译得到的结果,更是操作系统相关的。假设,一段程序被编译成了X86下的硬程序,但是无法同时运行在Windows上和Linux上(Windows和Linux操作系统都可以装在X86架构的CPU上),如果程序一开始是在Windows操作系统下编译的,那这段程序就无法运行在其他比如Linux操作系统中。

    也就是说,编译与操作系统和CPU这二者都是相关的。

    3、编译过程

    事实上,仅仅将程序通过编译改写成汇编指令或机器指令,在操作系统上还不能直接运行。实际上广义的编译,其实包括预处理、编译、汇编、链接这整个过程。

    1. 预处理,就是把代码里引入的其他代码,插入到这段代码中,形成一个代码文件。
    2. 编译,就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的,也就是说C代码和python代码,代码逻辑如果相同,编译完的结果其实是一样的。
    3. 汇编,就是把汇编指令转为机器码的过程,机器码可以被CPU直接执行。
    4. 链接,就是将一段我们需要的已经编译好的其他库,与我们的汇编结果连起来,这样才是最终程序完整的形式,操作系统才可以运行。不同操作系统编译好的其他库形式不同,而且链接的方式也不同,得到最终程序的形式也不同,所以编译好的程序只能在特定的操作系统下运行。
    展开全文
  • 我们的代码会经过这4个环节,从而形成最终文件,c语言作为编译语言,用来向计算机发出指令。让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动。 预处理, 展开头文件/宏...

    楔子

    我们在各自的电脑上写下代码,得明白我们代码究竟是如何产生的,不想了解1,0什么的,但这几个环节必须掌握吧。

    我们的代码会经过这4个环节,从而形成最终文件,c语言作为编译语言,用来向计算机发出指令。让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动。

     

    预处理, 展开头文件/宏替换/去掉注释/条件编译                      (test.i main .i)
    编译,    检查语法,生成汇编                                                      ( test.s  main .s)
    汇编,   汇编代码转换机器码                                                         (test.o main.o)
    链接     链接到一起生成可执行程序                                              a.out
     

    预处理

    预处理如锲子中所言,是一种展开,下表是常用的一些预处理命令

    还有下列几种预处理宏(是双下划线)

    __LINE__ 表示正在编译的文件的行号
    __FILE__表示正在编译的文件的名字__DATE__表示编译时刻的日期字符串,例如: "25 Dec 2007"
    __TIME__ 表示编译时刻的时间字符串,例如: "12:30:55"
    __STDC__ 判断该文件是不是定义成标准 C 程序
    我的vs2013不是定义的标准c语言

     

    宏函数很好用,是直接展开,在这我顺便说一下宏的好处和坏处。

    宏优点1代码复用性2提高性能

    宏缺点1 不可调试(预编译阶段进行了替换),2无类型安全检查3可读性差,容易出错。

    这里附上《c和指针》中的一张表格,总结宏和函数十分到位,我就不多说了

     

    宏函数很皮,#define定义一个比如判断大小,替换常量,很是方便。

    不过我现在也就用下,#define ERROR_POWEROFF -1,#define _CRT_SECURE_NO_WARNINGS 1这样的和编译器有关的东西,不会去写宏函数,宏函数这东西,可读性特别差,在c++中,一般用const/枚举/内联去替代宏。

    但是,define宏在某些方面真的是非常好用,我很推荐。

    1.替代路径

    #define ENG_PATH_1 C:\Program Files (x86)

    2.针对编译器版本不兼容报错

    #define _CRT_SECURE_NO_WARNINGS 1

    3.条件编译

    #ifdef 标识符
    程序段 1
    #else
    程序段 2
    #endif

    4.使用库中的宏

    vc++中有许多有意思的宏,都是大牛们写出来的,真的是充满智慧,十分刁钻,怎么学也学不完,我个人担心出错就很少写宏,用函数代替了。在以后的博客中我会记录一些常用的,充作笔记。

    emmm,当然,还有其他许多重要的预处理。

    比如

    include

    #include <filename>

    尖括号是预处理到系统规定的路径中去获得这个文件(即 C 编译系统所提供的并存放在指定的子目录下的头文件)。找到文件后,用文件内容替换该语句。如stdio.h

    #include“filename”

    “”则是预处理我们自己第三方的文件,如程序员小刘写的Date.h,我们就可以include“Date.h”

    #error 预处理,#line 预处理,#pragma 预处理

    #error 预处理指令的作用是,编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。

    这个我没写过,但碰到过很多次,在编写mfc代码中,拉入控件时我加入密码框控件,OS编译时会自动弹出#error 提示我该编辑框为密码,注意明文问题

    #line 的作用是改变当前行数和文件名称,如#line 28  liu 

    目前我没使其派上用场,但了解为好。

    #pragma 是比较重要且困难的预处理指令。

    #pragma once 

    这个的做用就是防止头文件多次包含

    当然,还有另外一种风格,防止被包含,我同时给出来

    是巧妙地利用了define宏

    #ifndef _SOME_H
    #define _SOME_H
    
    
    ...//(some.h头文件内容)
    
    
    #endif

    变量的防止重复定义则利用extern,在头文件中不初始化只声明。引用该头文件即可,在链接过程中。就可以使用到这个变量。

    (附:extern在c++中经常用于  extern "C"  告诉编译器下面是c语言风格)

    #pragma warning

    #pragma warning( disable : 4507 34; once : 4385; error : 164 )
    等价于:
    #pragma warning(disable:4507 34) // 不显示 4507 和 34 号警告信息
    #pragma warning(once:4385) // 4385 号警告信息仅报告一次
    #pragma warning(error:164) // 把 164 号警告信息作为一个错误。

    另外还有

    #pragma pack

    使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
    使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
    在#pragma pack (n)和#pragma pack ()之间的代码按 n 个字节对齐。

    字节对齐,我将另起炉灶,在另外一篇博客中归纳总结。

     

    #pragma pack(push) //保存当前对其方式到 packing stack
    #pragma pack(push,n) 等效于
    #pragma pack(push)
    #pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按 n 字节对齐
    #pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐

    #运算符和##预算符

    #define SQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)));

    这段代码中#就是帮助x作为一个变量,表现出来,而不是一个简单的字母

    如果有#,SQR(3)运算出来就是

    The square of 3  is 9

    如果没有# SQL(3)运算出来就是

    The square of x  is 9

    ##预算符

    ##把两个语言符号组合成单个语言符号

    编译

    编译阶段是检查语法,生成汇编,这个属于程序员的必备知识,我们学习一门语言第一步就是知晓语法,其中比较生涩的有左值右值,指针的使用,内存的管理,数据结构的使用,这将会是一场持久战 ,贯穿在整个学习生涯。

    在这里我截取优先级问题,这个可能会通过编译但是不一定达到程序员想要的结果。

    在这里,我引用《c语言深度解剖》中的一张表格

    汇编

      汇编代码转换机器码   这个阶段,非底层的程序员不需要考虑, 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。

    链接

    开头我引用一下百度百科的介绍

    静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

    动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。

    将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。该可执行文件会变大很多,一般是调用自己电脑上的。

    静态库和应用程序编译在一起,在任何情况下都能运行,而动态库是动态链接,文件生效时才会调用。

    很多代码编译通过,链接失败就极有可能在静态库和动态库这出现了纰漏,要视情况解决。缺少相关所需文件,就会链接报错。这个时候就要检查下本地的链接库是不是缺损。

    展开全文
  • 编译原理

    2017-12-04 21:42:55
    一、 编译程序 1、 编译器是一种翻译程序,它用于将源语言(即用某种程序设计语言写成的)程序翻译为目标语言(即用二进制数表示的伪机器代码写成的)程序。后者在windows操作系统平台下,其文件的扩展名通常为....

    http://blog.csdn.net/shawjan/article/details/44652807

    一、 编译程序

    1、 编译器是一种翻译程序,它用于将源语言(即用某种程序设计语言写成的)程序翻译为目标语言(即用二进制数表示的伪机器代码写成的)程序。后者在windows操作系统平台下,其文件的扩展名通常为.obj。该文件通常还要经过进一步的连接,生成可执行文件(机器代码写成的程序,文件扩展名为.exe)。通常有两种方式进行这种翻译,一种是编译,另一种是解释。后者并不生成可执行文件,只是翻译一条语句、执行一条语句。这两种方式相编译比解释运行的速度要快得多。

    2、 编译过程的5个阶段:词法分析;语法分析;语义分析与中间代码产生;优化;目标代码生成。

    3、 在这五个阶段中,词法分析的任务是识别源程序中的单词是否有误,编译程序中实现这种功能的部分一般称为词法分析器。在编译器中,词法分析器通常仅作为语法分析程序的一个子程序以便在它需要单词符号时调用。在这一编译阶段中发现的源程序错误,称为词法错误。

    4、 语法分析阶段的目的是识别出源程序的语法结构(即语句或句子)是否错误,所以有时又常为句子分析。编译程序中负责这一功能的程序称为语法分析器或语法分析程序。在这一阶段中发现的错误称为语法错误。

    5、 C语言的(源)程序必须经过编译才能生成目标代码,再经过链接才能运行。PASCAL语言、FORTRAN语言的源程序也要经过这样的过程。通常将C、PASCAL、FORTRAN这样的语言统称为高级语言。而将最终的可执行程序称为机器语言程序。

    6、 在编译C语言程序的过程中,发现源程序中的一个标识符过长,超过了编译程序允许的范围,这个错误应在词法分析阶段发现,这种错误通常被称作词法错误。

     词法分析器的任务是以词法规则为依据对输入的源程序进行单词及其属性的识别,识别出一个个单词符号。

     词法分析的输入是源程序,输出是一个个单词的特殊符号,称为Token(标记或符号)。

     语法分析器的类型有:自下而上、自上而下。常用的语法分析器有:递归下降分析方法是一种自上而下分析方法, 算符优先分析法属于自下而上分析方法,LR分析法属于自下而上分析方法等等。

     通常用正规文法或正规式来描述程序设计语言的词法规则,而使用上下文无关文法来描述程序设计语言的语法规则。

     语法分析阶段中,处理的输入数据是来自词法分析阶段的单词符号。它们是词法分析阶段的终结符。

    7、 编译程序总框

    编译原理框架

    8、 在计算机发展的早期阶段,内存较小的不能一次完成程序的编译。这时通常将编译过程分成若干遍来完成。每一遍完成一部分功能,称为多遍编译。 
    与采用高级程序设计语言写的词法分析器相比,用汇编语言写的词法分析通常分析速度要快些。

    二. 词法与语法

    1、 程序语言主要由语法和语义两个方面来定义。

    2、 任何语言的程序都可看成是某字符集上的一个长字符串。

    3、 语言的语法:是指这样的一组规则(即产生式),用它可以生成和产生一个良定的程序。这些规则的一部分称为词法规则,另一部分称为语法规则。

    4、 词法规则:单词符号的形成规则;语法规则:语法单位(句子)的形成规则。语义规则:定义程序句子的意义。

    5、 一个程序语言的基本功能是描述数据和对数据的运算。

    6、 高级语言的分类:强制式语言;应用式语言;基于规则的语言;面向对象的语言。

    7、 一个语言的字母表为{a,b},则字符串ab的前缀有a、ε,其中ε不是真前缀。

    8、 字符串的连接运算一般不满足交换率。

    9、 文法G是一个四元组,或者说由四个元素构成,即非终结符集合VN、非终结符号集合VT 、开始符号S、产生式集合P,它可以形式化地表示成G =(VN,VT,S,P)。 
    按照文法的定义,这4个元素中终结符号集合是这个文法所规定的语言的字母表,产生式集合代表文法所规定的语言语法实体的集合。对上下文无关文法,通常我们只需要写出这个文法的产生式集合就可以确定这个文法的其他所有元素。其中,第一条产生式的左部符号为开始符号,而所有产生式的左部符号构成的集合就是该文法的非终结符集合。

     文法的例子: 
    设文法G=(VN,VT, S,P),其中P为产生式集合,它的每个元素的形式为产生式。

    10、如果文法G的一个句子存在两棵不同的最左语法分析树,则这个文法是无二义的。

    11、如果文法G的一个句子存在两棵不同的最右语法分析树,则这个文法是无二义的。

    12、如果文法G的一个句子存在两棵不同的语法分析树,则这个文法是无法判断是否是二义的。

    13、A为非终结符,如果文法存在产生式 ,则称 可以推导出 ;反之,称 可归约为 。

    14、乔姆斯基(Chomsky)将文法分为四类,即0型文法、1文法、2文法、3文法。 
    按照乔姆斯基对方法的分类,上下文无关文法是2型文法,2型文法的描述能力最强,3型文法又称为正规文法。

    15、产生式S→Sa | a产生的语言为L(G) = {an | n ≥ 1}。

    16、确定有限自动机DFA是非确定有限自动机NFA的特例;对任一非确定有限自动机能找到一个与之等价的确定有限自动机。

    17、DFA和NFA的主要区别有三点:一、DFA初态唯一,NFA初态不唯一;二、DFA弧标记为Σ上的元素,NFA弧标记为Σ*上的元素;三、DFA的函数为单射,NFA函数不是单射。

    18、有限自动机中两个状态S1和S2是等价的是指,无论是从S1还是S2出发,停于终态时,所识别的输入字的集合相同。

    19、自下而上的分析方法,是一个不断归约的过程。

    20、递归下降分析器:当一个文法满足LL(1)条件时,我们就可以为它构造一个不带回溯的自上而下分析程序。这个分析程序是由一组递归过程组成的,每个过程对应文法的一个非终结符。 
    这个产生式中含有的左递归是直接左递归。递归下降分析法中,必须要消除所有的左递归。递归下降分析法中的试探分析法之所以要不断用一个产生式的多个候选式进行逐个试探,最根本的原因是这些候选式有公共左因子。

    21、算符优先分析法是一种自下而上的分析方法,它适合分析各种程序设计语中的表达式,并宜于手工实现。目前最广泛的无回溯的“移进—归约”方法是自下而上分析方法。

    22、在表驱动预测分析器中,

    1)读入一个终结符a,若该终结符与栈项的终结符相同,并且不是结束标志$,则此时栈顶符号出栈;

    2)若此时栈项符号是终结符并且是,说明源程序有语法错误;

    3)若此时栈顶符号为,则分析成功。

    23、算符优先分析方法不存在使用形如 这样的产生式进行的归约,即只要求终结符的位置与产生式结构一致,从而使得分析速度比LR分析法更快。

    24、LR(0)的例子:

    产生式E→ E+T对应的LR(0)项目中,待归约的项目是E→ E+∙T,移进项目是E→ E∙+T,还有两个项目为E→ ∙E+T和E→ E+T∙。 
    当一个LR(0)项目集中含有两个归约项目时,称这个项目集中含有归约-归约冲突。

    25、LL(1)文法的产生式中一定没有公共左因子,即LL(1)文法中一定没有左递归。为了避免回溯,在LL(1)文法的预测分析表中,一个表项中至多只有一个产生式。 
    预测分析方法(即LL(1)方法),由一个栈,一个总控程序和一个预测分析表组成。其中构造出预测分析表是该分析方法的关键。

    26、LR(0)与SLR(1)两种分析方法相比,SLR(1)的能力更强。

    27、静态语义检查一般包括以下四个部分,即类型检查、控制流检查、名字匹配检查、一致性检查。 
    C语言编译过程中下述常见的错误都属于检查的范围:

    a) 将字符型指针的值赋给结构体类型的指针变量:类型检查。 
    b)switch语句中,有两个case语句中出现了相同的常量:一致性检查。 
    c)break语句在既不是循环体内、又不是break语句出现的地方出现:控制流检查。 
    d)goto语句中的标号在程序的函数中没有找到:一致性检查。 
    e)同一个枚举常量出现在两个枚举类型的定义当中:相关名字检查。

    28、循环优化中代码外提是指对循环中的有些代码,如果它产生的结果在循环过程中是不变的,就把它提到循环体外来;而强度削弱是指把程序中执行时间较长的运算替换为执行时间较短的运算。 (完)

    展开全文
  • From:http://blog.chinaunix.net/uid-22327815-id-3540305.html 从Hello World说程序运行机制:http://www.sohu.com/a/132798003_505868 C/C++中如何在main()函数之前执行一条语句?...(深入理解计算机系统...

     

    From:http://blog.chinaunix.net/uid-22327815-id-3540305.html

    从Hello World说程序运行机制:http://www.sohu.com/a/132798003_505868
    C/C++中如何在main()函数之前执行一条语句?:https://www.zhihu.com/question/26031933
    (深入理解计算机系统) bss段,data段、text段、堆(heap)和栈(stack):https://www.cnblogs.com/yanghong-hnu/p/4705755.html​​​​​​​

     

     

     

    前言

     

    C语言算是大学里接触的最早,用的最"多"的语言了,对于大部分学习计算机的学生基本上是从开始学习C语言起,凭借着一句经典的"hello, world!"迈入了计算机的世界的,初体味了一把这个世界还有个叫编程的活。作为系统级的开发首选语言,自诞生以来就屹立不倒,C语言的重要性是不言而喻的。做为一个菜鸟级别的程序员,使用C有些年,但对于C没有有真正的了解。我想有必要从新了解这门古老的语言背后的东西,知其然还要知其所以然,才能更好的使用这门语言。

     

     

    对于C语言编写的Hello World程序(如下),对于程序员来说肯定如雷贯耳,就是这样一个简单的程序,你真的了解她吗?

    #include <stdio.h>  
    int main()  
    {  
        printf("Hello World\n")  
        return 0;  
    }  

    对于下面这些问题,你脑子里能够马上反映出一个清晰、明显的答案吗?

    1. 程序为什么要被编译器编译之后才可以运行?
    2. 编译器在把C语言程序转换成可以执行的机器码的过程中做了什么?怎么做的?
    3. 最后编译出来的可执行文件里面是什么?除了机器码还有什么?他们怎么存放的?怎么组织的?
    4. #include <stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它怎么实现的?
    5. 不同的编译器(Microsoft VC、GCC)和不同的硬件平台(x86、SPARC、MIPS、ARM),以及不同的操作系统(Windows、Linux、UNIX、Solaris),最终编译出来的结果一样吗?为什么?
    6. Hello World程序是怎么运行起来的?操作系统是怎么装载它的?他从哪里开始执行?到哪儿结束?main函数之前发生了什么?main函数结束之后又发生了什么?
    7. 如果没有操作系统,Hello World可以运行吗?如果要在一台没有操作系统的机器上运行Hello World需要什么?应该怎么实现?
    8. printf是怎么实现的?他为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
    9. Hello World程序在运行时,它在内存中是什么样子的?

     

     

    C程序编译流程

     

    编译一个C程序可以分为四阶段,预处理阶段->生成汇编代码阶段->汇编阶段->链接阶段,这里以linux环境下gcc编译器为例。使用gcc时默认会直接完成这四个步骤生成可以执行的程序,但通过编译选项可以控制值进行某些阶段,查看中间的文件。

     

    gcc指令的一般格式为(man gcc  或者 gcc -h  查看更过选项帮助):

         gcc [选项] 要编译的文件 [选项] [目标文件]
         其中,目标文件可缺省,gcc默认生成可执行的文件名为:a.out
         gcc main.c                               直接生成可执行文件a.out
         gcc -E main.c -o hello.i                 生成预处理后的代码(还是文本文件)
         gcc –S main.c -o hello.s                生成汇编代码
         gcc –c main.c -o hello.o                生成目标代码

     

    C程序目标文件和可执行文件结构

    目标文件和可执行文件可以有几种不同的格式,有ELF(Excutable and linking Format,可执行文件和链接)格式,也有COFF(Common Object-File Format,普通目标文件格式)。虽然格式不一样,但具有一个共同的概念,那就是段(segments),这里段指二进制格式文件中的一块区域。
    linux下的可执行文件有三个段文本段(text)、数据段(data)、bss段,可用nm命令查看目标文件的符号清单。

    编译过程: 源文件-------->到可执行文件

     

    图引自《C专家编程》

     

     

     

    其中注意的BSS段,并没有保存未初始化段的映像,只是记录了该段的大小(应为该段没有初值,不管具体值),到了运行时再到内存为未初始化变量分配空间,这样可以节省目标文件空间。对于data段,只是保存在目标文件中,运行时直接载入。

     

    C程序的内存布局

    讲C语言内存管理的书籍或者博客?:https://www.zhihu.com/question/29922211

    1. readelf命令: http://man.linuxde.net/readelf
    2. 面试官问我:bss段的大小记录在哪里?:http://bbs.csdn.net/topics/390613528
    3. 内存区划分、内存分配、常量存储区、堆、栈、自由存储区、全局区:http://www.cnblogs.com/CBDoctor/archive/2011/12/24/2300624.html
    4. 常量存在内存中的那里?:http://bbs.csdn.net/topics/390510503

     

     

     

    运行过程: 可执行文件->内存空间

    不管是在Linux下C程序还是Windows下C程序,他们都是由正文段、数据段、BSS段、堆、栈等段构成的,只不过可能他们的各段分配地址不一样。Linux下的C程序正文段在低地址,而Windows下的C程序的正文段(代码段)在高地址。所有不用担心我用Linux环境和Windows环境共同测试带来不正确的数据。

                      

    C语言存储空间布局

    C语言一直由下面部分组成:

    1. 正文段code segment/text segment,.text段):或称 代码段,通常是用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。CPU执行的机器指令部分。( 存放函数体的二进制代码 。)
    2. 只读数据段(RO data,.rodata):只读数据段是程序使用的一些不会被改变的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要修改,因此只需放在只读存储器中。
    3. 已初始化读写数据段data segment,.data段):通常是用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。常量字符串就是放在这里的,程序结束后由系统释放(rodata—read only data)。已初始化读写数据段(RW data,.data):已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器空间,在程序执行时它们需要位于可读写的内存区域,并具有初值,以供程序读写。
               *只读数据段 和数据段统称为 数据段
    4. BSS段bss segment,.bss段:未初始化数据段(BSS,.bss)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。全局变量 和 静态变量 的存储是放在一块的。初始化的全局变量和静态变量在一块区域(.rwdata or .data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss), 程序结束后由系统释放。未初始化数据是在程序中声明,但是不具有初值的变量,这些变量在程序运行之前不需要占用存储空间。
             * 在 C++中,已经不再严格区分bss data了,它们共享一块内存区域
              * 静态存储区包括bbs段和data段
    5. heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆上被剔除(堆被缩减)。一般由程序员分配释放(new/malloc/calloc delete/free),若程序员不释放,程序结束时可能由 OS 回收。注意:它与数据结构中的堆是两回事,但分配方式倒类似于链表

    6. stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是我们函数大括号"{}"中定义的变量(不包括static声明的变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等调用结束后,函数的返回值也会被存放在回栈中。由于栈的先进先出特性,所有栈特别方便用来保存/恢复调用现场。从这个意义上讲,把堆栈看成一个寄存、交换临时数据的内存区。由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈

    详细验证可以参看 (C语言存储空间布局以及static详解:http://blog.csdn.net/thanksgining/article/details/41960369

    程序和目标的对应关系

    使用readelf和objdump解析目标文件(http://www.jianshu.com/p/863b279c941e

    int a = 0;      // a 在 data
    char *p1;       // p1 在 bss
    
    main()
    {
        int b;                      // b 在 stack
        char s[] = "abc";           // s 在 stack, abc\0 在常量区
        char *p2;                   // p2 在 stack
        char *p3 = "123456";        // p3 在 stack, 123456\0 在常量区
        static int c = 0;           // c 在 data
        p1 = (char *)malloc(10);    // 申请的10字节内存在 heap, bss中的指针指向heap中的内存
        p2 = (char *)malloc(20);    // 申请的20字节内存在 heap, stack中的指针指向heap中的内存
        strcpy(p1, "123456");       // 123456\0 在常量区,编译器可能会将它与 p3 所指向的 "123456\0" 优化成一块
    }

     

    堆和栈的区别

     

    管理方式:对于栈来讲,是由编译器自动管理;对于堆来说,释放工作由程序员控制,容易产生 memory leak。

    空间大小:一般来讲在 32 位系统下,堆内存可以达到接近 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在 VC6 下面,默认的栈空间大小大约是 1M。

    碎片问题:对于堆来讲,频繁的new/delete 势必会造成内存空间的不连续,从而造成大量碎片,使程序效率降低;对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出。

    生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

    分配方式:堆都是动态分配的,没有静态分配的堆;栈有 2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现。

    分配效率:栈是机器系统提供的数据结构,计算机会在底层分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高;  堆则是 C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,然后进行返回。显然,堆的效率比栈要低得多。

    无论是堆还是栈,都要防止越界现象的发生。

     

     

    关于 Global 和 Static 类型的一点讨论

    1. static 全局变量与普通的全局变量有什么区别 ?

    全局变量(外部变量)的定义之前再冠以 static 就构成了静态的全局变量。

    全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。

    这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。  而静态全局变量则限制了其作用域,  即只在定义该变量的源文件内有效,  在同一源程序的其它源文件中不能使用它。

    由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

    static 全局变量只初使化一次,防止在其他文件单元中被引用。

    2. static 局部变量和普通局部变量有什么区别 ?

    把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。             

    static 局部变量只被初始化一次,下一次依据上一次结果值。

    3. static 函数与普通函数有什么区别?

    static 函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件.static 函数在内存中只有一份(.data),普通函数在每个被调用中维持一份拷贝。

     

    对于data段,保存的是初始化的全局变量和stataic的局部变量,直接载入内存即可。 text段保存的是代码直接载入。BSS段从目标文件中读取BSS段大小,然后在内存中紧跟data段之后分配空间,并且清零(这也是为什么全局表量和static局部变量不初始化会有0值得原因)

     

    下面的图可以让你更直观的了解目标文件:

    上图是目标文件的典型结构,实际的情况可能会有所差别,但都是在这个基础上衍生出来的。

    ELF文件头:即上图中的第一个段。其中的header是目标文件的头部,里面包含了这个目标文件的一些基本信息。如该文件的版本、目标机器型号、程序入口地址等等。
    文本段:里面的数据主要是程序中的代码部分。
    数据段:程序中的数据部分,比如说变量。

    重定位段:重定位段包括了文本重定位和数据重定位,里面包含了重定位信息。一般来说,代码中都会存在引用了外部的函数,或者变量的情况。既然是引用,那么这些函数、变量并没存在该目标文件内。在使用他们的时候, 就要给出他们的实际地址(这个过程发生在链接的时候)。正是这些重定位表,提供了寻找这些实际地址的信息。理解了上面之后,文本重定位和数据重定位也就不难理解了。
    符号表:符号表包含了源代码中所有的符号信息 。 包括每个变量名、函数名等等。里面记录了每个符号的信息,比如说代码中有“student”这个符号,对应的在符号表中就包括这个符号的信息。包括这个符号所在的段、它的属性(读写权限)等相关信息。其实符号表最初的来源可以说是在编译的词法分析阶段。在做词法分析的时候,就把代码中的每个符号及其属性都记录在符号表中。
    字符串表:和符号表差不多的功能,存放了一些字符串信息。
    其中还有一点要说的是:目标文件都是以二进制来存储的,它本身就是二进制文件。
    现实中的目标文件会比这个模型要复杂些,但是它的思路都是一样的,就是按照类型来存储,再加上一些描述目标文件信息的段和链接中需要的信息。

     

     

    函数调用栈

    作为面向过程的语言,C基本的特色就是模块化、过程化。一个C程序或一个模块由一堆函数组成,然后程序执行,按代码的结构调用这些函数,完成功能。那么函数调用的背后编译器到底为我们做了什么呢?

    void fun(int a, double b)
    {
         int c = 300;
         c += 1;
    }
    int main()
    {
         fun(100, 200);
         return 0;
    }
    
    
    .globl _fun                             ;全局函数符号
         .def     _fun;     
    _fun:                                   ;函数fun入口
         pushl     %ebp                     ;保存ebp值
         movl     %esp, %ebp                ;采用ebp来访问栈顶
         subl     $4, %esp                  ;esp用来扩展堆栈分配局部变量空间
         movl     $300, -4(%ebp)            ;局部变量赋值
         leal     -4(%ebp), %eax            ;得到局部变量有效地址
         incl     (%eax)                    ;访问局部变量
         leave                              ;相当于movl ebp, esp   pop ebp  
         ret
    
    .globl _main
         .def     _main;    
    _main:                                   ;main函数入口
         ;....
         movl     $200, 4(%esp)              ; 参数入栈 
         movl     $100, (%esp)               ; 参数入栈
         call     _fun
         ;.....

     

     

    函数调用过程:

    参数按从右到左顺序放到栈顶上

    call调用,将返回地址ip入栈保存

    在栈上分配局部变量空间

    执行函数操作

     

    函数返回过程:

    ret会从栈上弹出返回地址

    执行调用前后面的代码

     

    由此得的结论是,函数调用一个动态的过程,调用的时候又有一个栈帧,调用的时候展开,结束的时候收缩。局部变量在运行到该函数的时候在栈上分配内存,这些内存实际上没有名字的,不同于数据段,有符号名字,局部变量在函数结束就销毁了。这也是什么局部变量同名互补干涉的原因,因为编译以后 ,根本就不是通过名字来访问的。

     

     

     

    全局变量

    全局变量有初始化或未初始化之分,初始化了的全局变量保存在data段,未初始化全局变量保存在BSS段,data段和BSS段都是程序的数据段

    int global1 = 100;
    int main()
    {
         global1 = 101;
         extern int global2;
         global2 = 201;
         return 0;
    }
    int global2 = 200;
    
         
    .globl _global1                    ;全局符号global1
         .data                         ;位于数据段
         .align 4
    _global1:
         .long     100                 ;全局变量初值
         ;.....
    .globl _main                          ;全局符号main 
         .def     _main;                  ;是一个函数
    _main:                                ;函数入口
         ;...
         movl     $101, _global1          ;通过符号访问全局变量
         movl     $201, _global2          ;通过符号访问全局变量,这个变量还未定义
         movl     $0, %eax
         leave
         ret
    .globl _global2               :全局符号golbal2
         .data                    ;位于数据段
         .align 4
    _global2:                     ;全局变量的定义,初始化值
         .long     200             
    
    
    int global1;
    int main()
    {
         global1 = 101;
         extern int global2;
         global2 = 201;
         return 0;
    }
    int global2;
    
        
    .globl _main
         .def     _main;     
    _main:
        ;....
         movl     $101, _global1  ;通过符号访问全局变量,这个符号可以在之后,或其他文件中定义
         movl     $201, _global2
         movl     $0, %eax
         leave
         ret
         .comm     _global1, 16     # 4         ;标明这是个未初始化全局变量,声明多个,但最后运行时在bss段分配空间
         .comm     _global2, 16     # 4

    可以得出结论:全局变量独立于函数存在,所有函数都可以通过符号访问,并且在运行期,其地址不变。

     

    编译与链接

    看下面这个程序链接出错,找不符号a,print, 但生成汇编代码并没有问题。这是因为编译的时候只是把符号地址记录下来,等到链接的时候该符号定义了才会变成具体的地址。如果链接的时候所有符号地址都有定义,那么生成可执行文件。如果有不确定地址的符号,则链接出错。

    #include<stdio.h>
    int main()
    {
         extern int a ;
         print("a = %d\n", a);
         return 0;
    }
    
         .file     "fun.c"
         .def     ___main;    
         .section .rdata,"dr"
    LC0:
         .ascii "a = %d\12\0"
         .text
    .globl _main
         .def     _main;     .
    _main:
         ;..
         movl     _a, %eax          ;通过符号访问全局变量a
         movl     %eax, 4(%esp)
         movl     $LC0, (%esp)
         call     _print            ;通过符号访问函数print
         movl     $0, %eax
         leave
         ret
         .def     _print;     ;说明print是个函数符号

     

    全局变量的链接属性

    全局变量的默认是extern的,最终存放在数据段,整个程序的所有文件都能访问,如果加上static则表明值能被当前文件访问。

    #include<stdio.h>
    static int a = 10;
    int main()
    {
         a = 20;
         return 0;
    }
    
        
         .data
         .align 4
    _a:                              ;全局变量a定义,少了glbal的声明
         .long     10
         .def     ___main;    
         .text
    .globl _main
         .def     _main;  
    _main:
         ; ...
         movl     $20, _a
         movl     $0, %eax
    
    去掉int a前面的static产生的汇编代码为:
    
    .globl _a                    ; global声明符号 a为全局
         .data
         .align 4
    _a:
         .long     10
         .def     ___main
         .text
    .globl _main
         .def     _main
    _main:
         ;...
         call     __alloca
         call     ___main
         movl     $20, _a
         movl     $0, %eax
    
    对于未初始化全局变量
    #include<stdio.h>
    static int a;
    int main()
    {
         a = 20;
         return 0;
    }
    
    .globl _main
         .def     _main;     .scl     2;     .type     32;     .endef
    _main:
        ;..
         movl     $20, _a
         movl     $0, %eax
         leave
         ret
         .lcomm _a,16          ; 多了个l表明是local的未初始化全局变量
    
    去掉int a前面的static
    .globl _main
         .def     _main;     .scl     2;     .type     32;     .endef
    _main:
         ;..
         movl     $20, _a
         movl     $0, %eax
         leave
         ret
         .comm     _a, 16     # 4          ;extern链接属性的未初始化全局变量

     

    static局部变量

    static局部变量具备外部变量的生存期,但作用域却和局部变量一样,离开函数就能访问

    #include<stdio.h>
    int fun()
    {
         static int a = 10;
         return (++a);
    }
    int main()
    {
         printf("a = %d\n",fun());
         printf("a = %d\n",fun());
    }
    
         .data
         .align 4
    a.0:                    ;static局部变量是放在代码段
         .long     10     ;分配空间初始化
         .text
    .globl _fun
         .def     _fun;   
    _fun:
         pushl     %ebp
         movl     %esp, %ebp
         incl     a.0
         movl     a.0, %eax
         popl     %ebp
         ret
         .def     ___main;   
         .section .rdata,"dr"

    编译实际还是还是把static局部变量放在数据段存储(要么怎么可能在程序运行期间地址不变呢),值不过符号名会动点手脚(这样出了函数就访问不了了),同时候 多个函数中定义同名的static局部变量,实际上是不同的内存单元,互补干涉了。

     

    a.out剖分

    a.out是目标文件的默认名字。也就是说,当编译一个文件的时候,如果不对编译后的目标文件重命名,编译后就会产生一个名字为a.out的文件。具体的为什么会用这个名字这里就不在深究了。有兴趣的可以自己google。我们现在就来研究一下hello world编译后形成的目标文件,这里用 C 来描述。

    简单的hellow world 源码:

    /*hello.c*/
    #include<stdio.h>
    int main()
    {
        int a=5;
        printf("hello world n");
        return 0;
    }

    为了在数据段中也有数据可放,这里增加了“int a=5”。如果在VC上的话,点击运行便能看到结果。为了能看清楚内部到底是如何处理的,我们使用GCC来编译。

    运行:gcc hello.c。再看我们的目录下,就多了目标文件a.out。

    现在我们想做的是看看a.out里到底有什么,可能有童鞋回想到用vim文本查看,当时我也是这么天真的认为。但a.out是何等东西,怎能这么简单就暴露出来呢 。是的,vim不行。“我们遇到的问题大多是前人就已经遇到并且已经解决的”,对,其中有一个很强悍的工具叫做objdump。有了它,我们就能彻底的去了解目标文件的各种细节,当然还有一个叫做readelf也很有用,这个在后面介绍。这两个工具一般Linux里面都会自带有有,可以自行google
     

    注:这里的代码主要是在Linux下用GCC编译,查看目标文件用的是Objdump、readelf。

    下面是a.out的组织结构:(每段的起始地址、、大小等等)。查看目标文件的命令是 objdump -h a.out

    就和上文中描述的目标文件的格式一样,可以看出是分类存储的。目标文件被分为了6段。

    从左到右,第一列(Idx Name)是段的名字,第二列(Size)是大小 ,VMA为虚拟地址,LMA为物理地址,File off是文件内的偏移。也就是这段相对于段中某一参考(一般是段起始)的距离。最后的Algn是对段属性的说明,暂时不用理会

    “text”段:代码段。
    “data”段:也就是上面说的数据段,保存了源代码中的数据,一般是以初始化的数据。
    “bss”段:也是数据段,存放那些未初始化的数据,因为这些数据还未分配空间,所以单独存放。
    “rodata”段:只读数据段,里面存放的数据是只读的。
    “cmment”存放的是编译器版本信息。

    剩下的两段对我们的讨论没有实际意义,就不再介绍。认为他们包含了一些链接、编译、装在的信息就可。

    注:这里的目标文件格式只是列出实际情况中主要部分。实际情况还有一些表未列出。如果你也在用Linux,可以用objdump -X 列出更详细的段内容。

     

    深入a.out

    上面部分通过实例说了目标文件中的典型的段,主要是段的信息,如大小 等相关的属性。那么这些段里面究竟有些什么东西呢,“text”段里到底存了什么东西,还是用我们的objdump。objdump -s a.out 通过-s选项就可以查看目标文件的十六进制格式。
    查看结果如下:
    如上图所示,列出了各段的十六进制表示形式。可以看出图中共分为两栏,左边的一栏是十六进制的表示, 右边则显示相应的信息。比较明显的如“rodata”只读数据段中就有 “hello world”。。
    你也可以查看“hello world”的ASCII值,对应的十六进制就是里面的内容了。“comment”上文中说的这个段包含了一些编译器的版本信息,这个段后面的内容就是了:GCC编译器,后面的是版本号。

     

    a.out反汇编

    编译的过程总是先把源文先变为汇编形式,再翻译为机器语言。看了这么多的a.out,再研究一下他的汇编形式是很必要的。
    objdump -d a.out可以列出文件的汇编形式。不过这里只列出了主要部分,即main函数部分,其实在main函数执行的开始和main函数执行以后都还有多工作要做。即初始化函数执行环境以及释放函数占用的空间等。
    上面的图中,左边是代码的十六进制形式,左边是汇编形式。对汇编熟悉的童鞋应该能看懂大部分,这里就不在多述。

     

    a.out头文件

    在介绍目标文件格式的时候,提到过头文件这个概念,里面包含了这个目标文件的一些基本信息。如该文件的版本、目标机器型号、程序入口地址等等。
    下图是文件头的形式:
    可以用readelf -h 来查看。(下图中查看的是 hello.o,它是源文件hello.c编译但未链接的文件。 这个和查看a.out 大部分是一样的)
    图中分为两栏,左边一栏表示的是属性,右边是属性值。第一行常被称为魔数。后面是一连串的数字,其中的具体含义就不多说了,可以自己去google。
    接下来的是一些和目标文件相关的信息。由于和我们要讨论的问题关系不大,这里就不展开讨论了。
    上面是内容用具体的实例说了目标文件内部的组织形式,目标文件只是产生可执行文件过程中的一个中间过程,对于程序是如何运行的还没做讨论,目标文件是如何转变为可执行文件以及可执行文件是如何执行的将在下面的部分中讨论

     

    对链接的简单认识

    链接通俗的说就是把几个可执行文件。如果程序A中引用了文件B中定义的函数,为了A中的函数能正常执行,就需要把B中的函数部分也放在A的源代码中,那么将A和B合并成一个文件的过程就是链接了。有专门的过程用来链接程序,称为链接器。他将一些输入的目标文件加工后合成一个输出文件。这些目标文件中往往有相互的数据、函数引用。
    上文中我们看过了hello world的反汇编形式,是一个还没有经过链接的文件,也就是说当引用外部函数的时候是不知道其地址的,如下图:
    上图中,cal指令就是调用了printf()函数,因为这时候printf()函数并不在这个文件中,所以无法确定它的地址,在十六进制中就用“ff ff ff ”来表示它的地址。等经过链接以后,这个地址就会变为函数的实际地址,应为连接后这个函数已经被加载进入这个文件中了。
    链接的分类:按把A相关的数据或函数合并为一个文件的先后可以把链接分为静态链接和动态链接。

     

    静态链接:

    在程序执行之前就完成链接工作。也就是等链接完成后文件才能执行。但是这有一个明显的缺点,比如说库函数。如果文件A 和文件B 都需要用到某个库函数,链接完成后他们连接后的文件中都有这个库函数。当A和B同时执行时,内存中就存在该库函数的两份拷贝,这无疑浪费了存储空间。当规模扩大的时候,这种浪费尤为明显。静态链接还有不容易升级等缺点。为了解决这些问题,现在的很多程序都用动态链接。

     

    动态链接:
    和静态链接不一样,动态链接是在程序执行的时候才进行链接。也就是当程序加载执行的时候。还是上面的例子 ,如果A和B都用到了库函数Fun(),A和B执行的时候内存中就只需要有Fun()的一个拷贝。

     

    对装载的简单解释

    我们知道,程序要运行是必然要把程序加载到内存中的。在过去的机器里都是把整个程序都加载进入物理内存中,现在一般都采用了虚拟存储机制,即每个进程都有完整的地址空间,给人的感觉好像每个进程都能使用完成的内存。然后由一个内存管理器把虚拟地址映射到实际的物理内存地址。
    按照上文的叙述, 程序的地址可以分为虚拟地址和实际地址。虚拟地址即她在她的虚拟内存空间中的地址,物理地址就是她被加载的实际地址。
    在上文中查看段 的时候或许你已经注意到了,由于文件是未链接、未加载的,所以每个段的虚拟地址和物理地址都是0.
    加载的过程可以这样理解:先为程序中的各部分分配好虚拟地址,然后再建立虚拟地址到物理地址的映射。其实关键的部分就是虚拟地址到物理地址的映射过程。程序装在完成之后,cpu的程序计数器pc就指向文件中的代码起始位置,然后程序就按顺序执行。

     

     

    预处理

     

    预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令,如“#include”、“#define'、”#if“,并删除注释行,还会添加行号和文件名标识以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。经过预编译的.i文件不包含任何宏定义,因为所有的宏已经被展开并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看已编译后的文件来确认问题。比如hello.c中第一行的 #include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并且把它直接插入到程序文本中,结果就得到了另一个C程序,通常是以 .i 作为文件扩展名。在该阶段,编译器将C源代码中的包含的头文件如stdio.h编译进来,用户可以使用gcc的选项”-E”进行查看。

    用法:#gcc -E main.c -o main.i
    作用:将main.c预处理输出main.i文件
    
    [user:test] ls
    main.c
    [user:test] gcc -E main.c -o main.i
    [user:test] ls
    main.c  main.i

    使用GCC -E参数完成。

    这里写图片描述

    预处理会干什么事情:

    • 展开所有的宏定义并删除 #define
    • 处理所有的条件编译指令,例如 #if #else #endif #ifndef …
    • 把所有的 #include 替换为头文件实际内容,递归进行
    • 把所有的注释 // 和 / / 替换为空格
    • 添加行号和文件名标识以供编译器使用
    • 保留所有的 #pragma 指令,因为编译器要使用
    • ……

    处理完成之后看看我们的Hello.i,发现原来8行代码现在变成了接近700行,因为将<stdio.h>的文件被替换进来了,在最后几行找到了我们自己Hello.c的代码:

    这里写图片描述

    使用系统默认的预处理器cpp完成。

    预处理除了使用GCC -E参数完成之外,我们还可以使用系统默认的预处理器cpp完成。如下所示

    这里写图片描述

    我们看看Hello.ii的代码:

    这里写图片描述

    虽然Hello.i和Hello.ii的代码对应的行数不同,但是内容却是一模一样的,只是中间空行的数量不同而已。

    OK ,接下来,继续向编译出发。

     

    编译

    编译是将源文件转换成汇编代码的过程,具体的步骤主要有:词法分析 -> 语法分析 -> 语义分析及相关的优化 -> 中间代码生成 -> 目标代码生成(汇编文件.s)。

    具体生成过程可以参考《编译原理》。在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。

    选项 -S
    用法:[user]# gcc –S main.i –o main.s
    作用:将预处理输出文件main.i汇编成main.s文件。
    
    [user:test] ls
    main.c main.i
    [user:test] gcc -S main.i -o main.s
    [user:test] ls
    main.c main.i main.s

     

    注意:gcc命令只是一个后台程序的包装,会根据不同的参数要求去调用预编译编译程序cc1(c)、汇编器as、连接器ld。

    使用GCC -S参数完成。

    这里写图片描述

    查看Hello.s发现已经是汇编代码了。

    这里写图片描述

    使用系统默认的编译器cc1完成这个过程。

     

    前面的预处理命令cpp可能大家的系统上都有,我们输入cp,然后Tab两下(Linux系统上表示提示补全命令),系统提示如下: 

    这里写图片描述

     

    倒数第二个命令就是cpp了。但是我们cc同样的过程的时候却发现: 

    这里写图片描述

     

    并没有cc1这个命令,但是cc1确实是Linux系统上默认的编译器呀,我们在系统上找找看: 

    这里写图片描述

     

    看上图第二条,/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/cc1,尝试着去看下: 

    这里写图片描述

     

    有可执行权限,那为何不试试能不能用来编译Hello.ii呢? 

    这里写图片描述

     

    好像没有什么报错,迫不及待的看看Hello.ss的内容:

    这里写图片描述

    发现和Hello.s的是一样的。编译成功。Goto 汇编。

     

    汇编

     

    汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码。汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本编译器中打开hello.o文件,看到的将是一堆乱码。

    选项 -c
    用法:[user]# gcc –c main.s –o main.o
    作用:将汇编输出文件main.s编译输出main.o文件。
    
    [user:test] ls
    main.c main.i main.s
    [user:test] gcc -c main.s -o main.o
    [user:test] ls
    main.c main.i main.o main.s

    使用GCC -c参数完成。

    这里写图片描述

    其实也可以查看下Hello.o的内容:

    这里写图片描述

    只是乱码罢了。要是想看,我们可以使用 hexedit, readelf 和 objdump 这三个工具。

    hexedit 只是个将二进制文件用十六进制打开的工具,我们执行:

    $ sudo yum install hexedit
    $ hexedit Hello.o

    可以看到:

    这里写图片描述

    最右边是源文件被翻译成可见字符,点.表示的都是不可见字符。这样看当然没有多大实际意义,但是一些输出的字符串Hello World,包括整个文件的类型ELF都是可以看到的。readelf和objdump我们后面再说。

     

    使用系统默认的汇编器as完成。

    这里写图片描述

    hexedit 看看 :

    这里写图片描述

    使用 cmp 命令比较Hello.oo和Hello.o

    这里写图片描述

    只有极少数字符不同。可能也是格式问题。下面就要进入链接这个阶段了,本篇博客就到这里吧。

    总结:上面的过程中,我们已经将Hello.c源程序经过预处理,编译,汇编阶段变成了二进制代码,这三个过程我们都是用两种方法完成的,一种是GCC + 参数的方法,另一种是使用系统默认的预处理器,编译器,汇编器。但是这两种方法都达到了我们的目的,那有关本文第二部分的问题GCC是什么?的答案,我之前之所以同意第三个答案:GCC是GUN编译系统的编译驱动程序,就是因为GCC编译的过程中,真正干活的还是我们系统默认的预处理器,编译器,汇编器,如果你还是不信,GCC -v显示过程看看不就好了。

    最后给它加上x权限。然后运行

    chmod a+x a.out
    ./a.out

     

     

    链接

    这阶段就是把汇编后的机器指令集变成可以直接运行的文件,而对目标文件进行链接主要是因为在目标文件中可能用到了在其他文件当中定义的字段(或者函数),通过链接来把多个不同目标文件关联到一起。比如有2个目标文件a和b,在 b中定义了一个函数"method",而在文件a中则使用到了b文件中的函数"method",通过链接文件a才能调用到函数"method",不然文件a根本就不知道到函数"method"底做了些什么操作。

    hello程序调用了一个printf函数,它是每个C编译器都会提供的标准C库中的一个函数,printf函数存在于一个名为printf.o的单独预编译好了的标准文件中,而这个文件必须以某种方式合并到我们的hello.o程序中,链接器(ld)就负责处理这种合并,结果就得到hello文件,他是一个可执行目标文件(简称:可执行文件),可以被加载到内存中,有系统执行。

    gcc的无选项的编译就是链接
    用法:[user]# gcc main.o -o main.elf
    作用:将编译输出文件main.o链接成最终可执行文件main.elf
    
    [user:test] ls
    main.c main.i main.o main.s
    [user:test] gcc main.o -o main.elf
    [user:test] ls
    main.c main.elf* main.i main.o main.s

    模块之间的通信有两种方式:一种是模块间的函数调用,另一种是模块间的变量访问。函数访问需知道目标函数的地址,变量访问也需要知道目标变量的地址,所以这两种方式都可以归结为一种方式,那就是模块间符号的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合。这个模块的拼接过程就是“链接”。
    在链接中,函数和变量统称为符号(symbol),函数名或变量名就是符号名(symbol name)。可以将符号看做是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(symbol table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是它们的地址。符号表中所有的符号分类:
    1、定义在本目标文件的全局符号,可以被其他目标文件引用。
    2、在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(external symbol),比如printf。
    3、段名,这种符号往往由编译器产生,它的值就是该段的起始地址,比如“.text”、“.data”。
    4、局部符号,这类符号只在编译单元内部可见。这些局部符号对于链接过程没有作用,链接器往往忽略它们。
    5、行号信息,即目标文件指令与源代码中代码行的对应关系。

    链接过程主要包括了地址和空间分配、符号决议和重定位。符号决议有时候也叫做符号绑定、名称绑定、名称决议,甚至还有叫做地址绑定、指令绑定,大体上它们的意思都一样,但从细节角度来区分,它们之间还存在一定区别,比如“决议”更倾向于静态链接,而“绑定”更倾向于动态链接,即它们所使用的范围不一样。
    每个目标文件都可能定义一些符号,也可能引用到定义咋其他目标文件的符号。重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器须要对某个符号的引用重定位时,它就是要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。

    更多可以参考 :《编译原理》、《程序员的自我修养》

     

     

     

     

     

     

     

    展开全文
  • 编译和链接的过程

    2018-07-22 23:08:24
    程序要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。接下来通过几个简单的例子来详细讲解一下这些过程。 对于上边用到的几个选项需要说明一下。 使用 gcc 命令不跟任何的选项的话,会默认执行...
  • C/C++程序编译过程详解 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件...
  • 【龙书】编译原理(第二版)学习与理解:1.也许我们这辈子都不会去实现一个编译器,但是我们至少要知道编译器是什么?为什么会需要编译器? ①编译器首先也是一种电脑程序。它会将用某种编程语言写成的源代码(原始...
  • 简单的编译流程

    2018-10-30 23:35:44
    简易编译器流程图: 一个典型的编译器,可以包含为一个前端,一个后端。前端接收源程序产生一个中间表示,后端接收中间表示继续生成一个目标程序。所以,前端处理的是跟源语言有关的属性,后端处理跟目标机器有关...
  • 编译原理:总结

    2019-01-08 11:33:57
    编译器概述 编译器的核心功能 编译器的核心功能是把源代码翻译成目标代码: 翻译!!!目标代码!!! 理解源代码:词法分析、语法...转化为等价的目标代码:中间代码生成、目标代码生成 ...语法分析器:单词流-&am
  • 编译执行和解释执行

    2019-07-09 17:15:44
    一、编译和解释 编译:将源代码一次性转换成目标代码的过程 类似英语中的全文翻译。 执行编译过程的程序叫做编译器。 解释:将源代码逐条转换成目标代码同时逐条运行的过程。 类似英语中的同声传译。 ...
  • 这段时间在学Android应用开发,在想既然是用Java开发的应该很好反编译从而得到源代码吧,google了一下,确实很简单,以下是我的实践过程。在此郑重声明,贴出来的目的不是为了去破解人家的软件,完全是一种学习的...
  • 你往往会去借鉴别人的应用是怎么开发的,那些漂亮的动画和精致的布局可能会让你爱不释手,作为一个开发者,你可能会很想知道这些效果界面是怎么去实现的,这时,你便可以对改应用的APK进行反编译查看。下面是我参考...
  • 我们都知道,Android程序打完包之后得到的是一个APK文件,这个文件是可以直接安装到任何Android手机上的,我们反编译其实也就是对这个APK文件进行反编译。Android的反编译主要又分为两个部分,一个是对代码的反编译...
  • luyten是一款操作简单、功能实用的java反编译工具,软件支持*.JAR、*.zip、*.class等类型文件的反编译操作,还原度非常高,支持更多功能设置,如显式导入、类型、合成组件等等,用户可根据不同的需求选择合适的显示...
  • Java反编译工具使用对比,最好用的Java反编译工具 --- JD-GUI 大家都知道,将源代码转换成二进制执行代码的过程叫“编译”,比如将C源代码编译成.exe可执行文件;那么把二进制执行代码转换成源代码的过程就叫“反...
  • 今天我们要来分享一些关于Java的反编译工具,反编译听起来是一个非常高上大的技术词汇,通俗的说,反编译是一个对目标可执行程序进行逆向分析,从而得到原始代码的过程。尤其是像.NET、Java这样的运行在虚拟机上的...
  • apktool功能:反编译出apk资源文件。 使用方式: 把apktool 解压到任意位置 执行 在dos 改目录下 执行 apktool d xxx.apk test ,便会把编译后的资源存入test文件夹下。
  • 2.dex2jar:该工具作用是将classes.dex文件,反编译出源码(如果apk未加固),反编译出文件,使用jd-gui工具进行查看; 3.Auto-Sign:自动签名工具,将重新打包的apk进行签名,如果不签名,无法安装使用...
  • APK反编译

    2018-05-17 14:55:52
    学习和开发Android应用有一段时间了,今天写一篇博客总结一下Android的apk文件反编译。我们知道,Android应用开发完成之后,我们最终都会将应用打包成一个apk文件,然后让用户通过手机或者平板电脑下载下来进行安装...
  • 我们平时在工作中经常会遇到一些已经被编译后的DLL,而且更加麻烦是没有源代码可以进行修改,只能针对这个DLL的文件进行修改才能得到我们想要的结果;本文将通过一个实例来演示如果完成一个简单的修改;我们将会用到...
1 2 3 4 5 ... 20
收藏数 3,133,006
精华内容 1,253,202
关键字:

编译