http://www.opencv.org.cn/forum/viewtopic.php?f=1&t=5161
写的真好
上次我已经说过,程序库分为静态和动态两种,今天先从程序的编译过程入手,简单介绍一下静态库和程序编译的基础知识,至于更加复杂更加深入的内容、以及动态库、运行时方面的东西,下次再说。
静态库是什么意思呢?举例来说,你自己写的某个程序用到了opencv和highgui中的某些函数或变量(总之是需要占用内存的东西,就需要一个地址),而opencv和highgui在发布时都是以库的形式发布,那么你的程序在最后从可重定位目标码生成可执行程序的过程中,编译器一定要知道你调用的外部符号的地址和确切的类型,这样才能生成正确的机器指令。
这样说可能还有点抽象,再具体一点,比如你的main.c中有这样一段代码(我没看过opencv,这里瞎举例):
- 代码: 全选
#include "highgui.h"
#include "opencv.h"
int main(int argc, char **argv)
{
int gui = highgui_gethandle();
return opencv_guihandle(gui);
}
那么,编译器在编译这个main.c的时候,就需要知道highgui_gethandle和opencv_guihandle这两个函数的具体类型,这样才能够判断这段程序在语法上是否正确,比如highgui_gethandle这个函数是否不需要参数、是否返回int值等。
那么,编译器靠什么知道语法是否正确呢?显然,需要你明确的给出这两个函数的原型声明,也就是说当编译器读到main.c的这两行函数调用语句的时候,必须在之前就已经知道了这两个函数的所有属性。所以你需要显式包含这两个头文件,这样编译器在编译阶段之前的预处理阶段会将这两个头文件(及其嵌套包含的其它头文件)的内容原封不动的插入main.c中,这样才能保证在接下来的编译阶段能够正确理解函数原型,从而准确的判断出程序代码的语法问题。在语法分析阶段,像AST、自动机、形式语言这些基本理论,就不是一两句话能说清楚的了。
那么编译器又是如何知道在哪里可以找到这两个头文件的呢?一般来说,在编译编译器的时候(这句话比较绕口,其实编译器也不过是个程序而已,因此编译器也一定是从源程序编译而来的,不可能是直接用机器码编写,就算anders本人当年也是用汇编的),会通过配置参数或在代码中指定的方式,默认指定一些头文件和库文件的搜索路径。这样,编译器在编译其它源程序的时候,遇到包含头文件的情况,会到这些缺省路径下去找。如果你的头文件不在缺省路径下,那么就需要程序员自己在编译指令中指定了,这就是我们通常看到的三种情况:
1、在vs、codeblock、eclipse之类的IDE中,通过option之类的菜单配置头文件路径
2、在Makefile中的编译指令中通过编译选项指定,比如gcc的-I参数
3、直接在编译指令中指定,比如gcc -I/home/test/opencv/include -I /home/test/highgui/include -c main.c
所以,经常有些人在编译程序的时候看到找不到某个包含的头文件的提示,现在应该知道怎么办了吧。
说完了头文件,回到主题--库文件。刚才大致讲了一下基本的编译过程,真的是非常非常粗略,还有许多细节和技术我都略过了,现在就从main.c变成了main.o(vc中是main.obj,其实都是一个意思,只是elf/pe/a.out等文件格式和内容摆放标准不同而已,其实内容都差不多),main.o中就不再是c程序了,而已经是二进制的机器代码,不过是可重定位代码。什么叫可重定位代码,是因为这时的main.o还只是一个半成品,是上不了台面的,无法直接运行。为什么呢?因为其中的highgui_gethandle和opencv_guihandle这两个函数都还不知道在哪里,所以编译器在从main.c生成main.o的过程中,这里的两条函数调用指令都是半成品指令,其中只有跳转部分是真实的,而跳转的目的地址是假的,同时会在其它地方(比如某个段中统一)存放需要重定位的指令内容,这里就是这两条跳转指令。
随后,进入链接阶段,链接器需要完成程序重定位和符号链接的任务,也就是说这时候就需要找到这两个函数究竟在哪里了。很显然,其实就是要找到opencv和highgui中分别包含有这两个函数的库文件。再次很显然,如果opencv发布了多个静态库文件,链接器是不可能逐个去解析看看哪个里边还有我需要的函数的,这就需要程序员自己指定了,指定的方式跟头文件搜索路径的指定方式类似,先给出实例如下:
gcc main.o -L/home/test/opencv/lib -L/home/test/highgui/lib -lcv -lhighgui
这条命令是什么意思?注意这里是main.o,而且没有-c参数,那么就意味告诉gcc我现在要做链接,gcc就会自动去调用链接器程序ld。
说到这里,聪明人应该已经明白了八九成了,上面的-L参数就是告诉链接器到哪里去找库,而-l参数就是告诉链接器去找哪个库。这里的-L参数很明显,没什么好说的,而-lcv是什么意思,链接器是怎么知道目标库的文件名的。这就有了一条隐含的潜规则,-l是表示要链接静态库,而在gcc工具族的实现中,规定-lxx指定去找的库文件名就一定是libxx.a,也就是说-lcv对应libcv.a,剩下的就不用再多说了。
vs之类的IDE中,就可能是通过project或option等菜单去指定,代替了这里的-L和-l,图形化,穿身皮而已,方便之余,对程序员隐藏了太多的技术细节,最后大家就越来越依赖于这些所谓的商业产品了。
说到这里,大家应该对自己的程序编译时,经常出现的这个函数找不到、那个变量找不到、这个库找不到、那个库版本不对之类的提示有办法了吧。
那么,当链接器在指定的搜索路径列表中找到了指定的链接库之后,就会去看看里边有没有我需要的函数,比如这里的highgui_gethandle和opencv_guihandle这两个函数,其中又有许多细节和技术,比如符号表、符号重载等,略过不说,说了更复杂,呵呵。如果能够找到这两个函数,链接器就会将main.o和这两个库文件、以及libc(这里边就是大家经常看到的malloc/free/strcpy/memcpy之类的)合成一个大文件,并将其中需要填充的跳转指令中的目的地址或外部变量地址填写完整,再指定程序的入口地址和指令,根据特定格式组织文件内容,最后就生成了一个完整的可执行程序。
遵循以上规则,完成了所有外部符号的定址和链接之后,这个可执行程序就可以执行了。
最后再说一点,就是静态库是如何生成的,libcv.a这样的文件究竟是什么东西。说白了,.a这种静态库,就是用ar命令把一堆.o文件按照特定格式组织起来,可以理解为类似tar或jar的打包过程,这样便于整个库的发布。所以也可以用ar命令从.a中解出所有的原始.o来,这一点都不奇怪。
关于程序的编译,其实还有更多内容可说,比如make、预处理、符号重载、符号表、词法、语法、语义、自动化、正则表达式等等等等,不过打字太累了,有机会再说吧。
如果上面这些你都真的完全懂了,那么理解c++、java、python、perl。。。这些东西的编译或解释过程对你来说应该都不再是问题,也绝不会再局限于各种各样的IDE中了,你应该能够一眼看穿vc、codeblock、eclipse之流的隐藏在花哨外衣之下的小花招,就能够做到返璞归真,发现IDE其实并不像你现象中的那么好。而如果能够做到这一步,那就没什么高级语言能够对你构成障碍了,在语言层面上,你已经几乎接近“无招胜有招”的境界。为什么是几乎呢,因为还差两点,那就是:
1、彻彻底底真真正正的理解计算机的工作原理、左值右值、内存地址、对象、多态等重要概念,要能够完全超脱于任何特定的语言去理解这些概念,这样才能理解python中为什么连函数都能是对象、而java的interface和c++的pure virtual class究竟是什么这类问题。
2、能够根据需要灵活设计并部署自己的程序设计语言,这就是境界了,对你来说,软件开发已经完全不再局限于具体的语言选择了,如果有必要,完全可以自己去实现一个语言。
当程序进入执行阶段之后,实际上是进入了一个更为广阔的外太空,这里有CPU、有操作系统、有内存管理、有动态库、有多进程多线程、有cache、有流水线、有栈、有堆、有用户态内核态、有段。。。。。。这些嘛,下次有机会再说吧,打字真是累啊。