C语言:探究Linux环境下编译C程序的整个过程
编译一个C程序可以分为四个过程:预处理,编译,汇编,链接。编写一个程序逐步了解整个过程:
// file: test.c
#include <stdio.h>
#define DEBUG printf
int add(int a, int b)
{
return (a+b);
}
int main()
{
int val = add(1, 2);
DEBUG("val = %d\n", val);
return 0;
}
.
预处理
将带有#的头文件等预处理命令替换成真正的内容,这会导致得到的文件体积增大。通常使用命令gcc -E test.c -o test.i
或者cpp test.c -o test.i
输出.i文件。打开文件可以看到头文件被替换成一系列的代码,再看我们自己定义的宏DEBUG已经被替换成printf了:
... // 省略以上头文件的内容
int main()
{
int val = add(1, 2);
printf("val = %d\n", val);
return 0;
}
.
编译
将预处理得到的文件进行翻译成汇编代码。C语言是一门高级语言,需要转换成汇编代码,但是汇编语言也还不是机器能识别的语言,机器能能识别的是0101这种二进制可执行程序,不急,现在才第2个步骤。编译阶段只是将它先翻译成汇编语言,可以使用命令gcc -S test.i -o test.s
或gcc -S test.c -o test.s
编译后查看.s汇编文件:
...// 省略
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $2, %esi
movl $1, %edi
call add
movl %eax, -4(%rbp)
movl $.LC0, %eax
movl -4(%rbp), %edx
movl %edx, %esi
movq %rax, %rdi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
...// 省略
.
汇编
前面说了汇编语言还不是机器能识别的代码,需要进一步处理成机器能识别的二进制代码即机器码。通常使用命令gcc -c test.c -o test.o
或gcc -c test.s -o test.o
或as test.s -o test.o
生成.o二进制文件。注意,在汇编这一步之前的文件都是文本文件而不是二进制文件,所以如果现在打开生成的文件看到的都是一堆乱码。
.
链接
前面说汇编之后生成了机器能识别的二进制代码了,但是这里还不能直接运行。简单点讲就是,程序目前还只是一个类似于框架的东西,那些printf函数你的文件里面没有实现,包含头文件只是这些函数的声明而已,知道这个东西,但实际上还没拥有。所以,需要把那些别人写好的函数原型“串”(链接)起来才可以实现功能。嗯,链接就是实现这个功能。
链接还分为两种链接方式:静态链接与动态链接。(可以参考一些库的制作,一起搅拌一下可能会更香:https://blog.csdn.net/weixin_44498318/article/details/105597170)
链接通常使用命令gcc test.o -o test
或ld的相关命令即可将标准库的文件链接进来,另外还可以使用-l、-I和-L来静态或动态链接一些库。将得到的可执行文件执行命令./test
即可
.
总结一下
事实上,我们平时编译一个程序的时候并不需要分开这么多个步骤,直接执行命令gcc test.c -o test
即可得到可执行文件。将编译过程分为4个步骤有一个好处就是:如果修改其中一个文件的一个地方,就不需要每个文件都进行这四个步骤,只编译刚才修改的那个文件,到时直接进行链接即可,有利于提高编译效率,这点在编写大项目中的Makefile中可以发挥优势。