编译、链接学习笔记(一)简述编译链接过程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013230511/article/details/77171165

一直很希望清楚的了解C语言是如何从编写代码到编译、链接成可执行文件,最后执行代码的整个过程。今天开始学习《程序员的自我修养》,并从读书的过程中做一些总结与思考,也希望从中可以将晦涩难懂的概念以我自己的理解以简单的语言总结出来。
书中所用到的例子都是以pc为例子,我也试着从mac与ios的角度试着以类比探究他们三者的区别与相同之处。

源代码的编译过程

源代码从文本,经过编译器的处理最终生成可执行文件的过程中一共经历了四个步骤,分别是预处理(prepressing)编译(compliation)汇编(assembly),和链接(linking)
下图是四个步骤以及对应的生成产物。

这里写图片描述

步骤1 预编译

预编译是整个编译过程最开始的工作,它的工作是做些代码的替换工作,过程中主要处理文件中以#开始的预编译指令,例如引用其他文件#include 、#define宏替换、#pragma等。

gcc编译器中预编译命令

gcc中使用-E选项进行编译可以输出预编译后的结果,结果输出在后缀为i的文件中。

gcc -E hello.c -o hello.i

主要的规则有以下几个
1. #define ,删除#define并展开所有定义,并做宏文本替换
2.#include,将声明的文件插入到指令的位置,而且插入的过程是递归进行的,也就是说会将所有使用到的文件递归引用
3.处理#if、#ifdef 、#else 、#endif等条件预编译指令
4.保留#pragma编译器命令
5.添加行号和文件名标识,比如 #2 “hello.c” 2

下面举一个简单的例子来说明预编译的输出结果。

例子是一个非常简单的c的名为hello.c的源文件,例子中使用到了上面所说的几种预编译命令,#include、#define、#pargma。

如引用stdio.h头文件,声明宏TEST_MARCO_1,使用条件编译命令修改TEST_MARCO_2宏的值,使用#pragma message在编译时控制台输出log。

#include <stdio.h>

#define TEST_MARCO_1 1
#pragma message("消息文本")

#ifdef TEST_MARCO_1
    #define TEST_MARCO_2 2
#elif
    #define TEST_MARCO_2 3
#endif

int main()
{
    int a = TEST_MARCO_1;
    int b = TEST_MARCO_2;
    printf("helloworld\n");
    return 1;
}

编译后控制台会因为#pargma message的原因输出一段文本
这里写图片描述

先看编译结果,因编译后的代码非常的冗长,此处只显示一些输出文件一头一尾的部分。

# 1 "hello.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 330 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "hello.c" 2
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h" 1 3 4
# 64 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sys/cdefs.h" 1 3 4
# 587 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sys/cdefs.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sys/_symbol_aliasing.h" 1 3 4

....


# 2 "hello.c" 2
# 12 "hello.c"
int main()
{
 int a = 1;
 int b = 2;
 printf("helloworld\n");
 return 1;
}

可以看到经过预编译后,#include<stdio.h>将头文件的绝对值路径编译了进来,并且也将其引用的其他路径也递归的编译了进来(如sys/cdefs.h、sys/_symbol_aliasing.h等)
而且源文件的#if、#elseif等条件编译命令也被去除了,int a = TEST_MARCO_1也被最终解析成了int a = 1

步骤2 编译

编译的过程是把预处理完的文件进行一系列的词法分析、语法分析、语义分析、中间代码生成,目标代码生成以及优化,最终生成汇编代码文件。

gcc编译器中编译

gcc中使用-S选项进行编译,结果输出在后缀为s的文件中。

gcc -S hello.i -o hello.s

编译的过程比较多也比较复杂,从比较概况的总结这个过程,用比较通俗的话便是,从无语义至有语义,从与机器无关至于机器有关,将源码从简单的字符串最终编译成计算机语义的汇编代码。
而整个编译过程分成编译前端与后端,前端负责生产与机器无关的中间代码,后端负责生成与机器有关的目标代码。

下面是编译过程的具体流程:
这里写图片描述

词法分析

源代码是由一个一个的字符组成,编译的第一步是将其中的字符序列使用扫描器(scanner)分割成一系列单词或符号,在编译器中称为记号(token)。
词法分析产生的记号一般可以分成“关键字”,“标识符”,“字面量(数字,字符串等)”,“特殊符号(+,-,=)”。
在计算机语言中,我们说的语法的不同,在编译系统中最直接的便是词法分析的方法不同导致的。

语法分析

从词法分析过程中得到的token序列,仅仅是简单的单词序列,并不能表达意义。语法分析这一过程,通过语法分析器(grammar parser)采用上下文无关语法分析手段,产生语法树。这个树是以表达式为节点的树。

语义分析

从语法分析过程中得到的语法树,仅仅是完成了语法层面的分析,无法了解这个语句是否真正有意义,是否合法。语义分析过程便是对表达式中的变量与类型进行判断,分析其是否语义不匹配。

中间代码生成

将语义分析的步骤中得到的标识后的语法树(commended syntax tree)通过源码级优化器(source code optimizer)做优化,生成中间代码

目标代码生成与优化

从上步骤中拿到的中间代码是与机器无关的,通过此步骤中代码生成器(code generator)生成与机器相关的目标代码。

自此编译阶段的代码便生成了与机器相关的汇编代码。

步骤3 汇编

汇编的过程是将汇编代码转换成可以执行的机器码,每一条汇编语句几乎都对应一条机器指令。
gcc中使用-c选项汇编或使用汇编器as命令执行

as hello.s -o hello.o 
gcc -c hello.s -o hello.o

步骤4 链接

链接过程是将多个目标文件链接起来得到最终可执行文件的过程。

小TPS

  1. 若想知道系统库头文件的具体路径,可以使用预编译命令,因预编译后会展示完整的文件路径
  2. 使用#pargma message指令可以在编译信息输出窗口中输出相应的信息,使用这个命令可以输出一些编译时重要的过程。

没有更多推荐了,返回首页