ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。
ELF 文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so。可执行文件:本文主要讨论的文件格式,总的来说,可执行文件的格式与上述两种文件的格式之间的区别主要在于观察的角度不同:一种称为连接视图(Linking View),一种称为执行视图(Execution View)。
一个典型的ELF文件有两种描述视图:program header和section header.
program header:是对程序运行时所使用的段的描述.
section header: 是对所有二进制段的描述.
每一个ELF文件是由一个ELF 文件头(ELF header)和其余的文件数据构成.这些文件数据包括一下一些内容:
·Program header table 描述0个或是多个段(segments)
·Section header table, 描述0个或是多个节(sections)
·要写到上面两个表中的数据.
段(segments)包含的是程序运行是必要的信息
节(sections)包含的是链接和重定向时所需要的重要数据
同一时间整个文件中的每个beyt不会属于一个以上的段,但是也可以存在不属于任何段的字节.
linux下ELF文件分析工具:
readelfis: 是一个unix下的二进制工具,用来显示一个或多个ELF文件的信息.
elfdump: 是一个Solaris命令,用来查看单个ELF文件的信息.
objdump: 可以查看ELF文件或是其它对象格式的更多信息.
关键词: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
编译和链接后产生ELF文件:
# cc test.c -o test# file testtest: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped
用mdb反汇编main函数:
# mdb test> main::dismain: pushl %ebpmain+1: movl %esp,%ebpmain+3: subl $0x10,%espmain+6: movl %ebx,-0x8(%ebp)main+9: movl %esi,-0xc(%ebp)main+0xc: movl %edi,-0x10(%ebp)main+0xf: pushl $0x80506ecmain+0x14: call -0x148 <LT:printf>main+0x19: addl $0x4,%espmain+0x1c: movl $0x0,-0x4(%ebp)main+0x23: jmp +0x5
可以看到,main+0x14处调用了函数printf,调用前把传递的字符串参数压入栈:
> 0x80506ec/s0x80506ec: hello world
“hello world”在ELF文件的.rodata1 section,处于test的代码段:
# /usr/ccs/bin/elfdump -c -N .rodata1 testSection 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> :rmdb: stop at main+0x14mdb: target stopped at:main+0x14: call -0x148 <LT: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 11Section 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::disPLT:printf: jmp *0x8060714PLT:printf: pushl $0x18PLT:printf: jmp -0x4b <0x8050504>LT:_get_exit_frame_monitor: jmp *0x8060718PLT:_get_exit_frame_monitor: pushl $0x20PLT:_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 testSection 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 testGlobal 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/naX0x80606fc:0x80606fc: 806071c0x8060700: d17fd9000x8060704: d17cb2600x8060708: d17108140x806070c: d1701e510x8060710: 805053a0x8060714: 805054a0x8060718: 805055a0x806071c: 1
可以看到,GOT的内容和ELF文件定义的初始值相比,有了一些变化:
> 0x80606fc,9/nap0x80606fc: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 <LT:atexit>_start+0x15: pushl $0x80506cc_start+0x1a: call -0x6e <LT: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 <LT: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 <LT:__fpstart>_start+0x6b: call +0x29 <__fsr>_start+0x70: call +0xd8 <_init>_start+0x75: call +0x9b
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 testSection 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 138050504 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 *0x8060708805051a pushl $0x0805051f jmp -0x1b <0x8050504>8050524 jmp *0x806070c805052a pushl $0x8805052f jmp -0x2b <0x8050504>8050534 jmp *0x8060710805053a pushl $0x10805053f jmp -0x3b <0x8050504>8050544 jmp *0x8060714 ---->跳转到0x805054a,即下一条指令805054a pushl $0x18805054f jmp -0x4b <0x8050504>8050554 jmp *0x8060718805055a pushl $0x20805055f jmp -0x5b <0x8050504>8050564 addb %al,(%eax)
或者包含符号信息:
> 0x8050504::dis -n 130x8050504: pushl 0x80607000x805050a: jmp *0x80607040x8050510: addb %al,(%eax)0x8050512: addb %al,(%eax)PLT=libc.so.1`atexit: jmp *0x8060708PLT=libc.so.1`atexit: pushl $0x0PLT=libc.so.1`atexit: jmp -0x1b <0x8050504>LT=libc.so.1`_fpstart: jmp *0x806070cPLT=libc.so.1`_fpstart: pushl $0x8PLT=libc.so.1`_fpstart: jmp -0x2b <0x8050504>LT:exit: jmp *0x8060710PLT:exit: pushl $0x10PLT:exit: jmp -0x3b <0x8050504>PLT:printf: jmp *0x8060714PLT:printf: pushl $0x18PLT:printf: jmp -0x4b <0x8050504>PLT:_get_exit_frame_monitor: jmp *0x8060718PLT:_get_exit_frame_monitor: pushl $0x20PLT:_get_exit_frame_monitor: jmp -0x5b <0x8050504>0x8050564: addb %al,(%eax)
在main+0x14处,继续单步运行:
> :smdb: target stopped atLT:printf: jmp *0x8060714
查看0x8060714即printf在GOT中的内容,其实就是PLT:printf中下一条push指令:
> *0x8060714=X 805054a> *0x8060714::dis -n 1PLT:printf: pushl $0x18PLT:printf: jmp -0x4b <0x8050504>
继续单部执行,马上就要把0x18压入栈,这个0x18就是printf在重定位表中的偏移量:
# /usr/ccs/bin/elfdump -c -N .rel.plt testSection 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 testDynamic 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 testRelocation 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/nap0x80504dc:0x80504dc: 0x80607080x80504e0: 0xf070x80504e4: 0x806070c0x80504e8: 0x10070x80504ec: 0x80607100x80504f0: 0x12070x80504f4: 0x80607140x80504f8: 0x1070x80504fc: 0x80607180x8050500: 0x1307
可以看到,printf的r_offset是0x8060714,r_info是0x107。对照前面的GOT各项的地址,可以发现,0x8060714 就是GOT[7]的地址。
> :smdb: target stopped atLT:printf: pushl $0x18
继续单步执行:
> :smdb: target stopped atLT:printf: jmp -0x4b <0x8050504>
地址0x8050504就是PLT0的地址:
> :smdb: target stopped at:0x8050504: pushl 0x8060700
0x8060700就是GOT[1],存储的就是Rt_map的首地址,相当于把Rt_map的首地址压栈:
> :smdb: target stopped at:0x805050a: jmp *0x8060704
0x8060704就是GOT[2],存储着runtime linker - ld.so的入口地址:
> :smdb: target stopped at:ld.so.1`elf_rtbndr: pushl %ebp
可以看到,这样控制权就由PLT这样转换到runtime linker了,显然,下面将进入runtime link editor来动态绑定了,我们查看目前栈的状态:
>
查看ld.so.1`elf_rtbndr函数的定义,这部分是平台相关的,我们只关心32bit x86部分的实现:
link:http://cvs.opensolaris.org/sourc ... tld/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::disld.so.1`elf_rtbndr: pushl %ebpld.so.1`elf_rtbndr+1: movl %esp,%ebpld.so.1`elf_rtbndr+3: pushl %eaxld.so.1`elf_rtbndr+4: pushl %ecxld.so.1`elf_rtbndr+5: pushl %edxld.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调用前栈的状况,可以看到,3个参数已经按顺序压入栈中:
>
elf_rtbndr会返回我们需要的printf在libc.so中的绝对地址吗?
用mdb在ld.so.1`elf_rtbndr返回处设置断点,继续执行:
> ld.so.1`elf_rtbndr+0x14:b> :cmdb: stop at ld.so.1`elf_rtbndr+0x14mdb: target stopped at:ld.so.1`elf_rtbndr+0x14:addl $0xc,%esp
检查一下函数返回值,它应该存在rax的寄存器中:
>
显然,d1741f39就是printf的绝对地址,它处于libc.so中:
> d1741f39::dis -wlibc.so.1`printf: pushl %ebplibc.so.1`printf+1: movl %esp,%ebplibc.so.1`printf+3: subl $0x10,%esplibc.so.1`printf+6: andl $0xfffffff0,%esplibc.so.1`printf+9: pushl %ebxlibc.so.1`printf+0xa: pushl %esilibc.so.1`printf+0xb: pushl %edilibc.so.1`printf+0xc: call +0x5
此时此刻,GOT中的printf的对应项GOT[7],即0x8060714地址处,已经被ld.so修改成printf的绝对地址:
> 0x80606fc,9/nap0x80606fc:0x80606fc: 0x806071c0x8060700: 0xd17fd9000x8060704: ld.so.1`elf_rtbndr0x8060708: libc.so.1`atexit0x806070c: libc.so.1`_fpstart0x8060710: PLT:exit0x8060714: libc.so.1`printf0x8060718: PLT:_get_exit_frame_monitor0x806071c: 1>
printf被成功解析后,ld.so修改了GOT[7],接着就应该把控制权转到libc的printf函数了。显然,在 ld.so.1`elf_rtbndr+0x17处的指令将会把eax寄存器中的printf的绝对函数地址存入栈中:
> ld.so.1`elf_rtbndr+0x17:b> :cmdb: stop at ld.so.1`elf_rtbndr+0x17mdb: target stopped at:ld.so.1`elf_rtbndr+0x17:movl %eax,0x8(%ebp)
此时栈中还没有printf的地址:
>
单步执行后,再观察栈,会发现,printf已经存入栈:
> :smdb: target stopped at:ld.so.1`elf_rtbndr+0x1a:popl %edx>
在ld.so.1`elf_rtbndr返回的前一刻,printf恰好成为ld.so.1`elf_rtbndr的返回地址:
> :smdb: target stopped at:ld.so.1`elf_rtbndr+0x23:ret>
这样,控制权就由ld.so到了我们要调用的函数 - printf:
> :smdb: target stopped at:libc.so.1`printf: pushl %ebp
至此,一个完整的动态绑定过程结束,此时可以再次反汇编我们的main函数:
> main::dismain: pushl %ebpmain+1: movl %esp,%ebpmain+3: subl $0x10,%espmain+6: movl %ebx,-0x8(%ebp)main+9: movl %esi,-0xc(%ebp)main+0xc: movl %edi,-0x10(%ebp)main+0xf: pushl $0x80506ecmain+0x14: call -0x148
可以看到,由于GOT[7]已经存储了printf的绝对地址,因此,反汇编结果发生了变化。
进程第一次调用printf的动态解析的过程如下:
main | VPLT:printf的第1条指令<---GOT[7]指向的地址 | | V |PLT:printf的第2条指令<---------+ | VPLT:printf的第3条指令 | VPLT0ld.so.1`elf_rtbndr | Vlibc.so.1`printf
如果该进程再次调用printf:
main | VPLT:printf的第1条指令<---GOT[7]指向的地址 | | V |libc.so.1`printf<---------+
3. elf_bndr函数
elf_rtbndr在32bit x86平台的源代码的位置在:
link:http://cvs.opensolaris.org/sourc ... tld/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