so库方法的调用过程

  1. 写在前面

So库,又名共享库,是Linux下最常见的文件之一,也是Android中最常见的文件之一,是一种ELF文件。这种so库是程序运行时,才会将这些需要的代码拷贝到对应的内存中。但程序运行时,这些地址早已经确定,那程序引用so库中的这些代码地址如何确定呢,这就是这次要整理学习的内容,即so库的加载过程。

  1. 静动态库

为了让程序员更加优雅,更加高效的写程序,每一个程序的完成都是采用分而治之的方法,即同一个程序或者项目每个程序员都会完成不同的功能,有的功能是可复用的,而对于一些公共的可复用的功能,会使用库的形式来完成。比如我们在不同模块中多次用到了一个方法ar_public(),我们就可以将其包装到一个公共的文件里面,这样就如果其他的地方有调用就可以把引用这个公共文件从而调用这个方法,这样就有了静态库。简单来说静态库是链接的时候将库中所有的程序拷贝进来,这样即使在执行阶段吧对应的库删掉都没有关系,因为此时对应方法的真实地址已经被linker(有的地方说这里拷贝的是部分方法,这里不是,是全部方法)。但是这样做有一个问题就是这个库里面不单单会有我要的这个方法,还有其他的方法,这便会导致程序内存空间过大。

于是又有了一个动态库的概念,动态库,又称共享库链接的时候它只包含需要的函数引用表,只有在执行的时候那些需要的函数才能被拷贝到内存中,而且在操作系统使用的是虚拟内存,使得一份共享库驻留在内存中被多个程序使用,也同时节约了内存。

  1. 位置无关(PIC

大家都知道,可执行文件在执行期的时候内存地址已经都确定了,而上面说的只有执行时才会确定那些函数拷贝到内存中,那基于这个特点大家第一想道的实现就是像那些段一样预留一个空间,但是这样做的一个最大问题就是会造成空间浪费,我们可以readelf去看下so库中的地址情况,从图一来看和data相关的地址都不是绝对地址(由于程序的起始加载地址都是从data开始,所以data相关的头如果不是绝对地址则可以认为加载的地址不固定)。

图一

在静态共享库中,如果库里面的代码发生改变,重新加载进来之后,我们必须保证它放到修改前的位置  ,否则我们还要为它找一个新的位置。而我们对于这个修改之后希望将动态库编译成可以在任意位置加载无需linker进行修改,这个叫做位置无关代码即PIC,也就是生成so库的-fPIC的那个PIC(这个指令就表示生成位置无关代码)。那如果是so库中有变量呢,这个时候应该怎么去找这个变量的地址,这个主要是通过相对寻址来找的即在64位中%rip+rel,如图二就是一段典型的找data的方法。

图二

  1. 静态分析:

这个代码无关的特性具体是怎么实现的呢,我们先从静态的角度来分析下这些是怎么执行的。自己写一个引用一个最常见的printf函数(如图三),编译之后通过最常用的objdump –d 反编译,先看下print_banner()对应的反编译代码(如图四)

图三

图四

从main函数开始,跳转到print_baner,而print_baner里面最主要的方法是callq400400这个pc值,我们再看下601018(rip+200C12)内存的内容。

图五

 使用GDB的看下对应的值是多少,发现这个值是0x400406<printf@plt+6>,即执行后面pushq $0x0,然后再jmpq到<printf@plt-0x10>,即pushq到,然后再jmp到,然后再退出,这样整个printf的方法就执行完了。从静态代码来看只是几次jmp和push就完成了这个在so库中调用printf的操作,的确是这样,不过是这些jmp到的方法有自己的规范和名称,这就是GOT和PLT。

图六

  1. GOTPLT

首先我们说过这些是一个规范的有名称的,那么每一个可执行文件只要有这种so库的调用就一定会为他分配特定的存储空间。我们使用readelf看下(图7)

                              图7

 

关于GOT(),也叫全局偏移表,由于这个表和静态变量或者静态函数的相对地址是固定的,所以这个表的作用一个很大的作用是用来寻址。在上图中要注意的是.got的权限,是具有写权限的,也就是说这个在后面是会修改里面的值的,这个大家可以在对应的/proc下面去看下,这个地址是在data区的,关于这个是如何的写我们后面再看。

在反汇编代码中有一个pushq $0x0的操作,这个实际上是将printf对应的GOT数组中的条目方法入栈,且printf的条目偏移地址为0x0,对应GOT条目是一个共享库符号值保留的,而这里的0x0实际上是push第四个GOT条目,即GOT[3],下面是出自计算机系统圣经的CSAPP中GOT表的截图(图中的printf就是和本文so库中的printf条目一样)。

                                 图8

 第一个条目是指.dynamic段;第二个条目是指存放link_map结构的地址,动态链接器利用该地址来对符号进行解析,第三个条目是存放了指向动态链接器_dl_runtime_resolve()函数的地址,该函数用来解析共享库函数的实际符号地址,第四个条目就是printf的PLT[1]地址,也就是<printf@plt>的地址。

下面说下PLT,在图五的反汇编中可以看到有很多的带plt的方法,这些都是plt表中对应的条目。在图五中可以看到首先进到的是<printf@plt>地址,这些汇编很简单,前面也说过这里的pushq 0x0是将GOT[3]入队,执行完<printf@plt>之后,执行的jmpq到<printf@plt-0x10>中,这里也很指令简单,只不过操作数比较复杂,先说下pushq 0x601008,这里地址就是前面说的GOT[1],即这个程序的link_map,下一条jmpq 0x601010,则是GOT[2],即_dl_runtime_resolve()函数的地址。后续控制权就交给动态链接器了,解析出printf的地址。

对printf的解析完成之后,后面所有的对PLT条目中printf的调用都会直接跳转到printf中,而不是重新再进行这些跳转。通过watch 第一次jmp的值就可以看到,执行完成之后值以及由0x400406变化到0xFFFFFFFFF7A62800。

                          图9

东一句西一句啰嗦了这么多,其实总结起来就是对so库的里面方法的调用:

  1. 调用函数先跑到被调用的so库中方法的PLT(printf@plt)方法里面;
  2. PLT代码做一次到GOT中地址的间接跳转;
  3. GOT条目存放了指向PLT的地址,该地址存放在push指令中;
  4. push $0x0指令将printf() GOT条目的偏移量压栈;
  5. 最后的printf() PLT指令是指向PLT-0代码的jmp指令;
  6. PLT-0的第一条指令将GOT[1]的地址压栈,GOT[1]中存放了指向printf()的link_map结构的偏移地址;
  7. PLT-0的第二条指令会跳转到GOT[2]存放的地址,该地址指向动态链接器的_dl_runtime_resolve函数,_dl_runtime_resolve函数会通过把printf()函数的符号值加到.got.plt节对应的GOT条目中,来处理重定位。
  8. 下一次再做跳转的时候PLT条目会直接跳转到函数本身

 

在这里补充几点这写的是so库方法的加载过程,而如果是仅是变量的话是由/lib/ld-linux.so.2填充的。关于_dl_runtime_resolve方法也可以去网上找下源码和实现,还有一些关于重定位相关的内容,等下次再总结分析吧,这个发生在so库之前,还有就是有的时候可以利用GOT的写权限做一些劫持的工作。

 

 

 

 

 

 

 

 

https://www.cnblogs.com/cdcode/p/5551649.html

https://blog.csdn.net/ylcangel/article/details/18145155

https://www.jianshu.com/p/eca50b89a423

https://docs.oracle.com/cd/E24847_01/html/E22196/chapter6-14428.html

https://www.cnblogs.com/fellow1988/p/6158240.html

https://blog.csdn.net/linyt/article/details/51635768

https://blog.csdn.net/conansonic/article/details/54634142

https://www.cnblogs.com/xingyun/archive/2011/12/10/2283149.html

https://www.freebuf.com/articles/system/135685.html

https://bbs.pediy.com/thread-221821.htm

https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html

https://www.cnblogs.com/LittleHann/p/4244863.html

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章