C/C++程序从编译到链接的过程

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

  1、编写源代码

  2、编译

  3、链接

  4、装载

  5、执行

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

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

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

  function.h

1 //function.h
2 #ifndef FIRST_OPTION
3 #define FIRST_OPTION
4 #define MULTIPLIER (3.0)
5 #endif
6
7 float add_and_multiply(float x,float y);

  function.c

 1 #include "function.h"
 2 int ncompletionstatus=0;
 3 float add(float x,float y){
 4         float z=x+y;
 5         return z;
 6 }
 7 float add_and_multiply(float x,float y){
 8         float z=add(x,y);
 9         z*=MULTIPLIER;
10         return z;
11 }

  main.c

 1 #include "function.h"
 2 extern int ncompletionstatus;
 3 int main(){
 4         float x=1.0;
 5         float y=5.0;
 6         float z;
 7         z=add_and_multiply(x,y);
 8         ncompletionstatus=1;
 9         return 0;
10 }

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

  预处理阶段:

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

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

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

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

# 1 "function.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "function.c"
# 1 "function.h" 1

float add_and_multiply(float x,float y);
# 2 "function.c" 2
int ncompletionstatus=0;
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文件的汇编文件,如下所示。

 1         .file   "function.c"
 2 .globl ncompletionstatus
 3         .bss
 4         .align 4
 5         .type   ncompletionstatus, @object
 6         .size   ncompletionstatus, 4
 7 ncompletionstatus:
 8         .zero   4
 9         .text
10 .globl add
11         .type   add, @function
12 add:
13         pushl   %ebp
14         movl    %esp, %ebp
15         subl    $20, %esp
16         flds    8(%ebp)
17         fadds   12(%ebp)
18         fstps   -4(%ebp)
19         movl    -4(%ebp), %eax
20         movl    %eax, -20(%ebp)
21         flds    -20(%ebp)
22         leave
23         ret
24          .size   add, .-add
25 .globl add_and_multiply
26         .type   add_and_multiply, @function
27 add_and_multiply:
28         pushl   %ebp
29         movl    %esp, %ebp
30         subl    $28, %esp
31         movl    12(%ebp), %eax
32         movl    %eax, 4(%esp)
33         movl    8(%ebp), %eax
34         movl    %eax, (%esp)
35         call    add
36         fstps   -4(%ebp)
37         flds    -4(%ebp)
38         flds    .LC1
39         fmulp   %st, %st(1)
40         fstps   -4(%ebp)
41         movl    -4(%ebp), %eax
42         movl    %eax, -20(%ebp)
43         flds    -20(%ebp)
44         leave
45         ret
46         .size   add_and_multiply, .-add_and_multiply
47         .section        .rodata
48         .align 4
49 .LC1:
50         .long   1077936128
51         .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
52         .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文件的反汇编代码了,代码如下所示。

  

 1 [[email protected] Desktop]$ objdump -D function.o
 2
 3 function.o:     file format elf32-i386
 4
 5
 6 Disassembly of section .text:
 7
 8 00000000 <add>:
 9    0:    55                       push   %ebp
10    1:    89 e5                    mov    %esp,%ebp
11    3:    83 ec 14                 sub    $0x14,%esp
12    6:    d9 45 08                 flds   0x8(%ebp)
13    9:    d8 45 0c                 fadds  0xc(%ebp)
14    c:    d9 5d fc                 fstps  -0x4(%ebp)
15    f:    8b 45 fc                 mov    -0x4(%ebp),%eax
16   12:    89 45 ec                 mov    %eax,-0x14(%ebp)
17   15:    d9 45 ec                 flds   -0x14(%ebp)
18   18:    c9                       leave
19   19:    c3                       ret
20
21 0000001a <add_and_multiply>:
22   1a:    55                       push   %ebp
23   1b:    89 e5                    mov    %esp,%ebp
24   1d:    83 ec 1c                 sub    $0x1c,%esp
25   20:    8b 45 0c                 mov    0xc(%ebp),%eax
26   23:    89 44 24 04              mov    %eax,0x4(%esp)
27   27:    8b 45 08                 mov    0x8(%ebp),%eax
28   2a:    89 04 24                 mov    %eax,(%esp)
29   2d:    e8 fc ff ff ff           call   2e <add_and_multiply+0x14>
30   32:    d9 5d fc                 fstps  -0x4(%ebp)
31   35:    d9 45 fc                 flds   -0x4(%ebp)
32   38:    d9 05 00 00 00 00        flds   0x0
33   3e:    de c9                    fmulp  %st,%st(1)
34   40:    d9 5d fc                 fstps  -0x4(%ebp)
35   43:    8b 45 fc                 mov    -0x4(%ebp),%eax
36   46:    89 45 ec                 mov    %eax,-0x14(%ebp)
37   49:    d9 45 ec                 flds   -0x14(%ebp)
38   4c:    c9                       leave
39   4d:    c3                       ret
40
41 Disassembly of section .bss:
42
43 00000000 <ncompletionstatus>:
44    0:    00 00                    add    %al,(%eax)
45     ...
46
47 Disassembly of section .rodata:
48
49 00000000 <.rodata>:
50    0:    00 00                    add    %al,(%eax)
51    2:    40                       inc    %eax
52    3:    40                       inc    %eax
53
54 Disassembly of section .comment:
55
56 00000000 <.comment>:
57    0:    00 47 43                 add    %al,0x43(%edi)
58    3:    43                       inc    %ebx
59    4:    3a 20                    cmp    (%eax),%ah
60    6:    28 47 4e                 sub    %al,0x4e(%edi)
61    9:    55                       push   %ebp
62    a:    29 20                    sub    %esp,(%eax)
63    c:    34 2e                    xor    $0x2e,%al
64    e:    34 2e                    xor    $0x2e,%al
65   10:    37                       aaa
66   11:    20 32                    and    %dh,(%edx)
67   13:    30 31                    xor    %dh,(%ecx)
68   15:    32 30                    xor    (%eax),%dh
69   17:    33 31                    xor    (%ecx),%esi
70   19:    33 20                    xor    (%eax),%esp
71   1b:    28 52 65                 sub    %dl,0x65(%edx)
72   1e:    64 20 48 61              and    %cl,%fs:0x61(%eax)
73   22:    74 20                    je     44 <add_and_multiply+0x2a>
74   24:    34 2e                    xor    $0x2e,%al
75   26:    34 2e                    xor    $0x2e,%al
76   28:    37                       aaa
77   29:    2d                       .byte 0x2d
78   2a:    33 29                    xor    (%ecx),%ebp
79     ...

  可以看到,里面包含了.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中的反汇编代码

 1 Disassembly of section .text:
 2
 3 00000000 <main>:
 4    0:    55                       push   %ebp
 5    1:    89 e5                    mov    %esp,%ebp
 6    3:    83 e4 f0                 and    $0xfffffff0,%esp
 7    6:    83 ec 20                 sub    $0x20,%esp
 8    9:    b8 00 00 80 3f           mov    $0x3f800000,%eax
 9    e:    89 44 24 14              mov    %eax,0x14(%esp)
10   12:    b8 00 00 a0 40           mov    $0x40a00000,%eax
11   17:    89 44 24 18              mov    %eax,0x18(%esp)
12   1b:    8b 44 24 18              mov    0x18(%esp),%eax
13   1f:    89 44 24 04              mov    %eax,0x4(%esp)
14   23:    8b 44 24 14              mov    0x14(%esp),%eax
15   27:    89 04 24                 mov    %eax,(%esp)
16   2a:    e8 fc ff ff ff           call   2b <main+0x2b>   //注意这里
17   2f:    d9 5c 24 1c              fstps  0x1c(%esp)
18   33:    c7 05 00 00 00 00 01     movl   $0x1,0x0     //注意这里
19   3a:    00 00 00
20   3d:    b8 00 00 00 00           mov    $0x0,%eax
21   42:    c9                       leave
22   43:    c3                       ret    

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

 1 080483e4 <main>:
 2  80483e4:    55                       push   %ebp
 3  80483e5:    89 e5                    mov    %esp,%ebp
 4  80483e7:    83 e4 f0                 and    $0xfffffff0,%esp
 5  80483ea:    83 ec 20                 sub    $0x20,%esp
 6  80483ed:    b8 00 00 80 3f           mov    $0x3f800000,%eax
 7  80483f2:    89 44 24 14              mov    %eax,0x14(%esp)
 8  80483f6:    b8 00 00 a0 40           mov    $0x40a00000,%eax
 9  80483fb:    89 44 24 18              mov    %eax,0x18(%esp)
10  80483ff:    8b 44 24 18              mov    0x18(%esp),%eax
11  8048403:    89 44 24 04              mov    %eax,0x4(%esp)
12  8048407:    8b 44 24 14              mov    0x14(%esp),%eax
13  804840b:    89 04 24                 mov    %eax,(%esp)
14  804840e:    e8 9b ff ff ff           call   80483ae <add_and_multiply>  //注意这里
15  8048413:    d9 5c 24 1c              fstps  0x1c(%esp)
16  8048417:    c7 05 98 96 04 08 01     movl   $0x1,0x8049698   //注意这里
17  804841e:    00 00 00
18  8048421:    b8 00 00 00 00           mov    $0x0,%eax
19  8048426:    c9                       leave
20  8048427:    c3                       ret
21  8048428:    90                       nop
22  8048429:    90                       nop
23  804842a:    90                       nop
24  804842b:    90                       nop
25  804842c:    90                       nop
26  804842d:    90                       nop
27  804842e:    90                       nop
28  804842f:    90                       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,可以看到

1 SYMBOL TABLE:
2 08049690 l    d  .bss    00000000              .bss
3 08049690 l     O .bss    00000001              completed.5974
4 08049694 l     O .bss    00000004              dtor_idx.5976
5 08049698 g     O .bss    00000004              ncompletionstatus   //注意这里

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

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

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

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

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

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

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

  

    

  

         

时间: 2024-10-11 21:41:14

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

C程序的编译和链接(一)

本文主要讲述了一个C程序从源代码到目标文件所经过的步骤,介绍了编译系统,预处理.编译.汇编和链接的相关知识. 一.编译系统 一个C程序的生命周期从高级C语言程序开始.想要在系统上执行.c程序,每条C语句都必须翻译为低级的机器语言指令,将这些指令按照可执行目标程序的格式打包,以二进制磁盘文件的形式存放,这就是可以由系统执行的可执行目标文件.这些工作包含如下图所示的四个过程,由编译系统完成. 一般而言,编译系统包括预处理器.编译器.汇编器和链接器. 注意一下上图中各个阶段输出的文件格式是文本文件还是

程序的编译与链接过程

今天在看gnu tools时发现了这方面的2个资源. 一本书是<程序员的自我修养:链接.装载与库> 云风的blog有提到这本书,让我意外的是这本书居然是国人所著.有空花精力淘回来拜读. 进入云风的blog有提到另一本国外经典<loaders and linkers>,我打算拜读一下. 其实最好的资料当属gcc和gnu-ld的manual,先读读这2本书再看manual也无不可.

详解编译、链接

被隐藏了的过程    现如今在流行的集成开发环境下我们很少需要关注编译和链接的过程,而隐藏在程序运行期间的过程可不简单,即使使用命令行来编译一个源代码文件,简单的一句"gcc hello.c"命令就包含了非常复杂的过程. 1 #include<stdio.h> 2 3 int main() 4 { 5 printf("Hello word\n"); 6 return 0; 7 } 在Linux系统下使用gcc编译程序时只须简单的命令: $gcc hell

学习记录:gcc/g++ 编译与链接

gcc/g++ 编译与链接 编译与链接的过程可以分解为四个步骤:预处理.编译.汇编.链接 预处理:源代码文件和相关的头文件,被预处理器cpp预处理成一个后缀为 .i 的文件(选项:-E) 编译:把预处理完的文件进行一系列的词法分析.语法分析.语义分析以及优化后,产生相应的汇编代码文件,后缀为 .s,(选项 :-S ) 汇编:把编译阶段生成的 .s 文件转成二进制目标代码,后缀为.o,(选项:-c) 链接:把每个源代码模块独立地编译,然后按照要将它们"组装"起来.链接的主要内容就是把各个

《程序员的自我修养》 第二章——编译和链接

摘自http://blog.chinaunix.net/uid-26548237-id-3839979.html <程序员的自我修养>第二章——编译和链接 2.1 被隐藏了的过程    C语句的经典,“Hello World”程序几乎是每个程序员闭着眼睛都能写出的,编译运行一气呵成,基本成了程序入门和开发环境测试的默认标准. #include <stdio.h> int main() { printf("Hello World\n"); return 0; 在L

程序的编译链接过程

还是从HelloWorld开始说吧... #include <stdio.h> int main(int argc, char* argv[]) { printf("Hello World!\n"); return 0; } 从源文件Hello.cpp编译链接成Hello.exe,需要经历如下步骤: 可使用以下命令,直接从源文件生成可执行文件 linux: gcc -lstdc++ Hello.cpp -o Hello.out // 要带上lstdc参数,否则会报undef

C++应用程序在Windows下的编译、链接(二)COFF/PE文件结构

2.1概述 在windows操作系统下,可执行文件的存储格式是PE格式:在Linux操作系统下,可执行文件的存储格式的WLF格式.它们都是COFF格式文件的变种,都是从COFF格式的文件演化而来的. 在windows平台下,目标文件(.obj),静态库文件(.lib)使用COFF格式存储:而可执行文件(.exe),动态链接库文件(.dll)使用PE格式存储.静态库文件其实就是一堆目标文件的集合. 在“WinNT.h”头文件中定义了COFF格式文件,以及PE格式文件的数据结构.这些定义是一系列的结

简单程序的编译链接三种方法(编译多个源文件,静态链接库、动态链接库)

一个程序简单的程序如下: 1 hello.h #ifndef HELLO_H#define HELLO_H void hello(const char *name); #endif 2 hello.c #include <stdio.h>#include <stdlib.h> void hello(const char *name){ printf("hello %s\n",name);} 3 main.c #include <stdio.h>#in

读书笔记:程序员的自我修养-----第二章(编译和链接)

自己之前一直以为目标文件是经过汇编生成.s之后编译生成的,好菜. 源程序到可执行程序过程:预处理  编译 汇编 链接 预处理: 展开宏定义,处理条件预编译指令,插入头文件,删除注释,添加行号和文件名标示,保留#pragma编译器指令. 编译: 词法分析:词法扫描器按照词法规则产生记号,根据记号放入到相应表中.比如讲标示符放入符号表,将数字和字符串常量防盗文字表等. 语法分析:产生以表达式为节点的语法树 语义分析:语义分析器所能分析的只是静态语义(声明,类型的匹配,类型的转换),语义分析之后,每个