一段C语言程序
打开任何一个C语言的教程,首先自然是展示一段 Hello World 的程序,类似于下面这段:
#include <stdio.h>
int main()
{
/* 我的第一个 C 程序 */
printf("Hello, World! \n");
return 0;
}
运行上面这段程序也很简单,直接执行 gcc hello.c 即可实现在屏幕上打印出 Hello, World! 的效果,今天就在仔细分析一下这段代码的执行。
基本结构
从汇编语言,也就是底层的视角来分析上面这段C语言程序:
- 首行的 #include 类似于Java或其他语言中的 import,导入我们程序中用到的函数定义以及其他一些公共声明等信息,供编译器进行识别、解析;
- int main() 封装了汇编的指令片段调用过程,包括保存返回地址、开辟栈帧、传递参数、传递返回值等;
- {} 大括号定义的指令片段的范围;
- printf 封装call指令和参数传递;
- return 0; 将返回值放在约定的寄存器eax。
处理过程
预处理
在真正编译C语言程序之前,会先进行一步预处理过程,include <stdio.h> 被称作宏定义,也是在这个阶段进行处理,实际就是将 stdio.h 文件的内容复制展开到当前文件中。可以通过如下命令将预处理后的结果进行导出到demo.i
// 输出仅预处理后的文件
gcc -o demo.i -E demo.c
编译
源文件经过编译器编译,生成汇编程序文件
汇编
汇编器执行生成二进制文件。
链接
经过汇编过程的程序仍然有可能不能执行,因为我们的代码很可能被分散在多个文件中,它们之间又存在调用关系,比如上面的main函数中调用了 printf 函数,printf 实际是C运行时库(CRT)提供的函数,其封装了操作系统的系统调用的基本功能,一些硬件操作、网络IO、基础的集合数据类型、算法帮助我们进行开发,减少开发工作量。
链接器将多个代码片段的文件组合在一起。将被调用代码片段的地址放到调用处实现代码的整合。因此,在仅经过编译、汇编,没有进行链接过程的程序,由于调用的方法还不知道真实的地址,因此生成结果文件中,还是一个待解析的假的地址,这个地址被称作“符号”,将符号替换为真实地址的过程,被称作“符号解析”。依赖的文件加载到其使用的地方时,默认的基于0地址的地址会被修改,赋值到真实的地址上,这个过程叫文件重定位。
因此,链接器的作用是:找到引用的函数、数据的地址,符号解析 + 文件重定位。
小结
一段C程序,经过预处理、编译器、汇编器、链接器后实现从源代码到执行文件的转变。在各个阶段都有对应的官方手册提供给我们进行参考:
- C语言的使用可以通过编译器的说明文档的支持进行参考;
- 汇编器也有其官方手册来规范汇编程序的编写,参考GAS;
- 汇编器执行生成的二进制文件也是有对应的格式要求说明的,Linux平台用的最多的是ELF,也有对应的参考手册;
- 二进制对象文件经过链接器进行链接,实现多个指令片段之间的关联,生成最终可执行的二进制程序,基于不同CPU平台也有不同的手册,我们用的较多的是Intel手册。