C/C++程序是怎么运行的?编译过程
一个C/C++程序运行经历的过程:预处理、编译、汇编、链接、执行。
- 预处理:将预处理指令(可以简单理解为#开头的正确指令)转换为实际代码中的内容(展开头文件、宏替换、删注释、条件编译)。生成后缀为“.i”的文件。
- 编译(和优化):将预处理后的文件转换成汇编语言。生成后缀为“.s”的文件
- 汇编:由汇编生成的文件(汇编代码)翻译为二进制目标文件(机器码)。生成后缀为“.o”的文件
- 链接:多个目标文件(二进制)结合库函数等综合成的能直接独立执行的执行文件。生成后缀为“.out”的文件
gcc无法进行库文件的链接,即无法编译完成步骤4;而g++则能完整编译出可执行文件。(实质上,g++从步骤1-步骤3均是调用gcc完成,步骤4连接则由自己完成)
【注】其中源程序、修改了的源程序和汇编程序都是文本文件,而可重定位目标程序和可执行目标程序都是二进制文件。
其中编译阶段所有代码都是单独编译的,只是在最终的链接阶段所有的目标文件。
预处理(Pre-Processing).c 👉 .i 一个C程序→另外一个C程序
首先我们准备一个简单的 Hello World 程序,命名为 main.c
#include<stdio.h>
#define info "Hello, world\n"
int main()
{
// A simple program.
printf(info);
return 0;
}
预处理阶段做的事情就是 预处理器(cpp)根据以字符#
开头的代码修改 原始的C程序。
- 比如
#include <stdio.h>
,将头文件stdio.h
中的内容 加到 程序文本中。 - 比如
#define info "Hello, world\n"
,会将 宏定义 的info 替换 成字符串“Hello, world\n”(当然我这里只是为了举例,一般我们不这么写) - 此外,会将 注释 比如// A simple program. 删除。
预处理是直接对源文件进行处理(不关注语法规则),然后得到另一个C程序,通常以 .i 作为文件扩展名。
使用gcc -E main.c -o main.i
命令得到预处理后的C程序main.i
gcc -E main.c -o main.i
我们可以在Linux系统(我这里用的是Ubuntu)下直接使用gcc -E main.c -o main.i命令得到预处理后的C程序main.i。
我截取了一部分(总共有800+行),可以很明显的看到:
- 引入了头文件
- info被替换了
- 注释没了
- 还添加了一些特殊的标记,告诉编译器每行的来源,以便它可以使用它们来产生合理的错误消息。
编译(Compilation).i 👉 .s 另一个C程序→汇编语言
编译阶段做的事情就是编译器(cc1)将C程序main.i翻译成汇编语言程序main.s。
- 检查C程序的语法错误
- 将文件翻译成中间代码,即汇编语言
- 可选地优化翻译后的中间代码,获得更好的性能
我们可以使用gcc -S main.i -o main.s
得到翻译后的汇编程序main.s,截取部分如下。
gcc -S main.i -o main.s
.file"main.c".text.LC0:.string"Hello, world".text.globl mainmain:.LFB0:.cfi_startproc pushq%rbp movq%rsp,%rbp.......LFE0:.size main,.-main.ident"GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0".section.note.GNU-stack,"",@progbits
其中的语句比如pushq %rbp描述了一条低级机器语言指令。汇编语言是有用的,它为不同高级语言的不同汇编器提供了通用的输出语言,C汇编器和Fortran汇编器产生的输出文件都是一样的汇编语言。
汇编和优化(Assembling).s👉.o 汇编语言→机器语言指令
汇编阶段做的事情就是汇编器(as)
- 将main.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,
- 并将结果保存在目标文件main.o(Windows下为xx.obj而Linux下为xx.o)中。
main.o是一个二进制文件,如果我们使用文本编辑器打开main.o,会看到一堆乱码。
我们可以使用gcc -c main.s -o main.o
得到可重定位目标程序main.o,如下所示。
gcc -c main.s -o main.o
LF>?@@UH䈍=距ello,worldGCC:(Ubuntu7.3.0-27ubuntu1~18.04)7.3.0zRx#main.cmain_GLOBAL_OFFSET_TABLE_puts?ÿÿ C ??ÿÿ.symtab.strtab.shstrtab.rela.text.data.bss.rodata.comment.note.GNU-stack.rela.eh_frame @0&90d+BWR@@
不出所料的一堆乱码?。
链接(Linking).o + ... + .o 👉可执行程序
我们的main.c程序中使用了 printf函数,printf函数是每个C编译器都提供的标准C库中的一个函数,它存在于一个名为printf.o的单独预编译好了的目标文件中。
链接阶段做的事情就是链接器(ld)
- 将需要用到的目标文件比如main.o和printf.o进行合并,
- 并生成一个可执行(目标)文件,可以被加载到内存中,由系统执行。
实际上我们直接执行gcc main.c -o main命令得到的就是可执行文件,Windows下为.exe,Linux下默认为具有可执行权限的a.out,当然我们使用了-o来自定义输出文件名。
gcc main.c -o main
执行(executing)
./main