日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

计算机科学基础知识(四): 动态库和位置无关代码

發(fā)布時間:2023/12/2 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 计算机科学基础知识(四): 动态库和位置无关代码 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

一、前言

本文主要描述了動態(tài)庫以及和動態(tài)庫有緊密聯(lián)系的位置無關(guān)代碼的相關(guān)資訊。首先介紹了動態(tài)庫和位置無關(guān)代碼的源由,了解這些背景知識有助于理解和學(xué)習(xí)動態(tài)庫。隨后,我們通過加-fPIC和不加這個編譯選項分別編譯出兩個relocatable object file,看看編譯器是如何生成位置無關(guān)代碼的。最后,我們自己動手編寫一個簡單的動態(tài)庫,并解析了一些symbol Visibility、動態(tài)符號表等一些相關(guān)基本概念。

本文中的描述是基于ARM MCU,GNU/linux平臺而言的,本文是個人對動態(tài)庫的理解,如果有錯誤,請及時指出。

?

二、背景介紹

位置無關(guān)代碼實際上是和動態(tài)庫概念緊密聯(lián)系在一起的,本章首先描述為何會提出動態(tài)庫的概念,然后解釋動態(tài)庫為何需要編譯成PIC的代碼。

1、為何會提出動態(tài)庫的概念?

引入靜態(tài)庫后,解決了一些問題,但是仍然存在下面的弊端:

(1)任何對靜態(tài)庫的升級都需要rebuild(或者叫做relink)的過程

(2)通用的函數(shù)(例如標準IO函數(shù)scanf和printf)存在于各個靜態(tài)鏈接的程序中,導(dǎo)致編譯后的靜態(tài)可執(zhí)行程序的size比較大,在各個可執(zhí)行程序中,這些通用的函數(shù)代碼是重復(fù)的,占用了磁盤和內(nèi)存資源

正因為如此,動態(tài)庫和動態(tài)鏈接的概念被提出來來解決這些問題。動態(tài)庫也是一種ELF格式的對象文件,在運行的時候,它可以被加載到任何的地址執(zhí)行。

2、動態(tài)庫為何需要編譯成PIC的代碼?

無論是動態(tài)庫還是靜態(tài)庫,其本質(zhì)都是代碼共享。對于靜態(tài)庫,其代碼以及數(shù)據(jù)都是在各個靜態(tài)鏈接的可執(zhí)行文件中有一份copy,所有符號的地址已經(jīng)確定,因此在loading的時候,OS會比較輕松。不過這種代碼共享無法在run time的時候共享代碼,從而導(dǎo)致了資源的浪費。當然,它的好處就是簡單、速度快(無需dynamic linker來重定位符號)。對于靜態(tài)編譯,static linker將多個編譯單元(.o文件和庫文件)整合成一個模塊,因此,進入run time,實際上只有一個執(zhí)行模塊。對于動態(tài)鏈接,在run time的時候,除了可執(zhí)行文件這個模塊,該可執(zhí)行文件所依賴的各個動態(tài)庫也是一個個的運行模塊,這時候,可執(zhí)行文件調(diào)用動態(tài)庫的符號實際上是就是需要引用其他運行模塊的符號了。對于可執(zhí)行文件而言,loader將其加載到哪個地址并不關(guān)鍵,反正每個進程都有自己獨一無二的地址空間,可執(zhí)行文件可以mapping到各自virtual memory space的相同地址也無妨,不過對于動態(tài)庫模塊而言,就有些麻煩了。如果我們不將動態(tài)庫編譯成PIC的也就是意味著loader一定要把動態(tài)庫加載到某個特定的地址(該地址編譯的時候就確定了)上它才可以正確的執(zhí)行。假設(shè)我們有A B C D四個動態(tài)庫,假設(shè)程序P1依賴A B兩個動態(tài)庫,P2依賴C D兩個動態(tài)庫,那么A B和C D的動態(tài)庫的加載地址有重疊也沒有關(guān)系,P1和P2可以同時運行。但是如果有一個新的程序P3依賴A B C D四個動態(tài)庫,那么前面為動態(tài)庫分配的加載地址就不能正常工作了。當然,重新為這四個動態(tài)庫分配load address(讓地址不重疊)也是ok的,但是這樣一來,P1雖然沒有使用C D這兩個動態(tài)庫,但是P1的地址空間還是要保留C D動態(tài)庫的那段地址,對于地址這樣寶貴資源,這么浪費簡直是暴殄天物。更重要的是:這樣的機制實際上對進程虛擬地址的管理就變得非常復(fù)雜了,假設(shè)A B C D是分配了一段連續(xù)的地址,如果C動態(tài)庫更新了,size變大了,原本分配的地址空間不夠了,怎么辦?我們必須再尋找一個新的地址段來加載C動態(tài)庫。如果系統(tǒng)只有四個動態(tài)庫起始還是OK的,如果動態(tài)庫非常非常多……怎么辦?更糟的是:不同的系統(tǒng)使用不同的動態(tài)庫,管理起來更令人頭痛

最好的方法就是將動態(tài)庫編譯成PIC(Position Independent Code),也就是說動態(tài)庫可以被加載到任何地址并正確運行。

?

三、動手實踐:觀察PIC的.o文件的反匯編結(jié)果

1、源代碼foo.c

#include <stdio.h>
int xxx = 0x1234;
int yyy;
int foo(void)
{
? yyy = 0x5678;
? printf("xxx=%x yyy=%x\n", xxx, yyy);
? return xxx;
}

2、觀察foo.o文件中的符號定位信息

使用arm-linux-gcc –c foo.c將source code編譯成relocatable file。我們來看看正文段中的relocation信息:

00000030? 00000e1c R_ARM_CALL??????? 00000000?? printf
00000044? 00000f02 R_ARM_ABS32?????? 00000004?? yyy
0000004c? 00000c02 R_ARM_ABS32?????? 00000000?? xxx

R_ARM_ABS32是一種ARM平臺上的absolute 32-bit relocation,在32 bit的ARM平臺上,這種重定位的方式是沒有任何約束的,可以將地址重定位到4G地址空間的任何位置。具體實現(xiàn)方式需要參考反編譯的匯編代碼,我們來看看匯編代碼是如何訪問yyy這個數(shù)據(jù)的:

……
?? 8:?? e59f2034??????? ldr???? r2, [pc, #52]?? ; 44 <.text+0x44>
?? c:?? e59f3034??????? ldr???? r3, [pc, #52]?? ; 48 <.text+0x48>
? 10:?? e5823000??????? str???? r3, [r2]
……
? 44:?? 00000000??????? .word?? 0x00000000
? 48:?? 00005678??????? .word?? 0x00005678

具體做法非常的簡單,在這段代碼的后面(也是.text section的一部分)給出一個32-bit的跳板memory(上面黑色加粗的那一行),位于<.text+0x44>,這個memory用于保存yyy符號的運行地址。由于同在一個正文段,因此它們之間的offset是確定的,使用“l(fā)dr???? r2, [pc, #52] ”這樣的PC-relative的訪問指令可以訪問到y(tǒng)yy變量的地址,通過“str???? r3, [r2]”可以將yyy變量的內(nèi)容保存到r3中。

下面我們我們再看看函數(shù)符號的訪問。R_ARM_CALL這種類型的重定位信息主要用于函數(shù)調(diào)用的(對應(yīng)的ARM指令就是BL和BLX),實現(xiàn)也很簡單,如下:

……
? 30:?? ebfffffe??????? bl????? 0

……

BL指令是一個PC-relative指令,會將控制權(quán)交給相對于當前PC值的一個地址上去(同時設(shè)定lr寄存器),bl這條指令的0~23個bit(用imm24表示))用來表示相對與PC的偏移地址,最終跳轉(zhuǎn)到的地址是PC+(imm24在低位添加00b,然后做符號擴展),也就是正負32M的區(qū)域(注意:BL不能任意跳轉(zhuǎn)4G范圍的地址空間)。之所以添加兩個0是因為offset地址總是4字節(jié)對齊的。

對于靜態(tài)鏈接,很簡單,雖然那些重定位信息在正文段,但是沒有關(guān)系,在程序loading之前,static linker可以修改正文段的內(nèi)容。

3、編譯PIC的.o文件并觀察

編譯成位置無關(guān)代碼也就意味著這段代碼多半是動態(tài)庫的一部分,需要動態(tài)加載到一個編譯時候未知的地址上。也就是說上文中使用的方法已經(jīng)不行了,編譯時候符號的地址還是不確定的,因此static linker無法將地址填入到.text section中。在loading的時候,雖然知道了符號runtime address,但是正文段是read only的,也無法修改。怎么辦呢?我們來一起看看程序如何實現(xiàn)。

使用arm-linux-gcc -fPIC–c foo.c將source code編譯成relocatable file。我們來看看正文段中的relocation信息:

Relocation section '.rel.text' at offset 0x4e0 contains 5 entries:
Offset???? Info??? Type??????????? Sym.Value? Sym. Name
00000048? 00000f1b R_ARM_PLT32?????? 00000000?? printf
00000064? 00001019 R_ARM_BASE_PREL?? 00000000?? _GLOBAL_OFFSET_TABLE_
00000068? 0000111a R_ARM_GOT_BREL??? 00000004?? yyy
00000070? 00000d1a R_ARM_GOT_BREL??? 00000000?? xxx

我們首先看看_GLOBAL_OFFSET_TABLE_這個符號,看起來和傳說中的GOT(Global Offset Table)有關(guān)。那么什么是GOT呢?它有什么作用呢?我們先回到c代碼,思考一下對xxx符號的訪問。這時候,我們能確定xxx的runtime address嗎?當然不能,離loading還遠著呢,這時候我們能確定訪問xxx的代碼(.text section中)和xxx符號(.data section)之間offset嗎?也不能,因為還有多個.o文件最后被link成一個動態(tài)庫。怎么辦?我們必須借助一個橋梁來讓數(shù)據(jù)訪問變得Position Independent,這個橋梁就是GOT(Global Offset Table)。當然GOT必須是可讀可寫的,因為后續(xù)在run time的時候還要修改其內(nèi)容。_GLOBAL_OFFSET_TABLE_就是定義了GOT在memory中的位置。因此64那個位置的重定位信息和GOT相關(guān),R_ARM_BASE_PREL這個relocation type則說明這個重定位信息說明該位置保存了GOT offset。由于目前還是.o文件,還沒有確定最后GOT信息,因此需要這個relocation的信息,一旦完成動態(tài)庫的編譯,這個relocation entry就不需要了。

R_ARM_GOT_BREL這個type說明這個重定位信息是一個描述GOT entry和GOT起始位置的offset。例如:yyy這個符號還需要relocation,那么它的relocation位于正文段offset是0x68的位置,其內(nèi)容保存了yyy符號在GOT entry中的地址和GOT起始位置的偏移。OK,有了這些鋪墊,可以看看程序?qū)yy這個數(shù)據(jù)是如何訪問的:

……
?? c:?? e59f4050??????? ldr???? r4, [pc, #80]?? ; 64 <.text+0x64>
? 10:?? e08f4004??????? add???? r4, pc, r4 ---------------獲得GOT的起始位置的地址
? 14:?? e59f304c??????? ldr???? r3, [pc, #76]?? ; 68 <.text+0x68> -----獲得yyy符號在GOT中的offset
? 18:?? e7942003??????? ldr???? r2, [r4, r3] --------------獲得yyy符號的runtime address
? 1c:?? e59f3048??????? ldr???? r3, [pc, #72]?? ; 6c <.text+0x6c>
? 20:?? e5823000??????? str???? r3, [r2] ---------------設(shè)定yyy符號的內(nèi)容
……
? 64:?? 0000004c??????? .word?? 0x0000004c-----GOT offset
? 68:?? 00000000??????? .word?? 0x00000000-----yyy的地址在GOT中的偏移
? 6c:?? 00005678??????? .word?? 0x00005678

由此可見,PIC的代碼對全局數(shù)據(jù)的訪問都是通過GOT來完成的,從而做到了位置無關(guān)。

?

四、動手實踐:觀察動態(tài)庫的反匯編結(jié)果

1、如何生成動態(tài)庫?

我們準備動手做一個動態(tài)庫了,先看source code,一如既往的簡單(注意:我們不建議導(dǎo)出動態(tài)庫中的數(shù)據(jù)符號,這里主要是為了描述動態(tài)庫的概念而這么做的):

int xxx = 0x1234;

int yyy;
int foo(void)
{
? yyy = 0x5678;
? return xxx;
}

通過下面的命令可以編譯出一個libfoo的動態(tài)庫:

arm-linux-gcc -shared -fPIC -o libfoo.so foo.c

-shared告知gcc生成share object文件,而-fPIC則告訴gcc請生成位置無關(guān)代碼。

2、觀察符號表的變化

我們在relocatable object中已經(jīng)對符號表進行了描述:對靜態(tài)編譯的程序而言,.o文件中的符號表一是要對外宣稱自己定義了哪些符號,另外一個是向外宣布自己引用了哪些符號,需要其他模塊來支持。有了這些信息,static linker才能整合各個relocatable object file中的資源,互通有無,最后融合成一個靜態(tài)的可執(zhí)行程序。因此,實際上,對于靜態(tài)的可執(zhí)行程序,在加載執(zhí)行的時候,其符號表已經(jīng)沒有任何意義了(不過可以方便debug),對于CPU而言,其執(zhí)行就是要知道地址就OK了(靜態(tài)編譯程序所有的符號都已經(jīng)定位了),符號什么的它不關(guān)心,因此,實際上符號表可以刪除。如果你愿意,你可以通過strip命令來進行實驗,看看tripped和not stripped的elf文件有什么不同。

然而,計算機科學(xué)的發(fā)展是不斷前進的,當有了動態(tài)庫之后,符號表會怎樣呢?我們自己可以動手生成一個動態(tài)鏈接的可執(zhí)行程序或者動態(tài)庫并觀察其中的符號表信息(恰好上一節(jié)已經(jīng)生成一個libfoo.so,就它吧)。通過readelf工具,我們可以看到,動態(tài)鏈接的程序中有兩個符號表,一個是大家之前就熟悉的.symtab section(我們稱之符號表),另外一個就是.dynsym section(動態(tài)符號表)。這兩個符號表都有自己對應(yīng)的string table,分別是.strtab和.dynstr section。

.symtab section我們前面的文章都有描述,為何又增加了一個.dynsym section呢?我們先假設(shè)我們編譯出來的動態(tài)庫只有一個符號表,那么當使用strip命令刪除符號表以及對應(yīng)的字符串表之后會怎樣?當其他程序調(diào)用該動態(tài)庫提供的接口API函數(shù)的時候,dynamic linker還能找到對應(yīng)的API函數(shù)符號嗎?當然不行,符號表都刪除了還想怎樣。靜態(tài)鏈接的程序之所以可以strip掉符號表以及對應(yīng)的字符串表那是因為程序中所有符號都已經(jīng)塵埃落定(所有符號已經(jīng)重定位),因此strip后也毫無壓力,但是動態(tài)鏈接的情況下,程序中的沒有定位的符號以及動態(tài)庫中宣稱的符號都需要有一個特別的符號表(是正常符號表的子集)來保存動態(tài)鏈接符號的信息,這個表就是動態(tài)連接符號表(.dynsym section)。

OK,最后總結(jié)一下:符號表(.symtab section)是指導(dǎo)static linker工作的,運行的時候可以不需要。動態(tài)符號表(.dynsym section)是給dynamic linker用的,程序(或者動態(tài)庫)運行的時候,dynamic linker用動態(tài)符號表的信息來定位符號。

3、Binding Property和Symbol Visibility

我們在講述relocatable object file的時候已經(jīng)給出了binding屬性(binding property)的解釋。一個符號可能有g(shù)lobal、local和weak三種binding property。這個binding property主要是被static linker用來進行.o之間的符號解析(symbol resolution)的。Bind屬性之外還有一個屬性我們一直沒有描述(通過readelf觀察符號表的時候,該屬性對應(yīng)列的名字是Vis的那個),我們稱之Symbol Visibility或者符號的可見性。之所以前面的文章中沒有描述主要是因為Symbol visibility是和動態(tài)庫以及動態(tài)鏈接相關(guān)的。

當引入動態(tài)連接和動態(tài)庫的概念之后,代碼和數(shù)據(jù)的共享會變得復(fù)雜一些。和binding property不一樣,Symbol Visibility是針對運行模塊(動態(tài)鏈接的可執(zhí)行程序或者動態(tài)庫)之間的相互引用。例如我們有A.o B.o C.o三個編譯模塊,static linker將這三個.o文件link成一個libABC.so文件。A.o模塊要調(diào)用B.o中的一個函數(shù)bb,那么bb函數(shù)就一定需要是一個GLOBAL類型的,但是bb函數(shù)并不是動態(tài)庫libABC.so的接口API(或者稱之export symbol),也就是說,為了更好的封裝性,我們希望bb這個函數(shù)對外不可見,dynamic linker看不到這個符號,bb不參與動態(tài)符號解析。如果動態(tài)庫導(dǎo)出所有的符號,那么,在動態(tài)鏈接的時候,符號沖突的可能性就非常的大,特別是對于那些大型項目,可能該項目涉及的每個動態(tài)庫都是由不同team負責(zé)的。除了模塊的封裝性之外,Symbol Visibility也是和程序的性能有關(guān)。如果導(dǎo)出太多的符號,除了占用更多的內(nèi)存,還意味著增加loading time和dynamic linking time。

看,不控制Symbol Visibility的危害還是很大D,這時候閱讀本文的你估計一定會問:那么控制Symbol Visibility哪家強呢?我推薦使用大殺器static關(guān)鍵字,簡單,實用,人人會。給function或者全局變量加上static關(guān)鍵字,別說是對dynamic linker(運行模塊之間的引用)進行了限制,就是static linker(.o 文件之間的引用)也是拿他毫無辦法。當然,缺點也很明顯:不能在動態(tài)庫的多個.o之間共享。在這種場景下,我們需要求助其他方法了,對于gcc,我們可以用下面的方法:

符號類型 符號名字 __attribute__ ((visibility ("xxx")));

其中xxx指明了該符號的Symbol Visibility屬性,Symbol Visibility屬性可以設(shè)定為:

(1)DEFAULT(雖然命名是default,但是有些public的味道)。該屬性的符號被導(dǎo)出,該符號可以被其他運行模塊訪問

(2)PROTECTED。同DEFAULT,不過該符號不能被overridden。也就是說,如果一個動態(tài)庫中的符號是PROTECTED,那么動態(tài)庫中的代碼訪問該符號是享有優(yōu)先權(quán)的,即便其他的運行模塊定義了同名的符號。

(3)HIDDEN。HIDDEN的符號不會被導(dǎo)出,不參與動態(tài)鏈接。

(4)INTERNAL。其他運行模塊不能訪問該類型的符號。

回到上一節(jié)描述的這個source code,其中有三個符號:xxx、yyy和foo,都是被導(dǎo)出的,可以被其他的模塊調(diào)用。如果你有興趣,可以自己試著控制符號的visibility,看看效果如何。

4、動態(tài)庫文件的加載

libfoo這個shared object elf文件的加載是根據(jù)Program header進行的。在ELF file header中可以看到該動態(tài)庫共計4個program header,如下:

Program Headers:
? Type?????????? Offset?? VirtAddr?? PhysAddr?? FileSiz MemSiz? Flg Align
? LOAD?????????? 0x000000 0x00000000 0x00000000 0x005c0 0x005c0 R E 0x8000
? LOAD?????????? 0x0005c0 0x000085c0 0x000085c0 0x00118 0x00120 RW? 0x8000

? DYNAMIC??????? 0x0005cc 0x000085cc 0x000085cc 0x000e0 0x000e0 RW? 0x4
? GNU_STACK????? 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW? 0x4

帶有LOAD標記的那些program header entry會被mapping到進程地址空間上去。第一項是code segment,由于動態(tài)庫的代碼是PIC的,因此其VirtAddr和PhysAddr都是0,表示可以運行在任意地址上。第二項是data segment,在實際中,動態(tài)庫的code和data segment都是連續(xù)加載的,因此,如果code segment的run time地址是0的話,那么data segment的地址應(yīng)該是0x5c0,不過由于code segment是0x8000對齊的,因此data segment的地址被設(shè)定為0x85c0。當然,如果實際該動態(tài)庫被加載到了進程的X虛擬地址上的話,data segment的runtime地址應(yīng)該是X + 0x85c0。對于動態(tài)庫而言,其code segment可以被多個進程共享,也就是說,雖然code segment被加載到不同的進程的不同的虛擬地址空間,但是其物理地址是一樣的,只不過各個進程設(shè)定自己的page table就OK了。對于code segment,各個進程都有自己的副本,不可能共享的。

沒有LOAD標記,這說明第三項和第四項(DYNAMIC這個entry下一節(jié)描述)都是和進程加載無關(guān)的(不占用進程虛擬地址空間)。GNU_STACK是用來告訴操作系統(tǒng),當加載ELF文件的時候,如果控制stack的屬性。這是和系統(tǒng)安全相關(guān)(通過stack來攻擊系統(tǒng)),我們在relocatable object file的時候已經(jīng)描述,這里略過(https://wiki.gentoo.org/wiki/Hardened/GNU_stack_quickstart中有更詳細的信息)。

5、如何找到動態(tài)鏈接的信息

和靜態(tài)鏈接的可執(zhí)行序程序相比,DYNAMIC那個program header entry是動態(tài)庫文件特有的。既然是動態(tài)庫,當然要參與動態(tài)鏈接的過程,因此動態(tài)庫的ELF文件需要提供一些dynamic linking信息給OS以及dynamic linker,DYNAMIC那個program header entry就是起這個作用的。dynamic segment只包含了一個section,名字是.dynamic。需要注意的是.dynamic section也是data segment的一部分被加載到了進程的地址空間中。下面我們仔細看看libfoo.so的Dynamic section的內(nèi)容:

Dynamic section at offset 0x5cc contains 24 entries:
? Tag??????? Type???????????????????????? Name/Value
0x00000001 (NEEDED)???????????????????? Shared library: [libc.so.6]
0x0000000c (INIT)?????????????????????? 0x460
0x0000000d (FINI)?????????????????????? 0x5ac
0x00000019 (INIT_ARRAY)???????????????? 0x85c0
0x0000001b (INIT_ARRAYSZ)?????????????? 4 (bytes)
0x0000001a (FINI_ARRAY)???????????????? 0x85c4
0x0000001c (FINI_ARRAYSZ)?????????????? 4 (bytes)
……

我們先不著急看具體的各個項次的含義,我們先看看section table中對.dynamic的描述:

[Nr] Name????????????? Type??????????? Addr???? Off??? Size?? ES Flg Lk Inf Al
……
[16] .dynamic????????? DYNAMIC???????? 000085cc 0005cc 0000e0 08? WA? 3?? 0? 4

由此可知,.dynamic section是有Entry size的,也就是說,這個section中的內(nèi)容是按照8個byte形成一個個的entry,下面的這個Elf32_Dyn(對于64bit的CPU,對應(yīng)是Elf64_Dyn)數(shù)據(jù)結(jié)構(gòu)可以解析這8個bytes:

typedef struct {
? Elf32_Sword??? d_tag;??????????? /* Dynamic entry type */
? union??? {
????? Elf32_Word d_val;??????????? /* Integer value */
????? Elf32_Addr d_ptr;??????????? /* Address value */
??? } d_un;
} Elf32_Dyn;

d_tag定義dynamic entry的類型,而根據(jù)tag的不同,附加數(shù)據(jù)d_un可能是一個整數(shù)類型d_val,其含義和具體的tag相關(guān),或者附加數(shù)據(jù)是一個虛擬地址d_ptr。了解了這些信息后,我們可以來解析.dynamic section的具體內(nèi)容了。

dynamic tag是NEEDED這個entry標識libfoo這個動態(tài)庫依賴的object文件。ldd工具可以打印出給定程序或者動態(tài)庫的share library的依賴關(guān)系,本質(zhì)上ldd就是應(yīng)用了NEEDED這個tag信息。對于libfoo.so這個動態(tài)庫,它會依賴libc.so.6這個動態(tài)庫,也就是c庫了。不過,你可能會奇怪,我們c代碼沒有引用任何的c庫函數(shù)啊,怎么會依賴c庫呢?其實這和靜態(tài)鏈接的hello world程序類似,我們在講靜態(tài)鏈接的時候已經(jīng)描述了,你可以在build libfoo.so的時候加上-v的選項,這時候你可以從不斷滾動的屏幕信息中找到答案:你的c代碼不是一個人在戰(zhàn)斗。你可以可以從.text中看到一些端倪,例如.text中有一個call_gmon_start的函數(shù),這個函數(shù)本來就不是我們的c代碼定義的符號,我們的c代碼只定義了foo函數(shù)以及xxx、yyy這兩個變量符號。本來以為在.text中只有foo的定義,call_gmon_start是從那里冒出來的呢?實際上這個符號定義在crti.o中(在最后生成libfoo.so的動態(tài)庫的時候,有若干個crt*.o參與其中)。libfoo.so定義了call_gmon_start這個函數(shù),那么什么時候調(diào)用呢?這又回到了linux下動態(tài)庫的結(jié)構(gòu)這個問題上:雖然動態(tài)庫定義了一些符號(函數(shù)或者全局變量),但是,我們希望在調(diào)用這些函數(shù)或者訪問這些變量之前,先執(zhí)行一些初始化的代碼(這發(fā)生在動態(tài)庫加載的時候,dlopen的時候,由dynamic linker負責(zé))。這些初始化代碼被放到一些特殊的section(例如.init),libfoo.so的.init section的反匯編結(jié)果如下:

00000460 <_init>:
460:??? e52de004???? str??? lr, [sp, #-4]!
464:??? e24dd004???? sub??? sp, sp, #4??? ; 0x4
468:??? eb000009???? bl??? 494 -----以上來自crti.o

這里可以存放動態(tài)庫自己定義的初始化函數(shù),當然我們這么簡單的動態(tài)庫當然沒有。
46c:??? e28dd004???? add??? sp, sp, #4??? ; 0x4------以下來自crtn.o
470:??? e8bd8000???? ldmia??? sp!, {pc}

INIT(對應(yīng).init section)到FINI_ARRAYSZ這些entry都是和該動態(tài)庫的初始化和退出函數(shù)相關(guān)的。當dynamic linker open這個動態(tài)庫的時候(dlopen)會執(zhí)行初始化函數(shù),當dynamic linker close這個動態(tài)庫的時候(dlclose)會執(zhí)行退出函數(shù)。還有很多dynamic tag,這里主要關(guān)注結(jié)構(gòu),暫且略過,一言以蔽之,dynamic linker可以通過.dynamic section找到所有它需要的動態(tài)鏈接信息。

6、動態(tài)庫中訪問全局變量

我們來看看foo中如何訪問yyy這個符號的。yyy的重定位信息如下(.rel.dyn section中):

000086bc? 00000815 R_ARM_GLOB_DAT??? 000086dc?? yyy

符號表中可以查到GOT的位置:

56: 000086ac???? 0 OBJECT? LOCAL? HIDDEN? ABS _GLOBAL_OFFSET_TABLE_

當然0x86ac是一個offset,并不是run time address,畢竟只有l(wèi)oading后才知道其具體的地址信息。如果該動態(tài)庫被loading到address_libfoo,那么GOT實際應(yīng)該位于address_libfoo+0x86ac。而yyy符號的地址在address_libfoo+0x86bc,dynamic linker會在適當?shù)臅r間把真實的yyy符號的地址寫入到這個位置的。由此可見,在offset是0x000086bc(GOT中的某個entry)的位置上保存了yyy符號的重定位信息。

……
568:??? e59f202c???? ldr??? r2, [pc, #44]??? ; 59c <.text+0x108> ---獲取GOT到當前指令的偏移
56c:??? e08f2002???? add??? r2, pc, r2 --------------獲取GOT的絕對地址
570:??? e59f3028???? ldr??? r3, [pc, #40]??? ; 5a0 <.text+0x10c> ---獲取yyy在GOT中的偏移
574:??? e7921003???? ldr??? r1, [r2, r3] --------------從GOT entry找到y(tǒng)yy的絕對地址
578:??? e59f3024???? ldr??? r3, [pc, #36]??? ; 5a4 <.text+0x110> ---r3被賦值0x5678
57c:??? e5813000???? str??? r3, [r1] ---------------給yyy賦值
……
59c:??? 00008138???? .word??? 0x00008138 -----------指令到GOT的偏移
5a0:??? 00000010???? .word??? 0x00000010 -----------yyy符號在GOT中的offset
5a4:??? 00005678???? .word??? 0x00005678
5a8:??? 00000018???? .word??? 0x00000018

雖然不知道GOT的絕對地址,但是在靜態(tài)鏈接的時候,代碼段的代碼和GOT的偏移是已經(jīng)確定的(loading的時候是按照program header中的信息進行l(wèi)oading,code segment和data segment是連續(xù)的),因此,在指令中可以通過59c這個橋梁獲取GOT的首地址,加上entry偏移就可以獲取指定符號的GOT入口地址,從該GOT入口地址中可以取出runtime的符號的絕對地址。

總結(jié)

以上是生活随笔為你收集整理的计算机科学基础知识(四): 动态库和位置无关代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。