欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux下从原文件到可执行文件的过程

程序员文章站 2024-03-04 09:15:53
...
一、引言
对于c和C++而言,编译器从源文件生成可执行文件需要一个过程,那么从源文件(.h,.cpp,.c文件)到一个二进制的可执行文件这个过程中发生了什么呢,所以需要再次做一个总结:

二、流程图

Linux下从原文件到可执行文件的过程

可以看到整个从源文件到生成可执行文件的过程中,总共有四个过程,
这四个过程可以先总结为:
1、源代码通过预编译阶段,生成一个预处理文件.i文件;
2、.i文件经过编译生成汇编.s文件;
3、.s汇编文件经过汇编器生成.o的目标文件;
4、.o目标文件经过链接器链接生成可执行文件。

现在我们分别在看一下这四个过程分别做了什么事情。

三、预编译

预编译阶段主要是做一些预处理,比如宏的替换,头文件的包含处理,去除源码中的空格和注释,预编译条件的判断等操作,下面经过一段测试代码来说明这些情况:

#include "main.h"
#include <stdio.h>
//#include "main.h"
#define N 5
#define DEBUG 0
#define NUM 2
int main()
{
#ifdef DEBUG
printf("define debug\n"); //print debug
#else
printf("do not debug\n"); //do not debug
#endif
int a[N] = {0};
#if NUM == 1
printf("NUM is 1\n");
a = 9;
#elif NUM == 2
printf("NUM is 2\n");
#else
printf("default NUM\n");
#endif
return 0;
}

可以看到上述代码有函数,语句,宏定义,有预编译条件语句,有注释,有头文件包含等。

我们执行下面语句将源文件预编译后的文件输出:

gcc -E main.c > main.i
输出后,截取一部分main.i文件的内容如下:

1 "main.c"
2 # 1 "<命令行>"
3 # 1 "main.c"
4
5 # 1 "main.h" 1
6
7
8
9 int a = 1;
10 int b = 2;
11 int c = 3;
12 # 3 "main.c" 2
13 # 1 "/usr/include/stdio.h" 1 3 4
14 # 27 "/usr/include/stdio.h" 3 4
15 # 1 "/usr/include/features.h" 1 3 4

..........................

56 typedef unsigned char __u_char;
57 typedef unsigned short int __u_short;
58 typedef unsigned int __u_int;
59 typedef unsigned long int __u_long;
60
61
62 typedef signed char __int8_t;
63 typedef unsigned char __uint8_t;
64 typedef signed short int __int16_t;
65 typedef unsigned short int __uint16_t;
66 typedef signed int __int32_t;
67 typedef unsigned int __uint32_t;
68
69
70
71
72 __extension__ typedef signed long long int __int64_t;
73 __extension__ typedef unsigned long long int __uint64_t;
...........................
...........................
# 943 "/usr/include/stdio.h" 3 4
856
857 # 4 "main.c" 2
858
859
860
861
862
863
864 int main()
865 {
866
867
868 printf("define debug\n");
869
870
871
872
873 int a[5] = {0};
874
875
876
877
878 printf("NUM is 2\n");
879
880
881
882 return 0;
883 }

与原始的main.c文件相比较,可以看到下面的几点区别:

1.宏的展开,将N替换为5

2.预定义条件的判断,可以看到只输出条件判断正确的语句;

3.去掉了源代码中的注释;

4.头文件的展开,可以看到大串的代码都是头文件stdio.h的文件,还有自定义头文件main.h的展开。


.编译

编译阶段会对经过预处理后文件进行语法,词法的检查,将main.i文件翻译成文本文件main.s文件,该文件中都是一系列的汇编语言组成的指令,可以用如下命令执行生成main.s文件

gcc -S main.i > main.s
执行完成后,可以看到main.s文件中为汇编指令,截取如下:

1 .file "main.c"
2 .globl a
3 .data
4 .align 4
5 .type a, @object
6 .size a, 4
7 a:
8 .long 1
9 .globl b
10 .align 4
11 .type b, @object
12 .size b, 4
13 b:
14 .long 2
15 .globl c
16 .align 4
17 .type c, @object
18 .size c, 4
19 c:
20 .long 3
21 .section .rodata
22 .LC0:
23 .string "define debug"
24 .LC1:
25 .string "NUM is 2"
26 .text
27 .globl main
28 .type main, @function
29 main:
30 .LFB0:
31 .cfi_startproc
32 pushl %ebp

.汇编

汇编阶段将是汇编器将上一阶段中的汇编代码翻译成机器语言指令,把这些指令打包成可重定位目标程序格式,并将结果保存在main.o文件中,该文件是一堆二进制代码,,他的编码方式是机器语言指令而不是字符,所以打开后看到的是乱码。

可以通过下面命令生成main.o文件

gcc -c main.c -o main.o

.链接

链接阶段就需要链接器进行链接操作,链接阶段主要是将所有需要的.o文件以及对应的静态库或者动态库链接后生成可执行文件,可以加载到内存中,由系统执行。

这里有动态库和静态库只说,他们具体具体的区别与联系可以看下一篇博文;

Linux环境下,静态库是以.a结束的文件,静态库在链接的时候将库里面的代码直接拷贝到可执行程序中。

Linux环境下,动态库是.so结尾的文件,动态库在链接的时候只是创建一些符号表,而在运行的时候才将代码装入到内存中,映到运行时相应进程的需地址空间。

七.总结

以上为从源代码到可执行程序中间经历的步骤,因为在一次面试中有问道相应的知识,所以在这里座椅个总结,以备日后查看。