首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程:
1.预处理阶段
2.词法与语法分析阶段
3.编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 (.obj文件)
4.连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息。(生成.exe文件)
编译器在编译时是以C文件为单位进行的,也就是说如果你的项目中一个C文件都没有,那么你的项目将无法编译,连接器是以目标文件为单位,它将一个或多个目标文件进行函数与变量的重定位,生成最终的可执行文件,在PC上的程序开发,一般都有一个main函数,这是各个编译器的约定,当然,你如果自己写连接器脚本的话,可以不用main函数作为程序入口!!!
有了这些基础知识,再言归正传,为了生成一个最终的可执行文件,就需要一些目标文件,也就是需要C文件,而这些C文件中又需要一个main函数作为可执行程序的入口,那么我们就从一个C文件入手,假定这个C文件内容如下:
#include
#include "mytest.h"
int main(int argc,char **argv)
{
test = 25;
printf("test.................%d/n",test);
}
头文件内容如下:
int test;
现在以这个例子来讲解编译器的工作:
1.预处理阶段:编译器以C文件作为一 个单元,首先读这个C文件,发现第一句与第二句是包含一个头文件,就会在所有搜索路径中寻找这两个文件,找到之后,就会将相应头文件中再去处理宏,变量, 函数声明,嵌套的头文件包含等,检测依赖关系,进行宏替换,看是否有重复定义与声明的情况发生,最后将那些文件中所有的东东全部扫描进这个当前的C文件 中,形成一个中间“C文件”
2.编译阶段,在上一步中相当于将那个头文件中的test变量扫描进了一个中 间C文件,那么test变量就变成了这个文件中的一个全局变量,此时就将所有这个中间C文件的所有变量,函数分配空间,将各个函数编译成二进制码,按照特 定目标文件格式生成目标文件,在这种格式的目标文件中进行各个全局变量,函数的符号描述,将这些二进制码按照一定的标准组织成一个目标文件
3.连接阶段,将上一步成生的各个目标文件,根据一些参数,连接生成最终的可 执行文件,主要的工作就是重定位各个目标文件的函数,变量等,相当于将个目标文件中的二进制码按一定的规范合到一个文件中
来自:https://www.cnblogs.com/CarpenterLee/p/5994681.html
前言
C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。
编写hello world C程序:
// hello.c
#include <stdio.h>
int main(){
printf("hello world!\n");
}
编译过程只需:
$ gcc hello.c # 编译
$ ./a.out # 执行
hello world!
这个过程如此熟悉,以至于大家觉得编译事件很简单的事。事实真的如此吗?我们来细看一下C语言的编译过程到底是怎样的。
上述gcc命令其实依次执行了四步操作:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.链接(Linking)。
示例
为了下面步骤讲解的方便,我们需要一个稍微复杂一点的例子。假设我们自己定义了一个头文件mymath.h
,实现一些自己的数学函数,并把具体实现放在mymath.c
当中。然后写一个test.c
程序使用这些函数。程序目录结构如下:
├── test.c
└── inc
├── mymath.h
└── mymath.c
程序代码如下:
// test.c
#include <stdio.h>
#include "mymath.h"// 自定义头文件
int main(){
int a = 2;
int b = 3;
int sum = add(a, b);
printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}
头文件定义:
// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sum(int a, int b);
#endif
头文件实现:
// mymath.c
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
1.预处理(Preprocessing)
预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc
的预处理是预处理器cpp
来完成的,你可以通过如下命令对test.c
进行预处理:
gcc -E -I./inc test.c -o test.i
或者直接调用cpp
命令
$ cpp test.c -I./inc -o test.i
上述命令中-E
是让编译器在预处理之后就退出,不进行后续编译过程;-I
指定头文件目录,这里指定的是我们自定义的头文件目录;-o
指定输出文件名。
经过预处理之后代码体积会大很多:
X | 文件名 | 文件大小 | 代码行数 |
---|---|---|---|
预处理前 | test.c | 146B | 9 |
预处理后 | test.i | 17691B | 857 |
预处理之后的程序还是文本,可以用文本编辑器打开。
2.编译(Compilation)
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。编译的指定如下:
$ gcc -S -I./inc test.c -o test.s
上述命令中-S
让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码test.s
,这也是文本文件,内容如下:
// test.c汇编之后的结果test.s
.file "test.c"
.section .rodata
.LC0:
.string "a=%d, b=%d, a+b=%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $2, 20(%esp)
movl $3, 24(%esp)
movl 24(%esp), %eax
movl %eax, 4(%esp)
movl 20(%esp), %eax
movl %eax, (%esp)
call add
movl %eax, 28(%esp)
movl 28(%esp), %eax
movl %eax, 12(%esp)
movl 24(%esp), %eax
movl %eax, 8(%esp)
movl 20(%esp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
请不要问我上述代码是什么意思!-_-
3.汇编(Assemble)
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc
汇编过程通过as
命令完成:
$ as test.s -o test.o
等价于:
gcc -c test.s -o test.o
这一步会为每一个源文件产生一个目标文件。因此mymath.c
也需要产生一个mymath.o
文件
4.链接(Linking)
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
命令大致如下:
$ ld -o test.out test.o inc/mymath.o ...libraries...
结语
经过以上分析,我们发现编译过程并不像想象的那么简单,而是要经过预处理、编译、汇编、链接。尽管我们平时使用gcc
命令的时候没有关心中间结果,但每次程序的编译都少不了这几个步骤。也不用为上述繁琐过程而烦恼,因为你仍然可以:
$ gcc hello.c # 编译
$ ./a.out # 执行
参考文献
1.https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html
2.http://www.trilithium.com/johan/2005/08/linux-gate/
3.https://gcc.gnu.org/onlinedocs/gccint/Collect2.html