32位elf格式中的10種重定位型別

Editor發表於2018-08-20

32位elf格式中的10種重定位型別

我之前在另外一個論壇發過一篇這樣的主題,但是當時還剩下一些疑問沒有想清楚,最近利用業餘時間再次學習了ellf格式,針對10種重定位型別重新做了總結,希望分享出來,可以帶給初學者一點幫助。    首先需要知道的是,一個程式從原始碼到被執行,當中經歷了3個過程:

編譯:將.c檔案編譯成.o檔案,不關心.o檔案之間的聯絡;

靜態連結:將所有.o檔案合併成一個.so或a.out檔案,處理所有.o檔案節區在目標檔案中的佈局;

動態連結:將.so或a.out檔案載入到記憶體,處理載入檔案在的記憶體中的佈局。

32位elf格式中的10種重定位型別

c程式中引用全域性變數的語句,經過編譯得到的機器碼會包含一個地址值部分,機器碼執行時,該值必須為變數在記憶體中的絕對地址,呼叫函式的語句,經過編譯得到的機器碼也包含一個地址值部分,機器碼執行時,該值必須為記憶體中函式地址與下一條指令地址的偏移。但是在編譯、靜態連結,甚至動態連結之後,該地址值部分可能暫時無法滿足最終要求,從而必須相應設定一個重定項,要求後續過程對該值進行修改,重定項一方面標記了地址值的位置,另一方面提供了計算正確地址值的方法和計算引數。

    對於區域性變數的使用,由於程式執行時,esp暫存器儲存的一定是棧頂的記憶體地址,那麼從邏輯上講,編譯階段就可以確定所有區域性變數執行時的記憶體地址,所以不需要設定重定項。

    另外,elf格式中設計了10種不同的重定位型別,是由於不同場合,對地址值進行重新計算的方法和引數不同:

· 引用變數的指令中,需要使用變數的絕對地址,而函式呼叫指令,需要使用函式與下一條指令地址的相對地址。

· -fPIC編譯選項,可以決定實體記憶體中的同一份.so映象,是否可以被多個程式共享。

· 靜態ld是將.o檔案按節區"撕開",將各個.o檔案中相同型別的節,合併為.so或a.out檔案中的一個段,而動態ld則是維持.so檔案"原狀",合併到程式的虛擬記憶體空間。

32位elf格式中的10種重定位型別

具體來講,以下表格包含了生成各種型別重定項的情況:

· 32位elf格式中的10種重定位型別

· 全域性變數,在不加-fPIC編譯生成的.o檔案中,每個引用處對應一個R_386_32重定位項,非static全域性變數,在不加-fPIC編譯生成的.so檔案中,每個引用處對應一個R_386_32重定位項;

· static全域性變數,在不加-fPIC編譯生成的.so檔案中,每個引用處對應一個R_386_RELATIVE重定位項;

· 非static全域性變數,在加-fPIC編譯生成的.o檔案中,每個引用處對應一個R_386_GOT32重定位項;

· static全域性變數,在加-fPIC編譯生成的.o檔案中,每個引用處對應一個R_386_GOTOFF重定位項;

· 非static全域性變數,在加-fPIC編譯生成的.so檔案中,每個引用處對應一個R_386_GOLB_DAT重定位項;

· a.out中利用extern引用.so中的變數,每個引用處對應一個R_386_COPY重定位項;

· 非static函式,在不加-fPIC編譯生成的.o和.so檔案中,每個呼叫處對應一個R_386_PC32重定位項;

· 非static函式,在加-fPIC編譯生成的.o檔案中,每個呼叫處對應一個R_386_PLT32重定位項;

· 非static函式,在加-fPIC編譯生成的.so檔案中,每個呼叫處對應一個R_386_JMP_SLOT重定位項;

· 全域性變數,在加-fPIC編譯生成的.o檔案中,會額外生成R_386_PC32和R_386_GOTPC重定位項,非static函式,在加-fPIC編譯生成的.o檔案中,也會額外 生成R_386_PC32和R_386_GOTPC重定位項。

· 1.  R_386_32

·     公式:S+A

·     S:重定項中VALUE成員所指符號的記憶體地址

·     A:被重定位處原值,表示"引用符號的記憶體地址"與S的偏移

32位elf格式中的10種重定位型別

· 

·     將g.c編譯成g.o檔案,觀察包含的重定項資訊:

32位elf格式中的10種重定位型別

· "00000005 R_386_32 g1":編譯器連g1在哪個.o檔案都不知道,當然更不知道g1執行時的地址,所以在g.o檔案中設定一個重定項,要求後續過程根據"S(g1記憶體地址)+A(0)",修改g.o映象中0x05偏移處的值;

· "0000002f R_386_32 .bss":g4在g.o檔案.bss節的0偏移處(由於載入時必然知道.bss的內容為全0,就是說elf檔案只需要記錄.bss的位置和大小,不需要安排空間記錄.bss內容,而且就算檔案中為.bss節安排了空間,也無法區分g4在.bss節的什麼位置,所以g4在.bss節中的偏移,要通過檢視.bss節起始位置和g4符號的位置來驗證),要求後續過程根據"S(g.o映象中.bss的記憶體地址)+A(0)",修改g.o映象中0x2f偏移處的值;

· "0000003c R_386_32 .data":g5在g.o檔案.data節的0x04偏移處,要求後續過程根據"S(g.o映象中.data的記憶體地址)+A(0x04)",修改g.o映象中0x3c偏移處的值;

· "00000015 R_386_32 g2":g2在g.o檔案的.bss節,要求後續過程根據"S(g2記憶體地址)+A(0)",修改g.o映象中0x15偏移處的值;

· "00000022 R_386_32 g3":g3在g.o檔案的.data節,要求後續過程根據"S(g3記憶體地址)+A(0)",修改g.o映象中0x22偏移處的值。

    g1與g4/g5重定項區別:當前沒有g1位置的任何線索,所以希望延遲到載入時,通過搜尋動態符號表確定g1的記憶體地址,而g4/g5在g.o的.bss/.data節中,並且有static屬性,不可能被外部引用,載入到記憶體必然還在g.o映象的.bss/.data節中,所以編譯器使用.bss/.data作為重定位計算引數,可以避免後續過程搜尋動態符號表,提高重定位效率;

    g2/g3與g4/g5重定項區別:g2/g3雖然和g4/g5一樣,也在g.o的.bss/.data節中,但g2/g3可以被外部引用,在一種特殊情況下,g2/g3會被安排到其它地方,如果仍然使用在g.o映象中.bss/.data的地址進行重定位,就會導致程式執行的邏輯錯誤,稍後介紹R_386_COPY型別時,會詳細說明。

2.  R_386_RELATIVE

    公式:B+A

    B:.so檔案載入到記憶體中的基地址

    A:被重定位處原值,表示引用符號在.so檔案中的偏移

    將上述g.o檔案,連結成libg.so檔案,重定位資訊如下:

32位elf格式中的10種重定位型別

· "00000560 R_386_32 g1":任然沒有g1位置的任何線索,所以重定項保持原有的計算方法和引數;

· "00000570 R_386_32 g2":不確定是否需要放棄.bss中的位置,所以仍然使用g2的記憶體地址進行重定位計算;

· "0000057d R_386_32 g3":不確定是否需要放棄.data中的位置,所以仍然使用g3的記憶體地址進行重定位計算;

· "0000058a R_386_RELATIVE *ABS*":.so檔案.bss段的第一項用於儲存.bss本身的位置,g.o的.bss節被安排在了libg.so的0x2024處,所以靜態ld根據g.o中的R_386_32重定項,進一步精確了g4在libg.so的0x2024偏移處,但g4的記憶體地址,還需要加上libg.so的載入地址,所以重定位型別轉換為R_386_RELATIVE;

· "00000597 R_386_RELATIVE *ABS*":.so檔案.data段的第一項用於儲存.data本身的位置,g.o的.bss節被安排在了libg.so的0x2018處,所以靜態ld根據g.o中的R_386_32重定項,進一步精確了g4在libg.so的0x201c偏移處,但g5的記憶體地址,還需要加上libg.so的載入地址,所以重定位型別轉換為R_386_RELATIVE。

· 

· 3.  R_386_COPY

·     公式:無

32位elf格式中的10種重定位型別

· 

·     將g.c編譯為libg.so,main.c編譯為a.out,由於a.out引用了libg.so中的全域性變數g,從而可以出現說明R_386_32型別時提到的特殊情況:

· 32位elf格式中的10種重定位型別

· a.out中使用了libg.so中的全域性變數g,這樣就必須等到執行階段,確定了libg.so在程式空間的位置後,才能知道g的絕對地址,不細想的話,可能會認為通過設定一個R_386_32或R_386_RELATIVE重定項,就能解決問題了。

·     但遺憾的是,a.out的.text段,不可以有重定項:

· 32位elf格式中的10種重定位型別

· 上圖希望展示的是,在一個程式的建立過程中,a.out是最先對映到該程式的虛擬空間,然後才會對映所依賴的.so。換句話說,在a.out載入的時候,仍然不知道g的地址,而如果等載入libg.so時再處理重定項,雖然知道g的地址了,但a.out的.text段所在記憶體頁,這時已經被設定為只讀,也無法進行重定位。


·     所以,針對這種情況,靜態ld會將g轉移到a.out的.bss段。由於a.out的載入地址,是在靜態連結階段就確定的(通過連結指令碼設定,32位系統預設設定為0x8048000),從而靜態ld也可以知道g的執行時地址,那麼就不需要重定項了,但同時又帶來2個新的問題:

·     a. 畢竟libg.so中的g才是是原生的,怎麼保證遵循libg.so中g的初始值?

·         其實這就是設計R_386_COPY型別的用意,它表示讓動態ld載入libg.so時知道g的初始值後,將值複製到記憶體中a.out的.bss段。

·         但是如果再仔細想想,其實靜態連結階段,就有機會從libg.so中讀取g的初始值,並且如果不將g安排在a.out的.bss段,而是安排在.data段,儲存空間也具備了,按道理就不需要R_386_COPY型別了。個人猜測,可能是設計者本著.data只儲存顯式賦初值的變數的原則,而沒有這樣實現。

·     b. g既然已經轉移到新地地方了,怎麼保證lig.so和a.out的.text段使用同一處的g?

·         分析R_386_32型別時,已經看到g2/g3和g4/g5一樣,分別在g.o的.bss/.data節,重定項中卻仍然使用g2/g3作為計算引數,其實就是為了在這種情況下,放棄使用本身.bss/.data段中的g,而使用a.out中的g。

· 
4.  R_386_PC32

·     公式:SP

·     S:重定項中VALUE成員所指符號的記憶體地址

·     A:被重定位處原值,表示"被重定位處"與"下一條指令"的偏移

·     P:被重定位處的記憶體地址

· 32位elf格式中的10種重定位型別

· 

·     將f.c編譯成f.o檔案,觀察包含的重定項資訊:

· 32位elf格式中的10種重定位型別

· 00000011 R_386_PC32 f1":編譯器連f1指令塊在哪個.o檔案都不知道,當然更不知道f1執行時的地址,所以在g.o檔案中設定一個重定項,要求後續過程根據"S(f1記憶體地址)+A(-4)-P(被重定位處記憶體地址)",即"S(f1記憶體地址)-(p+4)(下一條指令記憶體地址)",修改g.o檔案中0x11偏移處的值;

· "00000016 R_386_PC32 f2":f2類似分析R_386_32型別時的g2/g3,雖然在g.o檔案,但有可能被外部呼叫,所以和f1一樣,編譯器在g.o檔案中設定一個重定項,要求後續過程根據"S(f2記憶體地址)+A(-4)-P(被重定位處記憶體地址)",即"S(f2記憶體地址)-(p+4)(下一條指令記憶體地址)",修改g.o檔案中的0x16偏移處的值。

·     由於呼叫函式的指令中,要求的是相對地址,並且編譯階段就能確定f3()與fun()的偏移,即"f3載入地址(B+0x05)-下一條指令記憶體地址(B+1f)=0xe6ffffff",載入到記憶體也不會發生改變,所以0x1b處不需要被重定位。

· 

·     將上述f.o檔案,連結為libf.so,靜態ld無法對R_386_PC32重定項做進一步處理,這樣,載入時動態ld會通過搜尋動態符號表,確定libf.so映象中0x53c/0x541處的地址值,保證執行時能呼叫到到f1()/f2()函式:

· 32位elf格式中的10種重定位型別

· 5.  R_386_GOTPC

·     公式:GOTP

·     GOT:執行時,.got段的結束地址

·     A:被重定位處原值,表示"被重定位處"在機器碼中的偏移

·     P:被重定位處的記憶體地址

· 32位elf格式中的10種重定位型別

·     由於程式執行時,eip暫存器儲存的一定是當前指令的記憶體地址,雖然eip暫存器不直接提供給軟體使用,但是有間接的方法可以獲取,那麼從邏輯上講,所有跟程式碼區有固定偏移的內容,編譯和靜態連結階段,就可以確定它們的記憶體地址。利用這個特點,可以將程式碼區中的被重定位處轉移出去,加-fPIC選項將g.c編譯為g.o,並連結為libg.so,可以驗證這一點:

32位elf格式中的10種重定位型別

·   g.o中包含3個重定項:

· "00000004 R_386_PC32 __x86.get_pc_thunk.cx":R_386_PC32重定位型別已經介紹過了,這條重定項可以保證,執行時當前指令可以呼叫到編譯器自動生成的__x86.get_pc_thunk.cx()函式,由於call指令會將下一條指令記憶體地址B+0x513壓棧,這樣從該函式經過一次再回到B+0x513處的指令時,當前指令的記憶體地址B+0x513就存到了ecx暫存器;

· "0000000a R_386_GOTPC _GLOBAL_OFFSET_TABLE_":要求靜態ld根據"GOT(B+.got結束位置在libg.so中的偏移)+A(2)-P(B+0x515)",即libg.so檔案中.got結束位置相對0x515處機器碼的偏移,修改g.o檔案中0x0a偏移處的值,這樣執行時加上ecx暫存器中當前指令的記憶體地址後,就是.got段結束位置的記憶體地址;

· "00000010 R_386_GOT32 g1":要求靜態ld在目標檔案中生成.got表,並在.got表中安排4位元組儲存g1地址,這樣程式碼區就可以從.got表中獲取g1地址,而.got表執行時的結束地址,以及儲存g1地址的位置在.got表中的偏移,靜態連結階段都是知道的,從而就不需要對程式碼區進行重定位。

·     靜態ld在libg.so中設定了.got段,並將程式碼區中的重定位處,轉移到.got段中:

· "00001fec R_386_GLOB_DAT g1":0x1fec處用於儲存執行g1的記憶體地址,但當前沒有g1位置的任何線索,所以留下重定項,要求後續過程進行修改。

·     對於R_386_32、R_386_RELATIVE型別的重定項,由於被重定位處在程式碼區,而重定項計算引數的地址,在不同程式中是不同的,所以不同程式對.so程式碼區的修改要求就不同,這樣就不能共享同一份實體記憶體中的.so映象。

·     R_386_GLOB_DAT的優勢就在於,它將"散落"在程式碼區的被重定位處,集中轉移到.got表中,從而大大減小了不可共享區域,如下圖所示,程式B希望載入.so檔案時,發現記憶體中已經存在該.so的映象了,就直接對映到自己的虛擬空間,動態ld在處理重定項時,僅需要修改小小的.got段,並通過COW(寫時複製)機制,建立了一個.got副本,從而也可以保證與其它程式互不干擾。

· 32位elf格式中的10種重定位型別

· 

· 6.  R_386_GOT32

·     公式:G

·     G:引用符號的地址指標,相對於GOT的偏移

· 32位elf格式中的10種重定位型別

將g.c編譯成g.o檔案,觀察包含的重定項資訊:

32位elf格式中的10種重定位型別

· "00000010 R_386_GOT32 g1":要求靜態ld根據"G(g1地址指標相對GOT的偏移)",修改g.o映象0x10偏移處的值;

· "00000023 R_386_GOT32 g2":要求靜態ld根據"G(g2地址指標相對GOT的偏移)",修改g.o映象0x23偏移處的值;

· "00000033 R_386_GOT32 g3":要求靜態ld根據"G(g3地址指標相對GOT的偏移)",修改g.o映象0x33偏移處的值;

· 

· 7.  R_386_GOLB_DAT

·     公式:S

·     S:重定項中VALUE成員所指符號的記憶體地址

·     將上述g.o連結為libg.so檔案,發現被重定位處都被集中轉移到.got段中:

· 32位elf格式中的10種重定位型別

· 8.  R_386_GOTOFF

·     公式:S-GOT

·     S:重定項中VALUE成員所指符號的記憶體地址

·     GOT:執行時,.got段的結束地址

· 32位elf格式中的10種重定位型別

· 

·     將g.c編譯為g.o檔案,並且連結為libg.so檔案,觀察包含的重定項資訊:

32位elf格式中的10種重定位型別

· 編譯階段不知道g.o中的.bss/.data節會被連結到libg.so中的什麼位置,所以設定了R_386_GOTOFF重定項,要求靜態ld根據"S(.bss/.data記憶體地址)-GOT(執行時.got結束地址)",修改被重定位處的值。

·     前面已經介紹過,執行時ecx暫存器儲存的一定是.got的結束地址,再加上g4/g5具有static屬性,載入到記憶體後,仍然在libg.so映象的.bss/.data段中,那麼通過g4/g4在.bss/.data中的偏移,以及.bss/.data與.got結束位置的偏移,在靜態連結階段就能知道執行時g4/g5的記憶體地址,從而libg.so中就不存在對g4/g5引用處的重定項了。

· 

· 9.  R_386_PLT32

·     公式:LP

·     L:<重定項中VALUE成員所指符號@plt>的記憶體地址

·     A:被重定位處原值,表示"被重定位處"相對於"下一條指令"的偏移

·     P:被重定位處的記憶體地址

32位elf格式中的10種重定位型別

· 將f.c編譯成f.o檔案,觀察包含的重定項資訊:

32位elf格式中的10種重定位型別

· 0000001d R_386_PLT32 f1":要求靜態ld生成<f1@plt>函式,並根據"L(<f1@plt>函式地址)+A(-4)-P",即<f1@plt>相對於下一條指令的相對地址,修改f.o映象中0x1d偏移處的值;

· "00000022 R_386_PLT32 f2":要求靜態ld生成<f2@plt>函式,並根據"L(<f2@plt>函式地址)+A(-4)-P",即<f2@plt>相對於下一條指令的相對地址,修改f.o映象中0x22偏移處的值。

·     可以看出,原始碼中呼叫f1()、f2()函式的語句,對應的機器碼,並沒有直接跳轉到f1、f2指令塊,而是呼叫了<f1@plt>、<f2@plt>函式,接下來通過分析R_386_JMP_SLOT型別就會知道,這兩個函式相當於"中間跳板",用於實現重定項的延遲處理。

· 

· 10.R_386_JMP_SLOT

·     公式:S(與R_386_GLOB_DAT的公式一樣,但對於動態ld,R_386_JMP_SLOT型別與R_386_RELATIVE等價)

·     S:重定項中VALUE成員所指符號的記憶體地址

·     將上述f.o,連結為libf.so檔案,觀察包含的重定項資訊:

· 32位elf格式中的10種重定位型別

· "00002018 R_386_JUMP_SLOT f1":被重定位處在libf.so映象的0x2018偏移處(.got.plt段中),0x567處的指令第一次被執行時,由_dl_runtime_resolve()函式根據"S(f1記憶體地址)",修改被重定位處的值;

· "0000200c R_386_JUMP_SLOT f2":被重定位處在libf.so映象的0x200c偏移處(.got.plt段中),0x56c處的指令第一次被執行時,由_dl_runtime_resolve()函式根據"S(f2記憶體地址)",修改被重定位處的值;

·     R_386_JMP_SLOT是10種型別中最複雜的,必須先要了解.got.plt前三項的含義:

· 第1項:用於儲存.dynamic段的記憶體地址,初始值為.dynamic段在libg.so檔案中的偏移;

· 第2項:用於儲存記憶體中libf.so模組的id,用於區分各個已載入的.so模組;

· 第3項:用於儲存_dl_runtime_resolve()函式的記憶體地址,由動態ld完成填寫。

·     另外,通過前面的介紹可以知道,ebx暫存器存的一定是.got結束地址B+0x2000,然後按照圖中標記的執行順序,在大腦中連續2次模擬執行B+0x567處的指令,就會看出如下規律:

· 第一次執行時,B+0x410處的jmp指令,會跳轉到B+0x416處(因為0x18(%ebx)指向B+0x2018處,而此處初始值為B+0x416),接著將被重定位處地址壓棧,並再跳轉到0x3d0將libf.so模組id壓棧,最終進入_dl_runtime_resolve()函式,確定f1地址後覆蓋到B+0x2018處;

· 第二次執行時,由於B+0x2018處已經是f1的地址了,從而B+0x410處的jmp指令,就會直接進入f1()函式。

·     對f2()函式的呼叫同理,這樣做雖然會多一次跳轉,但是保證了程式執行的平滑性,避免大量呼叫libg.so中的函式時,在載入.so時出現"卡頓"的現象,而且有時候很多分支根本沒有機會被執行,所以這是一種折衷的處理。

· 

· 參考:

·     https://docs.oracle.com/cd/E23824_01/html/819-0690/

·     http://www.cnblogs.com/catch/p/3857964.html

·     https://www.cnblogs.com/lanrenxinxin/p/5573018.html


· 本文由看雪論壇skyun原創,轉載請註明來自看雪論壇

· 轉載請註明出處:https://bbs.pediy.com/thread-246373.htm

相關文章