ELF文件加载与动态链接(二)

GOT应该保存的是puts函数的绝对虚地址,这里为什么保存的却是puts@plt的第二条指令呢? 原来“解释器”将动态库载入内存后,并没有直接将函数地址更新到GOT表中,而是在函数第一次被调用时,才会进行函数地址的重定位,这样做的好处是可以加快程序加载速度,尤其对大型程序来说。有关这方面的更详细的信息,可以搜索“动态链接库的延迟绑定技术”。

继续看第二条指令,pushq $0x0代表什么? 查看Hello world程序的重定位节:

ezreal@ez:~/workdir$ readelf -a hello
...
Relocation section '.rela.plt' at offset 0x398 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
000000601028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0

其中的第一项就是puts的重定位信息,$0x0即代表puts相对于.rela.plt节的相对偏移。类似的可以看到$0x1代表__libc_start_main的相对偏移,$0x2代表__gmon_start__的相对偏移。

而OFFSET这个域表示的是puts函数在GOT表项中的位置,0000000000601018,从puts@plt的第一条指令可以看出这一点。

向堆栈中压入这个偏移量的目的一是找到puts函数的符号名,二是找到puts函数地址在GOT表项中的位置,以便后面定位到puts的相对虚地址时写入到这个GOT表项。

puts@plt的第三条指令就跳到了PLT0的位置。这条指令只是将0x400400这个数值压入堆栈,它实际上是GOT表项的第二个元素即GOT[1],上面写了GOT[1]是共享库链表的地址。

接着PLT0的第二条指令即跳到了GOT[2]中所保存的地址,即_dl_runtime_resolve函数的入口。_dl_runtime_resolve的定义如下:

_dl_runtime_resolve:
    pushl %eax      # Preserve registers otherwise clobbered.
    pushl %ecx
    pushl %edx
    movl 16(%esp), %edx # Copy args pushed by PLT in register.  Note
    movl 12(%esp), %eax # that `fixup' takes its parameters in regs.
    call _dl_fixup      # Call resolver.
    popl %edx       # Get register content back.
    popl %ecx
    xchgl %eax, (%esp)  # Get %eax contents end store function address.
    ret $8          # Jump to function address.

_dl_runtime_resolve的汇编代码请参考: http://althing.cs.dartmouth.edu/secref/resources/plt-got.txt

从调用puts函数到现在,总共有两次压栈操作,一次是压入puts函数的重定向信息的偏移量(pushq $0x0),一次是GOT[1](pushq 0x200c02(%rip))。上面汇编指令的两次movl操作就是将这两个数据分别取到edx和eax,然后调用_dl_fixup找到puts函数的实际加载地址,将它写到GOT中,并把这个地址压入eax作为_dl_runtime_resolve函数的返回值。xchagl指令恢复eax寄存器,并将puts函数地址压到栈顶,这样当执行ret指令后,控制就转移到puts函数内部。ret指令同时也完成了清栈动作,使栈顶指向puts函数的返回地址(main函数中callq的下一条指令),这样当puts函数返回时程序就走到正确的位置执行。

当第二次调用puts函数时,由于其地址已经存在GOT对应表项中,直接根据地址跳转就可以。下面画了一张图理解上述过程:

总结

用户通过shell执行程序,shell通过exceve进入系统调用。【用户态】
sys_execve经过一系列过程,并最终通过ELF文件的处理函数load_elf_binary将用户程序和ELF“解释器”加载进内存,并将控制权交给“解释器”。【内核态】
ELF“解释器”进行相关共享库的加载,并最终把控制权交给用户程序。由“解释器”处理用户程序运行过程中符号的动态解析。【用户态】

参考资料

    1. ELF文件格式分析 —— 滕启明

    2. ELF文件的加载和动态链接过程

    3. PLT/GOT探索,http://althing.cs.dartmouth.edu/secref/resources/plt-got.txt

    4. Stackoverflow,_dl_runtime_resolve — When do the shared objects get loaded in to memory?

    5. rip相对寻址,http://bbs.pediy.com/showthread.php?t=193425

    6. RIP相对寻址技术,一句话总结,http://www.vxjump.net/files/virus_analysis/X64_vir_infect.txt

原文地址:https://www.cnblogs.com/gm-201705/p/9901553.html