Solaris學習筆記(3)




作者: Badcoffee
Email: [email protected]
Blog: http://blog.csdn.net/yayong
2006年3月


很久以前就看過alert7寫的那篇ELF 動態解析符號過程(修訂版),大概是他在學習ELF文件格式時寫的吧。OpenSolaris之後,其內核所有代碼全世界都可以訪問到,於是就有了這 篇文章。本文僅用於學習交流目的,因此沒有經過嚴格校對,錯誤再所難免,如果有勘誤或疑問請與我聯繫。

關鍵詞:Dynamic binding/ld.so/mdb/link map/Solaris

1. 基本概念

Link-Editor - 鏈接器:即ld(1),輸入一個或多個輸入文件(*.o/*.so/*.a),經過連接和解釋數據,輸出一個目標文件(*.o/*.so/*.a/可執行 文件)。ld通常作爲編譯環境的一部分來執行。

Runtime Linker - 動態鏈接器: 即ld.so.1(1), 在運行時刻處理動態的可執行程序和共享庫,把可執行程序和共享庫綁定在一起創建一個可執行的進程。

Shared objects - 共享對象: 也叫共享庫,是動態鏈接系統的基礎。共享對象類似與動態可執行文件,但共享對象沒有被指定虛擬內存地址。 共享對象可以在系統中多個應用程序共同使用和共享。

Dynamic executables - 動態可執行文件:通常依賴於一個或者多個共享對象。 爲了產生一個可以執行的進程,一個或者多個共享對象必須綁定在動態可執行文件上。

runtime linker主要負責以下幾方面工作:

1.分析可執行文件中包含的動態信息部分(對ELF文件來說就是.dynamic section)來決定該文件運行所需的依賴庫;
2.定位和裝載這些依賴庫,分析這些依賴庫所包含的動態信息部分,來決定是否需裝載要任何附加的依賴庫;
3.對動態庫進行必要的重定位,在進程的執行期間綁定這些對象;
4.調用這些依賴庫提供的初始化函數(ELF文件來說就是.init section,而且順序是先執行依賴庫的,再執行可執行文件的);
5.把控制權轉交給應用程序;
6.在應用程序執行期間,能被再調用,來執行延後的函數綁定(即動態解析);
7.在應用程序調用dlopen(3C)打開動態庫和用dlsym(3C)綁定這些庫的符號時,也要被調用;

2. 測試與驗證


寫一個最簡的測試程序test.c:
#include <stdio.h>
int main(int agrc, char *argv[])
{
printf ("hello world/n");
return 0;
}

編譯和鏈接後產生ELF文件:
# cc test.c -o test
# file test
test: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped

用mdb反彙編main函數:
# mdb test
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $0x10,%esp
main+6: movl %ebx,-0x8(%ebp)
main+9: movl %esi,-0xc(%ebp)
main+0xc: movl %edi,-0x10(%ebp)
main+0xf: pushl $0x80506ec
main+0x14: call -0x148 <PLT:printf>
main+0x19: addl $0x4,%esp
main+0x1c: movl $0x0,-0x4(%ebp)
main+0x23: jmp +0x5 <main+0x28>
main+0x28: movl -0x4(%ebp),%eax
main+0x2b: movl -0x8(%ebp),%ebx
main+0x2e: movl -0xc(%ebp),%esi
main+0x31: movl -0x10(%ebp),%edi
main+0x34: leave
main+0x35: ret

可以看到,main+0x14處調用了函數printf,調用前把傳遞的字符串參數壓入棧:
> 0x80506ec/s
0x80506ec: hello world

“hello world”在ELF文件的.rodata1 section,處於test的代碼段:
# /usr/ccs/bin/elfdump -c -N .rodata1 test

Section Header[13]: sh_name: .rodata1
sh_addr: 0x80506ec sh_flags: [ SHF_ALLOC ]
sh_size: 0xd sh_type: [ SHT_PROGBITS ]
sh_offset: 0x6ec sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4

用mdb在main+0x14處設置斷點,然後運行程序:
> main+0x14:b
> :r
mdb: stop at main+0x14
mdb: target stopped at:
main+0x14: call -0x148 <PLT:printf>
程序在調用printf之前停止,我們計算一下printf的地址:

> main+0x14-0x148=X
8050544

驗證一下,地址0x8050544是否正確:

# /usr/ccs/bin/elfdump -s -N .symtab test | grep printf
[38] 0x08050544 0x00000000 FUNC GLOB D 0 UNDEF printf
# /usr/ccs/bin/elfdump -s -N .dynsym test | grep printf
[1] 0x08050544 0x00000000 FUNC GLOB D 0 UNDEF printf
在test文件的.symtab和.dynsym section都可以找到符號表中包含printf,符號表實際上是一個數組,數組元素定義如下:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;

printf的st_value就是0x08050544,在ELF的可執行文件中,這就是printf的虛存地址,而這恰好就是我們mdb中計算的地 址。

我們同樣可以用nm(1)命令確認這一點:
# /usr/ccs/bin/nm -x test | grep printf
[Index] Value Size Type Bind Other Shndx Name
......
[38] |0x08050544|0x00000000|FUNC |GLOB |0 |UNDEF |printf
printf的st_shndx的值是UNDEF,說明printf未在test中定義。既然程序可以鏈接通過,那麼printf肯定存在於它依賴的共享 庫中。

test依賴的共享庫如下:
# ldd test
libc.so.1 => /lib/libc.so.1
libm.so.2 => /lib/libm.so.2
當一個程序有多個共享庫依賴時,runtime linker是按照一定的順序運行各個庫的.init函數的,即前面提到的步驟4,查看順序用ldd -i:
# ldd -i /usr/bin/cp
libcmdutils.so.1 => /lib/libcmdutils.so.1
libavl.so.1 => /lib/libavl.so.1
libsec.so.1 => /lib/libsec.so.1
libc.so.1 => /lib/libc.so.1
libm.so.2 => /lib/libm.so.2

init object=/lib/libc.so.1
init object=/lib/libavl.so.1
init object=/lib/libcmdutils.so.1
init object=/lib/libsec.so.1
test依賴的庫只有libc(3LIB)和libm(3LIB),libm是數學庫,因此printf一定在libc(3LIB)中。我們知道,在 libc(3LIB)庫中,包含了System V, ANSI C, POSIX等多種標準的函數實現。

查看libc.so的符號表中的printf:
# /usr/ccs/bin/nm -x /usr/lib/libc.so | grep  "|printf___FCKpd___13quot;
[Index] Value Size Type Bind Other Shndx Name
......
[7653] |0x00061f39|0x00000105|FUNC |GLOB |0 |11 |printf
libc.so中printf的st_value是0x00061f39,由於libc.so是一個共享庫,因此這個地址只是printf在 libc.so中的偏移量,需要和libc.so的加載地址相加纔可以得出真正的虛存地址,而這個地址纔是真正的printf函數的代碼入口。

libc.so中printf的st_shndx的值爲11,當st_shndx是數值是,代表改函數所在的section header的索引號:
# /usr/ccs/bin/elfdump -c /usr/lib/libc.so | grep 11
Section Header[11]: sh_name: .text
sh_size: 0x110 sh_type: [ SHT_SUNW_SIGNATURE ]
ELF文件test中的.symtab和.dynsym都包含了printf,而且st_value都相同,但是我們看到如果strip以後,nm命令沒 有輸出,這是因爲test文件中的.symtab section被去除的原因:
# /usr/ccs/bin/strip test
# /usr/ccs/bin/elfdump -s -N .symtab test | grep printf
# /usr/ccs/bin/nm -x test1 | grep printf
# /usr/ccs/bin/elfdump -s -N .dynsym test | grep printf
[1] 0x08050544 0x00000000 FUNC GLOB D 0 UNDEF printf

實際上只有.dynsym才被影射入內存,.dynsym是實現動態鏈接必須的信息,.symtab根本不會影射入內存。

在test創建的進程中,printf位於地址8050544,用mdb反彙編printf的代碼:
> 8050544::dis
PLT:printf: jmp *0x8060714
PLT:printf: pushl $0x18
PLT:printf: jmp -0x4b <0x8050504>
PLT:_get_exit_frame_monitor: jmp *0x8060718
PLT:_get_exit_frame_monitor: pushl $0x20
PLT:_get_exit_frame_monitor: jmp -0x5b <0x8050504>
................

可以看到,實際上,printf的代碼只有3條指令,顯然,這並不是真正printf的實現,而是叫做PLT的其中部分代碼。


Global Offset Table - 全局偏移量表:GOT存在於可執行文件的數據段中,用於存放位置無關函數的絕對地址。GOT表中的絕對地址實際上是在運行階段時,在位置無關函數首次被 runtime linker解析後才確定。在此之前,GOT中的初值主要是爲了幫助PLT跳轉到runtime linker,把控制權轉交給它的動態綁定函數。

其實,.got的初值在test文件中已經定義:

# /usr/ccs/bin/elfdump -c -N .got test

Section Header[14]: sh_name: .got
sh_addr: 0x80606fc sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x20 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x6fc sh_entsize: 0x4
sh_link: 0 sh_info: 0
sh_addralign: 0x4

# /usr/ccs/bin/elfdump -G test

Global Offset Table Section: .got (8 entries)
ndx addr value reloc addend symbol
[00000] 080606fc 0806071c R_386_NONE 00000000
[00001] 08060700 00000000 R_386_NONE 00000000
[00002] 08060704 00000000 R_386_NONE 00000000
[00003] 08060708 0805051a R_386_JMP_SLOT 00000000 atexit
[00004] 0806070c 0805052a R_386_JMP_SLOT 00000000 __fpstart
[00005] 08060710 0805053a R_386_JMP_SLOT 00000000 exit
[00006] 08060714 0805054a R_386_JMP_SLOT 00000000 printf
[00007] 08060718 0805055a R_386_JMP_SLOT 00000000 _get_exit_frame_monitor

可以看到,在ELF文件中的GOT共有8個表項:

    GOT[0]是保留項,被初始化爲.dynamic section的起始地址。
    GOT[1]和GOT[2]初值爲0,在裝入內存後初始化。
    GOT[3]-GOT[7],被初始化成了對應符號的在PLT中第2條指令的地址。

GOT的結束地址也可以根據section header中的sh_size計算出來:

> 0x80606fc+20=X
806071c
而test運行到main+0x14斷點處,查看GOT:

> 0x80606fc,9/naX
0x80606fc:
0x80606fc: 806071c
0x8060700: d17fd900
0x8060704: d17cb260
0x8060708: d1710814
0x806070c: d1701e51
0x8060710: 805053a
0x8060714: 805054a
0x8060718: 805055a
0x806071c: 1

可以看到,GOT的內容和ELF文件定義的初始值相比,有了一些變化:
> 0x80606fc,9/nap
0x80606fc:
0x80606fc: 0x806071c --->未改變,.dynamic section的起始地址
0x8060700: 0xd17fd900 --->改變,Rt_map首地址,也是link_map首地址
0x8060704: ld.so.1`elf_rtbndr --->改變,Runtime linker的入口
0x8060708: libc.so.1`atexit --->改變,已經被ld.so解析成絕對地址
0x806070c: libc.so.1`_fpstart --->改變,已經被ld.so解析成絕對地址
0x8060710: PLT:exit --->未改變,還未解析,指向PLT:exit的第2條指令
0x8060714: PLT:printf --->未改變,還未解析,指向PLT:printf的第2條指令
0x8060718: PLT:_get_exit_frame_monitor --->未改變,還未解析,指向PLT:_get_exit_frame_monitor的第2條指令
0x806071c: 1


在此時,runtim linker把link map和自己的入口函數地址填入了GOT[1]和GOT[2]中,並且atexit和_fpstart已經被解析成絕對地址。這是因爲每個可執行文件的實 際入口是_start例程,這個例程執行中會調用atexit和_fpstart,然後才調用main函數:

> _start::dis
_start: pushl $0x0
_start+2: pushl $0x0
_start+4: movl %esp,%ebp
_start+6: pushl %edx
_start+7: movl $0x806071c,%eax
_start+0xc: testl %eax,%eax
_start+0xe: je +0x7 <_start+0x15>
_start+0x10: call -0x64 <PLT:atexit>
_start+0x15: pushl $0x80506cc
_start+0x1a: call -0x6e <PLT:atexit>
_start+0x1f: leal 0x80607f4,%eax
_start+0x25: movl (%eax),%eax
_start+0x27: testl %eax,%eax
_start+0x29: je +0x17 <_start+0x40>
_start+0x2b: leal 0x80607f8,%eax
_start+0x31: movl (%eax),%eax
_start+0x33: testl %eax,%eax
_start+0x35: je +0xb <_start+0x40>
_start+0x37: pushl %eax
_start+0x38: call -0x8c <PLT:atexit>
_start+0x3d: addl $0x4,%esp
_start+0x40: movl 0x8(%ebp),%eax
_start+0x43: movl 0x80607d4,%edx
_start+0x49: testl %edx,%edx
_start+0x4b: jne +0xc <_start+0x57>
_start+0x4d: leal 0x10(%ebp,%eax,4),%edx
_start+0x51: movl %edx,0x80607d4
_start+0x57: andl $0xfffffff0,%esp
_start+0x5a: pushl %edx
_start+0x5b: leal 0xc(%ebp),%edx
_start+0x5e: movl %edx,0x80607f0
_start+0x64: pushl %edx
_start+0x65: pushl %eax
_start+0x66: call -0xaa <PLT:__fpstart>
_start+0x6b: call +0x29 <__fsr>
_start+0x70: call +0xd8 <_init>
_start+0x75: call +0x9b <main>
_start+0x7a: addl $0xc,%esp
_start+0x7d: pushl %eax
_start+0x7e: call -0xb2 <PLT:exit>
_start+0x83: pushl $0x0
_start+0x85: movl $0x1,%eax
_start+0x8a: lcall $0x7,$0x0
_start+0x91: hlt

Procedure Linkage Table - 過程鏈接表:PLT存在於每個ELF可執行文件的代碼段,它和可執行文件的數據段中的GOT來一起決定位置無關函數的絕對地址。首先,第一次調用位置無關 函數時,會進入相應函數的PLT入口,PLT的指令會從GOT中讀出默認地址,該地址正好是PLT0的入口地址,PLT0會把控制權交給runtime linker,由runtime linker解析出該函數的絕對地址,然後將這個絕對地址存入GOT,然後,該函數將被調用。然後,當再次調用該函數時,由於GOT中已經存放了該函數入 口的絕對地址,因此PLT對應的指令會直接跳轉到函數絕對地址,而不會再由runtime linker解析。

PLT的一般格式如下:

.PLT0:pushl got_plus_4
      jmp *got_plus_8
      nop; nop
      nop; nop
.PLT1:jmp *name1_in_GOT
      pushl $offset@PC
      jmp .PLT0@PC ...
.PLT2:jmp *name2_in_GOT
      push $offset
      jmp .PLT0@PC
.PLT2:jmp *name3_in_GOT
      push $offset
      jmp .PLT0@PC


可以通過elfdump來實際查看test文件驗證一下:
# /usr/ccs/bin/elfdump -c -N .plt test

Section Header[8]: sh_name: .plt
sh_addr: 0x8050504 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x60 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x504 sh_entsize: 0x10
sh_link: 0 sh_info: 0
sh_addralign: 0x4

這樣,PLT的結束地址也可以計算出來:
> 0x8050504+0x60=X
8050564
根據.plt的起始和結束地址可以反彙編:

> 0x8050504::dis -a -n 13
8050504 pushl 0x8060700 ---->pushl got_plus_4,指向Rt_map地址
805050a jmp *0x8060704 ---->jmp *got_plus_8,跳轉到Runtime linker的入口
8050510 addb %al,(%eax)
8050512 addb %al,(%eax)
8050514 jmp *0x8060708
805051a pushl $0x0
805051f jmp -0x1b <0x8050504>
8050524 jmp *0x806070c
805052a pushl $0x8
805052f jmp -0x2b <0x8050504>
8050534 jmp *0x8060710
805053a pushl $0x10
805053f jmp -0x3b <0x8050504>
8050544 jmp *0x8060714 ---->跳轉到0x805054a,即下一條指令
805054a pushl $0x18
805054f jmp -0x4b <0x8050504>
8050554 jmp *0x8060718
805055a pushl $0x20
805055f jmp -0x5b <0x8050504>
8050564 addb %al,(%eax)

或者包含符號信息:

> 0x8050504::dis -n 13
0x8050504: pushl 0x8060700
0x805050a: jmp *0x8060704
0x8050510: addb %al,(%eax)
0x8050512: addb %al,(%eax)
PLT=libc.so.1`atexit: jmp *0x8060708
PLT=libc.so.1`atexit: pushl $0x0
PLT=libc.so.1`atexit: jmp -0x1b <0x8050504>
PLT=libc.so.1`_fpstart: jmp *0x806070c
PLT=libc.so.1`_fpstart: pushl $0x8
PLT=libc.so.1`_fpstart: jmp -0x2b <0x8050504>
PLT:exit: jmp *0x8060710
PLT:exit: pushl $0x10
PLT:exit: jmp -0x3b <0x8050504>
PLT:printf: jmp *0x8060714
PLT:printf: pushl $0x18
PLT:printf: jmp -0x4b <0x8050504>
PLT:_get_exit_frame_monitor: jmp *0x8060718
PLT:_get_exit_frame_monitor: pushl $0x20
PLT:_get_exit_frame_monitor: jmp -0x5b <0x8050504>
0x8050564: addb %al,(%eax)


在main+0x14處,繼續單步運行:
> :s
mdb: target stopped at:
PLT:printf: jmp *0x8060714
查看0x8060714即printf在GOT中的內容,其實就是PLT:printf中下一條push指令:

> *0x8060714=X
805054a
> *0x8060714::dis -n 1
PLT:printf: pushl $0x18
PLT:printf: jmp -0x4b <0x8050504>

繼續單部執行,馬上就要把0x18壓入棧,這個0x18就是printf在重定位表中的偏移量:

# /usr/ccs/bin/elfdump -c -N .rel.plt test

Section Header[7]: sh_name: .rel.plt
sh_addr: 0x80504dc sh_flags: [ SHF_ALLOC SHF_INFO_LINK ]
sh_size: 0x28 sh_type: [ SHT_REL ]
sh_offset: 0x4dc sh_entsize: 0x8
sh_link: 3 sh_info: 8
sh_addralign: 0x4

# /usr/ccs/bin/elfdump -d test

Dynamic Section: .dynamic
index tag value
[0] NEEDED 0x111 libc.so.1
[1] INIT 0x80506b0
[2] FINI 0x80506cc
[3] HASH 0x80500e8
[4] STRTAB 0x805036c
[5] STRSZ 0x137
[6] SYMTAB 0x80501cc
[7] SYMENT 0x10
[8] CHECKSUM 0x5a2b
[9] VERNEED 0x80504a4
[10] VERNEEDNUM 0x1
[11] PLTRELSZ 0x28
[12] PLTREL 0x11
[13] JMPREL 0x80504dc ---> 重定位表.rel.plt的基地址
[14] REL 0x80504d4
[15] RELSZ 0x30
[16] RELENT 0x8
[17] DEBUG 0
[18] FEATURE_1 0x1 [ PARINIT ]
[19] FLAGS 0 0
[20] FLAGS_1 0 0
[21] PLTGOT 0x80606fc


直接查看重定位表內容:
# /usr/ccs/bin/elfdump -r  test

Relocation Section: .rel.data
type offset section with respect to
R_386_32 0x80607f8 .rel.data __1cG__CrunMdo_exit_code6F_v_

Relocation Section: .rel.plt
type offset section with respect to
R_386_JMP_SLOT 0x8060708 .rel.plt atexit
R_386_JMP_SLOT 0x806070c .rel.plt __fpstart
R_386_JMP_SLOT 0x8060710 .rel.plt exit
R_386_JMP_SLOT 0x8060714 .rel.plt printf
R_386_JMP_SLOT 0x8060718 .rel.plt _get_exit_frame_monitor

其中,printf是4項,而在32位x86平臺上,重定位表的每項的長度爲8字節,定義如下:
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
因此,printf在重定位表中偏移量=(4-1)*8=24,即16進制的0x18。

用mdb查看實際內存中的重定位表:
> 0x80504dc,a/nap
0x80504dc:
0x80504dc: 0x8060708
0x80504e0: 0xf07
0x80504e4: 0x806070c
0x80504e8: 0x1007
0x80504ec: 0x8060710
0x80504f0: 0x1207
0x80504f4: 0x8060714
0x80504f8: 0x107
0x80504fc: 0x8060718
0x8050500: 0x1307

可以看到,printf的r_offset是0x8060714,r_info是0x107。對照前面的GOT各項的地址,可以發現,0x8060714 就是GOT[7]的地址。

> :s
mdb: target stopped at:
PLT:printf: pushl $0x18
繼續單步執行:
> :s
mdb: target stopped at:
PLT:printf: jmp -0x4b <0x8050504>
地址0x8050504就是PLT0的地址:

> :s
mdb: target stopped at:
0x8050504: pushl 0x8060700
0x8060700就是GOT[1],存儲的就是Rt_map的首地址,相當於把Rt_map的首地址壓棧:

> :s
mdb: target stopped at:
0x805050a: jmp *0x8060704
0x8060704就是GOT[2],存儲着runtime linker - ld.so的入口地址:
> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr: pushl %ebp
可以看到,這樣控制權就由PLT這樣轉換到runtime linker了,顯然,下面將進入runtime link editor來動態綁定了,我們查看目前棧的狀態:
> <esp,10/nap
0x804734c:
0x804734c: 0xd17fd900 ----> Rt_map的首地址
0x8047350: 0x18 ----> printf對應項重定位表中的偏移量
0x8047354: main+0x19 ----> printf返回後應跳轉的地址
0x8047358: 0x80506ec
0x804735c: 0x8047460
0x8047360: 0x8047354
0x8047364: 0xd17fb840
0x8047368: 0x8047460
0x804736c: 0x804738c
0x8047370: _start+0x7a
0x8047374: 1
0x8047378: 0x8047398
0x804737c: 0x80473a0
0x8047380: _start+0x1f
0x8047384: _fini
0x8047388: ld.so.1`atexit_fini

查看ld.so.1`elf_rtbndr函數的定義,這部分是平臺相關的,我們只關心32bit x86部分的實現:

link:http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/i386/boot_elf.s

   288 #if defined(lint)
289
290 extern unsigned long elf_bndr(Rt_map *, unsigned long, caddr_t);
291
292 void
293 elf_rtbndr(Rt_map * lmp, unsigned long reloc, caddr_t pc)
294 {
295 (void) elf_bndr(lmp, reloc, pc);
296 }
297
298 #else
299 .globl elf_bndr
300 .globl elf_rtbndr
301 .weak _elf_rtbndr
302 _elf_rtbndr = elf_rtbndr / Make dbx happy
303 .type elf_rtbndr,@function
304 .align 4
305
306 elf_rtbndr:
307 pushl %ebp
308 movl %esp, %ebp
309 pushl %eax
310 pushl %ecx
311 pushl %edx
312 pushl 12(%ebp) / push pc
313 pushl 8(%ebp) / push reloc
314 pushl 4(%ebp) / push *lmp
315 call elf_bndr@PLT / call the C binder code
316 addl $12, %esp / pop args
317 movl %eax, 8(%ebp) / store final destination
318 popl %edx
319 popl %ecx
320 popl %eax
321 movl %ebp, %esp
322 popl %ebp
323 addl $4,%esp / pop args
324 ret / invoke resolved function
325 .size elf_rtbndr, .-elf_rtbndr
326 #endif

315行調用的elf_bndr是平臺相關代碼,函數原型如下:
   290 extern unsigned long    elf_bndr(Rt_map *, unsigned long, caddr_t);
因此在elf_rtbndr的312-314這幾行,實際上是爲調用elf_bndr做傳遞參數的準備:
   312     pushl    12(%ebp)        / push返回地址 main+0x19
313 pushl 8(%ebp) / push重定位表的對應printf項的偏移量 0x18
314 pushl 4(%ebp) / push Rt_map的首地址,0xd17fd900
根據32位x86的ABI,壓棧順序是從右到左,正好吻合elf_bndr的參數順序和類型定義。

通過在elf_bndr函數調用前設置斷點來驗證一下:
> ld.so.1`elf_rtbndr::dis
ld.so.1`elf_rtbndr: pushl %ebp
ld.so.1`elf_rtbndr+1: movl %esp,%ebp
ld.so.1`elf_rtbndr+3: pushl %eax
ld.so.1`elf_rtbndr+4: pushl %ecx
ld.so.1`elf_rtbndr+5: pushl %edx
ld.so.1`elf_rtbndr+6: pushl 0xc(%ebp)
ld.so.1`elf_rtbndr+9: pushl 0x8(%ebp)
ld.so.1`elf_rtbndr+0xc: pushl 0x4(%ebp)
ld.so.1`elf_rtbndr+0xf: call +0x14c5d <ld.so.1`elf_bndr>
ld.so.1`elf_rtbndr+0x14: addl $0xc,%esp
ld.so.1`elf_rtbndr+0x17: movl %eax,0x8(%ebp)
ld.so.1`elf_rtbndr+0x1a: popl %edx
ld.so.1`elf_rtbndr+0x1b: popl %ecx
ld.so.1`elf_rtbndr+0x1c: popl %eax
ld.so.1`elf_rtbndr+0x1d: movl %ebp,%esp
ld.so.1`elf_rtbndr+0x1f: popl %ebp
ld.so.1`elf_rtbndr+0x20: addl $0x4,%esp
ld.so.1`elf_rtbndr+0x23: ret
> ld.so.1`elf_rtbndr+0xf:b
> :c
mdb: stop at ld.so.1`elf_rtbndr+0xf
mdb: target stopped at:
ld.so.1`elf_rtbndr+0xf: call +0x14c5d <ld.so.1`elf_bndr>
下面檢查ld.so.1`elf_bndr調用前棧的狀況,可以看到,3個參數已經按順序壓入棧中:
> <esp,10/nap
0x8047330:
0x8047330: 0xd17fd900
0x8047334: 0x18
0x8047338: main+0x19
0x804733c: 3
0x8047340: libc.so.1`_sse_hw
0x8047344: libc.so.1`__flt_rounds
0x8047348: 0x804736c
0x804734c: 0xd17fd900
0x8047350: 0x18
0x8047354: main+0x19
0x8047358: 0x80506ec
0x804735c: 0x8047460
0x8047360: 0x8047354
0x8047364: 0xd17fb840
0x8047368: 0x8047460
0x804736c: 0x804738c
>
elf_rtbndr會返回我們需要的printf在libc.so中的絕對地址嗎?

用mdb在ld.so.1`elf_rtbndr返回處設置斷點,繼續執行:

> ld.so.1`elf_rtbndr+0x14:b
> :c
mdb: stop at ld.so.1`elf_rtbndr+0x14
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x14:addl $0xc,%esp
檢查一下函數返回值,它應該存在rax的寄存器中:
> <eax=X
d1741f39
顯然,d1741f39就是printf的絕對地址,它處於libc.so中:
> d1741f39::dis -w
libc.so.1`printf: pushl %ebp
libc.so.1`printf+1: movl %esp,%ebp
libc.so.1`printf+3: subl $0x10,%esp
libc.so.1`printf+6: andl $0xfffffff0,%esp
libc.so.1`printf+9: pushl %ebx
libc.so.1`printf+0xa: pushl %esi
libc.so.1`printf+0xb: pushl %edi
libc.so.1`printf+0xc: call +0x5 <libc.so.1`printf+0x11>
libc.so.1`printf+0x11: popl %ebx
libc.so.1`printf+0x12: addl $0x6d0b6,%ebx
libc.so.1`printf+0x18: movl 0x244(%ebx),%esi

此時此刻,GOT中的printf的對應項GOT[7],即0x8060714地址處,已經被ld.so修改成printf的絕對地址:
> 0x80606fc,9/nap
0x80606fc:
0x80606fc: 0x806071c
0x8060700: 0xd17fd900
0x8060704: ld.so.1`elf_rtbndr
0x8060708: libc.so.1`atexit
0x806070c: libc.so.1`_fpstart
0x8060710: PLT:exit
0x8060714: libc.so.1`printf
0x8060718: PLT:_get_exit_frame_monitor
0x806071c: 1
>
printf被成功解析後,ld.so修改了GOT[7],接着就應該把控制權轉到libc的printf函數了。顯然,在 ld.so.1`elf_rtbndr+0x17處的指令將會把eax寄存器中的printf的絕對函數地址存入棧中:
> ld.so.1`elf_rtbndr+0x17:b
> :c
mdb: stop at ld.so.1`elf_rtbndr+0x17
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x17:movl %eax,0x8(%ebp)
此時棧中還沒有printf的地址:
> <esp,10/nap
0x80473cc:
0x80473cc: 3
0x80473d0: libc.so.1`_sse_hw
0x80473d4: libc.so.1`__flt_rounds
0x80473d8: 0x80473fc
0x80473dc: 0xd17fd900
0x80473e0: 0x18
0x80473e4: main+0x19
0x80473e8: 0x80506ec
0x80473ec: 0x80474f4
0x80473f0: 0x80473e8
0x80473f4: 0xd17fb840
0x80473f8: 0x80474f4
0x80473fc: 0x8047420
0x8047400: _start+0x7a
0x8047404: 1
0x8047408: 0x804742c
單步執行後,再觀察棧,會發現,printf已經存入棧:
> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x1a:popl %edx
> <esp,10/nap
0x80473cc:
0x80473cc: 3
0x80473d0: libc.so.1`_sse_hw
0x80473d4: libc.so.1`__flt_rounds
0x80473d8: 0x80473fc
0x80473dc: 0xd17fd900
0x80473e0: libc.so.1`printf
0x80473e4: main+0x19
0x80473e8: 0x80506ec
0x80473ec: 0x80474f4
0x80473f0: 0x80473e8
0x80473f4: 0xd17fb840
0x80473f8: 0x80474f4
0x80473fc: 0x8047420
0x8047400: _start+0x7a
0x8047404: 1
0x8047408: 0x804742c

在ld.so.1`elf_rtbndr返回的前一刻,printf恰好成爲ld.so.1`elf_rtbndr的返回地址:
> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x23:ret
> <esp,10/nap
0x8047350:
0x8047350: libc.so.1`printf
0x8047354: main+0x19
0x8047358: 0x80506ec
0x804735c: 0x8047460
0x8047360: 0x8047354
0x8047364: 0xd17fb840
0x8047368: 0x8047460
0x804736c: 0x804738c
0x8047370: _start+0x7a
0x8047374: 1
0x8047378: 0x8047398
0x804737c: 0x80473a0
0x8047380: _start+0x1f
0x8047384: _fini
0x8047388: ld.so.1`atexit_fini
0x804738c: 0
這樣,控制權就由ld.so到了我們要調用的函數 - printf:
> :s
mdb: target stopped at:
libc.so.1`printf: pushl %ebp
至此,一個完整的動態綁定過程結束,此時可以再次反彙編我們的main函數:
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $0x10,%esp
main+6: movl %ebx,-0x8(%ebp)
main+9: movl %esi,-0xc(%ebp)
main+0xc: movl %edi,-0x10(%ebp)
main+0xf: pushl $0x80506ec
main+0x14: call -0x148 <PLT=libc.so.1`printf>
main+0x19: addl $0x4,%esp
main+0x1c: movl $0x0,-0x4(%ebp)
main+0x23: jmp +0x5 <main+0x28>
main+0x28: movl -0x4(%ebp),%eax
main+0x2b: movl -0x8(%ebp),%ebx
main+0x2e: movl -0xc(%ebp),%esi
main+0x31: movl -0x10(%ebp),%edi
main+0x34: leave
main+0x35: ret
>
可以看到,由於GOT[7]已經存儲了printf的絕對地址,因此,反彙編結果發生了變化。

進程第一次調用printf的動態解析的過程如下:

main
|
V
PLT:printf的第1條指令<---GOT[7]指向的地址
| |
V |
PLT:printf的第2條指令<---------+
|
V
PLT:printf的第3條指令
|
V
PLT0
ld.so.1`elf_rtbndr
|
V
libc.so.1`printf

如果該進程再次調用printf:

main
|
V
PLT:printf的第1條指令<---GOT[7]指向的地址
| |
V |
libc.so.1`printf<---------+


3. elf_bndr函數

elf_rtbndr在32bit x86平臺的源代碼的位置在:
link:http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/i386/i386_elf.c

要實現動態綁定,elf_bndr應至少完成如下工作:

3.1 確定要綁定的符號

下面部分elf_bndr的代碼就是根據重定位表來確定要綁定的符號:

   231     /*
232 * Use relocation entry to get symbol table entry and symbol name.
233 */
234 addr = (ulong_t)JMPREL(lmp);
235 rptr = (Rel *)(addr + reloff);
236 rsymndx = ELF_R_SYM(rptr->r_info);
237 sym = (Sym *)((ulong_t)SYMTAB(lmp) + (rsymndx * SYMENT(lmp)));
238 name = (char *)(STRTAB(lmp) + sym->st_name);
239

JMPREL,SYMTAB,SYMENT,STRTAB這些宏都能從函數第1個入口參數lmp指針,即Rt_map指針中得到下面elfdump中看到 的值:

# /usr/ccs/bin/elfdump -d test

Dynamic Section: .dynamic
index tag value
[0] NEEDED 0x111 libc.so.1
[1] INIT 0x80506b0
[2] FINI 0x80506cc
[3] HASH 0x80500e8
[4] STRTAB 0x805036c --->STRTAB(lmp)的值,字符串表基地址
[5] STRSZ 0x137
[6] SYMTAB 0x80501cc --->SYMTAB(lmp)的值,符號表基地址
[7] SYMENT 0x10 --->SYMENT(lmp)的值,符號表元素的長度
[8] CHECKSUM 0x5a2b
[9] VERNEED 0x80504a4
[10] VERNEEDNUM 0x1
[11] PLTRELSZ 0x28
[12] PLTREL 0x11
[13] JMPREL 0x80504dc --->JMPREL(lmp)的值,重定位表基地址
[14] REL 0x80504d4
[15] RELSZ 0x30
[16] RELENT 0x8
[17] DEBUG 0
[18] FEATURE_1 0x1 [ PARINIT ]
[19] FLAGS 0 0
[20] FLAGS_1 0 0
[21] PLTGOT 0x80606fc

因此,addr的值就是0x80504dc,它實際上是test進程的重定位表的地址。

reloff是第二個參數,在前面查找printf的過程中,我們知道它的值爲0x18,因此rptr的值爲:

rptr = addr + reloff = 0x80504dc + 0x18 = 80504f4

前面已經用mdb查看實際內存中的重定位表的內容:
# mdb test
> 0x80504dc,a/nap
0x80504dc:
0x80504dc: 0x8060708
0x80504e0: 0xf07
0x80504e4: 0x806070c
0x80504e8: 0x1007
0x80504ec: 0x8060710
0x80504f0: 0x1207
0x80504f4: 0x8060714
0x80504f8: 0x107
0x80504fc: 0x8060718
0x8050500: 0x1307
因此rptr->r_offset=0x8060714,rptr->r_info=0x107,實際上這個rptr就指向 printf在重定位表中的相應項,而rptr->r_offset就對應着printf在GOT中的的地址,即GOT[7]地址。

ELF_R_SYM這個宏實際上是向右位移8位,因此rsymndx的值實際上是:

rsymndx = ELF_R_SYM(rptr->r_info)= 0x107 << 8 = 1

sym就是printf的符號表中的記錄:

sym = 0x80501cc + (1 * 0x10) = 0x80501dc

因此name的地址是,它指向printf字符串:

name = 0x805036c + 1 = 0x805036d

# mdb test
> 80501dc,2/nap
0x80501dc:
0x80501dc: 1 ---> sym->st_name
0x80501e0: PLT:printf ---> sym->st_value
> 0x805036d/s
0x805036d: printf ---> name的值
>
可見,根據給定符號對應的重定位表的偏移量,就可以找到該符號的符號表的記錄,進而確定其名字字符串。

3.2 遍歷所有依賴庫的符號表查找給定符號
   244     llmp = LIST(lmp)->lm_tail;
245
246 /*
247 * Find definition for symbol.
248 */
249 sl.sl_name = name;
250 sl.sl_cmap = lmp;
251 sl.sl_imap = LIST(lmp)->lm_head;
252 sl.sl_hash = 0;
253 sl.sl_rsymndx = rsymndx;
254 sl.sl_flags = LKUP_DEFT;
255
256 if ((nsym = lookup_sym(&sl, &nlmp, &binfo)) == 0) {
257 eprintf(ERR_FATAL, MSG_INTL(MSG_REL_NOSYM), NAME(lmp),
258 demangle(name));
259 rtldexit(LIST(lmp), 1);
260 }
261

在256行的lookup_sym函數會根據傳入的符號名和link map返回共享庫中對應的符號表記錄的指針nsym,&nlmp, &binfo是另外的兩個返回值。因此,真正確定符號位置的關鍵參數就是sl參數了,其定義如下:

link:http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/include/rtld.h

775 typedef struct {
776 const char *sl_name; /* symbol name */
777 Rt_map *sl_cmap; /* callers link-map */
778 Rt_map *sl_imap; /* initial link-map to search */
779 ulong_t sl_hash; /* symbol hash value */
780 ulong_t sl_rsymndx; /* referencing reloc symndx */
781 uint_t sl_flags; /* lookup flags */
782 } Slookup;
783

可以看到,sl中包含的信息主要有3類:

符號相關的:*sl_name,sl_hash,sl_rsymndx,唯一地確定符號,sl_hash將用於符號查找 linkmap: *sl_cmap, *sl_imap, 維護着依賴庫加載、ld.so控制信息搜索控制標誌: sl_flags,此標誌直接影響下級調用的code path

要確定一個給定符號在哪一個依賴庫,以及其在共享庫的絕對地址,link map起着關鍵的作用,下面是Rt_map定義:

http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/include/rtld.h:
   64 typedef struct rt_map    Rt_map;

459 struct rt_map {
460 /*
461 * BEGIN: Exposed to rtld_db - don't move, don't delete
462 */
463 Link_map rt_public; /* public data */
..................................................................................
485 struct fct *rt_fct; /* file class table for this object */
486 Sym *(*rt_symintp)(); /* link map symbol interpreter */
487 void *rt_priv; /* private data, object type specific */
488 Lm_list *rt_list; /* link map list we belong to */
..................................................................................
523 };
Rt_map的起始地址處定義了一個結構Link_map,它的定義如下:
   422 typedef struct link_map    Link_map;

422 typedef struct link_map Link_map;
423
424 struct link_map {
425 unsigned long l_addr; /* address at which object is mapped */
426 char *l_name; /* full name of loaded object */
427 #ifdef _LP64
428 Elf64_Dyn *l_ld; /* dynamic structure of object */
429 #else
430 Elf32_Dyn *l_ld; /* dynamic structure of object */
431 #endif
432 Link_map *l_next; /* next link object */
433 Link_map *l_prev; /* previous link object */
434 char *l_refname; /* filters reference name */
435 };
可以看到實際上多個Rt_map是可以通過雙向鏈表鏈接起來。

下面用mdb來查看正在運行着的test的Rt_map,0xd17fd900就是解析printf時傳遞給elf_bndr的首地址:
> 0xd17fd900,20/nap
0xd17fd900:
0xd17fd900: 0x8050000
0xd17fd904: 0x8047ff5
0xd17fd908: 0x806071c
0xd17fd90c: 0xd17fdd40
0xd17fd910: 0
0xd17fd914: 0
0xd17fd918: 0xd17fdbe8
0xd17fd91c: 0x8050000
0xd17fd920: 0x10820
0xd17fd924: 0x10820
0xd17fd928: 0x20421605
0xd17fd92c: 0x602
0xd17fd930: 0
0xd17fd934: 0xd17fdb78
0xd17fd938: 0
0xd17fd93c: 0
0xd17fd940: 0
0xd17fd944: 0
0xd17fd948: 0
0xd17fd94c: 0xd16d00d8
0xd17fd950: 0
0xd17fd954: 0
0xd17fd958: 0
0xd17fd95c: 0x80506f9
0xd17fd960: ld.so.1`elf_fct
0xd17fd964: ld.so.1`elf_find_sym
0xd17fd968: 0xd17fda00
0xd17fd96c: ld.so.1`lml_main
0xd17fd970: 0xffffffff
0xd17fd974: 0
0xd17fd978: 0
0xd17fd97c: 0x1901
Rt_map結構的成員rt_fct是指向struct fct結構的指針,struct fct結構定義如下:

71 typedef struct fct {
72 int (*fct_are_u_this)(Rej_desc *); /* determine type of object */
73 ulong_t (*fct_entry_pt)(void); /* get entry point */
74 Rt_map *(*fct_map_so)(Lm_list *, Aliste, const char *, const char *,
75 int); /* map in a shared object */
76 void (*fct_unmap_so)(Rt_map *); /* unmap a shared object */
77 int (*fct_needed)(Lm_list *, Aliste, Rt_map *);
78 /* determine needed objects */
79 Sym *(*fct_lookup_sym)(Slookup *, Rt_map **, uint_t *);
80 /* initialize symbol lookup */
81 int (*fct_reloc)(Rt_map *, uint_t); /* relocate shared object */
82 Pnode *fct_dflt_dirs; /* list of default dirs to */
83 /* search */
84 Pnode *fct_secure_dirs; /* list of secure dirs to */
85 /* search (set[ug]id) */
86 Pnode *(*fct_fix_name)(const char *, Rt_map *, uint_t);
87 /* transpose name */
88 char *(*fct_get_so)(const char *, const char *);
89 /* get shared object */
90 void (*fct_dladdr)(ulong_t, Rt_map *, Dl_info *, void **, int);
91 /* get symbolic address */
92 Sym *(*fct_dlsym)(Grp_hdl *, Slookup *, Rt_map **, uint_t *);
93 /* process dlsym request */
94 int (*fct_verify_vers)(const char *, Rt_map *, Rt_map *);
95 /* verify versioning (ELF) */
96 int (*fct_set_prot)(Rt_map *, int);
97 /* set protection */
98 } Fct;

可以看到,這個結構中抽象出了一個二進制對象所有相關的操作函數表,根據二進制對象的類型,它可以實際動態綁定函數到不同類型的二進制文件操作函數上,這 種實現方式充分體現了操作系統中面向對象設計思想,這使得ld.so擴展新的可執行文件格式的支持變得相當容易。

ELF文件和a.out文件格式的相關代碼分別如下,僅供參考:

http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/common/elf.c http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/common/a.out.c

用mdb檢查test進程的操作函數,可以看到,由於test的類型是ELF文件,因此elf.c定義的函數表綁定到了rt_fct上:
> ld.so.1`elf_fct,10/nap
ld.so.1`elf_fct:
ld.so.1`elf_fct:
ld.so.1`elf_fct:ld.so.1`elf_are_u
ld.so.1`elf_fct+4: ld.so.1`elf_entry_pt
ld.so.1`elf_fct+8: ld.so.1`elf_map_so
ld.so.1`elf_fct+0xc: ld.so.1`elf_unmap_so
ld.so.1`elf_fct+0x10: ld.so.1`elf_needed
ld.so.1`elf_fct+0x14: ld.so.1`lookup_sym
ld.so.1`elf_fct+0x18: ld.so.1`elf_reloc
ld.so.1`elf_fct+0x1c: ld.so.1`elf_dflt_dirs
ld.so.1`elf_fct+0x20: ld.so.1`elf_secure_dirs
ld.so.1`elf_fct+0x24: ld.so.1`elf_fix_name
ld.so.1`elf_fct+0x28: ld.so.1`elf_get_so
ld.so.1`elf_fct+0x2c: ld.so.1`elf_dladdr
ld.so.1`elf_fct+0x30: ld.so.1`dlsym_handle
ld.so.1`elf_fct+0x34: ld.so.1`elf_verify_vers
ld.so.1`elf_fct+0x38: ld.so.1`elf_set_prot
ld.so.1`elf_secure_dirs: ld.so.1`__rtld_msg+0x133e
與rt_fct類似的是Rt_map的另一個成員,rt_symintp,它實際上指向了真正的符號解析函數elf_find_sym:
....................................
0xd17fd964: ld.so.1`elf_find_sym
....................................
正是elf_find_sym,完成了真正的符號表查找工作。

用mdb來遍歷從0xd17fd900起始的Rt_map的雙向鏈表:
> 0xd17fd900,6/nap
0xd17fd900:
0xd17fd900: 0x8050000 --->test加載地址
0xd17fd904: 0x8047ff5 --->Rt_map對應的二進制對象名,此處是test
0xd17fd908: 0x806071c
0xd17fd90c: 0xd17fdd40 --->後向指針,指向libc.so的link map
0xd17fd910: 0 --->前向指針,此處爲NULL,表明是linkmap list的頭
0xd17fd914: 0
> 0x8047ff5/s
0x8047ff5: test --->名字驗證
> 0xd17fdd40,6/nap
0xd17fdd40:
0xd17fdd40: 0xd16e0000 --->libc.so加載地址
0xd17fdd44: 0xd17fdcd0 --->Rt_map對應的二進制對象名,此處是/lib/libc.so.1
0xd17fdd48: 0xd17afa3c
0xd17fdd4c: 0 ---->後向指針,是NULL,表明是linkmap list的尾
0xd17fdd50: 0xd17fd900 ---->前向指針,指向test的link map
0xd17fdd54: 0
> 0xd17fdcd0/s
0xd17fdcd0: /lib/libc.so.1
與可執行文件不同,共享庫中並沒有在ELF文件的.text section頭中規定共享庫的加載地址,而只是給出了相對地址,待被裝載後才重新確定:
# /usr/ccs/bin/elfdump -c -N .text /usr/lib/libc.so

Section Header[11]: sh_name: .text
sh_addr: 0x1f370 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x89895 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x1f370 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x10
而實際上,通過遍歷linkmap list,ld.so可以確定所有linkmap list中的二進制對象的實際裝載地址。

這裏libc.so的實際地址是0xd16e0000,可以通過pmap(1)驗證得到的地址是否正確:
# pmap -x 1597
1597: test
Address Kbytes RSS Anon Locked Mode Mapped File
08046000 8 8 8 - rwx-- [ stack ]
08050000 4 4 - - r-x-- test
08060000 4 4 4 - rwx-- test
D16C0000 24 12 12 - rwx-- [ anon ]
D16D0000 4 4 4 - rwx-- [ anon ]
D16E0000 764 764 - - r-x-- libc.so.1
D17AF000 24 24 24 - rw--- libc.so.1
D17B5000 8 8 8 - rw--- libc.so.1
D17C8000 140 140 - - r-x-- ld.so.1
D17FB000 4 4 4 - rwx-- ld.so.1
D17FC000 8 8 8 - rwx-- ld.so.1
-------- ------- ------- ------- -------
total Kb 992 980 72 -
同樣的,共享庫中符號表的st_value也不是該符號的絕對地址,而是偏移量,例如,libc.so中符號表中printf的取值是:
# /usr/ccs/bin/elfdump -s -N .dynsym /usr/lib/libc.so | grep " printf___FCKpd___70quot;
[2416] 0x00061f39 0x00000105 FUNC GLOB D 34 .text printf
那麼,如果lookup_sym函數得到printf在libc.so中的符號表記錄的指針,那麼很容易計算得出printf的絕對地址。

本例中,共享庫中printf在符號表中st_value的取值和libc.so的裝載地址都已經確定了,因此printf的絕對地址是:
> 0xd16e0000+0x00061f39=X
d1741f39
如果用mdb反彙編這個地址,d1741f39就是printf在libc.so的真正入口:
> d1741f39::dis -w
libc.so.1`printf: pushl %ebp
libc.so.1`printf+1: movl %esp,%ebp
libc.so.1`printf+3: subl $0x10,%esp
libc.so.1`printf+6: andl $0xfffffff0,%esp
libc.so.1`printf+9: pushl %ebx
libc.so.1`printf+0xa: pushl %esi
libc.so.1`printf+0xb: pushl %edi
libc.so.1`printf+0xc: call +0x5 <libc.so.1`printf+0x11>
libc.so.1`printf+0x11: popl %ebx
libc.so.1`printf+0x12: addl $0x6d0b6,%ebx
libc.so.1`printf+0x18: movl 0x244(%ebx),%esi
前面我們遍歷link map是從0xd17fd900開始的,這個地址指向的Rt_map節點碰巧是整個linkmap list的頭節點。實際上,0xd17fd900指向的Rt_map的準確含義是調用者的link map,假設符號解析的調用是從共享庫發出的,那麼這個地址指向的Rt_map就未必是頭節點了。

實際上,每個進程的Rt_map都指向一個全局變量lml_main,通過該變量即可找到這個進程完整的linkmap list.

Rt_map結構成員rt_list指針就指向lml_main全局變量,它實際上是Lm_list結構,定義如下:
   799 extern Lm_list        lml_main;    /* main's link map list */
Lm_list定義如下:
   239 typedef    struct {
240 /*
241 * BEGIN: Exposed to rtld_db - don't move, don't delete
242 */
243 Rt_map *lm_head; /* linked list pointers to active */
244 Rt_map *lm_tail; /* link-map list */
.....................................................................
263 } Lm_list;
這樣,實際上通過rt_list->lm_head即可定位到進程的linkmap list的頭節點了,elf_bndr函數就是這樣做的:
   250     sl.sl_cmap = lmp;                 --->指向調用者的Rt_map
251 sl.sl_imap = LIST(lmp)->lm_head; --->取得進程的link map list的頭節點
因此,要確定給定符號存在於哪一個依賴的共享庫時,需要遍歷所有linkmap list中的節點時,就需要使用sl.sl_imap。

實際上,ld.so爲mdb提供了專門的命令,以方便與ld.so相關的數據結構的查看:

讓test進程運行:
# mdb test
> main+0x14:b
> :c
mdb: stop at main+0x14
mdb: target stopped at:
main+0x14: call -0x148 <PLT:printf>
裝載ld.so模塊:
> ::load ld.so
查看目前ld.so管理的所有Rt_map:
> ::Rt_maps
Link-map lists (dynlm_list): 0x8046368
----------------------------------------------
Lm_list: 0xd17fb220 (LM_ID_BASE)
----------------------------------------------
lmco rtmap ADDR() NAME()
----------------------------------------------
[0xc] 0xd17fd900 0x08050000 test
[0xc] 0xd17fdd40 0xd16e0000 /lib/libc.so.1
----------------------------------------------
Lm_list: 0xd17fb1e0 (LM_ID_LDSO)
----------------------------------------------
[0xc] 0xd17fd590 0xd17c8000 /lib/ld.so.1

只查看test進程的Rt_maps列表:
> 0xd17fd900::Rt_maps -v
----------------------------------------------
Rt_map located at: 0xd17fd900
----------------------------------------------
NAME: test
PATHNAME: /export/home/personal/blog/test
ADDR: 0x08050000 DYN: 0x0806071c
NEXT: 0xd17fdd40 PREV: 0x00000000
FCT: 0xd17fb054 TLSMODID: 0
INIT: 0x00000000 FINI: 0x00000000
GROUPS: 0x00000000 HANDLES: 0x00000000
DEPENDS: 0xd16d00d8 CALLERS: 0x00000000
DYNINFO: 0xd17fda80 REFNAME:
RLIST: 0x00000000 RPATH:
LIST: 0xd17fb220 [ld.so.1`lml_main]
FLAGS: 0x20421605
[ ISMAIN,RELOCED,ANALYZED,INITDONE,FIXED,MODESET,INITCALL,INITCLCT ]
FLAGS1: 0x00000602
[ RELATIVE,NOINITFINI,USED ]
MODE: 0x00001901
[ LAZY,GLOBAL,WORLD,NODELETE ]
----------------------------------------------
Rt_map located at: 0xd17fdd40
----------------------------------------------
NAME: /lib/libc.so.1
ADDR: 0xd16e0000 DYN: 0xd17afa3c
NEXT: 0x00000000 PREV: 0xd17fd900
FCT: 0xd17fb054 TLSMODID: 0
INIT: 0xd1788c10 FINI: 0xd1788c30
GROUPS: 0x00000000 HANDLES: 0x00000000
DEPENDS: 0xd16d02e0 CALLERS: 0xd16d0120
DYNINFO: 0xd17fdee0 REFNAME:
RLIST: 0x00000000 RPATH:
LIST: 0xd17fb220 [ld.so.1`lml_main]
FLAGS: 0x20420604
[ RELOCED,ANALYZED,INITDONE,MODESET,INITCALL,INITCLCT ]
FLAGS1: 0x00004402
[ RELATIVE,USED,SYMSFLTR ]
MODE: 0x00001901
[ LAZY,GLOBAL,WORLD,NODELETE ]

查看test的Rt_map對用的Lm_list結構:
> 0xd17fb220::Lm_list
Lm_list: 0xd17fb220 (LM_ID_BASE)
----------------------------------------------
lists: 0xd17fd3f0 Alist[used 1: total 8]
----------------------------------------------
head: 0xd17fd900 tail: 0xd17fdd40 ---->可以看到,這裏有link map list的頭尾節點指針
audit: 0x00000000 preexec: 0xd17fdd40
handle: 0x00000000 obj: 2 init: 0 lazy: 0
flags: 0x00000821
[ BASELM,ENVIRON,STARTREL ]
tflags: 0x00000000
>
不難想象,順序遍歷linkmap list,查找當前庫是否包含printf符號,如果包含就返回指向符號表記錄的指針,這就是lookup_sym接下來要做的工作。

3.3 算出符號絕對地址,並存儲到GOT中該符號的對應項中


下面的代碼相當容易理解:
   262     symval = nsym->st_value;
263 if (!(FLAGS(nlmp) & FLG_RT_FIXED) &&
264 (nsym->st_shndx != SHN_ABS))
265 symval += ADDR(nlmp);
symval即printf在libc.so的符號表的st_value。nlmp則返回包含printf的libc的指向Rt_map指針的指針。

263行是保證包含給定符號庫是不是固定地址映像的二進制文件,FLAGS(nlmp)可以從返回的Rt_map中得到二進制對象的類型。 264行則是判斷取得的符號的類型是不是絕對地址。

libc.so是共享庫,因此,最終運行到265行,將st_value與ADDR(nlmp),即libc的基地址相加,得出絕對地址。


下面的代碼會把printf的絕對地址存儲到GOT[7]中,因此首先要得到GOT[7]的地址:
   281     if (!(rtld_flags & RT_FL_NOBIND)) {
282 addr = rptr->r_offset;
在3.1小節,我們已經知道rptr->r_offset就對應着printf在GOT中的的地址,即GOT[7]地址。

下面對addr的改變只發生在當前調用者的Rt_map,即0xd17fd900指向的Rt_map,不是固定影射的二進制對象,我們知道test文件是 固定影射的,因此下面2條語句在printf解析時,根本不會執行:
   283         if (!(FLAGS(lmp) & FLG_RT_FIXED))
284 addr += ADDR(lmp);
最終,304行的語句會將printf的絕對地址存入GOT[7]中:
   285         if (((LIST(lmp)->lm_tflags | FLAGS1(lmp)) &
286 (LML_TFLG_AUD_PLTENTER | LML_TFLG_AUD_PLTEXIT)) &&
287 AUDINFO(lmp)->ai_dynplts) {
..............................................................................
..............................................................................
..............................................................................
299 } else {
300 /*
301 * Write standard PLT entry to jump directly
302 * to newly bound function.
303 */
304 *(ulong_t *)addr = symval;
305 }
306 }

4. lookup_sym -> _lookup_sym -> elf_find_sym

實際上,爲了提高在符號表中查找符號的效率,ELF文件中包含了一個.hash section,可以利用其中的hash表來進行符號查找:
# /usr/ccs/bin/elfdump -h test

Hash Section: .hash
bucket symndx name
0 [1] printf
1 [2] environ
[3] _PROCEDURE_LINKAGE_TABLE_
3 [4] _DYNAMIC
5 [5] _edata
[6] ___Argv
6 [7] _etext
[8] _init
7 [9] __fsr_init_value
9 [10] main
[11] _mcount
10 [12] _environ
11 [13] _GLOBAL_OFFSET_TABLE_
15 [14] _lib_version
16 [15] atexit
[16] __fpstart
18 [17] __fsr
[18] exit
[19] _get_exit_frame_monitor
19 [20] _end
[21] _start
21 [22] _fini
24 [23] __environ_lock
27 [24] __longdouble_used
28 [25] __1cG__CrunMdo_exit_code6F_v_

12 buckets contain 0 symbols
10 buckets contain 1 symbols
6 buckets contain 2 symbols
1 buckets contain 3 symbols
29 buckets 25 symbols (globals)
ELF文件的.hash section提供了hash表本身,以及hash表元素的數目即nbuckets,每個hash表的bucket可能對應一個chain,chain的 每一個元素是下一個符號在字符串表中的索引,這樣這個chain相當於一個字符串索引值組成的list。這樣,給定一個符號名,通過ELF規範定義的 hash函數,可以求得一個bucket號,再根據bucket號,遍歷其對應的chain,對比字符串,來查找符號:
1. hn = elf_hash(sym_name) % nbuckets; 
2. for (ndx = hash[ hn ]; ndx; ndx = chain[ ndx ]) {
3. symbol = sym_tab + ndx;
4. if (strcmp(sym_name, str_tab + symbol->st_name) == 0)
5. return (load_addr + symbol->st_value); }
利用mdb,我們可以得到完整的解析printf時的代碼路徑:
bash-3.00# mdb test
> main+0x14:b
> :c
mdb: stop at main+0x14
mdb: target stopped at:
main+0x14: call -0x148 <PLT:printf>
> ld.so.1`elf_find_sym::dis !grep strcmp
ld.so.1`elf_find_sym+0xbf: call +0x14e14 <ld.so.1`strcmp>
> ld.so.1`elf_find_sym+0xbf:b
> :c
mdb: stop at ld.so.1`elf_find_sym+0xbf
mdb: target stopped at:
ld.so.1`elf_find_sym+0xbf: call +0x14e14 <ld.so.1`strcmp>
> $c
ld.so.1`elf_find_sym+0xbf(80472e8, 80473ac, 80473b0)
ld.so.1`_lookup_sym+0x6e(d17fd900, 80472e8, 80473ac, 80473b0, c)
ld.so.1`lookup_sym+0x1d7(8047358, 80473ac, 80473b0)
ld.so.1`elf_bndr+0xf8(d17fd900, 18, 8050691)
ld.so.1`elf_rtbndr+0x14(18, 8050691, 80506ec, 80474f4, 80473e8, d17fb840)
0xd17fd900(1, 804742c, 8047434)
_start+0x7a(1, 804755c, 0, 8047561, 8047583, 8047597)
>
lookup_sym函數根據給定的符號名,通過hash函數算出其在hash表中的bucket號:

2492 if (slp->sl_hash == 0)
2493 slp->sl_hash = elf_hash(name);
_lookup_sym中循環遍歷了linkmap list,對每個依賴庫調用了SYMINTP來解析符號:
  2438     for (; lmp; lmp = (Rt_map *)NEXT(lmp)) {
2439 if (callable(slp->sl_cmap, lmp, 0)) {
2440 Sym *sym;
2441
2442 slp->sl_imap = lmp;
2443 if ((sym = SYMINTP(lmp)(slp, dlmp, binfo)) != 0)
2444 return (sym);
2445 }
2446 }
如果是ELF文件,SYMINTP對應的則是elf_find_sym函數,它在給定ELF對象的指定bucket中的chain list來查找符號。

查找對比符號必然要調用strcmp函數,因此我們可以利用dtrace腳本來觀察這種比較是如何進行的:
#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
printf("Target pid: %d/n", $target);
}
pid$target::main:entry
{
self->main=1;
}
pid$target::main:return
{
self->main=0;
}
pid$target::elf_find_sym:entry
/self->main==1/
{
self->trace=1;
}
pid$target::elf_find_sym:return
/self->main==1 && self->trace==1 /
{
self->trace=0;
}
pid$target::strcmp:entry
/self->main==1 && self->trace==1 /
{
printf("/n%s`%s(%s,%s)/n", probemod, probefunc,copyinstr(arg0),copyinstr(arg1));
}
運行dtrace腳本來觀察每次elf_find_sym調用strcmp時的入口參數:
# ./test.d -c ./test
hello world
Target pid: 3934
LM1`ld.so.1`strcmp(rintf,rintf)
LM1`ld.so.1`strcmp(rintf,rintf)
LM1`ld.so.1`strcmp(edata,findbuf)
LM1`ld.so.1`strcmp(__Argv,findbuf)
..............................................

可以看到,strcmp在查找printf時只對比了rintf而不是printf,這是爲什麼呢?查看代碼可以找到答案:
  1869         if ((*strtabname++ != *name) || strcmp(strtabname, &name[1])) {
1870 if ((ndx = chainptr[ndx]) != 0)
1871 continue;
1872 return ((Sym *)0);
1873 }
1874

1869行代碼是一個語言或表達式,首先比較兩個字符串的首字符,如果不相等,則或表達式已經爲真,接下來的strcmp就不會被執行。這樣做,可以減低 符號查找時帶來的調用strcmp的開銷。  

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