C语言编译过程详解

前言

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)。编辑器

C_complie

示例

为了下面步骤讲解的方便,咱们须要一个稍微复杂一点的例子。假设咱们本身定义了一个头文件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;
}

1.预处理(Preprocessing)

预处理用于将全部的#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

预处理以后的程序仍是文本,能够用文本编辑器打开。

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

相关文章
相关标签/搜索