摘要:总结了编译器编译的整个过程以及链接器的职责,分别介绍了每一步具体做了哪些事,最后通过一个实例演示了这个过程。


一、整个编译过程描述

    这里有一幅图,可以描述整个编译过程:

编译器架构的王者llvm 编译器例程_编译过程

    预处理:gcc –E file.c –o hello.i

    1.处理所有的注释,以空格代替。
    2.将所有的#define删除,并且展开所有的宏定义。
    3.处理条件编译指令#if, #ifdef, #elif, #else, #endif。
    4.处理#include,展开被包含的文件。
    5.保留编译器需要使用的#pragma指令。


    编译:gcc –S file.c –o hello.s

    1.对预处理进行词法分析,语法分析和语义分析。

    词法分析:分析关键字,标示符,立即数等是否合法。

    语法分析:分析表达式是否遵循语法规则。

    语义分析:在语法分析的基础上进一步分析表达式是否合法。

    分析结束后进行代码优化生成相应的汇编代码文件。


    汇编:gcc –c file.c –o hello.o

    1.汇编器将汇编代码转变为机器可以执行的指令。

    2.每个汇编语句几乎都对应一条机器指令。


    链接器

    连接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接。


二、实例分析编译过程

    例程file.c如下:

<span style="font-size:18px;">//#include <stdio.h>
#include "file.h"

#define LOW  0
#define HIGH 255

int max(int a, int b)
{
    return MAX(a,b);
}

int main()
{
    
    int c = max(LOW, HIGH); // Call max to get the larger number

    return 0;
}</span>

    file.h如下:

<span style="font-size:18px;">/*
This is the header file for test.c
*/
 
#define MAX(a,b) (((a)>(b)) ? (a) : (b))
 
int g_global = 10;</span>



第一步,预处理,预处理做了什么呢?我们用预处理指令操作,看得到了什么。

    #gcc–E file.c –o file.i

    生成的file.i文件如下:

<span style="font-size:18px;"># 1 "file.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "file.c"
 
# 1 "file.h" 1
 
/*所有注释被以空格代替,这里第一行的#include <stdio.h>没有被展开是因为注释会被空格代替*/
 
 
 
 
int g_global = 10;//include中的内容被展开
# 3 "file.c" 2
 
 
 
 
int max(int a, int b)
{
   return (((a)>(b)) ? (a) : (b));//#define宏被删除并展开
}
 
int main()
{
 
   int c = max(0, 255);//#define的值这里直接替换到程序里,本身被删除
 
   return 0;
}</span>



下一步编译,使用命令:

    #gcc –S file.c –o file.s

    得到的内容如下:

<span style="font-size:18px;"> .file  "file.c"
.globl g_global
    .data
    .align4
    .type  g_global, @object
    .size  g_global, 4
g_global:
    .long  10
    .text
.globl max
    .type  max, @function
max:
    pushl  %ebp
    movl   %esp, %ebp
    movl   8(%ebp), %eax
    cmpl   %eax, 12(%ebp)
    cmovge 12(%ebp), %eax
    popl   %ebp
    ret
    .size  max, .-max
.globl main
    .type  main, @function
main:
    pushl  %ebp
    movl   %esp, %ebp
    subl   $24, %esp
    movl   $255, 4(%esp)
    movl   $0, (%esp)
    call   max
    movl   %eax, -4(%ebp)
    movl   $0, %eax
    leave
    ret
    .size  main, .-main
    .ident "GCC: (GNU) 4.5.1 20100924 (Red Hat4.5.1-4)"
    .section   .note.GNU-stack,"",@progbits</span>

    编译的步骤其实是进行一系列词法语法语义分析之后,将c语言翻译到汇编语言。

汇编步骤,使用下面的指令:

    #gcc file.c –o file.o

    这时候生成了一个file.o文件如下:

   

编译器架构的王者llvm 编译器例程_c语言_02

    这里指的注意的是,为什么file.o的颜色和其他之前两个生成的不一样,因为它具有可执行的权限,到了这一步,file.o里面就是机器可以执行的指令了,这时候用ue打开是这样的:

   

编译器架构的王者llvm 编译器例程_编译器_03

链接器的行为,因为一个代码不可能一个文件写完,我们会按照功能分开写,这时候链接器就有组织有规律的把这些模块组装起来,这里比如函数调用了libc库,链接器可以用动态方式和静态方式进行链接,关于什么是动态链接什么是静态链接,我的别的帖子详细介绍了,这里不多做赘述。无论是静态还是动态,链接器连接起来之后,我们就得到了最终的可执行的文件,file,或者默认的a.out。

    最后总结一下:

    编译器主要工作分为三步:第一,预处理;第二,编译;第三,汇编。

    链接器把各个独立的模块链接成可执行文件。

    这篇帖子就总结到这里吧,如有不正确的地方,还请指出,大家共同进步!