1. 程式人生 > >ELF(七)可重定位目標,部分摘自深入理解作業系統,深入理解linx核心

ELF(七)可重定位目標,部分摘自深入理解作業系統,深入理解linx核心

可重定位目標

         重定位是將EFL檔案中的未定義符號關聯到有效值的處理過程。在main.o中,這意味著對printf和puts的未定義的引用必須替換為該程序的虛擬地址空間中適當的機器程式碼所在的地址。在目標中用到的相關符號之處,都必須替換。

         對使用者空間程式符號的替換,核心並不涉及其中,因為所有的替換操作都是由外部工具完成的。對核心模組來說,情況有所不同,因為核心所收到的模組裸資料,與其儲存在二級制檔案中的形式完全相同,核心本身需要負責重定位操作。

         在每個目標檔案中,都有一個專門的表,包含了重定位項,標識了需要進行重定位之處。每個表項都包含下列資訊:

         1)一個偏移量,指定了修改的項的位置

         2)對符號的引用(符號表的索引),提供了需要插入到重定位位置的資料

重定位步驟

         1)重定位節和符號定義。

         連結器將所有相同型別的節合併為同一型別的新的聚合節。例如來自輸入模組的.data節全部合併成一個節,這個節成為輸出可執行目標檔案的.data節。然後連結器將執行時儲存器地址賦給新的聚合節,賦給輸入模組定義的每個節,以及賦給輸入模組定義的每個符號。當這一步完成時,程式中的每個指令和全域性變數都有唯一的執行時儲存器地址了。

         2)重定位節中的符號引用。

         在這一步中,連結器修改程式碼節和資料節中對每個符號的引用,使得他們指向正確的執行時地址。為了執行這一步,連結器依賴於稱之為重定位條目的可重定位目標模組中的資料結構。

重定向條目

         當彙編器生成一個目標模組時,它並不知道資料和程式碼最終將存放在儲存器中的什麼位置。它也不知道這個模組引用的任何外部定義的函式和全域性變數。所以,無論何時彙編器遇到對最終位置未指定目標引用,它就會生成一個重定位條目,告訴連結器在將目標檔案合併可執行檔案時如何修改這個引用。程式碼重定位條目放在.rel.text中。已經初始化資料的重定位條目放在.rel.data中。

資料結構

         由於技術原因,有兩種型別的重定位資訊,由兩種稍有不同的資料結構表示。第一種型別稱之為普通重定位。SHT_REL型別的節中的重定位項由以下資料結構定義:

         /*Relocation table entry without addend (in section of type SHT_REL).  */

typedef struct

{

 Elf32_Addr   r_offset;            /* Address */指定需要重定位的項的位置

 Elf32_Word  r_info;                         /* Relocation type andsymbol index */提供了符號表中的一個位置,同時還包括重定位型別的有個資訊。這是通過將值劃分為兩部分來達到的。

r_info == int symbol:24,type:8;

} Elf32_Rel;

         另一種型別,稱之為需要新增常數的重定位項,只出現在SHT_RELA型別的節中。資料結構如下:

/* Relocation table entry with addend (insection of type SHT_RELA).  */

typedef struct

{

 Elf32_Addr   r_offset;            /* Address */

 Elf32_Word  r_info;                         /* Relocation type andsymbol index */

 Elf32_Sword         r_addend;                  /* Addend */加數,計算重定位是,將根據重定位型別,對該值進行不同的處理。

} Elf32_Rela;

截圖例子

         為說明如何使用重定位資訊,我們看一下此前的main.c測試程式。首先readelf顯示檔案中所有的重定位項,如下所示:

在程式執行時或者連結main.o產生可執行檔案時,如果某些機器程式碼引用了虛擬地址空間中位置尚不明確的函式或者符號,則將使用Offset列的資訊。main的組合語言程式碼呼叫了若干函式,分別位於偏移量0x26的puts和位於0x40的printf。這些可以使用objdump工具看到:

在puts和printf函式的地址已經確定後,必須將其插入到指定的偏移量處,以便生成能夠正確執行的可執行程式碼。

重定位型別

         ELF定義了很多重定位型別,對每種支援的體系結構,都有一個獨立的集合。這些型別大部分用於生成動態或與裝載位置無關的程式碼。在一些平臺上,特別是IA32平臺,還必須彌補許多設計錯誤和歷史包袱。幸運的是,Linux核心只對模組的重定位感興趣,因此用以下兩種重定位型別就可以了:

1)相對重定位

2)絕對重定位

相對重定位生成的重定位表項指向相對於程式計數器(pc,亦即指令指標)指定的記憶體地址。這些主要用於子例程呼叫。另一種重定位生成絕對地址,從名字就能看出。通常,這種重定位項指向記憶體中在編譯時就已知的資料,例如字串常數。

         在IA32系統上,和兩種重定位型別由常數R_386_PC_32(相對重定位)和R_386_32(絕對重定位)表示。重定位結果計算如下:

            R_386_32:Result= S+A

         R_386_PC_32:Result=S-P+A

A代表加數值,在IA32體系結構上,由重定位位置處的記憶體內容隱士提供(一般為操作碼後面的數值)。S是符號表中儲存的符號的值,而P代表重定位的位置偏移量,換言之,即算出的資料寫入到二進位制檔案中的位置偏移量(修改處的執行時地址或者偏移,對於目標檔案P為修訂處段內的偏移,對可執行檔案P為執行時的地址)。如果加數值為0,那麼絕對重定位只是將符號表中的符號的值插入在重定位位置。但在相對重定位中,需要計算符號位置和重定位位置之間的差值。換言之,需要通過計算確定符號與重定位位置相距多少位元組。

         在這兩種情況下,都會加上加數值,因而使得結果產生一個線性位移。

舉例說明

         這是一個難點,但是掌握了技巧也很簡單。

首先協助說明的三個c原始檔:

a.c


b.c


main.c

         首先我們明確的是重定向發生在連結的時候,當多個輸入最終連結成一個目標檔案的時候,當符號解析完成之後。

         在此例子中,a.c和b.c沒有外部引用,所以在.o檔案中不存在重定位表項,只有main.o存在,如圖:

 


         從main.c原始檔也不難看出,它引用了外面sum和mul符號,並且引用了一個全域性a符號。並且從上圖也可以看出。並且a的重定位型別為絕對定位,而其他兩個為相對重定位。

並且可以得到:

         a第一處r_offset : 0x1a 重定位的位元組處 並且從第二幅圖可以得知a的大小為4個位元組

         a第二處r_offset :     0x3f

         sumr_offset    :     0x29

         mulr_offset :       0x4e

計算a的重定位後的地址:根據公式S+A

S為目標檔案符號表中的對應的地址(為什麼不是.o檔案呢,因為.o檔案的符號表中的地址都是重定位REL型別,都為0):

如圖:


可以得到S(a) = 0x8049684,接下來要確定加值A,這要看main.o 的彙編程式碼了,利用objdump來檢視該.text節的彙編程式碼:

Disassembly of section .text:

00000000 <main>:

   0:        8d 4c 24 04            lea    0x4(%esp),%ecx

   4:        83 e4 f0             and    $0xfffffff0,%esp

   7:        ff 71 fc                 pushl  0xfffffffc(%ecx)

   a:        55                          push   %ebp

   b:        89 e5                        mov    %esp,%ebp

   d:        51                          push   %ecx

   e:        83 ec 24              sub    $0x24,%esp

  11:        c7 45 f8 0c 00 00 00        movl  $0xc,0xfffffff8(%ebp)

  18:        8b 15 00 00 00 00        mov    0x0,%edx

         怎麼讀這些程式碼呢?其實很簡單,從這些彙編程式碼可以看出它的每行包含個部分:

         1)左邊兩欄中最左面一欄是上面所有機器指令的位元組數(16進位制),緊接著其後的是機器程式碼(十六進位制形式)

         其實真正的二進位制程式碼是這樣的(十六進位制),前兩行8d 4c 24 04 83 e4 f0,工具為了方便閱讀。

我在工具的基礎上為了方便說明這兩欄,只提取了前面兩欄(好比一個大的位元組陣列從零開始)。

 0:   8d 4c 24 04  以前是0個位元組,下標是從0開始,其中8d是操作碼

 4:   83 e4 f0    上面一共四個位元組,此行小標也是從4開始,83是操作碼

         2)最後一欄是彙編指令

         lea    0x4(%esp),%ecx

    and   $0xfffffff0,%esp

這沒什麼好講的就是彙編指令。

明白了這些就應該很容易看下面的圖片了:


在圖片中我特意標出了0x1a後面的4個位元組的位置,其後0x00000000=0所有加值為0,則Result=0x8049684+0=0x8049684,也就是目標檔案的1a後面的後4個位元組要被替換為該值。現在我們驗證一下:

從圖中可以看出,在1a(對應15)開始後的4個位元組為84 96 04 08,因為是小端表示法,所所以倒過來就是08049684和上面的一樣。


         相對重定位的計算:

以sum為例(由前面的截圖和計算可以得知r_offset=0x29):

         S(sum)= 0x8048414


         A(sum)= 0xfffffffc = -4(2的補碼形式)

        

         P(sum)= 0x80483cc+1(操作指令一個位元組) = 0x80483cd --執行時地址

    Result= S+A-P

                    = 0x8048414-4-0x80483cd

                    = 0x43

         如圖e8之後是00 00 0043(小端)。

在執行時,call指令將放在0x0x80483cc處,當cpu執行call指令時,pc的值為0x0x80483cc+1+4=0x0x80483d1,即緊隨著call指令之後的指令的地址。為了執行這條指令,cpu執行以下的步驟:

         1push PC onto stack

2 PC <- PC+0x43 = 0x0x80483d1 +0x43=  0x8048414

該地址恰好是swap的第一條指令的地址。