相信每个学习编程的朋友,都应该会写过hello world。但各位在第一次写这个程序的时候,有没有产生过一个疑问,计算机是如何识别出我们用英文字符写出的代码,并最终运行起来的呢?老实说,我第一个写hello world的语言是C语言,在写这个程序的时候,只是机械敲完代码,大概了解了每行代码的意思,然后编译运行看了效果之后。没有更多地去想这个问题了,当时只是知道要经过一个编译器编译阶段,才能让机器明白我们想干的事情。今天这篇笔记先从程序的编译过程说起。
我们以这个经典的C语言版本的hello world代码为例
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("Hello World!\n");
return 0;
}
首先我们知道,计算机硬件是无法直接理解这个C语言文本文件想要干什么的。我们需要C编译器来把这段代码“翻译”成计算机能够理解的二进制代码,然后由操作系统创建一个进程,将二进制代码从硬盘加载到进程的内存中,然后才能开始运行。
程序编译过程
C编译器不止一种,作为Linux开发者,我只会以gcc为例来说明。gcc的说明网上有很多,这里不再重复,本笔记重点关注重要的过程。
我们在编译hello world的时候,一般会直接使用gcc -o HelloWorld HelloWorld.c。但其实使用这条命令的时候,中间发生的事情有很多,gcc帮我们做了整合。完整的重要步骤有:预处理,编译,汇编,链接这几步。
预处理 | 预处理器的工作,主要包含头文件解析,宏替换等步骤,这一步输入文件是C语言文本,输出是引入了头文件和宏替换后的文本文件。 这一步单独进行: gcc -E -o HelloWorld.i HellWorld.c 这个.i文件里就是预处理后的结果 |
编译 | C语言编译器的工作,输入预处理后的文件,输出汇编语言文件,将C语言转换为汇编语言。 这一步单独进行: gcc -S -o HelloWorld.s HelloWorld.i |
汇编 | 汇编器的工作,输入为汇编文件,输出为目标文件(object,一般是.o文件),目标文件是程序的二进制代码,包括了程序所用的数据和代码 这一步单独进行: gcc -c -o HelloWorld.o HelloWorld.s |
链接 | 连接器的工作,输入为目标文件(一个或多个),输出为可执行的二进制程序,这个程序可以被操作系统所识别和运行。链接的过程中除了HelloWorld.o本身的数据和代码外,还会处理这个程序所依赖的动态库(比如libc等)。 这一步单独进行: gcc -o HelloWorld HelloWorld.o |
练习:分别执行这几个步骤,并查看对应的输出文件内容(汇编、链接后的二进制文件,可用readelf来解析,在后面内存管理章节我们会深入了解elf文件)。
程序的加载运行
完成了程序的编译之后,我们就得到了一个HelloWorld程序文件,这个文件保存在我们的硬盘上。程序如果要运行,还需要将它变为进程。程序和进程的区别,如果不知道请百度。
我们一般会通过命令行输入命令或GUI双击程序图标的方式来告诉操作系统去打开某个程序。操作系统拿到命令后,会先创建一个进程,为进程分配内存并创建并初始化好进程管理信息,然后操作系统会将硬盘中的程序数据读出解析,并将数据和代码放到内存对应位置上。做完这些工作后,进程就开始执行HelloWorld中的指令并在屏幕上输出hello world了。
总结
本篇笔记中仅简单描述了linux系统中C语言代码到可执行程序执行的重要几个步骤。并未深入展开,后续笔记我们会更深入地讨论。