zoukankan      html  css  js  c++  java
  • 程序员的自我修养二编译和链接

    通常的开发环境都是流行的集成开发环境(IDE)。这样的IDE一般都是将编译和链接的过程一步完成,通常将这种编译和链接合并到一起的过程称为构建

    2.1被隐藏了的过程

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

    上面代码文件名为:hello.c,编译代码的过程可以分解为4个步骤:

    • 预处理
    • 编译
    • 汇编
    • 链接

    2.1.1 预编译

    源代码文件hello.c和相关头文件,如stdio.h等被预编译器cpp预编译成一个.i文件。
    对于C++程序来说,源代码文件的扩展名可能是.cpp或.cxx,头文件的扩展名可能是.hpp,而编译后的文件扩展名是.ii。

    如下图,我这里使用的是Ubuntu的系统:
    这里编译上面的程序

    这里可以看到生成了一个.i的文件,下面打开它:(只截取了文件开头和结尾)

    预编译过程主要处理规则如下:

    • 将所有的”#define”删除,并且展开所有宏定义。
    • 处理所有条件预编译指令,比如”#if”
    • 处理”#include”预编译指令,将包含的文件插入到该预编译指令的位置
    • 删除所有注释”//”和”/**/”
    • 添加行号和文件
    • 保留所有的#pragme编译器指令,因为编译器需要使用它们

    经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开。

    2.1.2 编译

    编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。现在版本的GCC把预编译和编译两个步骤合并成一个步骤,使用叫做ccl的程序来完成这两个步骤。

    下面我们把预处理完的文件进行编译:

    打开它看看

        .file    "hello.c"
        .section    .rodata
    .LC0:
        .string    "Hello World"
        .text
        .globl    main
        .type    main, @function
    main:
    .LFB0:
        .cfi_startproc
        pushq    %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $.LC0, %edi
        call    puts
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
    .LFE0:
        .size    main, .-main
        .ident    "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
        .section    .note.GNU-stack,"",@progbits
    View Code

    2.1.3 汇编

    汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令,只需要根据汇编指令和机器指令的对照表一一翻译就可以了

    2.1.4 链接

    在这一步,我们将把很多的文件链接起来得到最终的可执行文件。

    2.2 编译器做了什么

    编译器就是将高级语言翻译成机器语言的一个工具。
    使用机器语言或汇编语言编写的程序依赖于特定的机器,一个为某种CPU编写的程序在另外一种CPU下完全无法运行,要重新编写。
    20世纪六七十年代诞生了很多高级语言,如:FORTRAN,C语言等。
    高级语言使得程序员们能够更加关注程序逻辑本身,而尽量少考虑计算机本身的限制,如字长、内存大小、通信方式、存储方式等。
    编译过程一般可以分为6步:扫描、语法分析、语音分析、源代码优化、代码生成和目标代码优化。
    下图是从源代码到最终目标代码的过程:

    2.2.1词法分析

    array[index]=(index+4)*(2+6)

    首先源代码程序被输入到扫描器,扫描器只进行简单的词法分析,运用一种类似于有限状态机的算法可以将源代码的字符序列分割成一系列的记号
    将上面程序扫描后,产生16个记号:

    记号类型
    array 标识符
    [ 左方括号
    index 标识符
    ] 右方括号
    = 赋值
    ( 左圆括号
    index 标识符
    + 加号
    4 数字
    右圆括号
    * 乘号
    ( 左圆括号
    2 数字
    + 加号
    6 数字
    ) 右圆括号

    词法分析产生的记号一般可以分为如下几类:

    • 关键字
    • 标识符
    • 字面量(包含数字,字符串等)
    • 特殊符号(加号,等号)

    扫描除了识别记号,还完成其他工作,比如:将标识符存放到符号表,将数字、字符串常量放到文字表等。

    2.2.2 词法分析

    语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用上下文无关语法的分析手段。
    由语法分析器生产语法树就是以表达式为节点的树。
    上面的例子就是由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组成的复杂语句。它在经过语法分析器以后形成如下图语法树:

    如果出现表达式不合法,编译器就会报告语法分析阶段的错误。

    2.2.3 语义分析

    语义分析由语义分析器来完成。
    编译器所能分析的语义是静态语义:编译期可以确定的语义。通常包含声明、类型的匹配、类型的转换。
    对应的动态语义:只有在运行期才能确定的语义。
    经过语义分析阶段以后,整个语法树的表达式都被标识了类型。

    从上图可以看出,每个表达式都被标识了类型。语义分析器还对符号表里的符号类型也做了更新。

    2.2.4 中间语言生成

    源代码级优化器会在源代码级别进行优化,在上例中,(2+6)这个表达式可以被优化掉,因为它的值在编译期就可以被确定下来。

    直接在语法树上优化比较困难,所有源代码优化器往往将整个语法树转换成中间代码,它是语法树的顺序表示,它已经非常接近目标代码了。但它一般和机器运行时环境无关。中间代码由很多种类型,在不同编译器中有着不同的形式,常见的:三地址码和P-代码。
    基本三地址码:
    x=y op z
    将变量y和z进行op操作后赋值给x。op操作可以是算数运算,也可以是其他任何可以应用到y和z的操作。三地址码就是有3个变量地址。
    中间代码使得编译器可以被分为前端和后端。编译器前端负责产生与机器无关的中间代码,编译器后端将中间代码转换为目标机器代码。

    2.2.5 目标代码生成与优化

    源代码级优化器产生中间代码标志着下面的过程都属于编译器后端,编译器后端包含代码生成器、目标代码优化器。代码生成器将中间代码转换成目标机器代码,这个过程依赖于目标机器,不用机器有不同的字长、寄存器、整数数据类型和浮点数据类型。

    movl index,%ecx               ;value of index to ecx
    addl $4,%ecx                  ;ecx=ecx+4
    mull $8,%ecx                  ;ecx=ecx*8
    movl index,%eax               ;value of index to eax
    movl %ecx,array(,eax,4)       ;array[index]=ecx

    最后目标代码优化器对上面代码进行优化,选择合适的寻址方式、使用位移来代替乘法运算等。
    这里乘法运算有一个相对复杂的基址比例变址寻址的lea指令完成,随后由一条mov指令完成最后的赋值操作。

    movl index , %edx
    leal 32(,%edx,8), %eax
    movl %eax,array(,%edx,4)

    在上面index和array的地址还没有确定。在之后链接的时候才确定。
    实际上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

    2.3 链接器年龄比编译器长

    以前,人们是使用打孔的方式来编写程序,那么当程序修改的时候,需要重新计算目标地址的位置,这个过程叫做重定位

    之后的汇编语言采用了符号,它用来表示一个地址,这个地址可能是一段子程序的起始地址,也可以是一个变量的起始地址。
    随着软件的规模越来越大,代码开始分成了模块。模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的属于静态语言的C/C++模块之间通信有两种方式,一种是模块间的函数调用,另外一种是模块间的变量访问。函数访问须知道目标的函数地址,变量访问要知道变量的地址。所以两种方式可以归结为一种,那就是模块间符号引用。模块间依靠符号来通信类似拼图,定义符号的模块多i出一块区域,引用该符号的模块刚好少一个区域,两者一拼接刚好完美组合。这个模块的拼接过程就是:链接

    2.4 模块拼装——静态链接

    我们把每个源代码独立编译,然后按照须要将它们组装起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块相互引用的部分都处理好,使得各个模块之间能够正确的衔接。
    它的工作就是把一个指令对其他符号地址引用加以修正。链接过程主要包括地址和空间分配符号决议重定位等。
    最基本的静态链接过程如下图:

     每个模块的源代码文件(.c)经过编译器编译成目标文件(.o或,obj),目标文件和库一起链接形成最终可执行文件。而最常见的库就是运行时库,它时支持程序运行的基本函数的集合。库其实就是一组目标文件的包。

  • 相关阅读:
    HDU 4611 Balls Rearrangement 数学
    Educational Codeforces Round 11 D. Number of Parallelograms 暴力
    Knockout.Js官网学习(简介)
    Entity Framework 关系约束配置
    Entity Framework Fluent API
    Entity Framework DataAnnotations
    Entity Framework 系统约定配置
    Entity Framework 自动生成CodeFirst代码
    Entity Framework CodeFirst数据迁移
    Entity Framework CodeFirst尝试
  • 原文地址:https://www.cnblogs.com/Tan-sir/p/7280776.html
Copyright © 2011-2022 走看看