C程序变为一个可执行程序的过程
C语言变成可执行文件的过程包括预处理、编译、汇编和链接四个主要步骤。具体介绍如下:
预处理阶段
- 头文件展开:预处理会将
#include
指令替换为相应头文件的内容,确保代码中引用的库或API声明被正确包含。 - 宏替换:预处理还会处理所有
#define
定义的宏,将其展开为对应的代码或值。 - 条件编译:此阶段还将处理#
if、#ifdef
等条件编译指令,根据条件决定是否将相应代码包含在最终文件中。 - 注释去除:所有的注释将被删除,以减少代码量并提高编译效率。
如果在Linux环境下可以使用gcc
编译器输入指令gcc -E test.c -o test.i
就可以对一个C代码进行预处理,然后使用vi
工具可以查看。如果不使用-o
参数它会直接在终端中输出,这是因为预处理由于只是进行头文件和宏定义的简单替换,并没有经过编译或者其他的步骤,所以默认它不会生成文件。以下边的代码为例:
#include <stdio.h>
#define FD_TEST 20
int main()
{
//这是一行注释,并不会编译到可执行文件中去
int fd = FE_TEST;
printf("%d\n",fd);
return 0;
}
这里使用包含了一个#include <stdio.h>
的头文件,所以它会被替换成原来的内容,而且宏定义会被替换成它实际的值,并且用户所写的注释会在这一步删掉。
这是/usr/include/stdio.h
;里边的一部分内容,截取下来和重定向后的文件内容进行对比。
这是test.log
里边的文件内容,可以发现它确实是把#include <stdio.h>
这一句话的内容替换成了整个头文件的内容。并且经过替换以后它里边是没有注释的,显然是编译器为了提高编译效率将注释删除减少了代码量。
再看到最后它也将代码里边的注释删除了,而且将宏定义改为它实际的值。
其实这里还有一点需要提一下,当用户自己编写的头文件时,不在开头使用条件编译#ifndef...#define...endif
这样的字眼的话,如果在一个c文件中重复包含,那么就是相当于多次将用户编写的头文件里边的内容放到文件的开头。假如用户自己编写了一个几百行的头文件,多次重复包含也就是相当于前边多了好几个几百行。就大大的减少了代码的编译效率。所以一般在头文件的开头使用#ifndef...#define...endif
的字眼去帮助用户去避免重复包含头文件。
编译阶段
-
词法分析:编译器将源代码拆分成一个个独立的词法单元(token),以便于进一步处理。
-
语法分析:编译器检查代码是否符合C语言的语法规则,这一阶段会生成一个抽象语法树(AST)。
-
语义分析:编译器检查变量声明、函数调用等是否正确,并确保代码逻辑上没有错误。
-
代码优化:编译器会对代码进行优化,以提高生成的机器代码的效率。
-
目标代码生成:最后,编译器将优化后的代码转换为汇编语言,并存储在临时文件中等待进一步处理。
在Linux环境中使用gcc
编译器然后使用指令gcc -S test.c
可以对C文件进行编译,编译以后会生成一个后缀名为.s
的汇编文件。可以使用vi
工具查看生成的test.s
文件
mov
指令是汇编里边一个常用的指令,mov x29 sp
这句话就是将堆栈指针的值赋值给寄存器x29
汇编阶段
-
汇编代码生成:汇编器将编译阶段生成的汇编语言代码转化为机器能够直接执行的机器指令。
-
格式转换:这些机器指令会被包装成特定格式的目标文件(例如ELF格式),以便于链接阶段进行处理。
-
符号表生成:汇编器还会生成一个符号表,记录代码中全局标识符及其地址信息,为后续链接做准备。
在Linux环境中使用gcc
编译器加指令gcc -c test.s
就可以生成一个后缀名为.o
的目标文件。经过汇编操作以后,它文件里边的内容就是机器指令了,如果使用vi
指令也看不出什么。
链接阶段
- 段表合并:链接器将各个目标文件的段表信息合并,形成一个统一的段表。
- 符号表解析和重定位:链接器处理所有未解析的符号引用,例如函数调用或外部变量,确保各模块之间能够正确连接。
- 虚拟地址分配:链接器为程序分配虚拟地址空间,并确定每个段的最终位置。
- 生成可执行文件:链接器将所有处理过的目标文件以及所需的库文件合并,生成最终的可执行文件。
最后在Linux环境中使用gcc test.o
文件就可以将生成的目标文件进行链接。最后生成一个后缀名为.out
的可执行文件。