ELF动态连接时,会PLT(procesure linkage table)的方式来进程链接其他模块的函数。就是不会把所有的函数都链接好,而是在第一次去调用的时候去连接
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char **argv)
{
puts("Hello World\n");
exit(0);
}
我的ubantu是64位的,要编译32为的可执行文件,要加参数-m32, 但是还要安装支持32位的插件
sudo apt-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib
在通过下面的命令来进行编译,即可生成32位的可以行文档
gcc -m32 -no-pie -g -o plt plt.c
通过objdump 命令可以查看到plt的段信息
kayshi@ubuntu:~/code/compile/dynamic_link$ objdump -h plt
11 .plt 00000040 080482f0 080482f0 000002f0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 08048330 08048330 00000330 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
..............
20 .dynamic 000000e8 08049f14 08049f14 00000f14 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000004 08049ffc 08049ffc 00000ffc 2**2
CONTENTS, ALLOC, LOAD, DATA
22 .got.plt 00000018 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
- 使用gdb plt来查看内部是怎么链接的
kayshi@ubuntu:~/code/compile/dynamic_link$ gdb plt
(gdb) disass main //使用这个命令可以打印main函数的指令的位置和汇编的信息
Dump of assembler code for function main:
0x08048456 <+0>: lea 0x4(%esp),%ecx
0x0804845a <+4>: and $0xfffffff0,%esp
0x0804845d <+7>: pushl -0x4(%ecx)
0x08048460 <+10>: push %ebp
0x08048461 <+11>: mov %esp,%ebp
0x08048463 <+13>: push %ebx
0x08048464 <+14>: push %ecx
0x08048465 <+15>: call 0x8048390 <__x86.get_pc_thunk.bx>
0x0804846a <+20>: add $0x1b96,%ebx
0x08048470 <+26>: sub $0xc,%esp
0x08048473 <+29>: lea -0x1af0(%ebx),%eax
0x08048479 <+35>: push %eax
0x0804847a <+36>: call 0x8048300 <puts@plt> //这里是调用put函数的位置
0x0804847f <+41>: add $0x10,%esp
0x08048482 <+44>: sub $0xc,%esp
0x08048485 <+47>: push $0x0
0x08048487 <+49>: call 0x8048310 <exit@plt>
End of assembler dump.
(gdb) b *0x0804847a //在调用put函数的地址前加断点,
Breakpoint 1 at 0x804847a: file plt.c, line 6.
(gdb) r //运行
Starting program: /home/kayshi/code/compile/dynamic_link/plt
Breakpoint 1, 0x0804847a in main (argc=1, argv=0xffffd5b4) at plt.c:6
6 puts("Hello World\n");
(gdb)
通过上面可以看出put函数是链接函数,名字被修改成了puts@plt 位置在0x8048300。对比上面objdump 显示的段信息可以得知,这个地址位于.plt段的内部。
- 通过si进行单步执行
可以通过x/4i $pc 查看当前pc的位置和该位置下面的地址及汇编信息,4表示显示4条
(gdb) si
0x08048300 in puts@plt ()
(gdb) x/4i $pc
=> 0x8048300 <puts@plt>: jmp *0x804a00c
0x8048306 <puts@plt+6>: push $0x0
0x804830b <puts@plt+11>: jmp 0x80482f0
0x8048310 <exit@plt>: jmp *0x804a010
(gdb)
这里可以得知 puts@plt会跳到*0x804a00c内部的地址区执行,带星号表示会到这个地址内部的地址去执行,类似指针。
- 产看一下0x804a00c放的是什么地址 x/wr 0x804a00c
(gdb) x/wr 0x804a00c
0x804a00c: 0x08048306
(gdb)
放的是0x08048306,这里说明0x804a00c里面的地址还没有执行put的的地址,而是0x08048306,也就是上面命0x08048300下面的地址。如果链接已经完成的话0x804a00c会放put的地址,现在只是继续向下执行。
- 继续输入si执行
(gdb) si
0x08048306 in puts@plt ()
(gdb) x/4i $pc
=> 0x8048306 <puts@plt+6>: push $0x0 //执行push
0x804830b <puts@plt+11>: jmp 0x80482f0
0x8048310 <exit@plt>: jmp *0x804a010
0x8048316 <exit@plt+6>: push $0x8
(gdb) si
0x0804830b in puts@plt ()
(gdb) x/4i $pc
=> 0x804830b <puts@plt+11>: jmp 0x80482f0 //跳到这个地址,.plt的开头地址
0x8048310 <exit@plt>: jmp *0x804a010
0x8048316 <exit@plt+6>: push $0x8
0x804831b <exit@plt+11>: jmp 0x80482f0
(gdb) si
0x080482f0 in ?? ()
(gdb) x/4i $pc
=> 0x80482f0: pushl 0x804a004 //内部是0xf7ffd940
0x80482f6: jmp *0x804a008
0x80482fc: add %al,(%eax)
0x80482fe: add %al,(%eax)
(gdb) si
0x080482f6 in ?? ()
(gdb) x/4i $pc
=> 0x80482f6: jmp *0x804a008 //跳到这里地址内部的地址执行
0x80482fc: add %al,(%eax)
0x80482fe: add %al,(%eax)
0x8048300 <puts@plt>: jmp *0x804a00c
(gdb) x/wr 0x804a008 //查看0x804a008 放的是0xf7fead90,这个是链接器ld的地址
0x804a008: 0xf7fead90
(gdb) si
0xf7fead90 in ?? () from /lib/ld-linux.so.2 //进入链接器执行
(gdb) x/4i $pc
=> 0xf7fead90: push %eax
0xf7fead91: push %ecx
0xf7fead92: push %edx
0xf7fead93: mov 0x10(%esp),%edx
(gdb)
通过这可以发现本来在在.plt的300 -30b这段地址执行,后来跳到2f0这个地址去执行了,这个地址是.plt的首地址,在开始这个位置存在调用链接器ld的指令。就是本来在这个的段的中间执行,现在pc又指向这个段的开始执行,那么随着指针向下移动。这个0x8048300 最开始执行puts@plt函数的地址,还会再次执行。就是在这个过程程puts@plt地址会指向真正的put函数。 因为当指针再次指向0x8048300 之前 ,调用的链接器ld的地址,就是上面的0x804a008 这个地址内部的地址,链接器ld会把puts@plt原本指向0x804a00c这个地址内部的地址,由原来的0x08048306变成put函数的首地址。
- 多次输入ni快速执行,跳过ld 的过程
(gdb) ni
0xf7feadab in ?? () from /lib/ld-linux.so.2 //ld链接完成了
(gdb) x/4i $pc
=> 0xf7feadab: ret $0xc
0xf7feadae: xchg %ax,%ax
0xf7feadb0: push %esp
0xf7feadb1: addl $0x8,(%esp)
(gdb) ni
0xf7e47360 in puts () from /lib32/libc.so.6 //0xf7e47360 是libc.so下的put函数的地址位置
(gdb) x/4i $pc
=> 0xf7e47360 <puts>: push %ebp
0xf7e47361 <puts+1>: mov %esp,%ebp
0xf7e47363 <puts+3>: push %edi
0xf7e47364 <puts+4>: push %es
- 查看一次0x804a00c的地址是否更改
(gdb) x/wr 0x804a00c
0x804a00c: 0xf7e47360
(gdb)
到这里ld链接器,真正的吧puts@plt链接到了put函数上.
由
puts@plt -> 0x804a00c->0x08048306
变成了
puts@plt -> 0x804a00c->0xf7e47360
接下来就是执行put函数了,如果这个进程再次使用put函数,就可以直接跳到put函数指定的位置执行了。因为地址确定了。
这是动态链接的延迟绑定(PLT)
总结:通过上面的执行过程可以发现。需要动态链接的函数,都会以别名的方式放在.plt这个段(例如puts@plt),他们指向的位置是.plat.got 这里,如果地址确定好了,就直接执行函数,如果没有确定好。就会回到.plt的开头位置去调用链接器ld来把地址确定好之后。就可以执行具体的函数了