静态库的缺点:

  1. 库函数被包含在每一个运行的进程中,会造成主存的浪费。
  2. 目标文件的size过大
  3. 每次更新一个模块都需要重新编译,更新困难,使用不方便。

动态库: 是一个目标文件,包含代码和数据,它可以在程序运行时动态的加载并链接。修改动态库不需要重新编译目标文件,只需要更新动态库即可。动态库还可以同时被多个进程使用。在linux下生成动态库 gcc -c a.c  -fPIC -o a.o     gcc -shared -fPIC a.o -o a.so.     这里的PIC含义就是生成位置无关代码,动态库允许动态装入修改,这就必须要保证动态库的代码被装入时,可执行程序不依赖与动态库被装入的位置,即使动态库的长度发生变化也不会影响调用它的程序。

动态链接器:

在加载可执行文件时,加载器发现在可执行文件的程序头表中有.interp段,其中包含了动态连接器路径ld-linux.so . 加载器加载动态链接器,动态链接器完成相应的重定位工作后,再将控制权交给可执行文件。

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R
[正在请求程序解释器:/lib64/ld-linux-x86-.so.]

位置无关代码PIC:

其中动态库用到的一个核心概念就是与代码无关PIC。共享库代码位置可以是不确定的,即使代码长度发生变化也不影响调用它的程序,动态链接器是不会把可执行文件的代码段数据段与动态链接库合并的。那么这里牵涉到模块内与模块间的引用和跳转问题。

模块内的跳转和引用:

目标文件与静态库中模块内的跳转大致相同。如下代码:

//b.c
static int temp = ;
extern int a;
void add(int c)
{
a += c;
temp += c;
} //a.c
int a = 1000;
void add(int c);
int main()
{
    int c = 123;
    add(c);
    return 0; }

我们将b.c编译为动态库,这里的temp就是模块内的引用,我们将动态库反编译可以看到

 00000000000006a8 <add>:
6a8: push %rbp
6a9: e5 mov %rsp,%rbp
6ac: 7d fc mov %edi,-0x4(%rbp)
6af: 8b 2a mov 0x20092a(%rip),%rax # 200fe0 < _DYNAMIC+0x1d0>
6b6: 8b mov (%rax),%edx
6b8: 8b fc mov -0x4(%rbp),%eax
6bb: c2 add %eax,%edx
6bd: 8b 1c mov 0x20091c(%rip),%rax # 200fe0 < _DYNAMIC+0x1d0>
6c4: mov %edx,(%rax)
481 6c6: 8b 15 5c 09 20 00 mov 0x20095c(%rip),%edx # 201028 < temp>
482 6cc: 8b 45 fc mov -0x4(%rbp),%eax
483 6cf: 01 d0 add %edx,%eax
484 6d1: 89 05 51 09 20 00 mov %eax,0x200951(%rip) # 201028 < temp> 785 0000000000201028 <temp>:             #数据段temp
786   201028:   38 30                   cmp    %dh,(%rax)

这里的mov 0x20095c(%rip),%edx  取rip中下一条指令地址+0x20095c 的数据放到edx寄存器中,地址为0x201028 正是temp的地址。模块内的函数跳转与此类似。

模块间的跳转和引用:

首先看一下全局变量的引用,看上例中b.so的反编译 这里引用了一个全局变量a 它的定义在另一个模块a.o 中

 00000000000006a8 <add>:
6a8: push %rbp
6a9: e5 mov %rsp,%rbp
6ac: 7d fc mov %edi,-0x4(%rbp)
6af: 8b 2a mov 0x20092a(%rip),%rax # 200fe0 < _DYNAMIC+0x1d0>
6b6: 8b mov (%rax),%edx
6b8: 8b fc mov -0x4(%rbp),%eax
6bb: c2 add %eax,%edx
6bd: 8b 1c mov 0x20091c(%rip),%rax # 200fe0 < _DYNAMIC+0x1d0>
Disassembly of section .got: 0000000000200fd0 <.got>:
... Disassembly of section .got.plt: <_GLOBAL_OFFSET_TABLE_>:
: 0e adc %cl,(%rsi)
: and %al,(%rax)

这里的mov 取地址是一个got数组的某个地址。GOT 全局偏移表,在data段的开始处的一个指针数组,每一个指针可以指向一个全局变量, GOT与引用数据的指令之间的相对距离固定,编译器为GOT每一项生成一个重定位项,加载时 动态链接器对GOT中的各项进行重定位,填入引用的地址。(32位 占4个字节  64位 8个字节)

每一个引用全局数据的目标模块都有一张自己的GOT,那么就需要一个额外的寄存器来保持GOT表目的地址。至于模块间的函数调用和跳转也可以使用此模块,但是这种情况下过程调用都要求三条额外的指令,Linux这里就使用了叫做延迟绑定的技术,将过程调用的绑定推迟到第一次调用该过程。这种技术是通过俩个数据结构之间的交互来实现,即GOT 和PLT 全局偏移表和过程链接表, 如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT,GOT是data节的一部分,PLT是text节的一部分<深入理解计算机系统>

上图为 32 位linux。 从GOT[3]开始才是函数调用地址。我们将a.c b.so编译链接为可执行文件a, 反汇编a 观察函数add的调用(无关代码已省略)。(64位机器)

Disassembly of section .plt:

 <add@plt-0x10>:
: ff 0a pushq 0x200a82(%rip) # <_GLOBAL_OFFSET_TABLE_+0x8>
: ff 0a jmpq *0x200a84(%rip) # <_GLOBAL_OFFSET_TABLE_+0x10>
40058c: 0f 1f nopl 0x0(%rax) <add@plt>:
400590: ff 25 82 0a 20 00 jmpq *0x200a82(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18> // ////++++++++++++++++++++++
: pushq $0x0
40059b: e9 e0 ff ff ff jmpq <_init+0x20> 00000000004005a0 <__libc_start_main@plt>:
4005a0: ff 7a 0a jmpq *0x200a7a(%rip) # <_GLOBAL_OFFSET_TABLE_+0x20>
4005a6: pushq $0x1
4005ab: e9 d0 ff ff ff jmpq <_init+0x20> 00000000004006b0 <main>:
4006b0: push %rbp
4006b1: e5 mov %rsp,%rbp
4006b4: ec sub $0x10,%rsp
4006b8: c7 fc 7b movl $0x7b,-0x4(%rbp)
4006bf: 8b fc mov -0x4(%rbp),%eax                
4006c2: c7 mov %eax,%edi                                      
006c4: e8 c7 fe ff ff callq 400590 <add@plt>           //call add--------------------------------
4006c9: b8 mov $0x0,%eax
4006ce: c9 leaveq
4006cf: c3 retq Disassembly of section .got: 0000000000600ff8 <.got>:
... Disassembly of section .got.plt:
#data段  无效反编译代码
<_GLOBAL_OFFSET_TABLE_>:
: GOT[0]   GOT    64位下每个条目8个字节   32位是4个字节
601008:   GOT[1] 链接器标示信息
  601010  GOT[2]  动态连接器入口地址
: add %dl,0x4005(%rsi)
60101d: add %al,(%rax)
60101f: a6 add %ah,0x4005(%rsi)
: add %al,(%rax)
: b6 add %dh,0x4005(%rsi)
60102d: add %al,(%rax)
...

我们可以看到main函数中跳转call add 跳转到了地址0x400590 处,执行 jmpq *0x200a82(%rip) 指令 跳转到 0x601018的地址 ,还是跳到GOT数组中的add位置。这个位置其实就是 jmpq *0x200a82(%rip)的下一条指令地址400596: 68 00 00 00 00 pushq $0x0。我们可以使用gdb看一下。

(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004006b0 <+0>: push %rbp
0x00000000004006b1 <+1>: mov %rsp,%rbp
0x00000000004006b4 <+4>: sub $0x10,%rsp
=> 0x00000000004006b8 <+8>: movl $0x7b,-0x4(%rbp)
0x00000000004006bf <+15>: mov -0x4(%rbp),%eax
0x00000000004006c2 <+18>: mov %eax,%edi
0x00000000004006c4 <+20>: callq 0x400590 <add@plt>
0x00000000004006c9 <+25>: mov $0x0,%eax
0x00000000004006ce <+30>: leaveq
0x00000000004006cf <+31>: retq
End of assembler dump.
(gdb) disassemble 0x400590
Dump of assembler code for function add@plt:
0x0000000000400590 <+0>: jmpq *0x200a82(%rip) # 0x601018 <add@got.plt> 这个地址就是GOT数组中add对应的那条
0x0000000000400596 <+6>: pushq $0x0
0x000000000040059b <+11>: jmpq 0x400580
End of assembler dump.
(gdb) x 0x601018
0x601018 <add@got.plt>: 0x00400596

pushq $0x0 这条指令将add符号的ID压入栈,然后 jmpq  0x400580 它将会跳转到PLT[0]接下来的指令就是:

 <add@plt-0x10>:
: ff 0a pushq 0x200a82(%rip) # <_GLOBAL_OFFSET_TABLE_+0x8>   链接器标示信息入栈  前面还有个ID
: ff 0a jmpq *0x200a84(%rip) # <_GLOBAL_OFFSET_TABLE_+0x10>  跳转到链接器入口地址
40058c: 0f 1f nopl 0x0(%rax)

跳转到动态连接器之后,链接器根据这两个栈中的信息(其实就是重定位描述符地址和索引值)得到 add的实际地址, 在将地址放入GOT[3]中 通过gdb详细看一下。

(gdb) n
add(c);  运行  add之前
(gdb) x /32x 0x601010
0x601010: 0xf7df0210 0x00007fff 0x00400596 0x00000000  
这时GOT[3]add地址还不是add的实际地址 而是<add@plt>: jmp的目的地址0x00400596
0x601020 <__libc_start_main@got.plt>: 0xf7839a20 0x00007fff 0x004005b6 0x00000000
0x601030: 0x00000000 0x000003e8 0x00000000 0x00000000
0x601040: 0x00000000 0x00000000 0x00000000 0x00000000
0x601050: 0x00000000 0x00000000 0x00000000 0x00000000
0x601060: 0x00000000 0x00000000 0x00000000 0x00000000
0x601070: 0x00000000 0x00000000 0x00000000 0x00000000
0x601080: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) print add
$ = {<text variable, no debug info>} 0x7ffff7bd96a8 <add>   add函数的地址
(gdb) n
return ;
(gdb) x /32x 0x601010
0x601010: 0xf7df0210 0x00007fff 0xf7bd96a8 0x00007fff  
进入add GOT[3]中地址变为了 add的实际地址 再之后的
0000000000400590 <add@plt>: jmpq *0x200a82(%rip) 就会直接跳到add的地址 开始执行指令。 0x601020 <__libc_start_main@got.plt>: 0xf7839a20 0x00007fff 0x004005b6 0x00000000
0x601030: 0x00000000 0x00000463 0x00000000 0x00000000
0x601040: 0x00000000 0x00000000 0x00000000 0x00000000
0x601050: 0x00000000 0x00000000 0x00000000 0x00000000
0x601060: 0x00000000 0x00000000 0x00000000 0x00000000
0x601070: 0x00000000 0x00000000 0x00000000 0x00000000
0x601080: 0x00000000 0x00000000 0x00000000 0x00000000

这里可以看到被引用的函数调用之前 GOT中的地址并没有被值为函数的实际地址。之后实际地址就被装入,跳转到相应地址开始执行,之后就可以直接跳转运行了,只需要一个跳转指令即可。

Linux 链接详解----动态链接库的更多相关文章

  1. Linux 链接详解----静态链接实例分析

    由Linux链接详解(1)中我们简单的分析了静态库的引用解析和重定位的内容, 下面我们结合实例来看一下静态链接重定位过程. /* * a.c */ ; void add(int c); int mai ...

  2. Linux 链接详解(2)

    可执行文件加载执行过程: 上一节我们说到ELF文件格式,静态库的符号解析和重定位的内容.这一节我们来分析一下可执行文件. 由上一节我们知道可执行文件也是ELF文件,当程序被加载器加载到内存时是按照EL ...

  3. Linux 链接详解(1)

    可执行文件的生成过程: hello.c ----预处理--->  hello.i ----编译----> hello.s -----汇编-----> hello.o -----链接- ...

  4. Linux 系统结构详解

    Linux 系统结构详解 Linux系统一般有4个主要部分: 内核.shell.文件系统和应用程序.内核.shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序.管理文件并使用系统 ...

  5. Linux命令详解之—pwd命令

    Linux的pwd命令也是一个非常常用的命令,本文为大家介绍下Linux中pwd命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux pwd命令用于显示工作目录. 执行pwd指 ...

  6. Linux权限详解 命令之 chmod:修改权限

    权限简介 Linux系统上对文件的权限有着严格的控制,用于如果相对某个文件执行某种操作,必须具有对应的权限方可执行成功. Linux下文件的权限类型一般包括读,写,执行.对应字母为 r.w.x. Li ...

  7. Linux 目录详解 树状目录结构图

    1.树状目录结构图 2./目录 目录 描述 / 第一层次结构的根.整个文件系统层次结构的根目录. /bin/ 需要在单用户模式可用的必要命令(可执行文件):面向所有用户,例如:cat.ls.cp,和/ ...

  8. [转帖]Linux文件系统详解

    Linux文件系统详解 https://www.cnblogs.com/alantu2018/p/8461749.html 贼复杂.. 从操作系统的角度详解Linux文件系统层次.文件系统分类.文件系 ...

  9. Linux命令详解之—tail命令

    tail命令也是一个非常常用的文件查看类的命令,今天就为大家介绍下Linux tail命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux tail命令主要用来从指定点开始将文 ...

随机推荐

  1. 启动报错 Unsupported major.minor version 51.0

    Unsupported major.minor version 51.0错误, 是使用jdk6启动jdk7编译的项目,更换jdk7就好了,或者用jdk6重新打包项目. 解决起来也很方便:打开excli ...

  2. thinkphp使用phpqrcode生成带logo二维码

    //二维码图片保存路径 $pathname = date("Ymd",time()); $pathname = "./Qrcode/" . $pathname; ...

  3. 关于对vector3及其衍生变量的理解

    关于对vector3,vector2类及其衍生变量的理解 vector3简单来讲即表示向量和点的系统类,这个结构用于处理向量和点,也包含许多做向量运算的函数. 而vector2即少一维向量的类,用于处 ...

  4. 使用python实现计算器功能

    学习python过程中的作业.实现了+.-.×./.及幂运算,支持括号优先级. 代码为python3.5 import re def formatEquation(string): string = ...

  5. 如何获取系统Home(Launcher)应用判断用户是否处于home界面

    要把我们的应用程序作为home(launcher应用),只需要在AndroidManifest.xml中添加: <category android:name="android.inte ...

  6. HDFS的接口(命令行接口和Java接口)--笔记

    HDFS 文件的系统访问的接口 1.Hadoop的shell命令脚本 hadoop fs -ls   列出某一个目录下的文件 hadoop fs -lsr 递归的方式列出所有文件 hadoop fs ...

  7. App网络管理

    安卓开发一般都需要进行日志管理,常用操作老司机已为你封装完毕,你可以用这份工具进行管理,具体可以查看源码,现在为你开车,Demo传送门. 站点 系统日志输出工具类 → AppKeyBoardMgr g ...

  8. 【转载】以Java的视角来聊聊SQL注入

    以Java的视角来聊聊SQL注入 原创 2017-08-08 javatiku Java面试那些事儿 在大二就接触过sql注入,之前一直在学习windows逆向技术,认为web安全以后不是自己的从业方 ...

  9. 蓝桥杯-算法训练--ALGO-4 结点选择

    本人是一个刚刚接触C++不久的傻学生~记录一些自己的学习过程.大神路过可以批评指正~ 刚学动态规划,水平还很渣,一下子不知道从何下手,借鉴了一下这位大哥的文章 http://www.cnblogs.c ...

  10. outline

    a标签 两种button按钮  默认带有一个虚线 outline  当他们被单击 和 激活以后   outline和border 很类似 ,但是有不同 1.outline 不能针对特定的边赋值 ,也就 ...