一、源码编写编译
经过几天的学习,对动态链接进行一些简单的摸索,并将学习总结记录如下。
首先编写两个测试源码文件:
[ main.c ]
1 #include <stdio.h> 2 3 extern int subVal; 4 void SubFunc(void); 5 int mainVal = 3; 6 7 void MainFunc(void) 8 { 9 10 } 11 12 int main() 13 { 14 puts("In Main."); 15 16 mainVal = 6; 17 subVal = 6; 18 19 MainFunc(); 20 SubFunc(); 21 22 return 0; 23 }
[ subFunc.c ]
1 #include <stdio.h> 2 3 static int staticSubVal = 3; 4 int subVal = 3; 5 6 static void StaticTestFunc(void) 7 { 8 9 } 10 11 void TestFunc(void) 12 { 13 14 } 15 16 void SubFunc() 17 { 18 puts("In SubFunc."); 19 20 staticSubVal = 8; 21 subVal = 8; 22 23 StaticTestFunc(); 24 TestFunc(); 25 }
编译源代码:
gcc -shared -fPIC -o libsubFunc.so subFunc.c
gcc -o main main.c -lsubFunc -L.
二、查看对应的ELF文件及反汇编代码
① main ELF:
...
[11] .init PROGBITS 0000000000000630 00000630
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000000650 00000650
0000000000000030 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000000680 00000680
0000000000000008 0000000000000008 AX 0 0 8
[14] .text PROGBITS 0000000000000690 00000690
00000000000001c2 0000000000000000 AX 0 0 16
[22] .got PROGBITS 0000000000200fb0 00000fb0
0000000000000050 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000201000 00001000
0000000000000014 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000201014 00001014
000000000000000c 0000000000000000 WA 0 0 4
[25] .comment PROGBITS 0000000000000000 00001014
000000000000002b 0000000000000001 MS 0 0 1
...
② main DISASM:
00000000000007a1 <main>:
7a1: 55 push %rbp
7a2: 48 89 e5 mov %rsp,%rbp
7a5: 48 8d 3d b8 00 00 00 lea 0xb8(%rip),%rdi # 864 <_IO_stdin_used+0x4>
7ac: e8 af fe ff ff callq 660 <puts@plt>
7b1: c7 05 55 08 20 00 06 movl $0x6,0x200855(%rip) # 201010 <mainVal>
7b8: 00 00 00
7bb: c7 05 4f 08 20 00 06 movl $0x6,0x20084f(%rip) # 201014 <subVal>
7c2: 00 00 00
7c5: e8 d0 ff ff ff callq 79a <MainFunc>
7ca: e8 a1 fe ff ff callq 670 <SubFunc@plt>
7cf: b8 00 00 00 00 mov $0x0,%eax
7d4: 5d pop %rbp
7d5: c3 retq
③ libsubFunc.so ELF:
...
[ 9] .init PROGBITS 0000000000000560 00000560
0000000000000017 0000000000000000 AX 0 0 4
[10] .plt PROGBITS 0000000000000580 00000580
0000000000000030 0000000000000010 AX 0 0 16
[11] .plt.got PROGBITS 00000000000005b0 000005b0
0000000000000008 0000000000000008 AX 0 0 8
[12] .text PROGBITS 00000000000005c0 000005c0
000000000000011c 0000000000000000 AX 0 0 16
[20] .got PROGBITS 0000000000200fd8 00000fd8
0000000000000028 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000201000 00001000
0000000000000028 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000201028 00001028
0000000000000010 0000000000000000 WA 0 0 8
[23] .bss NOBITS 0000000000201038 00001038
0000000000000008 0000000000000000 WA 0 0 1
...
④ libsubFunc.so DISASM:
00000000000006a8 <SubFunc>:
6a8: 55 push %rbp
6a9: 48 89 e5 mov %rsp,%rbp
6ac: 48 8d 3d 32 00 00 00 lea 0x32(%rip),%rdi # 6e5 <_fini+0x9>
6b3: e8 d8 fe ff ff callq 590 <puts@plt>
6b8: c7 05 6e 09 20 00 08 movl $0x8,0x20096e(%rip) # 201030 <staticSubVal>
6bf: 00 00 00
6c2: 48 8b 05 17 09 20 00 mov 0x200917(%rip),%rax # 200fe0 <subVal@@Base-0x54>
6c9: c7 00 08 00 00 00 movl $0x8,(%rax)
6cf: e8 c6 ff ff ff callq 69a <StaticTestFunc>
6d4: e8 c7 fe ff ff callq 5a0 <TestFunc@plt>
6d9: 90 nop
6da: 5d pop %rbp
6db: c3 retq
学习总结一:
(1)与动态链接不同,静态链接在编译生成可执行程序后,程序中所有的符号地址都已经确定了下来,在运行时系统可以直接指向程序的Entry Point,默认情况则为_start,并将运行权限完全交给可执行程序。
(2)而动态链接时,由于动态库是共享的且其地址由系统分配至合适的区域,其地址是不能确定的。在运行可执行程序时,系统会将执行权限先交至链接器(例如:libc.so.6),所以动态链接的符号时在运行时进行链接的。为了增快速度,链接器会采用Lazy的绑定方式,即函数的调用地址会在在第一次调用时才进行绑定。
①第一次调用时,实际的调用地址还未绑定,会执行解析绑定程序:
②绑定程序结束后,函数的调用地址将填入GOT相应位置,下次调用时可直接跳转:
学习总结二:
(1)动态链接时,如果调用 static 的变量及函数,会采用相对寻址的方式,数据段会创建一份副本。
(2)调用非static的变量及函数时,都会通过PLT(procedure linkage table)间接调用。链接器链接时,为了增快速度,函数的调用地址会在在第一次调用时才进行绑定,解析绑定函数为_dl_runtime_resolve_xsave,参数需要两个,分别是重定位索引下标和所在模块ID,具体过程如下:
重定位表在ELF文件中可以找到:
三、运行调试验证
具体调用过程可如下调试所示:
───────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────
0x7ffff7bd36b3 <SubFunc+11> call puts@plt <0x7ffff7bd3590>
0x7ffff7bd36b8 <SubFunc+16> mov dword ptr [rip + 0x20096e], 8 <0x7ffff7dd4030>
0x7ffff7bd36c2 <SubFunc+26> mov rax, qword ptr [rip + 0x200917]
0x7ffff7bd36c9 <SubFunc+33> mov dword ptr [rax], 8
0x7ffff7bd36cf <SubFunc+39> call StaticTestFunc <0x7ffff7bd369a>
► 0x7ffff7bd36d4 <SubFunc+44> call TestFunc@plt <0x7ffff7bd35a0>
进入:
───────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────
► 0x7ffff7bd35a0 <TestFunc@plt> jmp qword ptr [rip + 0x200a7a] <0x7ffff7dd4020>
0x7ffff7bd35a6 <TestFunc@plt+6> push 1
0x7ffff7bd35ab <TestFunc@plt+11> jmp 0x7ffff7bd3580
↓
0x7ffff7bd3580 push qword ptr [rip + 0x200a82] <0x7ffff7dd4008>
0x7ffff7bd3586 jmp qword ptr [rip + 0x200a84] <0x7ffff7dec680>
↓
0x7ffff7dec680 <_dl_runtime_resolve_xsave> push rbx
0x7ffff7dec681 <_dl_runtime_resolve_xsave+1> mov rbx, rsp
执行到 0x7ffff7bd3586 时,栈如下所示:
────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffe3a8 —▸ 0x7ffff7ff5000 —▸ 0x7ffff7bd3000 ◂— jg 0x7ffff7bd3047
01:0008│ 0x7fffffffe3b0 ◂— 0x1
02:0010│ 0x7fffffffe3b8 —▸ 0x7ffff7bd36d9 (SubFunc+49) ◂— nop
03:0018│ rbp 0x7fffffffe3c0 —▸ 0x7fffffffe3d0 —▸ 0x5555555547e0 (__libc_csu_init) ◂— push r15
04:0020│ 0x7fffffffe3c8 —▸ 0x5555555547cf (main+46) ◂— mov eax, 0
由实际调用过程清楚可见,在调用 jmp qword ptr [rip + 0x200a84] <0x7ffff7dec680> 解析绑定函数前,的确传入两个参数,重写位下标 <TestFunc@plt+6> push 1 以及模块的ID push qword ptr [rip + 0x200a82] <0x7ffff7dd4008>
动态库使用时应编译成PIC形式,进程在使用时可使用相对寻址的方式,而无需修改代码中的地址从而产生副本代码浪费了内存,并且共享库所占物理内存可直接进行映射。关于数据部分,可执行程序程序模块在链接时就要将访问到时的数据地址确定下来,如果在可执行程序模块中使用了动态库中的全局变量,那么将在.bss段中创建一个副本,运行时则使用动态库中的初始值进行初始化,而动态库中GOT中该变量的指针也会指向此副本。
DSO的优点:1)动态链接库的好处是可以节省内存占用,资源共享;
2)团队之间成果物升级方便,无需重新编译;
刚接触该部分内容,理解不够透彻,如总结有问题,日后会维护修改。
本文图片摘自:zhuanlan.zhihu.com/p/25892385 感谢博主
更为详细的博文推荐:blog.csdn.net/taocr/article/details/52433614