“Hello,World”背后的故事

学习一门语言,经常都是从打印“Hello,World”开始的,打过招呼后,你便可以进入程序的新世界。

就拿经典的C语言举例,基本上每个程序员在上学时就可以闭着眼睛写下“Hello,World”,这也是检测开发环境是否能正常工作常用的小程序,就像有的人看能不能上网就输个百度试试(手动斜眼,程序员应该用谷歌).

//hello.c
#include 

int main()
{
    printf("Hello World\n");
    return 0;
}

我们使用gcc编译并运行该文件:

$ gcc hello.c -o hello
$ ./hello

输出结果:


“Hello,World”背后的故事_第1张图片
helloworld

其实,输出一行字符并没有那么简单,gcc帮我们处理了很多步,如果你用Visual Studio,运行按钮更是连编译指令都不用敲了,IDE是简化了很多步骤,但是深入探索背后的步骤是每个程序员必备的素养,更何况很多成熟的大型项目都是需要自己构建(Build)。

上述过程可以分为4个步骤:

  • 预处理 (Prepressing)
  • 编译 (Compilation)
  • 汇编 (Assembly)
  • 链接 (Linking)
“Hello,World”背后的故事_第2张图片
gcc编译过程

下面我们详述这一过程:

预处理##

预处理器cpphello.c及包含的头文件,这里就是stdio.h预编译成为一个hello.i的文件。
我们可以用以下命令只对hello.c进行预处理:

$ gcc -E hello.c -o hello.i

或者:

$ cpp hello.c > hello.i

你没看错,预处理器就是cpp,与C++扩展名.cpp没有关系,具体可以man cpp查看手册,其实gcc只是把预编译器,编译器,汇编器,链接器这一系列工具集成在一起,通过不同的参数去调用不同的部分或者全部调用

预处理做的工作:

  • 将所有的#define删除, 并且展开所有的宏定义,像#define MAX 1024,那么代码文件中所有的MAX都会被1024代替。
  • 处理所有的条件预编译指令,包括#if#ifdef#elif#else#endif。至于这些指令到底干嘛的,任何一本C语言教材都会有明确的解释。
  • 处理#include指令,将所有头文件插入到预编译指令的位置,这一过程是递归进行的,也就是说,头文件里包含的头文件也会被插入头文件里。良好的代码规范都指导我们使用头文件保护,避免重复包含头文件。
  • 删除所有注释///* ··· */。注释给人看的,机器不需要看注释。
  • 添加行号和文件名标识,比如打开刚刚的hello.i,int main()之前插入了一句# 2 "hello.c" 2,以便于编译器产生调试用的行号信息,这样产生编译错误或警告时,编译器就会给出文件名和行号。
    -保留所有的#pragma指令,编译器会使用它们。

经过预处理后,文件中所有的宏被展开,包含的文件也被插入,这时候就可以给编译器使用。

编译##

编译过程是整个程序构建的核心部分,包含了大量编译原理的知识,注明的参考书有龙书。
编译过程可以分为以下几个部分,每个部分深究起来都很耗费功夫,有机会可以自己实现:

  • 词法分析
  • 语法分析
  • 语义分析
  • 中间语言生成与优化

现在版本的gcc把预处理和编译两个步骤合二为一,使用一个叫cc1的程序完成这两个步骤,在我的计算机里位于“/usr/lib/gcc/i686-linux-gnu/4.8/cc1”
我们可以通过以下命令生成编译后的文件:

$ gcc -S hello.c -o hello.s

也可以直接使用cc1:

$ /usr/lib/gcc/i686-linux-gnu/4.8/cc1 hello.c

编译后生成汇编文件hello.s

汇编##

汇编器就是将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都对应一条机器指令。所以汇编器相对简单,只需要一一翻译就可以。
我们使用汇编器as完成如上工作:

$ gcc -c hello.s -o hello.o

或者

$ as hello.s - o hello.o

也可以直接从hello.c直接得到目标文件:

$ gcc -c hello.c -o hello.o

链接##

据说链接器的历史比编译器还长,像我们的“Hello,World”程序,生成的hello.o中包含了printf函数,头文件只包含了函数的申明,所以最后还需要链接到libc.a,其实需要链接的不仅仅是printf,我们用链接器ld链接以下这么多模块才能生成最终的可执行文件。

$ ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i686-linux-gnu/4.8/crtbeginT.o 
-L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_rh -lc --end-group 
/usr/lib/gcc/i686-linux-gnu/4.8/crtend.o /usr/lib/crtn.o

一个再复杂的软件也是如此,将源代码分别独立编译,再组装起来,这个过程就叫做链接,链接的主要目的,一个是将模块间依赖的函数调用打通,还有就是模块间共通的变量打通。

链接器所做的工作主要就是“调整地址”,写汇编代码时,有这么一句jmp foo,其实链接器就帮我们把foo翻译成运行时的地址。

链接的主要过程:

  • 地址和空间分配 (Address and Storage Allocation)
  • 符号决议 (Symbol Resolution)
  • 重定位 (Relocation)

举个例子,可以很清楚的解释这个过程,我们在main.c调用了另外一个文件func.c中的函数test(),那么当我们在main.c中每使用一次test()都必须知道test()的地址,但文件都是单独编译的,所以我们在main.c中的做法是暂时搁置test()的地址,当链接的时候,链接器会根据test符号,自动填入test()的地址,如果func.c重新编译了,test()地址会变化,但是编译时,没有改变的main.c并不会编译了,只是在链接时,会链接新的test()的地址。这个修正的过程也叫作重定位

链接还分为静态链接动态链接,这个以后会专门说。

如果觉得还不错,请点个赞吧~

你可能感兴趣的:(“Hello,World”背后的故事)