C语言编译过程详解

分手后的思念是犯贱 2022-05-14 01:35 436阅读 0赞

目录

前言

示例

1.预处理(Preprocessing)

2.编译(Compilation)

3.汇编(Assemble)

4.链接(Linking)

结语

参考文献


前言

C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。

编写hello world C程序:

  1. // hello.c
  2. #include <stdio.h>
  3. int main(){
  4. printf("hello world!\n");
  5. }

编译过程只需:

  1. $ gcc hello.c # 编译
  2. $ ./a.out # 执行
  3. hello world!

这个过程如此熟悉,以至于大家觉得编译事件很简单的事。事实真的如此吗?我们来细看一下C语言的编译过程到底是怎样的。

上述gcc命令其实依次执行了四步操作:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.链接(Linking)。

C\_complie

示例

为了下面步骤讲解的方便,我们需要一个稍微复杂一点的例子。假设我们自己定义了一个头文件mymath.h,实现一些自己的数学函数,并把具体实现放在mymath.c当中。然后写一个test.c程序使用这些函数。程序目录结构如下:

  1. ├── test.c
  2. └── inc
  3. ├── mymath.h
  4. └── mymath.c

程序代码如下:

  1. // test.c
  2. #include <stdio.h>
  3. #include "mymath.h"// 自定义头文件
  4. int main(){
  5. int a = 2;
  6. int b = 3;
  7. int sum = add(a, b);
  8. printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
  9. }

头文件定义:

  1. // mymath.h
  2. #ifndef MYMATH_H
  3. #define MYMATH_H
  4. int add(int a, int b);
  5. int sum(int a, int b);
  6. #endif

头文件实现:

  1. // mymath.c
  2. int add(int a, int b){
  3. return a+b;
  4. }
  5. int sub(int a, int b){
  6. return a-b;
  7. }

1.预处理(Preprocessing)

预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的,你可以通过如下命令对test.c进行预处理:

  1. gcc -E -I./inc test.c -o test.i

或者直接调用cpp命令

  1. $ cpp test.c -I./inc -o test.i

上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。

经过预处理之后代码体积会大很多:
























X 文件名 文件大小 代码行数
预处理前 test.c 146B 9
预处理后 test.i 17691B 857

预处理之后的程序还是文本,可以用文本编辑器打开。

即:

1.将所有的#define删除,并展开所有的宏定义;
2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;
3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
4.添加行号信息文件名信息,便于调试;
5.删除所有的注释:// /**/;
6.保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作。
7.生成.i文件。

包括(1)去注释 (2)宏替换 (3)头文件展开 (4)条件编译

2.编译(Compilation)

这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。编译的指定如下:

  1. $ gcc -S -I./inc test.c -o test.s

上述命令中-S让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码test.s,这也是文本文件,内容如下:

  1. // test.c汇编之后的结果test.s
  2. .file "test.c"
  3. .section .rodata
  4. .LC0:
  5. .string "a=%d, b=%d, a+b=%d\n"
  6. .text
  7. .globl main
  8. .type main, @function
  9. main:
  10. .LFB0:
  11. .cfi_startproc
  12. pushl %ebp
  13. .cfi_def_cfa_offset 8
  14. .cfi_offset 5, -8
  15. movl %esp, %ebp
  16. .cfi_def_cfa_register 5
  17. andl $-16, %esp
  18. subl $32, %esp
  19. movl $2, 20(%esp)
  20. movl $3, 24(%esp)
  21. movl 24(%esp), %eax
  22. movl %eax, 4(%esp)
  23. movl 20(%esp), %eax
  24. movl %eax, (%esp)
  25. call add
  26. movl %eax, 28(%esp)
  27. movl 28(%esp), %eax
  28. movl %eax, 12(%esp)
  29. movl 24(%esp), %eax
  30. movl %eax, 8(%esp)
  31. movl 20(%esp), %eax
  32. movl %eax, 4(%esp)
  33. movl $.LC0, (%esp)
  34. call printf
  35. leave
  36. .cfi_restore 5
  37. .cfi_def_cfa 4, 4
  38. ret
  39. .cfi_endproc
  40. .LFE0:
  41. .size main, .-main
  42. .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
  43. .section .note.GNU-stack,"",@progbits

请不要问我上述代码是什么意思!-_-

即:

1.扫描,语法分析,语义分析,源代码优化,目标代码生成,目标代码优化;
2.生成汇编代码;
3.汇总符号;
4.生成.s文件

3.汇编(Assemble)

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc汇编过程通过as命令完成:

  1. $ as test.s -o test.o

等价于:

  1. gcc -c test.s -o test.o

这一步会为每一个源文件产生一个目标文件。因此mymath.c也需要产生一个mymath.o文件

1.根据汇编指令和特定平台,把汇编指令翻译成二进制形式;
2.合并各个section,合并符号表;
3.生成.o文件

4.链接(Linking)

链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)

命令大致如下:

  1. $ ld -o test.out test.o inc/mymath.o ...libraries...

1.合并各个.obj文件的section,合并符号表,进行符号解析;
2.符号地址重定位;
3.生成可执行文件

结语

经过以上分析,我们发现编译过程并不像想象的那么简单,而是要经过预处理、编译、汇编、链接。尽管我们平时使用gcc命令的时候没有关心中间结果,但每次程序的编译都少不了这几个步骤。也不用为上述繁琐过程而烦恼,因为你仍然可以:

  1. $ gcc hello.c # 编译
  2. $ ./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

发表评论

表情:
评论列表 (有 0 条评论,436人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C/C++ 编译过程详解

    C 语言的编译链接过程要把我们编写的一个 c 程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件

    相关 C语言编译过程详解

    [C语言编译过程详解][C] 前言 C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。 编写he

    相关 c语言编译过程

    c语言的开发是首先写出所有的源代码,然后编译成为可执行代码,然后才可以被cpu执行。 c语言的源代码在编译器看来只不过是一堆字符串而已,在我们看来或许可以看得懂,但

    相关 C语言编译详解

    一、编译的概念:     编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行

    相关 C语言编译过程

    编译的概念:编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链

    相关 C语言编译过程

    C语言编译全过程介绍 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源