zoukankan      html  css  js  c++  java
  • Solaris学习笔记(3)


    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)
    阅读笔记:库绑定 - 我们应该让它更精确一些


    Technorati Tag: OpenSolaris
    Technorati Tag: Solaris


  • 相关阅读:
    MVC模式-----struts2框架(2)
    MVC模式-----struts2框架
    html的<h>标签
    jsp脚本元素
    LeetCode "Paint House"
    LeetCode "Longest Substring with At Most Two Distinct Characters"
    LeetCode "Graph Valid Tree"
    LeetCode "Shortest Word Distance"
    LeetCode "Verify Preorder Sequence in Binary Search Tree"
    LeetCode "Binary Tree Upside Down"
  • 原文地址:https://www.cnblogs.com/ainima/p/6330816.html
Copyright © 2011-2022 走看看