PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一)
級别: 初級
陳劍 ([email protected]), 軟體工程師, IBM
金戈, 進階軟體工程師, IBM
2004 年 2 月 01 日
作者首先簡單介紹了符号靜态解析和動态解析的不同之處,接着分析了和動态解析相關的概念,然後講述PowerPC Linux是如何作動态符号解析的,并輔以執行個體詳細說明,最後總結了i386和PowerPC實作動态解析的異同之處;本篇文章側重分析32位PowerPC Linux上的符号動态解析。 一. 前言
符号解析是Linux系統導入二進制可執行檔案的重要過程,它完成的工作包括将一個符号定位到實際的記憶體位址,并且要保證可以正确引用這些符号。按解析對象的不同它可以分為變量符号解析和函數符号解析;按解析方式的不同可以分為靜态解析和動态解析。
對于靜态解析的符号,它們的位址在檔案生成時就由link editor(在Linux下通常是ld)已經确定下來了;對于動态解析的符号,他們的位址在程式運作時才由dynamic linker(動态連結器,32位Linux平台下通常是/lib/ld.so.1)确定下來。我們可以這麼認為,如果一個符号在共享庫中定義,那麼當其他可執行檔案或共享庫引用這個符号時,就需要對它作動态解析。
變量符号的動态解析過程比較簡單,系統在載入程式過程中将變量symbol位址存入到GOT(Global Offset Table)中,引用變量symbol時首先計算出GOT表的實際位址,然後以它作為基址加上(變量symbol在GOT表中的偏移量)就可以從GOT表中取得該symbol的實際位址。下面以SUSE Linux Enterprise Server 8.1 for IBM pSeries為例,主要講述和示範32位PowerPC Linux下函數符号的動态解析過程。
PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一)
PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) 回頁首
二. 概念
在講述解析過程之前,先介紹一下在解析過程中要用到的基本概念。
1. ELF(Executable and Linkable Format)檔案
ELF是Linux預設采用的可執行檔案(包括共享庫,object檔案)的格式,具體規範參見參考文獻〔1〕、〔2〕。這裡需要提一下的是section這個概念:section是ELF檔案中一段互相聯系資訊,它可以是一段資料,也可以是一段代碼。比如可執行代碼資訊就放在.text section中,被使用者初始化的變量會放在.data section中,沒有被使用者初始化的變量會放在.bss section(bss是below stack segment的縮寫)中。還有其他的一些 section: .debug、 .hash、 .symtab、 .dynsym、 .plt、 .rel.plt 等等。 .dynsym(動态符号表)、 .plt(過程連結表)和.rel.plt(重定位表)和我們的話題有關。
2. 符号表(symbol table)
符号表記錄了程式中符号的定義資訊和引用資訊,它是一個結構數組,數組中的每個元素對應一個符号所有的資訊。我們可以在glibc的源代碼glibc-2.2/elf/elf.h中看到這個結構的c語言定義:
typedef struct{
Elf32_Word st_name; /* 符号名 (.string表的索引) */
Elf32_Addr st_value; /* 符号值(Symbol value) */
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
} Elf32_Sym;
對于可執行檔案和共享庫而言,符号值記錄了該符号的記憶體位址。可執行檔案知道運作時刻他們的位址,是以他們内部的引用符号在編譯時候就已經确定了;共享庫symbol的符号值(symbol value)就要等到共享庫被載入到記憶體中才确定下來。
我們可以用"readelf -s 檔案名"來檢視elf檔案的symbol值,一般會有兩個symbol表:.symtab(包含靜态符号和動态符号)和.dynsym(僅包含動态符号)表。下面是我自己機器上的一個輸出:
[email protected]:~/program/GOT> readelf -s test32
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 10010894 488 FUNC GLOBAL DEFAULT UND [email protected]_2.0
……………………
Symbol table '.symtab' contains 228 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 10000114 0 SECTION LOCAL DEFAULT 1
……………………
3.過程連結表(Procedure Linkage Table,PLT)
靜态解析函數符号很簡單,因為引用是在内部進行的,隻要用下面的指令就可實作:
bl resolved_symbol //resolved_symbol是被引用函數的入口點
動态解析符号則不然,由于link editor不能在編譯時就确定被引用函數的入口點,是以它隻能将控制權交給第三方,再由這個第三方來完成确定被引用函數入口點的任務,這個第三方就是過程連結表。
過程連結表的格式如下:
PLT表開頭的18個字(72位元組)為dynamic linker保留 如果可執行檔案或共享庫需要N個.PLTi入口,那麼緊跟着這18個字,link editor就會保留3*N個字(12*N位元組),開頭的2*N個字就是所有的.PLT入口,對于第i個引用符号,它的.PLT入口是(72+(i-1)*8)(1<=i<=N),剩下的N個字留給dynamic linker使用。
過程連結表雖然存在于檔案當中,但它的初始化是由dynamic linker在裝載可執行檔案和共享庫時完成的。下面是一個可能的被初始化的PLT表的内容:
.PLT:
.PLTresolve:
addis r12,r0,[email protected]
addi r12,r12,[email protected]
mtctr r12 //到此為此,r12和ctr中是dynamic linker的位址
addis r12,r0,[email protected]
addi r12,r12,[email protected]
bctr //此時r12中是共享庫symbol表的位址
.PLTcall:
addis r11,r11,[email protected]
lwz r11,[email protected](r11)
mtctr r11
bctr
8 個nop指令
.PLT1:
addi r11,r0,4*0
b .PLTresolve
. . .
.PLTi:
addi r11,r0,4*(i-1)
b .PLTresolve
. . .
.PLTN:
addi r11,r0,4*(N-1)
b .PLTresolve
.PLTtable:
<N word table begins here>
4. Relocation表
Relocation表總是和PLT表緊緊聯系在一起,也就是說,PLT表有多少個入口,Relocation表就有多少個入口,他們是一對一的關系。下面是Relocation結構的c語言定義:
typedef struct {
Elf32_Addr r_offset; //.PLTi的位址
Elf32_Word r_info; //包含symbol index資訊
} Elf32_Rel;
在上述過程連結表的例子中r11是Relocation表和PLT表的index,dynamic linker需要r11來找到對應于這次解析的Relocation表的元素,然後得到r_info,由于r_info中包含了symbol index資訊,我們就可确定是尋找哪個symbol的位址;得到symbol的位址後還需要r11來确定将這個位址正确重定位到那個PLT入口(r_offset儲存了.PLTi的位址)。 PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一)
PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) 回頁首
三. 函數符号動态解析過程
當調用一個外部的函數時,它傳輸控制到PLT 中跟該symbol 相關的那個entry (是在編譯時候由link editor完成的),然後将偏移量(就是Relocation表的index)置入r11,轉入到.PLTresolve處執行;.PLTresolve取得dynamic linker的位址,将共享庫的symbol表位址賦給r12後調用dynamic linker的解析函數_dl_runtime_resolve;_dl_runtime_resolve會根據r11取得和該PLT entry對應的Relocation entry,然後得到symbol index,結合r12找到該函數符号的載入位址loaded_addr,并從Relocation entry中得到.PLTi的位址,将.PLTi處的指令修改為b loaded_addr,完成該次符号解析。這樣以後若還有調用該函數的語句,就會直接跳往loaded_addr。
下面将以程式為例,示範SUSE SLES 8.1 for IBM pSeries是如何動态解析函數符号printf 的。
例子程式Sample.c
1 #include <stdio.h>
2
3 int main(int argc, char *argv[])
4 {
5 printf("Hello, world!\n");
6 return 0;
7 }
PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一)
PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) 回頁首
四. 過程示範
1.Run gdb:gdb sample
2.反彙編printf符号
(gdb) disassemble printf
Dump of assembler code for function printf:
0x1001089c <printf>: .long 0x0
……………………..
0x100108b4 <printf+24>: .long 0x0
End of assembler dump.
由于sample還沒有運作,是以printf函數還沒有被載入到記憶體中,它的.PLTi入口初始為0
3.反彙編main函數
(gdb) disassemble main
Dump of assembler code for function main:
0x10000448 <main>: stwu r1,-32(r1)
……………………..
0x10000470 <main+40>: bl 0x1001089c <printf>
……………………..
4. 設定斷點并運作sample (gdb) b *0x10000470
Breakpoint 1 at 0x10000470
(gdb) r
Starting program: /home/cj/program/GOT/sample
Breakpoint 1, 0x10000470 in main ()
(gdb) disassemble 0x1001089c
Dump of assembler code for function printf:
0x1001089c <printf>: li r11,4
0x100108a0 <printf+4>: b 0x1001086c <_GLOBAL_OFFSET_TABLE_+48>
0x100108a4 <printf+8>: li r11,8
0x100108a8 <printf+12>: b 0x1001086c <_GLOBAL_OFFSET_TABLE_+48>
……………………..
End of assembler dump.
我們發現0x1001089c處的代碼已經改變,表明在載入sample程式的過程中ld.so.1已經修改了printf函數的.PLTi入口。r11值為4,即是printf 在relocation表中的index。
5.反彙編0x1001086c
0x1001086c相當于.PLTresolve,它會調用_dl_runtime_resolve來解析符号
(gdb) disassemble 0x1001086c
Dump of assembler code for function _GLOBAL_OFFSET_TABLE_:
……………………..
0x1001084c <_GLOBAL_OFFSET_TABLE_+16>: addis r11,r11,4097
0x10010850 <_GLOBAL_OFFSET_TABLE_+20>: lwz r11,2220(r11)
0x10010854 <_GLOBAL_OFFSET_TABLE_+24>: mtctr r11
0x10010858 <_GLOBAL_OFFSET_TABLE_+28>: bctr
0x1001085c <_GLOBAL_OFFSET_TABLE_+32>: .long 0x0
0x10010860 <_GLOBAL_OFFSET_TABLE_+36>: .long 0x0
0x10010864 <_GLOBAL_OFFSET_TABLE_+40>: addis r11,r11,-4097
0x10010868 <_GLOBAL_OFFSET_TABLE_+44>: addi r11,r11,-2220
0x1001086c <_GLOBAL_OFFSET_TABLE_+48>: rlwinm r12,r11,1,0,30
0x10010870 <_GLOBAL_OFFSET_TABLE_+52>: add r11,r12,r11
0x10010874 <_GLOBAL_OFFSET_TABLE_+56>: li r12,-20344
0x10010878 <_GLOBAL_OFFSET_TABLE_+60>: addis r12,r12,16385
0x1001087c <_GLOBAL_OFFSET_TABLE_+64>: mtctr r12
//此時ctr中存放_dl_runtime_resolve的位址
0x10010880 <_GLOBAL_OFFSET_TABLE_+68>: li r12,22904
0x10010884 <_GLOBAL_OFFSET_TABLE_+72>: addis r12,r12,16386
//此時r12存放/lib/libc.so.6的symbol表的位址
0x10010888 <_GLOBAL_OFFSET_TABLE_+76>: bctr
0x1001088c <_GLOBAL_OFFSET_TABLE_+80>: .long 0x0
0x10010890 <_GLOBAL_OFFSET_TABLE_+84>: .long 0x0
0x10010894 <__libc_start_main>: b 0xfed8ff0 <__libc_start_main>
0x10010898 <__libc_start_main+4>: b 0x1001086c <_GLOBAL_OFFSET_TABLE_+48>
0x1001089c <printf>: li r11,4
0x100108a0 <printf+4>: b 0x1001086c <_GLOBAL_OFFSET_TABLE_+48>
0x100108a4 <printf+8>: li r11,8
0x100108a8 <printf+12>: b 0x1001086c <_GLOBAL_OFFSET_TABLE_+48>
……………………..
End of assembler dump.
(gdb) b *0x10010888
Breakpoint 2 at 0x10010888
(gdb) i r ctr
ctr 0x4000b088 1073787016
(gdb) disassemble 0x4000b088
Dump of assembler code for function _dl_runtime_resolve:
0x4000b088 <_dl_runtime_resolve>: stwu r1,-64(r1)
……………………..
0x4000b0c4 <_dl_runtime_resolve+60>: stw r0,8(r1)
0x4000b0c8 <_dl_runtime_resolve+64>: bl 0x4000ad3c <fixup>
0x4000b0cc <_dl_runtime_resolve+68>: mtctr r3
……………………..
0x4000b104 <_dl_runtime_resolve+124>: addi r1,r1,64
0x4000b108 <_dl_runtime_resolve+128>: bctr
End of assembler dump.
我們可以通過檢視glibc的源代碼來看_dl_runtime_resolve是如何得到symbol的位址值的,它在檔案glibc-2.2/sysdeps/powerpc/dl-machine.h中定義。 (gdb) ni
Hello, world!
0x10000474 in main ()
6.再次反彙編0x1001089c (gdb) disassemble 0x1001089c
Dump of assembler code for function printf:
0x1001089c <printf>: b 0xff0ec9c <printf>
0x100108a0 <printf+4>: b 0x1001086c <_GLOBAL_OFFSET_TABLE_+48>
……………………..
End of assembler dump.
對比上述第四個步驟,0x1001089c <printf>: li r11,4已經被修正為 0x1001089c <printf>: b 0xff0ec9c <printf>了,以後若還有調用printf的語句,控制權就會直接轉到0xff0ec9c,0xff0ec9c是printf函數的真正入口點。 PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一)
PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) PowerPC上ELF可執行檔案的符号解析(一)PowerPC上ELF可執行檔案的符号解析(一) 回頁首
五. 總結
從以上的分析和示範過程中我們可以看到32 位PowerPC的函數符号動态解析和i386 Linux體系結構的函數符号動态過程大體一緻,它們都是先跳轉到對應的.PLTi入口,儲存Relocation偏移量,然後跳轉到_dl_runtime_resolve,由它來完成尋找函數符号的任務。不同的地方是:1. 由于b指令的限制(b 指令可能第一次跳轉不到函數入口點),是以對于不能一次跳到函數入口點的情況就需要兩次跳轉(先将跳轉位址存到.PLTtable+4*(i-1),然後将.PLTi處的b .PLTresolve修改為b .PLTcall,由它用bctr指令來完成跳轉到函數入口點的任務); 2. i386 Linux将跳轉位址存入到GOT[x+i]中,PowerPC則根本不使用GOT表,而是通過直接修改.PLTi處的代碼來達到相同的功能。
From:http://yuxu9710108.blog.163.com/blog/static/2375153420082421213370/