最近打算抽空学习张绍文老师的《Android开发高手课》。
想要彻底理解本地监控APP内存的框架的实现原理。
发现理解起来都没有那么容易,在阅读代码的过程中,发现C++、linux、native hook、framework等方面的功底均有所不足。
张绍文老师说过:“看再多的文章,不去思考文章所讲的内容和意图也是没用的;思考再多,不去动手真正实践也是没用的。”
“把进阶的各个主题由点到线串联起来,但这背后必然少不了一些基础的、底层的知识进行支撑”。
这里就把空缺的知识进行补足。
动态链接器的自举
我们知道动态链接器本身也是一个共享对象,但是事实上它有一些特殊性:
对于普通共享对象文件来说,它的重定位工作由动态链接器来完成;
它也可以依赖于其他共享对象,其中的被依赖的共享对象由动态链接器负责链接和装载。
可是对于动态链接器本身来说,它的重定位工作由谁来完成?
它是否可以依赖于其他的共享对象?
这是一个“鸡生蛋,蛋生鸡”的问题,为了解决这种无休止的循环,动态链接器这个“鸡”必须有些特殊性。
- 首先是,动态链接器本身不可以依赖于其他任何共享对象;
- 其次是动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。
对于第一个条件我们可以人为地控制,在编写动态链接器时保证不使用任何系统库、运行库:
对于第二个条件,动态链接器必须在启动时有一段非常精巧的代码可以完成这项艰巨的工作而同时又不能用到全局和静态变量。这种具有一定限制条件的启动代码往往被称为自举(Bootstrap)。
这段注释写在白举代码的末尾,表示白举代码已经执行结束。“Now life is sane”,可以想象动态链接器的作者在此时大舒一口气,终于完成白举了,可以自由地调用各种函数并且随意访问全局变量了。
装载共享对象
完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table)。
然后链接器开始寻找可执行文件所依赖的共享对象,我们前面提到过“.dynamic”段中,有一种类型的入口是DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。
由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中。然后链接器开始从集合里取个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的 ELF 文件头和“.dynamic”段,然后将它相应的代码段和数据段映射到进程空间中。
如果这个 ELF 共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中。
如此循环直到所有依赖的共享对象都被装载进来为止,当然链接器可以有不同的装载顺序.
如果我们把依赖关系看作一个图的话,那么这个装载过程就是一个图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图,这取决于链接器,比较常见的算法一般都是广度优先的。
当一个新的共享对象被装载进来的时候,它的符号表会被合并到全局符号表中,所以当所有的共享对象都被装载进来的时候,全局符号表里面将包含进程中所有的动态链接所需要的符号。
重定位和初始化
当上面的步骤完成之后,链接器开始重新遍历可执行文件和每个共享对象的重定位表。
将它们的 GOT/PLT 中的每个需要重定位的位置进行修正。因为此时动态链接器已经拥有了进程的全局符号表,所以这个修正过程也显得比较容易,跟地址重定位的原理基本相同。
共享对象的init段
重定位完成之后,如果某个共享对象有“.init”段,那么动态链接器会执行“init”段中的代码,用以实现共享对象特有的初始化过程。
比如最常见的,共享对象中的 C++的全局/静态对象的构造就需要通过“init”来初始化。
共享对象的finit段
相应地,共享对象中还可能有“.finit”段,当进程退出时会执行“.finit”段中的代码,可以用来实现类似C++全局对象析构之类的操作。
进程的可执行文件的“.init”段
如果进程的可执行文件也有“.init”段,那么动态链接器不会执行它,因为可执行文件中的“.init”段和“.finit”段由序初始化部分代码负责执。
当完成了重定位和初始化之后,所有的准备工作就宣告完成了,所需要的共享对象也都已经装载并且链接完成了,这时候动态链接器就如释重负,将进程的控制权转交给程序的入口并且开始执行。
参考资料 –
《程序员的自我修养一一链接、装载与库》