动态连接是现在操作系统中程序的默认使用方式,非常重要。但是搞懂动态连接你必须真正掌握静态连接。不然你是看不明白的
在只有静态连接的世界里,所有的代码在运行之会被连接器做最后的加工,分配好运行地址。类似下面这样。
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: 48 83 ec 10 sub $0x10,%rsp
4004f5: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
4004fc: 48 8d 45 fc lea -0x4(%rbp),%rax
400500: 48 89 c6 mov %rax,%rsi
400503: bf 2c 10 60 00 mov $0x60102c,%edi
^------变量地址 被反汇编器翻译之后的地址
^------------指令+数据,因为x86是小端存储,所以绝对地址是反过来的存储的
400508: e8 07 00 00 00 callq 400514 <swap>
^------函数地址,被反汇编器翻译之后的地址
^------------指令+数据,07开始的数据是相对地址
40050d: b8 00 00 00 00 mov $0x0,%eax
400512: c9 leaveq
400513: c3 retq
静态连接就是把上面绝对地址跳转和相对地址跳转全部写死,整个程序作为一台精密的机器在运行。
而动态连接目的就是无论A程序的代码加载到哪个地址。B,C,D…程序都能访问到它。(我们把A程序 叫做共享对象,B,C,D程序叫做可执行程序。而一堆共享对象的集合就叫共享库或者叫动态库。)为了完成这个目标。我们要解决2个问题。
为了解决第一个问题我们,先创建一个共享库和访问它的可执行程序看看。
//lib.h
#ifndef LIB_H
#define LIB_H
int void fun(int i);
#endif
//lib.c
//#include
int fun(int i){
// printf("this msg from lib.so %d\n",i);
return i;
}
把上面的代码变成共享库。
gcc -shared -o lib.so lib.c
再创建一个访问它的可执行文件
#include"lib.h"
#include
int main(){
printf("%d\n",fun(0));
sleep(-1);
return 0;
}
gcc -o program program.c ./lib.so
运行./program 。一切正常。记住如果使用你自己的共享库一定要指定路径,不然gcc以为你用的是默认路径下的那些共享库。
接下来我们就要弄清楚程序是如何找到共享库的。回想静态连接也有一个找代码的过程那就重定位。而动态连接也是一样。但是细节上有所不同。
静态连接中我们可以这样按图索骥,elf 文件头->段表->符号表->重定位表,而动态连接也要先从段表找到相应的表才能完成动态连接的工作。我们也按顺序说。
如果一个elf 文件中有Dynamic段,那么它就需要动态连接。它就像目录一样。里面存着完成动态连接所需要的所有东西的地址。
(base) [root@10 桌面]# readelf -d program
Dynamic section at offset 0xe18 contains 25 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[./lib.so]
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000c (INIT) 0x400518
0x000000000000000d (FINI) 0x400744
0x0000000000000019 (INIT_ARRAY) 0x600e00
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e08
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x4003d8
0x0000000000000006 (SYMTAB) 0x4002d0
0x000000000000000a (STRSZ) 118 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 120 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x4004a0
0x0000000000000007 (RELA) 0x400488
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x400468
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x40044e
0x0000000000000000 (NULL) 0x0
(base) [root@10 桌面]# readelf -d lib.so
Dynamic section at offset 0xe18 contains 24 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000c (INIT) 0x520
0x000000000000000d (FINI) 0x664
0x0000000000000019 (INIT_ARRAY) 0x200df8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x200e00
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0x350
0x0000000000000006 (SYMTAB) 0x230
0x000000000000000a (STRSZ) 167 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x201000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x4f0
0x0000000000000007 (RELA) 0x430
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x410
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x3f8
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
(NEEDED) <—依赖的共享库名称
(INIT) <—初始化代码偏移量
(FINI) <—结束代码偏移量
(GNU_HASH) <—符号hash表偏移量。加快符号查找过程
(STRTAB) <—动态符号字符串表偏移量
(SYMTAB) <—指向动态连接的符号表偏移量.dynsym ,是.symtab的子集,只包含动态连接的符号。
(STRSZ) <—动态连接字符串大小
(SYMENT) <—单个动态连接符号大小
(RELA) <—动态连接重定位表偏移量
(RELASZ) <—动态连接重定位表大小
(RELAENT) <—单个动态连接重定位表元素大小
通过.dynsym我们可以找到动态符号表和动态重定位表
通过.dynmic表我们可以找到.dynsym,它是动态连接的符号表。这些符号同时也在.symtab(静态连接的符号表)里面。
(base) [root@10 桌面]# readelf -sD lib.so <----只显示自己已经定义的符号,
Symbol table of `.gnu.hash' for image:
Num Buc: Value Size Type Bind Vis Ndx Name
6 0: 0000000000201028 0 NOTYPE GLOBAL DEFAULT 21 _edata
7 0: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 22 _end
8 1: 0000000000201028 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
9 1: 0000000000000520 0 FUNC GLOBAL DEFAULT 9 _init
10 2: 0000000000000664 0 FUNC GLOBAL DEFAULT 12 _fini <---这些global函数,可以被别人使用。也叫导出函数
11 2: 0000000000000655 12 FUNC GLOBAL DEFAULT 11 fun
(base) [root@10 桌面]# readelf --dyn-sym lib.so <---显示所有定义或者引用的符号
Symbol table '.dynsym' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
6: 0000000000201028 0 NOTYPE GLOBAL DEFAULT 21 _edata
7: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 22 _end
8: 0000000000201028 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
9: 0000000000000520 0 FUNC GLOBAL DEFAULT 9 _init
10: 0000000000000664 0 FUNC GLOBAL DEFAULT 12 _fini
11: 0000000000000655 12 FUNC GLOBAL DEFAULT 11 fun
(base) [root@10 桌面]# readelf --dyn-sym program
Symbol table '.dynsym' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fun
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (2) <---这些引用别人的函数。也叫导入函数
6: 0000000000601044 0 NOTYPE GLOBAL DEFAULT 24 _edata
7: 0000000000601048 0 NOTYPE GLOBAL DEFAULT 25 _end
8: 0000000000601044 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
9: 0000000000400518 0 FUNC GLOBAL DEFAULT 11 _init
10: 0000000000400744 0 FUNC GLOBAL DEFAULT 14 _fini
动态连接的符号表结构和静态连接时一样的,所以各个列的含义都一样,这里就不多赘述了。但是有一点要搞明白。就是上面的value值。
不管是共享库还是引用共享库的可执行文件其实他们都是可以直接运行的,对你没听错,其实共享库和可执行文件对于操作系统来说都是一样的。比如我们可以直接运行下面的so
/lib64/ld-linux-x86-64.so.2
你可能会问为啥不运行我们自己的llib.so呢,那当然运行会报错才不运行啦,不信你看下面。
(base) [root@10 桌面]# ./lib.so
段错误(吐核)
为啥会这样呢,答案是2方面
(base) [root@10 桌面]# readelf -l lib.so
Elf 文件类型为 DYN (共享目标文件)
入口点 0x570
共有 7 个程序头,开始于偏移量64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
^-----逻辑地址都是0 。因为代码段和数据段内容的相对位置不会变,所以系统会把程序整体平移到内存的某个地方。这个过程叫做基址重置(bebasing)。
LOAD 0x0000000000000df8 0x0000000000200df8 0x0000000000200df8
0x0000000000000230 0x0000000000000238 RW 200000
........
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .jcr .data.rel.ro .dynamic .got
。。。。。。
[root@paas-controller-2:/home/ubuntu]$ cat /proc/19830/maps
555555554000-555555555000 r-xp 00000000 fd:00 67113449 /home/ubuntu/wen/lib.so
^-----------------可以看见装载到这了。对于进程来说0附近地址根本就不在自己的虚拟地址中,而我们的动态库中的函数都没链接,所以地址都是0,是不能访问的。
555555754000-555555756000 rw-p 00000000 fd:00 67113449 /home/ubuntu/wen/lib.so
^-----------------也可以看见数据段是紧接着代码段,所以代码和数据的相对位置是不变的
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
[root@paas-controller-2:/home/ubuntu]$
(base) [root@10 桌面]# objdump -d lib.so
。。。。。。。。。。。
Disassembly of section .text:
//可以 readelf -l 看程序的入口点
0000000000000570 <deregister_tm_clones>: <------程序入口
570: 48 8d 05 b8 0a 20 00 lea 0x200ab8(%rip),%rax # 20102f <_edata+0x7>
577: 48 8d 3d aa 0a 20 00 lea 0x200aaa(%rip),%rdi # 201028 <_edata>
57e: 55 push %rbp
57f: 48 29 f8 sub %rdi,%rax <------%rax=%rax-%rdi=0x4
582: 48 89 e5 mov %rsp,%rbp
585: 48 83 f8 0e cmp $0xe,%rax
589: 77 02 ja 58d <deregister_tm_clones+0x1d> <----- 因为$0xe>%rax 不跳转
58b: 5d pop %rbp
58c: c3 retq <----运行到这返回,返回退出
58d: 48 8b 05 44 0a 20 00 mov 0x200a44(%rip),%rax # 200fd8 <_ITM_deregisterTMCloneTable>
594: 48 85 c0 test %rax,%rax
597: 74 f2 je 58b <deregister_tm_clones+0x1b>
599: 5d pop %rbp
59a: ff e0 jmpq *%rax
59c: 0f 1f 40 00 nopl 0x0(%rax)
。。。。。。
#现在来看返回到哪了
[root@paas-controller-2:/home/ubuntu/wen]$ gdb ./lib.so <----使用调试器看看
###进图调试器后
(gdb) b *deregister_tm_clones <----打断点
Breakpoint 1 at 0x570
(gdb) r <----r/run 运行
(gdb) info frame <----查看当前堆栈
Stack level 0, frame at 0x7fffffffe438:
rip = 0x555555554570 in deregister_tm_clones; saved rip 0x1 <----函数的返回地址是0x1,访问不了,所以运行报错
Arglist at 0x7fffffffe428, args:
Locals at 0x7fffffffe428, Previous frame's sp is 0x7fffffffe438
Saved registers:
rip at 0x7fffffffe430
#我们在看看寄存器和内存证实一下是不是对的,
(gdb) i registers
。。。。
rbp 0x0 0x0 <---- 上一个栈帧的栈底,因为停在了进程初始化之后运行的第一个函数。所以上一个栈帧理论上是没有的。用一个非法地址填充。
rsp 0x7fffffffe430 0x7fffffffe430 <---- 上一个栈帧的栈顶
。。。。。
rip 0x555555554570 0x555555554570 <deregister_tm_clones>
。。。。
#因为函数停在栈帧初始化之前所以这个rsp表示上个栈的栈顶。
(gdb) x /1xg $sp
0x7fffffffe430: 0x0000000000000001 <---- 非法地址
........
而像 /lib64/ld-linux-x86-64.so.2 ,./lib64/ibc-2.17.so 这种能运行的共享库就不会像我们写的共享库那样返回到不存在的栈帧。他们最后都是走系统调用,让内核把自己整个进程销毁掉。那我们的为啥就不能向他们那样调用内核的函数呢?如果你想要和他们一样退出的有2种方法(1)手写汇编(直接触发中断),(2)调用别人的函数(系统调用)。
(1) 我们手写下面汇编代码就可以运行正常退出了,汇编解释可以看这个链接
//lib.c
int fun(int i){
asm(
"movl $1, %eax \n\t"
"movl $40, %ebx \n\t"
"int $0x80 "
);
return i;
}
#编译的时候记得一定要用 -e (entry) 指定入口到哦
gcc -shared -o lib.so -e fun lib.c
发现可以完美结束。但是这个太底层了,太原始了,如果你有资源打开了的话,你还要关闭这些资源。所以不推荐这样。
(2)我们试试调用别人写的函数看看。
#include
int fun(int i){
exit(0);
return i;
}
你会发现按照直接的方法编译不通过,提示让你用-fPIC.
(base) [root@10 桌面]# gcc -shared -o lib.so -e fun --save-temps lib.c
^--------------可以保存中间过程,下面会分析
/bin/ld: lib.o: relocation R_X86_64_PC32 against symbol `exit@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
/bin/ld: 最后的链结失败: 错误的值
collect2: 错误:ld 返回 1
(base) [root@10 桌面]#
那我们加上试试呢
(base) [root@10 桌面]# gcc -shared -o lib.so -e fun -fPIC lib.c
#可以编译通过了。但是运行起来还是报错
(base) [root@10 桌面]# ./lib.so
段错误(吐核)
发现还是不行。好了我们来写一个可以运行成功代码把。
#include
const char ldpath[] __attribute__ ((section (".interp"))) = "/lib64/ld-linux-x86-64.so.2";
int fun(int i){
exit(0);
return i;
(base) [root@10 桌面]# gcc -shared -o lib.so -e fun -fPIC lib.c
(base) [root@10 桌面]# ./lib.so
终于可以正常运行了,那么为啥要加上-fPIC和.interp就能运行呢?这就涉及到动态连接的核心思想了。还记得我们上面说的动态连接的文件里面只能使用相对地址。所以-fPIC和.interp就是在这个前提下让你用上未知地址的函数的方法。我们会在下面的章节详细解释-fPIC和.interp
PIC全称position-indepenent code(地址无关代码),为了搞清楚这个,我们看一下上节编译失败时保存的目标文件。
(base) [root@10 桌面]# readelf -r lib.o
重定位节 '.rela.text' 位于偏移量 0x220 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000011 000b00000002 R_X86_64_PC32 0000000000000000 exit - 4
^-------报错说R_X86_64_PC32类型的exit 函数在共享对象中不能用。除非你用fPIC来编译。
重定位节 '.rela.eh_frame' 位于偏移量 0x238 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
我们加上发PIC看看会 怎么样
(base) [root@10 桌面]# gcc -shared -o lib.so -e fun --save-temps -fPIC lib.c
(base) [root@10 桌面]# readelf -r lib.o
重定位节 '.rela.text' 位于偏移量 0x250 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000011 000c00000004 R_X86_64_PLT32 0000000000000000 exit - 4
^------变成这个了
重定位节 '.rela.eh_frame' 位于偏移量 0x268 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
(base) [root@10 桌面]#
要想搞清楚这些类型干啥用的,你就要理解地址无关代码什么意思。我们lib.so加点代码
#include
const char ldpath[] __attribute__ ((section (".interp"))) = "/lib64/ld-linux-x86-64.so.2";
static int static_var;
extern int inter_var;//可能在共享库外。也可能是在共享库的其他目标文件内
void inner_fun(){
}
int fun(int i){
// printf("this msg from lib.so %d\n",i);
static_var=1;
inter_var=100;
inner_fun();
exit(0);//定义在stdlib.h里面,是extern的
return i;
}
(base) [root@10 桌面]# gcc -shared -o lib.so -e fun --save-temps -fPIC lib.c
我们定义了各种类型的变量和函数,有模块内的也有模块外的,我们来看它们是如何寻址的
。。。。。。
00000000000006e0 <fun>:
6e0: 55 push %rbp
6e1: 48 89 e5 mov %rsp,%rbp
6e4: 48 83 ec 10 sub $0x10,%rsp
6e8: 89 7d fc mov %edi,-0x4(%rbp)
6eb: c7 05 3f 09 20 00 01 movl $0x1,0x20093f(%rip) # 201034
^----- x86是小端存储3f 09 20 00 其实就是0x20093f,此时rip=0x6f5 ,这条命令的意思就是给0x201034处的static_var变量赋值1
6f2: 00 00 00
6f5: 48 8b 05 e4 08 20 00 mov 0x2008e4(%rip),%rax # 200fe0
^-----------------------和上面一样获得inter_var的地址
6fc: c7 00 64 00 00 00 movl $0x64,(%rax)
702: b8 00 00 00 00 mov $0x0,%eax
707: e8 e4 fe ff ff callq 5f0 <inner_fun@plt>
^----------------------------------------------------跳转到 下一条指令 0x70c+0xfffffee4=0x5f0 处
70c: bf 00 00 00 00 mov $0x0,%edi
711: e8 ea fe ff ff callq 600 <exit@plt>
^------------------------------------------------------跳转到 下一条指令 0x716+0xfffffeea=0x600 处
。。。。
00000000000005f0 <inner_fun@plt>: <------这种plt结尾的函数是为了延迟绑定设计的。主要是为了提升动态加载的性能.延迟绑定的时候细说
5f0: ff 25 22 0a 20 00 jmpq *0x200a22(%rip) # 201018
^-------------跳转到这个地址。
5f6: 68 00 00 00 00 pushq $0x0
5fb: e9 e0 ff ff ff jmpq 5e0 <.plt> <----进行动态绑定的函数
0000000000000600 <exit@plt>:
600: ff 25 1a 0a 20 00 jmpq *0x200a1a(%rip) # 201020
606: 68 01 00 00 00 pushq $0x1
60b: e9 d0 ff ff ff jmpq 5e0 <.plt>
。。。。。。。
#我们在看看它们在哪个分段
。。。。。。
[20] .got PROGBITS 0000000000200fd8 00000fd8 <------外部变量在这
0000000000000028 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000201000 00001000 <------引用的函数最终跳转到这里面了。
0000000000000030 0000000000000008 WA 0 0 8
[22] .bss NOBITS 0000000000201030 00001030 <------- 未初始化的静态变量在这
0000000000000008 0000000000000000 WA 0 0 4
。。。。。
写了怎么多,就是想说明动态链接中是不能用绝对地址的。而R_X86_64_PC32虽然是相对寻址,但是如果这个类型在目标文件中,就必须在编译的时候重定位,也就是必须在编译的时候确定地址在哪。但是exit是c运行库的代码,根本就不在我们的符号表中。所以编译器直接报错了。如果你自己定义个同样类型的exit(就像我解决方案一),就能让编译器重定位到。自己写一个c库的核心函数,和整个底层打交道,想想就头疼不现实。
从上面分析的结果中。你会发现。所有引用的函数都在一个叫.got.plt 的section中。我们接着往下往下捋
可以使用下面命令看动态链接文件是不是PIC编译的
bash readelf -d /lib64/libc-2.17.so | grep TEXTREL
全局偏移表.got和.got.plt (global offset table,procedure linkage table),他是我们程序和外面程序交互的中转站。你所有对外部的访问都是通过它,更夸张的是,我们引用共享对象内自己定义的函数都要通过它。当你访问外部数据和函数时,操作系统中的一个程序(动态连接器)会在这个结构中填上外部对象的地址。所以只需读取.got和.got.plt中的值就能得到对应对象的地址。
##你会发现.got.plt 里面已经有值了。我们来看看这些地址都指向哪
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ readelf -x 21 lib.so
“.got.plt”节的十六进制输出:
NOTE: This section has relocations against it, but these have NOT been applied to this dump.
0x00201000 180e2000 00000000 00000000 00000000 .. .............
^------64位是8字节一组,且都是小端存储0x208e18,指向.dynamic section
0x00201010 00000000 00000000 f6050000 00000000 ................
^------0x5f6,指向<inner_fun@plt>的第二条指令
0x00201020 06060000 00000000 16060000 00000000 ................
^ ^------0x616,指向<__cxa_finalize@plt>的第二条指令
^------0x606,指向<exit@plt>的第二条指令
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ readelf -x 20 lib.so
“.got”节的十六进制输出:
0x00200fd8 00000000 00000000 00000000 00000000 ................
0x00200fe8 00000000 00000000 00000000 00000000 ................
0x00200ff8 00000000 00000000 ........
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ readelf -S lib.so
。。。。。。。
[19] .dynamic DYNAMIC 0000000000200e18 00000e18
^-------.got.plt第一个元素
00000000000001c0 0000000000000010 WA 4 0 8
。。。。。。。
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ objdump -d lib.so
。。。。。。。
Disassembly of section .plt:
00000000000005e0 <.plt>:
5e0: ff 35 22 0a 20 00 pushq 0x200a22(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
^-------.got.plt第二个元素,存着模块ID,之后延迟绑定细说
5e6: ff 25 24 0a 20 00 jmpq *0x200a24(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
^-------.got.plt第三个元素,存着绑定函数地址。延迟绑定细说
5ec: 0f 1f 40 00 nopl 0x0(%rax)
00000000000005f0 <inner_fun@plt>:
5f0: ff 25 22 0a 20 00 jmpq *0x200a22(%rip) # 201018
5f6: 68 00 00 00 00 pushq $0x0
^-------.got.plt第四个元素
5fb: e9 e0 ff ff ff jmpq 5e0 <.plt>
0000000000000600 <exit@plt>:
600: ff 25 1a 0a 20 00 jmpq *0x200a1a(%rip) # 201020
606: 68 01 00 00 00 pushq $0x1
^-------.got.plt第五个元素
60b: e9 d0 ff ff ff jmpq 5e0 <.plt>
0000000000000610 <__cxa_finalize@plt>:
610: ff 25 12 0a 20 00 jmpq *0x200a12(%rip) # 201028 <__cxa_finalize@GLIBC_2.2.5>
616: 68 02 00 00 00 pushq $0x2
^-------.got.plt第六个元素
61b: e9 c0 ff ff ff jmpq 5e0 <.plt>
经过上面的梳理你应该可以感受到got的作用。我们在之前的章节中多次强调,共享库的装载地址是不确定的,共享库中的代码只能用相对地址,而代码段和数据段是相对位置是固定的。所以可以用got来存储外部的绝对地址,用相对寻址来找到got。这样就可以解决共享库各个代码相互寻找的问题
我们简单总结一下。.got中存着外部数据的地址,.got.plt存着外部函数的地址,这些结构中的地址会在代码跑起来之前被操作系统中的一个程序(动态连接器)填充。
我们接着说一下延迟绑定。从上面的分析我们得到一个顺序。当在动态链接中引用一个函数xxxx,它指向xxxx@plt这个延迟绑定函数,xxxx@plt中的第一条指令就是跳转到.got.plt中存的地址。而.got.plt中 存的地址就是xxxx@plt的第二条指令地址,入栈函数id。xxxx@plt的第三条指令调用<.plt>函数。<.plt>函数会把函数id,模块id,传给绑定函数。我上面说过操作系统中有一个程序叫做动态连接器。它能填充.got和.got.plt表。而这个填充工作就是绑定函数做的。所以只要我们调用一次函数,就会触发这个绑定函数。如果不调用动态连接器就不会绑定。可以减少动态连接器一开始的工作,提升性能。
那么动态连接器是如何进行绑定的呢?
程序跑起来之前。动态链接会把所有用到的共享库装载到程序的进程中。然后获取所有的符号表。这样就知道各个符号的定义地址了。这样哪里引用了对应的函数和数据就把地址填充到那。而引用的位置就存在.rel.dyn和 .rel.plt。在静态链接中对应的是.rel.data和 .rel.text 。而这个填充的过程就叫做重定位。
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ readelf -r lib.so
重定位节 '.rela.dyn' at offset 0x4b8 contains 8 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000200e00 000000000008 R_X86_64_RELATIVE 6d0
000000200e08 000000000008 R_X86_64_RELATIVE 690
000000200e10 000000000008 R_X86_64_RELATIVE 200e10
000000200fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
^---------------从这开始重定位地方都在.pot表中,这里面都应该被填充外部数据的地址
000000200fe0 000200000006 R_X86_64_GLOB_DAT 0000000000000000 inter_var + 0
000000200fe8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
重定位节 '.rela.plt' at offset 0x578 contains 3 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000201018 000b00000007 R_X86_64_JUMP_SLO 00000000000006d9 inner_fun + 0
^---------------从这开始重定位地方都在.pot.plt表中,这里面都应该被填充外部函数的地址
000000201020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
000000201028 000600000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ readelf -S lib.so
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$ readelf --dyn-sym lib.so
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
^------------- UND表示目标文件中引用的对象
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND inter_var
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
7: 0000000000000730 28 OBJECT GLOBAL DEFAULT 13 ldpath
^------------- 数字表示目标文件中定义的对象
8: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 21 _edata
9: 0000000000201038 0 NOTYPE GLOBAL DEFAULT 22 _end
10: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
11: 00000000000006d9 7 FUNC GLOBAL DEFAULT 11 inner_fun
12: 00000000000006e0 54 FUNC GLOBAL DEFAULT 11 fun
(base) [6092003521@zte.intra@LIN-107F2060E3C cc]$
.rel.dyn和 .rel.plt,又名动态重定位表。你可能注意到R_X86_64_RELATIVE ,R_X86_64_GLOB_DAT,R_X86_64_JUMP_SLO这些类型。这些类型的作用就是告诉动态连接器如何填写地址。R_X86_64_GLOB_DAT,R_X86_64_JUMP_SLO 类型,符号地址是多少就填多少,而R_X86_64_RELATIVE的填地址方式叫做基址重置(bebasing),就是程序加载到的地址A加上符号在程序中的偏移量B。例如上面的前三个重定位符号都是在数据段中引用了代码段或数据段的地址。因为这些被引用代码段和数据段地址在装载后是会变的,但是他们的相对位置是不会变的。所以要用基址重置
我们上面介绍了动态连接器会帮完成引用外部对象的工作。而这个动态连接器必须要在.interp section中指定。
#看有没有.interp段
readelf -S | grep interp
#程序头也能看连接器
readelf -l /lib64/libc-2.17.so | grep interpret
#看依赖的连接库
ldd /lib64/libc-2.17.so
elf文件运行是从execve(用户态)->sys_execve(内核)->do_execve->search_binary_handle->load_elf_binary
共享库的搜索路径优先级 LD_PRELOAD环境变量>LD_LIBRAR_PATH环境变量>/etc/ld.so.config配置文件>/>/usr/local/lib64>/usr/lib64>lib64
动态连接器会在我们程序运行之前运行,这时候就像在宇宙大爆炸之前,什么都没有,不能调用任何函数和外部数据,它必须要重定位自己。然后装载所有依赖的共享库,重定位和初始化。
我们花了上万字解释了外部代码是如何被引用的。现在来看看多个程序引用同一个外部函数,它们的地址是不是相同的。
//建2个一样的文件分别叫做program1.c和program2.c
#include
int main(){
printf("program ");
}
#编译
gcc -fno-builtin -o program1 program1.c
gcc -fno-builtin -o program2 program2.c
#2个文件的重定位表是一样的,这里以 program1举例子了
[root@paas-controller1:/home/ubuntu/wen]$ readelf -r program1
重定位节 '.rela.dyn' 位于偏移量 0x380 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
重定位节 '.rela.plt' 位于偏移量 0x398 含有 3 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
^---------------------可执行文件中这个值就是逻辑地址
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
[root@paas-controller1:/home/ubuntu/wen]$ gdb ./program1 <-------调试这个程序
(gdb) b *main <-------------------------------------打断点
(gdb) r <-------------------------------------运行
(gdb) display /20i $pc <----------------------------------------输出提示显示更多的汇编指令
1: x/20i $pc
=> 0x40052d <main>: push %rbp
0x40052e <main+1>: mov %rsp,%rbp
0x400531 <main+4>: mov $0x4005e0,%edi
0x400536 <main+9>: mov $0x0,%eax
0x40053b <main+14>: callq 0x400410 <printf@plt>
0x400540 <main+19>: pop %rbp
0x400541 <main+20>: retq
0x400542: nopw %cs:0x0(%rax,%rax,1)
0x40054c: nopl 0x0(%rax)
(gdb) b *main+20 <-------------------------------------我们要让延迟绑定触发不然看不到print函数地址。
(gdb) n <-------------------------------------接着运行
(gdb) x /1xg 0x601018
0x601018: 0x00007ffff7a604a0 <----------------------------这个就是printf的地址,内核空间
#用上面同样的方法你会发现program2中的printf 也是这个地址
通过动态链接不仅可以引用共享库中的代码。而且比静态链接省内存。至此我们解答了一开始提出的问题二。
我们来看看一个进程栈中最早入栈的都是啥。
//lib.c
void fun(){
//建一个超级简单的可运行共享库
}
#编译成直接运行的共享库程序
gcc -shared -o lib.so -e fun --save-temps lib.c
#用gdb调试器调试一下
[root@paas-controller1:/home/ubuntu/wen]$ gdb ./lib.so
(gdb) b *fun <------------------------------------------入口打一个断点
Breakpoint 1 at 0x655
(gdb) r <-------------------------------------------让程序跑起来
Starting program: /home/ubuntu/wen/./lib.so
(gdb) i f <-------------------------------------------看一下栈帧
Stack level 0, frame at 0x7fffffffe558:
rip = 0x555555554655 in fun; saved rip 0x1
Arglist at 0x7fffffffe548, args:
Locals at 0x7fffffffe548, Previous frame's sp is 0x7fffffffe558 <---上一个栈帧的栈顶指针
Saved registers:
rip at 0x7fffffffe550
(gdb) x /550xg 0x7fffffffe550 <--------用16进制显示栈上面550个8字节内存单元
0x7fffffffe550: 0x0000000000000001 0x00007fffffffe7a7 <--------文件名地址,指向下面的那些字符串
0x7fffffffe560: 0x0000000000000000 0x00007fffffffe7bf <---------环境变量存储的地址
^-------------------------全是零表示分隔区域
0x7fffffffe570: 0x00007fffffffe7d2 0x00007fffffffe7ec
0x7fffffffe580: 0x00007fffffffe7f7 0x00007fffffffe807
0x7fffffffe590: 0x00007fffffffe815 0x00007fffffffe81f
0x7fffffffe5a0: 0x00007fffffffedbb 0x00007fffffffedcc
0x7fffffffe5b0: 0x00007fffffffedda 0x00007fffffffede8
0x7fffffffe5c0: 0x00007fffffffedf4 0x00007fffffffee10
0x7fffffffe5d0: 0x00007fffffffee33 0x00007fffffffee3e
0x7fffffffe5e0: 0x00007fffffffee53 0x00007fffffffee64
0x7fffffffe5f0: 0x00007fffffffee6d 0x00007fffffffeea0
0x7fffffffe600: 0x00007fffffffeeab 0x00007fffffffeec0
0x7fffffffe610: 0x00007fffffffeec8 0x00007fffffffeed5
0x7fffffffe620: 0x00007fffffffeef8 0x00007fffffffefb7
0x7fffffffe630: 0x00007fffffffefc5 0x0000000000000000 <---------分隔区域
0x7fffffffe640: 0x0000000000000021 0x00007ffff7ffd000 <--------- auxiliary vector 开始的地方,每一个元素由2个子元素组成。类型和值。
^--- auxiliary vector 类型 ^--- auxiliary vector 值,下面同理
0x7fffffffe650: 0x0000000000000010 0x00000000bfebfbff
0x7fffffffe660: 0x0000000000000006 0x0000000000001000
0x7fffffffe670: 0x0000000000000011 0x0000000000000064
0x7fffffffe680: 0x0000000000000003 0x0000555555554040 <------AT_PHDR,程序头的地址。
0x7fffffffe690: 0x0000000000000004 0x0000000000000038 <------AT_PHENT,程序头中元素的大小(字节)。
0x7fffffffe6a0: 0x0000000000000005 0x0000000000000007 <------AT_PHENT,程序头中元素数量 。
0x7fffffffe6b0: 0x0000000000000007 0x0000000000000000 <------AT_BASE,动态连接器的地址
0x7fffffffe6c0: 0x0000000000000008 0x0000000000000000
0x7fffffffe6d0: 0x0000000000000009 0x0000555555554655 <------AT_ENTRY ,入口地址,我们关注的
0x7fffffffe6e0: 0x000000000000000b 0x0000000000000000
0x7fffffffe6f0: 0x000000000000000c 0x0000000000000000
0x7fffffffe700: 0x000000000000000d 0x0000000000000000
0x7fffffffe710: 0x000000000000000e 0x0000000000000000
0x7fffffffe720: 0x0000000000000017 0x0000000000000000
---Type <return> to continue, or q <return> to quit---
0x7fffffffe730: 0x0000000000000019 0x00007fffffffe789
0x7fffffffe740: 0x000000000000001a 0x0000000000000000
0x7fffffffe750: 0x000000000000001f 0x00007fffffffefe0
0x7fffffffe760: 0x000000000000000f 0x00007fffffffe799
0x7fffffffe770: 0x0000000000000000 0x0000000000000000 <--------- auxiliary vector 结束的的地方,AT_NULL
0x7fffffffe780: 0x0000000000000000 0x74477c3edae94600 <---------UNspecified 的地方,未定义的地方
^---------分隔区域
0x7fffffffe790: 0x5b4dfd1f55163060 0x0034365f36387853
0x7fffffffe7a0: 0x2f00000000000000 0x7562752f656d6f68 <---------information block 的地方。环境变量在这里面。我们用字符串来显示他们
0x7fffffffe7b0: 0x2f6e65772f75746e 0x58006f732e62696c
。。。。。。。。。....。.。。。。。。。。。。。。。。。
(gdb) x /550sg 0x7fffffffe550 <--用字符串显示栈上面550个8字节内存单元
。。。。。。。。。....。.。。。。。。。。。。。。。。。
0x7fffffffe786: ""
0x7fffffffe787: ""
0x7fffffffe788: ""
0x7fffffffe789: "F\351\332>|Gt`0\026U\037\375M[Sx86_64"
0x7fffffffe7a0: ""
0x7fffffffe7a1: ""
0x7fffffffe7a2: ""
0x7fffffffe7a3: ""
0x7fffffffe7a4: ""
0x7fffffffe7a5: ""
0x7fffffffe7a6: ""
0x7fffffffe7a7: "/home/ubuntu/wen/lib.so"
0x7fffffffe7bf: "XDG_SESSION_ID=298"
0x7fffffffe7d2: "HOSTNAME=paas-controller1"
0x7fffffffe7ec: "TERM=xterm"
0x7fffffffe7f7: "SHELL=/bin/bash"
0x7fffffffe807: "HISTSIZE=1000"
0x7fffffffe815: "USER=root"
0x7fffffffe81f: "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31---Type to continue, or q to quit---
:*.tgz=01" ...
0x7fffffffe8e7: ";31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;"...
0x7fffffffe9af: "31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01"...
0x7fffffffea77: ";31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.ti"...
0x7fffffffeb3f: "ff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=0"...
0x7fffffffec07: "1;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf="...
0x7fffffffeccf: "01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*."...
0x7fffffffed97: "oga=01;36:*.spx=01;36:*.xspf=01;36:"
0x7fffffffedbb: "SUDO_USER=ubuntu"
0x7fffffffedcc: "SUDO_UID=1000"
0x7fffffffedda: "USERNAME=root"
0x7fffffffede8: "COLUMNS=208"
0x7fffffffedf4: "MAIL=/var/spool/mail/ubuntu"
0x7fffffffee10: "PATH=/sbin:/bin:/usr/sbin:/usr/bin"
0x7fffffffee33: "_=/bin/gdb"
0x7fffffffee3e: "PWD=/home/ubuntu/wen"
0x7fffffffee53: "LANG=zh_CN.UTF-8"
0x7fffffffee64: "LINES=31"
0x7fffffffee6d: "HISTIGNORE=*adduser*--user=*-p=*:*openssl*passwd *"
0x7fffffffeea0: "HOME=/root"
0x7fffffffeeab: "SUDO_COMMAND=/bin/su"
0x7fffffffeec0: "SHLVL=1"
0x7fffffffeec8: "LOGNAME=root"
0x7fffffffeed5: "LESSOPEN=||/usr/bin/lesspipe.sh %s"
---Type <return> to continue, or q <return> to quit---
0x7fffffffeef8: "PROMPT_COMMAND={ msg=$(history 1 | { read x y; echo $x $y | grep -v -E \"password|passwd|\\-\\-email|\\-\\-description\"; });logger -p local6.notice \"[euid=$(whoami)]\":$(who am i):[`pwd`]\"$msg\"; }"
0x7fffffffefb7: "SUDO_GID=1000"
0x7fffffffefc5: "HISTTIMEFORMAT=%F %T root "
0x7fffffffefe0: "/home/ubuntu/wen/lib.so"
0x7fffffffeff8: ""
0x7fffffffeff9: ""
0x7fffffffeffa: ""
通过上面的分析我们知道。进程的栈空间会在运行之前塞入一下东西。其中auxiliary vector 就像目录一样。其中AT_ENTRY是程序的入口
so-name是共享库的命令方式
ldconfig是共享库的安装程序。可以建立so的索引,减少so的搜索时间
[参考]
https://refspecs.linuxfoundation.org/ELF/zSeries/lzsabi0_zSeries/x895.html 【linux foundation process Process initialization
】
https://ftp.gnu.org/old-gnu/Manuals/bfd-2.9.1/html_mono/bfd.html 【linux bfd 】
https://www.gnu.org/software/binutils/ 【linux binutils】