一串代码从写入编译器到最后被执行,到底中途经历了什么?
c语言中有一个经典的例子helloworld,这是每一个程序员踏入编程之路的第一步,哈哈,一入佛门深似海,从此节操是路人。
刚开始我们很懵,不知道什么,也不敢多问,可是学习了很久之后,你发现你的问题变得多了,为什么就会被编译器给打印出来hello world?为什么必须这样子写才能被执行出来hello world?怎么存放,怎么执行?等等,你就是一个十万个为什么了。
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
好简单的代码,相信你是闭着眼睛都能写出来的。这不是重点,重点是怎么运行出来的,解决你的为什么。
这个代码被写出来,然后被gcc -o hello hello.c 就可以执行了,哇!真的执行出来了helloworld。刚开始你也会觉得很神奇,但是见多了就觉得没那么新奇了。
其实看着很简单的代码,可是对于编译器来说它是怎么处理的呢?
事实上,上述的过程被分解成了4个部分,分别是预处理、编译、汇编和链接。
那就一步一步来,反正一口也是吃不了一个胖子的。
预处理:
刚开始#include是被当成了一种惯性来写出来的。它是什么,为什么用?预处理可是专门为它量身打造。
预处理阶段stdio.h被预编译成一个.i的文件,对于c++程序来说,源代码是.cpp结尾,头文件是.hpp结尾的,那么被预编译以后成了.ii。
linux经常使用的命令是:gcc -E hello -o hello.i
预编译主要的工作(处理以#开头的预编译指令):
1.展开所有#define定义的宏;
2.处理所有的条件预编译指令,如:”if“,”ifdef”,”elif”,”else”,”endif”.
3.#include包含的文件按插入到文件指定的位置。
4.删除注释
5.添加行号
6.保留#pragme的预编译指令。
编译:
编译就是就接着预处理的文件,进行词法分析、语法分析、语义分析、优化代码、检查错误、汇总所有的符号。
linux的指令 :gcc -S hello.i -o hello.s
汇编:
根据编译完之后的.s文件,将.s文件转变成汇编指令。
linux指令:gcc -c hello.c -o hello.o
经过汇编后变成了.o文件。
大致画图说明一下,前面发生的事情。
那么这个二进制可重定向的二进制文件,是什么样子感觉很神奇呀!没事神秘还是要有的,一会在说。
链接:
链接可是一个桥梁,一个很重要的角色。
他主要负责:
1.合并个段
2.给符号分配地址(重定向)
3.符号解析
从此之后hello变成了可执行文件。
刚才前面不是留下了一个疑问,现在来看一下,目标文件经过一个链接之后,变成了一个可执行文件,他们两个有什么不一样吗?(不就是有了地址有什么了不起的)。
二进制可重定向目标文件
目标文件:
红色部分是和二进制可重定向目标文件的区别:
回归到原来的主干道上吧!不要迷路了!
将目标问价加载到虚拟地址空间之后,将main函数的入口地址写入pc寄存器。
还没有执行,目标文件被加载都了自己的虚拟地址空间。但是操作系统不认识这个虚拟地址呀!
我们得大费周章的加载到物理内存,让操作系统认识它。怎么加载呢,大致是这样子的。
这下子操作系统才认识,hello才会被执行,现在看来,是不是一个helloworld要被执行,也是很费劲的,没你想得那么简单。
所以好好爱惜你的操作系统吧!