编译的整个过程:预编译、编译、汇编、链接
编译分为四个步骤:
每个步骤将⽂件编译成别的格式,如下:
详解:
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!\n");
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⊙)。谢谢各位看官。不⾜的地⽅请不吝赐教。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。