免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1584 | 回复: 0
打印 上一主题 下一主题

Solaris学习笔记(3) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-03-08 13:44 |只看该作者 |倒序浏览
Solaris学习笔记(3)作者: BadcoffeeEmail: blog.oliver@gmail.comBlog: http://blog.csdn.net/yayong2006年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
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   
main+0x19:                      addl   $0x4,%esp
main+0x1c:                      movl   $0x0,-0x4(%ebp)
main+0x23:                      jmp    +0x5     
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   程序在调用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  |printfprintf的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.1test依赖的库只有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     |printflibc.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   
PLT:_get_exit_frame_monitor:    jmp    *0x8060718
PLT:_get_exit_frame_monitor:    pushl  $0x20
PLT:_get_exit_frame_monitor:    jmp    -0x5b   
................
可以看到,实际上,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+0x10:                    call   -0x64   
_start+0x15:                    pushl  $0x80506cc
_start+0x1a:                    call   -0x6e   
_start+0x1f:                    leal   0x80607f4,%eax
_start+0x25:                    movl   (%eax),%eax
_start+0x27:                    testl  %eax,%eax
_start+0x29:                    je     +0x17   
_start+0x2b:                    leal   0x80607f8,%eax
_start+0x31:                    movl   (%eax),%eax
_start+0x33:                    testl  %eax,%eax
_start+0x35:                    je     +0xb     
_start+0x37:                    pushl  %eax
_start+0x38:                    call   -0x8c   
_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+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   
_start+0x6b:                    call   +0x29   
_start+0x70:                    call   +0xd8   
_start+0x75:                    call   +0x9b   
_start+0x7a:                    addl   $0xc,%esp
_start+0x7d:                    pushl  %eax
_start+0x7e:                    call   -0xb2   
_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   
8050524                         jmp    *0x806070c
805052a                         pushl  $0x8
805052f                         jmp    -0x2b   
8050534                         jmp    *0x8060710
805053a                         pushl  $0x10
805053f                         jmp    -0x3b   
8050544                         jmp    *0x8060714            ---->跳转到0x805054a,即下一条指令
805054a                         pushl  $0x18
805054f                         jmp    -0x4b   
8050554                         jmp    *0x8060718
805055a                         pushl  $0x20
805055f                         jmp    -0x5b   
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   
PLT=libc.so.1`_fpstart:         jmp    *0x806070c
PLT=libc.so.1`_fpstart:         pushl  $0x8
PLT=libc.so.1`_fpstart:         jmp    -0x2b   
PLT:exit:                       jmp    *0x8060710
PLT:exit:                       pushl  $0x10
PLT:exit:                       jmp    -0x3b   
PLT:printf:                     jmp    *0x8060714
PLT:printf:                     pushl  $0x18
PLT:printf:                     jmp    -0x4b   
PLT:_get_exit_frame_monitor:    jmp    *0x8060718
PLT:_get_exit_frame_monitor:    pushl  $0x20
PLT:_get_exit_frame_monitor:    jmp    -0x5b   
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   
继续单部执行,马上就要把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就是PLT0的地址:
> :s
mdb: target stopped at:
0x8050504:      pushl  0x80607000x8060700就是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来动态绑定了,我们查看目前栈的状态:
>  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_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调用前栈的状况,可以看到,3个参数已经按顺序压入栈中:
> 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的寄存器中:
> 显然,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:          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的地址:
> 单步执行后,再观察栈,会发现,printf已经存入栈:
> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x1a:popl   %edx
>
在ld.so.1`elf_rtbndr返回的前一刻,printf恰好成为ld.so.1`elf_rtbndr的返回地址:
> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x23:ret
> 这样,控制权就由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   
main+0x19:                      addl   $0x4,%esp
main+0x1c:                      movl   $0x0,-0x4(%ebp)
main+0x23:                      jmp    +0x5     
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条指令
如果该进程再次调用printf:
main
   |
   V
PLT:printf的第1条指令
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
# 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:     0x1901Rt_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:          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   装载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   
> ld.so.1`elf_find_sym::dis !grep strcmp
ld.so.1`elf_find_sym+0xbf:      call   +0x14e14
> 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
> $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)
阅读笔记:库绑定 - 我们应该让它更精确一些
Technorati Tag:
OpenSolaris

Technorati Tag:
Solaris




本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/768/showart_82515.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP