链接器在把所需要的共享库加载到内存后,并没有把共享库中的函数的地址写到GOT表中,而是延迟到函数的第一次调用时,才会对函数的地址进行定位。(红线为第一次函数调用的顺序,蓝线为后续函数调用的顺序)
下面以puts函数为例分析第一次函数调用的过程:
链接器在把所需要的共享库加载到内存后,并没有把共享库中的函数的地址写到GOT表中,而是延迟到函数的第一次调用时,才会对函数的地址进行定位。(红线为第一次函数调用的顺序,蓝线为后续函数调用的顺序)
下面以puts函数为例分析第一次函数调用的过程:
(1)进入puts@plt,执行指令
(2)跳到GOT表中相应位置,此时GOT表里保存的是pushl n的地址,实际上就是顺序执行下一步
(3)执行pushl n,n为puts函数地址在GOT表中的位置,向堆栈中压入这个偏移量的主要作用就是为了找到puts函数的符号名以及puts函数地址在GOT表项中所占的位置,以便在函数定位完成后将函数的实际地址写到这个位置。
(4)跳到PTL0的位置,这步将GOT[1],即共享库链表的地址压栈
(5)顺序执行jmp GOT[2],GOT[2]保存的是_dl_runtime_resolve函数的入口
(6)执行_dl_runtime_resolve函数,该函数会找到puts函数的实际加载地址,并把它写到GOT表中,返回时就会进入puts函数内执行。
函数后续执行就会直接跳转的GOT表puts函数对应位置,该位置保存着puts函数的地址,直接进入puts函数。
GOT表
Global Offset Table,在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。
在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址(加载的共享库会形成一个链表);GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中
PLT表
Procedure Linkage Table,过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。
在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。
原文链接:https://de4dcr0w.github.io/Linux%E5%86%85%E6%A0%B8%E5%A6%82%E4%BD%95%E8%A3%85%E8%BD%BD%E5%92%8C%E5%90%AF%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%8F%AF%E6%89%A7%E8%A1%8C%E7%A8%8B%E5%BA%8F.html
以上是动态加载过程。
android中还有个/proc/虚拟文件夹,不在硬盘上。包含了各种进程和系统信息。self/maps XXX/maps
分别是自身进程映射和其他进程映射。
在maps中有个 Linker是共享库的加载/链接器,也可以称为解释器(interpreter)。(/system/bin/linker)
http://www.voidcn.com/article/p-kzjxymae-qr.html
获取其他进程函数地址一般是根据本地函数地址和模块的基址,加上远程模块的基址进行计算就可以得到远程函数的地址。
二进制文件解析
我们写个简单的程序
#include <unistd.h>
#include <stdlib.h>
int main(void) {
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
调用read函数的情况
.text:0000000000400686 mov rsi, rax ; buf
.text:0000000000400689 mov edi, 0 ; fd
.text:000000000040068E call _read
.text:0000000000400693 mov [rbp+var_24], eax
.text:0000000000400696 cmp [rbp+var_24], 0
.text:000000000040069A jns short loc_4006B5
首先进入elf的read.plt
.plt:0000000000400520 ; ssize_t read(int fd, void *buf, size_t nbytes)
.plt:0000000000400520 _read proc near ; CODE XREF: main+28↓p
.plt:0000000000400520 jmp cs:off_601028
.plt:0000000000400520 _read endp
.plt:0000000000400520
601028地址位于got.plt这个就是常说的got表。
.got.plt:0000000000601000 ; ===========================================================================
.got.plt:0000000000601000
.got.plt:0000000000601000 ; Segment type: Pure data
.got.plt:0000000000601000 ; Segment permissions: Read/Write
.got.plt:0000000000601000 _got_plt segment qword public 'DATA' use64
.got.plt:0000000000601000 assume cs:_got_plt
.got.plt:0000000000601000 ;org 601000h
.got.plt:0000000000601000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000601008 qword_601008 dq 0 ; DATA XREF: sub_4004F0↑r
.got.plt:0000000000601010 qword_601010 dq 0 ; DATA XREF: sub_4004F0+6↑r
.got.plt:0000000000601018 off_601018 dq offset write ; DATA XREF: _write↑r
.got.plt:0000000000601020 off_601020 dq offset __stack_chk_fail
.got.plt:0000000000601020 ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0000000000601028 off_601028 dq offset read ; DATA XREF: _read↑r
.got.plt:0000000000601030 off_601030 dq offset __libc_start_main
.got.plt:0000000000601030 ; DATA XREF: ___libc_start_main↑r
.got.plt:0000000000601038 off_601038 dq offset perror ; DATA XREF: _perror↑r
.got.plt:0000000000601040 off_601040 dq offset exit ; DATA XREF: _exit↑r
.got.plt:0000000000601040 _got_plt ends
.got.plt:0000000000601040
上面代码的read的位置在首次调用的时候填写的是dl_runtime_resolve()函数地址,通过此函数计算出read函数的真实地址后替换got.plt.read位置的函数,下次调用就可以直接获取read函数的地址。
参考:
https://xz.aliyun.com/t/5120