• 编译的整个过程:预编译、编译、汇编、链接


    编译分为四个步骤:

    每个步骤将文件编译成别的格式,如下:

    详解:

    1.预编译:

    预编译过程主要做4件事:
    ①展开头文件
    在写有#include <filename>或#include "filename"的文件中,将文件filename展开,通俗来说就是将fiename文件中的代码写入到当前文件中;
    ②宏替换
    ③去掉注释
    ④条件编译
    即对#ifndef #define #endif进行判断检查,也正是在这一步,#ifndef #define #endif的作用体现出来,即防止头文件被多次重复引用

    2.编译

    将代码转成汇编代码,并且在这个步骤中做了两件很重要的工作:
    ①编译器在每个文件中保存一个函数地址符表,该表中存储着当前文件内包含的各个函数的地址;
    ②因为这步要生成汇编代码,即一条一条的指令,而调用函数的代码会被编译成一条call指令,call指令后面跟的是jmp指令的汇编代码地址,而jmp指令后面跟的才是“被调用的函数编译成汇编代码后的第一条指令”的地址,但是给call指令后面补充上地址的工作是在链接的时候做的事情。

    3.汇编

    将汇编代码转成机器码

    4.链接

    编译器将生产的多个.o文件链接到一起生成一个可执行.exe文件;
    但是在这个过程中,编译器做的一个重要的事情是将每个文件中call指令后面的地址补充上;方式是从当前文件的函数地址符表中开始找,如果没有,继续向别的文件的函数地址符表中找,找到后填补在call指令后面,如果找不到,则链接失败。

    举例:

    说实话,很多人做了很久的C/C++,也用了很多IDE,但是对于可执行程序的底层生成一片茫然,这无疑是一种悲哀,可以想象到大公司面试正好被问到这样的问题,有多悲催不言而喻,这里正由于换工作的缘故,所以打算系统的把之前用到的C/C++补一补。这里权且当做抛砖引玉,大神飘过。

    【总述】

    从一个源文件(.c)到可执行程序到底经历了哪几步,我想大多数的人都知道,到时到底每一步都做了什么,我估计也没多少人能够说得清清楚楚,明明白白。

    其实总的流程是这样的。

    【第一步】编辑hello.c

    复制代码
    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 int main()
    4 {
    5         printf("hello world!
    ");
    6         return 0;
    7 }
    复制代码

    【第二步】预处理

    预处理过程实质上是处理“#”,将#include包含的头文件直接拷贝到hell.c当中;将#define定义的宏进行替换,同时将代码中没用的注释部分删除等

    具体做的事儿如下:

    (1)将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换

    (2)处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些

    (3)处理#include,将#include指向的文件插入到该行处

    (4)删除所有注释

    (5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行

    (6)保留#pragma编译器指令,因为编译器需要使用它们。

    gcc -E hello.c -o a.c可以生成预处理后的文件。通过查看文件内容和文件大小可以得知a.c讲stdio.h和stdlib.h包含了进来。

    【第三步】编译

    编译的过程实质上是把高级语言翻译成机器语言的过程,即对a.c做了这些事儿

    (1)词法分析,

    (2)语法分析

    (3)语义分析

    (4)优化后生成相应的汇编代码

    从 高级语言->汇编语言->机器语言(二进制)

    gcc -S hello.c -o a.s可以生成汇编代码

    汇编代码如下。

    复制代码
     1         .file   "hello.c"
     2         .section        .rodata
     3 .LC0:
     4         .string "hello world!"
     5         .text
     6         .globl  main
     7         .type   main, @function
     8 main:
     9 .LFB0:
    10         .cfi_startproc
    11         pushl   %ebp
    12         .cfi_def_cfa_offset 8
    13         .cfi_offset 5, -8
    14         movl    %esp, %ebp
    15         .cfi_def_cfa_register 5
    16         andl    $-16, %esp
    17         subl    $16, %esp
    18         movl    $.LC0, (%esp)
    19         call    puts
    20         movl    $0, %eax
    21         leave
    22         .cfi_restore 5
    23         .cfi_def_cfa 4, 4
    24         ret 
    25         .cfi_endproc
    26 .LFE0:
    27         .size   main, .-main
    28         .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    29         .section        .note.GNU-stack,"",@progbits
    复制代码

    gcc -c hello.c -o a.o将源文件翻译成二进制文件。类Uinx系统编译的结果生生成.o文件,Windows系统是生成.obj文件。

    编译的过程就是把hello.c翻译成二进制文件

    【第四步】链接

    就像刚才的hello.c它使用到了C标准库的东西“printf”,但是编译过程只是把源文件翻译成二进制而已,这个二进制还不能直接执行,这个时候就需要做一个动作,

    将翻译成的二进制与需要用到库绑定在一块。打个比方编译的过程就向你对你老婆说,我要吃雪糕。你只是给你老婆发出了你要吃雪糕的诉求而已,但是雪糕还没有到。

    绑定就是说你要吃的雪糕你的老婆已经给你买了,你可以happy。

    gcc hello.c -o a可以生成可执行程序。即gcc不带任何参数。ldd就可以看到你的可执行程序依赖的库。

    可以看到a.o的大小是1.1k,毕竟他只是把源文件翻译成二进制文件。a却有7k,应该是他多了很多“绳子”吧。在运行的时候这些“绳子”就将对应的库函数“牵过来”。很形象的比喻是不是?哈哈。libc.so.6 中就对咱们用的printf进行了定义。

    这就是编写的整个流程,(⊙o⊙)。谢谢各位看官。不足的地方请不吝赐教。

  • 相关阅读:
    学会用好 Visual Studio Code
    Alpha冲刺阶段博客汇总
    第二天敏捷冲刺
    第一天敏捷冲刺
    需求分析与设计
    软工网络15团队作业2——团队计划
    团队组队&灰化肥挥发会发黑
    Tomcat安装及部署
    正则表达式
    爬取腾讯疫情数据
  • 原文地址:https://www.cnblogs.com/mhq-martin/p/11898245.html
Copyright © 2020-2023  润新知