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

 

 

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