编译器是一个神奇的东西,它能够将我们所编写的高级语言源代码翻译成机器可识别的语言(二进制代码),并让程序按照我们的意图按步执行。那么,从编写源文件代码到可执行文件,到底分为几步呢?这个过程可以总结为以下5步:

  1、编写源代码

  2、编译

  3、链接

  4、装载

  5、执行

  今天主要说明的过程是编译和链接是怎么进行的。

  首先是编译,那么什么是编译?从广义上讲,编译就是将某种编程语言编写的代码转换成另一种编程语言描述的代码,严格一点儿来说,编译其实就是将高级语言编写的源代码翻译成低级语言(通常是汇编语言,甚至是机器代码)描述的代码的过程。这个过程由编译器完成,因此,我们可以把编译器看成这样的一种机器,它的输入是多个编译单元(编译代码是一个源代码文本文件),输出的是和多个编译单元一一对应的目标文件。

  为了简化说明,我们使用如下代码来演示这个过程。

  function.h

 //function.h
#ifndef FIRST_OPTION
#define FIRST_OPTION
#define MULTIPLIER (3.0)
#endif float add_and_multiply(float x,float y);

  function.c

 #include "function.h"
int ncompletionstatus=;
float add(float x,float y){
float z=x+y;
return z;
}
float add_and_multiply(float x,float y){
float z=add(x,y);
z*=MULTIPLIER;
return z;
}

  main.c

 #include "function.h"
extern int ncompletionstatus;
int main(){
float x=1.0;
float y=5.0;
float z;
z=add_and_multiply(x,y);
ncompletionstatus=;
return ;
}

  编译器要完成编译的功能,需要一系列的步骤。粗略的讲,编译的过程可分为预处理阶段、语言分析阶段、汇编阶段、优化阶段和代码生成阶段。

  预处理阶段:

    (1)、将#include关键字表示的含有定义的文件包含到源代码文件中

    (2)、处理#define,在代码中调用宏的位置将宏转化为代码

    (3)、根据#ifndef ,#ifdef,#elif和#endif指定的位置包含或者排除特定部分的代码

  对于上面的function.c文件,我们可以使用gcc命令--gcc -E function.c -o function.i对它只进行预处理而不进行相应的后续处理。生成的i文件如下所示。

#  "function.c"
# "<built-in>"
# "<command-line>"
# "function.c"
# "function.h" float add_and_multiply(float x,float y);
# "function.c"
int ncompletionstatus=;
float add(float x,float y){
float z=x+y;
return z;
}
float add_and_multiply(float x,float y){
float z=add(x,y);
z*=(3.0);
return z;
}

  可以看到,宏定义被替换成了(3.0)。

  语言分析阶段:

    (1)、词法分析阶段:将源代码分割成不可分割的单词

    (2)、语法分析阶段:将提取出来的单词连接成单词序列,并根据编程语言规则验证其顺序是否合理

    (3)、语义分析阶段:目的是发现符合语法规则的语句是否具有实际意义,比如讲两个整数相加并将结果赋值给一个对象的语句,虽然能通过语法规则的检查,但是可能无法通过语义的检查,例如这个对象的类没有重载赋值操作符

  汇编阶段:当源代码经过校验,其中不包含任何的语法错误时,编译器才会执行汇编阶段。在这个阶段中,编译器会将标准的语言集合转换成特定的CPU指令集的语言集合,不同的CPU会包含不同的指令集、寄存器和中断,所以不同的处理器要有不同的编译器对其支持。gcc编译器支持将输入的文件源代码转换成对应的ASCII编码的文本文件,其中包含了对应的汇编指令的代码行,汇编指令的格式包括AT&T和Intel两种,在Centos6.4上也是。

  我们对function.c文件运行gcc -S -masm=att function.c -o function.s命令,可以得到function.c文件的汇编文件,如下所示。

         .file   "function.c"
.globl ncompletionstatus
.bss
.align
.type ncompletionstatus, @object
.size ncompletionstatus,
ncompletionstatus:
.zero
.text
.globl add
.type add, @function
add:
pushl %ebp
movl %esp, %ebp
subl $, %esp
flds (%ebp)
fadds (%ebp)
fstps -(%ebp)
movl -(%ebp), %eax
movl %eax, -(%ebp)
flds -(%ebp)
leave
ret
.size add, .-add
.globl add_and_multiply
.type add_and_multiply, @function
add_and_multiply:
pushl %ebp
movl %esp, %ebp
subl $, %esp
movl (%ebp), %eax
movl %eax, (%esp)
movl (%ebp), %eax
movl %eax, (%esp)
call add
fstps -(%ebp)
flds -(%ebp)
flds .LC1
fmulp %st, %st()
fstps -(%ebp)
movl -(%ebp), %eax
movl %eax, -(%ebp)
flds -(%ebp)
leave
ret
.size add_and_multiply, .-add_and_multiply
.section .rodata
.align
.LC1:
.long
.ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
.section .note.GNU-stack,"",@progbits

  代码优化阶段:由源代码文件生成的最初版本的汇编代码之后,优化就开始了,优化的只要功能是将程序的寄存器使用率最小化,此外,通过分析能够预测出来实际上不需要执行的部分代码并删除

  代码生成阶段:优化完成的汇编代码会在这个阶段转换成对应的机器指令的二进制值,并写入目标文件的特定位置,每一个源文件都对应一个目标文件,每一个目标文件都将包含所有相关的节信息(也就是.text/.code/.bss),同时也会包含部分的描述信息,我们可以使用gcc -c function.c -o function.o对function.c文件只进行编译处理,生成的文件是function.o文件。

  对于.o文件,不能用vi直接打开,打开也是一对乱码。我们可以使用objdump的工具来查看.o文件的反汇编代码(我的Centos6.4上有这个软件,所以你的电脑上如果没有,可以装一个),使用objdump -D function.o即可在终端上打印出.o文件的反汇编代码了,代码如下所示。

  

 [song@localhost Desktop]$ objdump -D function.o

 function.o:     file format elf32-i386

 Disassembly of section .text:

  <add>:
: push %ebp
: e5 mov %esp,%ebp
: ec sub $0x14,%esp
: d9 flds 0x8(%ebp)
: d8 0c fadds 0xc(%ebp)
c: d9 5d fc fstps -0x4(%ebp)
f: 8b fc mov -0x4(%ebp),%eax
: ec mov %eax,-0x14(%ebp)
: d9 ec flds -0x14(%ebp)
: c9 leave
: c3 ret 0000001a <add_and_multiply>:
1a: push %ebp
1b: e5 mov %esp,%ebp
1d: ec 1c sub $0x1c,%esp
: 8b 0c mov 0xc(%ebp),%eax
: mov %eax,0x4(%esp)
: 8b mov 0x8(%ebp),%eax
2a: mov %eax,(%esp)
2d: e8 fc ff ff ff call 2e <add_and_multiply+0x14>
: d9 5d fc fstps -0x4(%ebp)
: d9 fc flds -0x4(%ebp)
: d9 flds 0x0
3e: de c9 fmulp %st,%st()
: d9 5d fc fstps -0x4(%ebp)
: 8b fc mov -0x4(%ebp),%eax
: ec mov %eax,-0x14(%ebp)
: d9 ec flds -0x14(%ebp)
4c: c9 leave
4d: c3 ret Disassembly of section .bss: <ncompletionstatus>:
: add %al,(%eax)
... Disassembly of section .rodata: <.rodata>:
: add %al,(%eax)
: inc %eax
: inc %eax Disassembly of section .comment: <.comment>:
: add %al,0x43(%edi)
: inc %ebx
: 3a cmp (%eax),%ah
: 4e sub %al,0x4e(%edi)
: push %ebp
a: sub %esp,(%eax)
c: 2e xor $0x2e,%al
e: 2e xor $0x2e,%al
: aaa
: and %dh,(%edx)
: xor %dh,(%ecx)
: xor (%eax),%dh
: xor (%ecx),%esi
: xor (%eax),%esp
1b: sub %dl,0x65(%edx)
1e: and %cl,%fs:0x61(%eax)
: je <add_and_multiply+0x2a>
: 2e xor $0x2e,%al
: 2e xor $0x2e,%al
: aaa
: 2d .byte 0x2d
2a: xor (%ecx),%ebp
...

  可以看到,里面包含了.tex/.bss/.data节的内容。以上就是所有编译阶段所完成的任务,我们现在得到的是一个个的目标文件。

  当编译完成后,下一步就是将编译出来的各个目标文件链接成一个可执行的文件,这个过程就是链接。

  最终生成的二进制文件中包含了多个相同类型的节(.text/.data/.bss),而这些节是从每一个独立的目标文件中摘取下来的,也就是说,如果我们把一个个的目标文件看成一块简单的拼贴,进程的内存映射看做是一副巨幅镶嵌的画,链接的过程就是将拼贴组合在一起,放置在镶嵌画的恰当的位置。链接的过程由链接器执行,它的最终任务是将独立的节组合成最终的程序内存映射节,与此同时解析所有的引用。

  链接阶段主要包括重定位和解析引用两个阶段。

    重定位:链接过程的第一个阶段仅仅进行拼接,其过程是将分散在单独目标文件中不同类型的节拼接到程序的内存映射节中,在每一个目标文件中,代码的地址范围都是从0开始的,但是在程序的内存映射中,地址范围并不都是从0开始的,所以我们要将目标文件中的地址范围转换成最终程序内存映射中更具体的地址范围。

    解析引用:在重定位结束后,就开始了解析引用。所谓解析引用,就是在位于不同部分的代码之间建立关联,使得程序变成一个紧凑的整体。引发链接问题的根本原因是--代码片段在不同的编译单元内,它们之间尝试相互引用,但是将目标文件拼接成程序内存映射之前,又不知道要引用对象的地址。,比如我们引用了其他源文件中的函数,怎么知道该函数的入口点呢,这就是链接阶段解析引用所解决的问题。我们使用在本文开头所使用的示例代码来说明这个问题。

    1、在function.c文件中,add_and_multiply函数调用了函数add,这两个函数在同一个源文件中,在这种情况下,函数add的内存映射地址值是一个已知量,因此这个调用是没有问题的;

    2、在main函数中,调用了add_and_multiply函数,并且引用了外部变量ncompletestatus,这时就会出现问题,我们不知道该函数和该外部变量的内存映射地址,实际上,编译器会假设这些符号未来会在进程的内存映射中存在,但是,直到生成完整的内存映射之前,这两项引用会一直被当成为解析引用。

    为了完成解析引用的任务,链接器需要完成:  

      (1)、检查拼接到内存映射的节

      (2)、找出哪些部分代码产生了外部调用

      (3)、计算该引用在程序内存映射中的具体位置

      (4)、最后,将机器指令中的伪地址替换成程序内存映射的实际地址,从而完成解析引用。

    为了展示示例程序的链接过程,我们需要先编译main.c和function.c

    运行命令gcc -c function.c main.c和gcc function.o main.o -o demoapp生成可执行的文件demoapp

    利用objdump查看main.o中的反汇编代码

 Disassembly of section .text:

  <main>:
: push %ebp
: e5 mov %esp,%ebp
: e4 f0 and $0xfffffff0,%esp
: ec sub $0x20,%esp
: b8 3f mov $0x3f800000,%eax
e: mov %eax,0x14(%esp)
: b8 a0 mov $0x40a00000,%eax
: mov %eax,0x18(%esp)
1b: 8b mov 0x18(%esp),%eax
1f: mov %eax,0x4(%esp)
: 8b mov 0x14(%esp),%eax
: mov %eax,(%esp)
2a: e8 fc ff ff ff call 2b <main+0x2b> //注意这里
2f: d9 5c 1c fstps 0x1c(%esp)
: c7 movl $0x1,0x0 //注意这里
3a:
3d: b8 mov $0x0,%eax
: c9 leave
: c3 ret

  上述代码中,在第16行和18中,main函数分别调用了自己和访问了地址0的值,这都是不应该出现的情况(其实我不懂汇编......囧),然后我们再来查看demoapp的反汇编代码,看一下和main函数的节

 080483e4 <main>:
80483e4: push %ebp
80483e5: e5 mov %esp,%ebp
80483e7: e4 f0 and $0xfffffff0,%esp
80483ea: ec sub $0x20,%esp
80483ed: b8 3f mov $0x3f800000,%eax
80483f2: mov %eax,0x14(%esp)
80483f6: b8 a0 mov $0x40a00000,%eax
80483fb: mov %eax,0x18(%esp)
80483ff: 8b mov 0x18(%esp),%eax
: mov %eax,0x4(%esp)
: 8b mov 0x14(%esp),%eax
804840b: mov %eax,(%esp)
804840e: e8 9b ff ff ff call 80483ae <add_and_multiply> //注意这里
: d9 5c 1c fstps 0x1c(%esp)
: c7 movl $0x1,0x8049698 //注意这里
804841e:
: b8 mov $0x0,%eax
: c9 leave
: c3 ret
: nop
: nop
804842a: nop
804842b: nop
804842c: nop
804842d: nop
804842e: nop
804842f: nop

  在main.o中,main起始的位置是0,而在demoapp中main起始地址变为0x080483e4,这就是重定位现象,另外,与上述main.o对应,第14行调用了函数add_and_multiply而不是调用了main自己,所以链接器完成了函数引用解析的功能,同时,在main.o中的第18行的0x0被修改为0x8049698,我们可以通过objdump来查看0x8049698地址中到底放了什么数据。

  执行objdump -x -j .bss demoapp,可以看到

 SYMBOL TABLE:
l d .bss .bss
l O .bss completed.
l O .bss dtor_idx.
g O .bss ncompletionstatus //注意这里

  在.bss段,地址0x08049698中放置着外部变量ncompletionstatus,于是,我们可以看到,链接器成功的完成了重定位和解析引用的功能。(但是我有一个疑问,ncompletionstatus在function.c中已经被初始化为0,为什么不是在.data段存放,而是在.bss中存放?请路过的大神解释一下)

  以上,就是程序编译和链接的全部过程,经过链接后的文件是一个可被执行的文件,可执行的文件总是会包含.data, .bss, .text节和其他的一些特殊的节,这些节通过拼接单独的目标文件中的节得到。

  需要注意的一点是,main不是程序执行时首先执行的代码,启动程序是整个程序首先执行的代码,而且启动程序时在链接之后才添加到程序的内存映射当中的,也就是说,可执行的文件并不完全是通过编译项目源代码文件生成的。启动代码有两种不同的形式:

  crt0:它是纯粹的入口点,这是程序代码的第一部分,在内核的控制下执行;

  crt1:它是更现代化的启动例程,可以在main函数执行前和程序终止后完成一些任务。

  这部分启动代码是OS自动添加给应用程序的,这也是可执行文件和动态库的唯一区别,动态库没有启动程序代码。

参考书籍:《高级C/C++编译技术》

  

    

  

         

C/C++程序从编译到链接的过程的更多相关文章

  1. 窥探C语言程序的编译、链接与.h文件

    概述 C语言程序从源文件经过编译.链接生成可执行文件.那么编译与链接分别做了什么? 开发中为什么使用.h编写函数的声明?接下来使用案例说清楚为什么这样编写代码. C语言程序的编译和链接 C语言程序从源 ...

  2. C程序的编译与链接

    编译器驱动程序 编译器驱动程序可以在用户需要时调用语言预处理器.编译器.汇编器和链接器. 例如使用GNU编译系统,我们需要使用如下命令来调用GCC驱动程序: gcc -o main main.c 编译 ...

  3. Linux中程序的编译和链接过程

    1.从源码到可执行程序的步骤:预编译.编译.链接.strip 预编译:预编译器执行.譬如C中的宏定义就是由预编译器处理,注释等也是由预编译器处理的. 编译: 编译器来执行.把源码.c .S编程机器码. ...

  4. 原创 C++应用程序在Windows下的编译、链接:第一部分 概述

    本文是对C++应用程序在Windows下的编译.链接的深入理解和分析,文章的目录如下: 我们先看第一章概述部分. 1概述 1.1编译工具简介 cl.exe是windows平台下的编译器,link.ex ...

  5. C++应用程序在Windows下的编译、链接(一)概述

    C++应用程序在Windows下的编译.链接(一)概述 本文是对C++应用程序在Windows下的编译.链接的深入理解和分析,文章的目录如下: 我们先看第一章概述部分. 1概述 1.1编译工具简介 c ...

  6. 详解C程序编译、链接与存储空间布局

    被隐藏了的过程 现如今在流行的集成开发环境下我们很少需要关注编译和链接的过程,而隐藏在程序运行期间的细节过程可不简单,即使使用命令行来编译一个源代码文件,简单的一句"gcc hello.c& ...

  7. 13_传智播客iOS视频教程_OC程序的编译链接

    C程序的编译.链接.执行怎么来的?在.C文件里面写上符合C语言部分的源代码.OC也是一样的.记住:OC程序的后缀名是.m. 为什么要链接?第一个.o的目标文件里面它启动不了.因为它没有启动代码我们要加 ...

  8. 关于一个程序的编译过程 zkjg面试

    http://blog.csdn.net/gengyichao/article/details/6544266 一 以下是C程序一般的编译过程: 从图中看到: 将编写的一个c程序(源代码 )转换成可以 ...

  9. C进阶—详解编译、链接

    被隐藏了的过程    现如今在流行的集成开发环境下我们很少需要关注编译和链接的过程,而隐藏在程序运行期间的过程可不简单,即使使用命令行来编译一个源代码文件,简单的一句"gcc hello.c ...

随机推荐

  1. Eclipse EE 发布项目导致 Tomcate 的配置文件 server.xml 还原

    在server.xml中配置SSL时,发现了每次发布项目都导致server.xml被还原了: <Connector port="8443" protocol="or ...

  2. 使用MongoDB C#官方驱动操作MongoDB

    想要在C#中使用MongoDB,首先得要有个MongoDB支持的C#版的驱动.C#版的驱动有很多种,如官方提供的,samus. 实现思路大都类似.这里我们先用官方提供的mongo-csharp-dri ...

  3. Java学习总结:飘逸的字符串

    Java学习:飘逸的字符串 前言 相信不管我们运用Java语言来开发项目还是进行数据分析处理,都要运用到和字符串相关的处理方法.这个社会处处有着和字符串相关的影子:日志.文档.书籍等.既然我们离不开字 ...

  4. Android开发之 Android应用程序详细解析

    我们继续的沿用上一篇所建立的应用. Android应用程序可以分为:应用程序源代码(.java),应用程序描述文件(.xml),各种资源. 可以这么理解: 安卓应用程序,通过java代码来实现其业务逻 ...

  5. 面向对象and类

    类和对象: 1.什么叫类:类是一种数据结构,就好比一个模型,该模型用来表述一类事物(事物即数据和动作的结合体),用它来生产真实的物体(实例). 2.什么叫对象:睁开眼,你看到的一切的事物都是一个个的对 ...

  6. MAC、IDFA、IMEI正则表达式

    一.安卓: MAC:接入网络的设备的序号,唯一值.用 16 进制数表示,由 0-9,A-F 组成,如:44:2A:60:71:CC:82 Uuid 正则表达式: ^([0-9a-fA-F]{2})(( ...

  7. ls文件与目录检视,文件内容查阅

    -a 列出所有的(含隐藏的)文件,包括.和.. -A 列出所有的(含隐藏的)文件,不包括.和.. -d 仅列出目录本身,而不是列出目录内的文件数据(常用) -f 不进行排序,直接列出结果,ls默认会以 ...

  8. uva 11054 wine trading in gergovia (归纳【好吧这是我自己起的名字】)——yhx

    As you may know from the comic \Asterix and the Chieftain's Shield", Gergovia consists of one s ...

  9. UVA 11983 Weird Advertisement --线段树求矩形问题

    题意:给出n个矩形,求矩形中被覆盖K次以上的面积的和. 解法:整体与求矩形面积并差不多,不过在更新pushup改变len的时候,要有一层循环,来更新tree[rt].len[i],其中tree[rt] ...

  10. 2014 Super Training #1 C Ice-sugar Gourd 模拟,扫描线

    原题 HDU 3363 http://acm.hdu.edu.cn/showproblem.php?pid=3363 给你一个串,串中有H跟T两种字符,然后切任意刀,使得能把H跟T各自分为原来的一半. ...