分享
 
 
 

Solaris学习笔记(3)

王朝other·作者佚名  2006-03-10
窄屏简体版  字體: |||超大  

Solaris学习笔记(3)

作者: Badcoffee

Email: blog.oliver@gmail.com

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的开销。

相关文档:

EXECUTABLE AND LINKABLE FORMAT (ELF)

Linker and Libraries Guide

ELF 动态解析符号过程(修订版)

X86汇编语言学习手记(3)

Solaris学习笔记(2)

阅读笔记:库绑定 - 我们应该让它更精确一些

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有