符号的作用域

在C/C++里,我们可以将变量名和函数名统称为符号。符号的作用域,是指该符号的有效作用范围。

对于变量来说,常见的有三种:局部变量、静态变量和全局变量。而函数则有静态函数和全局函数之分。

在学习编程语言时,我们通常从源文件的角度来理解,比如静态变量或者静态函数只能被它所在文件的其它函数访问或者调用,无法在其它文件里引用。但是#include的存在使得问题复杂了,比如在头文件里定义的静态函数当然可以在包含它的文件内引用。

由于动态共享库的存在,全局的概念也不像我们通常想像的那么简单。实际上,一个定义在动态共享库里的全局变量,可以被执行程序或者其它动态共享库引用,甚至一个定义在执行程序里面的全局变量,也可以被它链接的动态共享库引用。所以,全局的意思,是指整个进程的范围,而不仅仅是可执行程序了。

在这些由语言提供的作用域不够用的时候,我们还可以考虑使用GCC提供的可见度(visibility)这个作用域,通过把可见度设置为隐藏,全局变量的作用域被限制在其所在的执行程序或者动态共享库内。设置方法有很多种,比如使用pragma或者attribute在源代码里指定、使用命令行选项,以及在链接脚本里面指定。具体使用方法参见gcc手册。

其实,从程序逻辑的角度来说,我们根本就不需要这些区分。引入这么多概念,都是为了解决开发中的实际问题。尤其是在引入了动态共享库和动态链接之后,全局符号导致问题更加复杂,因此,如果没有特殊需要,尽可能缩小符号的作用域。

Android dynamic linker

/system/bin/linker是Android的dynamic linker。尽管它是个很小的程序,但它的实现里依然有不少值得学习的地方。

由于dynamic linker要在libc.so被载入之前工作,因此没有malloc可用。它使用了两个算法,为不同目的分配(并管理)内容。

一类内存是为了载入动态链接库的代码段和数据段,这块内存使用buddy memory allocation算法管理。虽然是个简洁的算法,但如果是第一次接触,想通过阅读代码的方式理解它的工作原理,还是有些难度的。幸好维基百科上的条目解释得非常清楚,在此基础上,一些实现上的小技巧也就好理解了。

另一类是使用array + freelist的方式管理soinfo数据。因为数据类型相同,可以使用静态分配的数组;由于soinfo数据可能动态添加和删除,使用array + freelist方式,可以在O(1)内找到空余的项目。

还有一个很神奇的地方,即如果要使用dlopen/dlclose之类的函数,需要链接libdl.so这个动态库,然而在运行时调用的,确是dynamic linker里面的实现。

简单地说,提供libdl.so是为了让linker知道,dlopen/dlclose这几个函数是存在的,在静态链接时不要报undefined reference之类的错误。在实际调用的时候,dynamic linker把这几个symbol的值解析为自己的实现,以达到代码复用,避免重复的目的。在具体实现里,libdl.so对应的soinfo,并不是像其它动态链接库一样,是通过分析对应的文件得来的,而是dynamic linker硬造出来的,使得dlopen/dlclose等symbol解析为dynamic linker内的实现。