C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。html
编写hello world C程序:linux
// hello.c #include <stdio.h> int main(){ printf("hello world!\n"); }
编译过程只需:shell
$ gcc hello.c # 编译 $ ./a.out # 执行 hello world!
这个过程如此熟悉,以致于你们以为编译事件很简单的事。事实真的如此吗?咱们来细看一下C语言的编译过程究竟是怎样的。ubuntu
上述gcc命令其实依次执行了四步操做:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.连接(Linking)。编辑器
为了下面步骤讲解的方便,咱们须要一个稍微复杂一点的例子。假设咱们本身定义了一个头文件mymath.h
,实现一些本身的数学函数,并把具体实现放在mymath.c
当中。而后写一个test.c
程序使用这些函数。程序目录结构以下:ide
├── 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); }
头文件定义:rest
// mymath.h #ifndef MYMATH_H #define MYMATH_H int add(int a, int b); int sum(int a, int b); #endif
头文件实现:code
// mymath.c int add(int a, int b){ return a+b; } int sub(int a, int b){ return a-b; }
预处理用于将全部的#include头文件以及宏定义替换成其真正的内容,预处理以后获得的仍然是文本文件,但文件体积会大不少。gcc
的预处理是预处理器cpp
来完成的,你能够经过以下命令对test.c
进行预处理:htm
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 |
预处理以后的程序仍是文本,能够用文本编辑器打开。
这里的编译不是指程序从源文件到二进制程序的所有过程,而是指将通过预处理以后的程序转换成特定汇编代码(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
请不要问我上述代码是什么意思!-_-
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫作目标文件,是二进制格式。gcc
汇编过程经过as
命令完成:
$ as test.s -o test.o
等价于:
gcc -c test.s -o test.o
这一步会为每个源文件产生一个目标文件。所以mymath.c
也须要产生一个mymath.o
文件
连接过程将多个目标文以及所需的库文件(.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