本文所介绍的是like-unix 系统下的相关工作原理,适合于初学者和app开发者。
作为一般的码农来说,大部分时间我们都在为各种逻辑而煞费苦心,然而你是否利用过短暂的瞬间想过(特别是在集成环境下编程的程序猿),你编写的程序到运行还需要哪些步骤呢?有些码农会说,我想它干啥,编译器都会为我全部处理好。是,现在的编译器确实很强大。但是你编译过程中是否遇到过错误呢?——特别是大型程序。比如,你是否遇到过这个错误“undefined reference to 【function】(function代表某个函数名称)”呢?当你遇到了错误知道检测程序的哪个部分吗?。这也是我写此文的目的,不是为了研究如何去编译一个程序,而是当我们遇到编译错误时,能够快速找到问题所在,及时处理问题。
首先我们明确几个术语:
源文件:就是程序猿使用各种编辑器敲出来or拷贝过来的程序猿能够看懂的代码,例如c语言的.c,.h文件,c++的.cpp和.h文件。
对象文件:源文件通过编译器编译后产生的机器码文件,如.o文件。
可执行文件:属于对象文件的一种,最后可以被操作系统加载running的文件。
码农编写完成的程序到运行大致要进行以下几个步骤:
如本例中所示,hello.c就是我们用c语言编写的程序,称为“源文件”。
在linux中我们使用“gcc -o hello hello.c”命令,首先gcc会进行一个“预处理”,包括“include”头文件包含处理、宏定义替换处理和条件编译处理,就是源文件中以“#”开头的代码,都属于预处理器要处理的。
当预处理完成后,会生成一个helo.i文件,这个文件也是个text文件——程序猿能看懂的文件。接着编译就上场了,编译器将c语言编写的源程序转换为汇编语言的hello.s文件,此时编译器就可以回家休息了。
hello.s文件也是个text文件——一般程序猿可看不懂,要懂汇编语言的才能理解其意思。那么此时的hello.s文件还不是computer能懂的语言,接着汇编器就该出场了。它将hello.s文件编译成对象文件hello.o文件——二进制文件,此时的汇编器也就完成自己的使命了。
既然hello.o已经是computer能看懂——程序猿看不懂——的文件了,是不是就可以running了?wait。虽然此时的hello.o文件已经是对象文件,但它还不能running 。why?想想即经典又简单的“hello world”程序(似乎每种语言讲解书的第一个程序都是它)中是否调用了库中的输出函数(比如c语言中的printf)。我们的程序中没有对这个函数实现,compter运行到调用输出函数那个指令,怎么会知道它的运行地址呢?这就是连接器的作用了。
连接器的工作就是将一个.o文件中一个函数(不在这个.o文件中定义)的引用和这个函数的定义确定清楚。简单来讲就是将所有的.o文件和引用到的静态库中的模块全都拷贝到一个新的对象文件中,这个新的对象文件中所引用的所有函数和变量都能在其中找到它们的实现。这个文件就是我们所说的可执行文件。到此该文件就可以被computer加载running了。
可执行文件的组织方式和我们的源代码会有很大区别,它可以被compter方便的加载到内存中。一般会分为几个section,比如代码 section和数据 section。每个代码和数据的virtual memory 地址都会被分配好(局部变量在running-time才分配)。
上面hello.c只是一个很简单的例子。在平时的工作中,为了提高代码的可重用性,一个程序一般都会包含很多实现文件和头文件。每个实现文件编译完成后都会对应一个.o文件(头文件在预处理过程会被拷贝到引用该头文件的实现文件中)。而“undefined reference to 【function】(function代表某个函数名称)”也容易出现,特别是在引用了多个库文件的时候。这就是连接器报出的错误,原因是连接器找不到这个函数的实现对象文件(即实现文件被编译后的.o文件)。