理解got和plt

共享库是现代操作系统的一个重要组成部分,但是我们对它背后的实现知之甚少。当然,很多文档从各个角度对动态库进行过介绍。希望我的这边文章能给对动态库的理解带来一种新的理解。

让我们以此开始——在elf格式中,重定位记录是一些允许我们稍后填写的二进制信息——链接阶段由编译工具填充或者在运行时刻由动态连接器填写。一个二进制的重定位记录从本质上说就是“确定符号X的值,然后把这个值放入二进制文件中的偏移量为Y的地方”——每一个重定向记录都有个特定的类型,这个类型在ABI文档中定义,用来准确的描述在实际中是如何确定X的值。

下面是一个简单的例子:

$ cat a.c
extern int foo;

int function(void) {
    return foo;
}
$ gcc -c a.c
$ readelf --relocs ./a.o

Relocation section '.rel.text' at offset 0x2dc contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000004  00000801 R_386_32          00000000   foo

在编译生成a.o文件的时候,编译器并不知道符号foo的值,所以产生一个重定位记录,表示“在最后的二进制文件中,把符号foo的地址填入偏移量为4的地方(相对于text 区而言)”。如果你观察下a.o的汇编结果,你就会发现在text区偏移量为4的地方,有4个字节为0,这四个字节最终将会填入真实的地址。

$ objdump --disassemble ./a.o

./a.o:     file format elf32-i386

Disassembly of section .text:

00000000 <function>:
   0:    55         push   %ebp
   1:    89 e5                  mov    %esp,%ebp
   3:    a1 00 00 00 00         mov    0x0,%eax
   8:    5d                     pop    %ebp
   9:    c3                     ret

在链接的时候,如果你编译的另外一个目标文件含有foo的地址,并且把这个目标文件与a.o一起编译为一个最终的可执行文件,那么重定位记录就会消失。但是仍然有很多的东西直到运行的时候才能确定,当编译一个可执行文件或者动态库的时候。正如我马上要解释的,PIC,与地址无关的代码一个很重要的原因。当你观察一个可执行文件,你会注意到它有一个固定的加载地址:

$ readelf --headers /bin/ls
[...]
ELF Header:
[...]
  Entry point address:               0x8049bb0

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
[...]
  LOAD           0x000000 0x08048000 0x08048000 0x16f88 0x16f88 R E 0x1000
  LOAD           0x016f88 0x0805ff88 0x0805ff88 0x01543 0x01543 RW  0x1000

这并不是地址无关。代码段(权限为RE,可读可执行)必须被加载到虚拟地址0x08048000,数据段(RW)必须被加载到0x0805ff88

This is fine for an executable, because each time you start a new process (fork andexec) you have your own fresh address space. Thus it is a considerable time saving to pre-calculate addresses
from and have them fixed in the final output (you can make position-independent executables, but that‘s another story).

这对于可执行文件来说很不错,因为每一次你创建一个新的进程(fork,然后exec),都会有一个全新的地址空间。当对于共享库来说就不是那么好了。关键点是,你可以为了达到你的目标而对共享库随意的组合。如果你的共享库必须要在固定的地址上运行,32位的系统的地址空间很快就不够用了。因此当你查看一个共享库,它们并不指定一个固定的加载地址:

$ readelf --headers /lib/libc.so.6
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
[...]
  LOAD           0x000000 0x00000000 0x00000000 0x236ac 0x236ac R E 0x1000
  LOAD           0x023edc 0x00024edc 0x00024edc 0x0015c 0x001a4 RW  0x1000

共享库还有第二个目的,代码分享。如果有一百个进程使用一个共享库,就没有必要在内存中产生100分代码拷贝。如果代码是完全只读,并且永远不会修改,那么每一个进程就可以分享相同的代码。然而,对于共享库有一个约束:对于每一个进程都必须有一份自己的数据实例。从头文件信息中也可以看到数据段相对于代码段有一个固定的偏移量。所以访问数据段的算法是很简单的:访问数据地址 = 当前地址+ 固定偏移。

但是,当前的地址有可能不是那么简单的知道:

$ cat test.c
static int foo = 100;

int function(void) {
    return foo;
}
$ gcc -fPIC -shared -o libtest.so test.c

foo位于数据段,与函数function中的指令有一个固定的偏移量。我们要做的就是找到它。在amd64上,这很简单:

000000000000056c <function>:
 56c:        55         push   %rbp
 56d:        48 89 e5               mov    %rsp,%rbp
 570:        8b 05 b2 02 20 00      mov    0x2002b2(%rip),%eax        # 200828 <foo>
 576:        5d                     pop    %rbp

上面的代码的意思是说“把与当前指令地址偏移0x2002b2处的值放入eax”。另一方面,i386并没有提供访问当前指定的能力。所以有一些限制:

0000040c <function>:
 40c:    55         push   %ebp
 40d:    89 e5                  mov    %esp,%ebp
 40f:    e8 0e 00 00 00         call   422 <__i686.get_pc_thunk.cx>
 414:    81 c1 5c 11 00 00      add    $0x115c,%ecx
 41a:    8b 81 18 00 00 00      mov    0x18(%ecx),%eax
 420:    5d                     pop    %ebp
 421:    c3                     ret

00000422 <__i686.get_pc_thunk.cx>:
 422:    8b 0c 24       mov    (%esp),%ecx
 425:    c3                     ret

这里的魔数是__i686.get_pc_thunk.cx。i386不允许我们得到当前指令的地址,但是我们可以得到一个已知的固定地址——__i686.get_pc_thunk.cx的值,cx中的值是call的返回地址,这里是0x414.我们做一个简单的算术:0x115c+0x414 = 0x1570.最终的数据和0x1588偏移了0x18个字节,查看汇编代码:

00001588 <global>:
    1588:       64 00 00                add    %al,%fs:(%eax)

正是100所处的地址。

现在我们越来越接近了,但是还是有很多的问题要处理。如果一个共享库可以被加载到任意的地址,那么,一个可执行文件或者其他的共享库,如何知道怎么访问它的数据或者调用它的函数呢?从理论上,我们是可以的,加载库,然后把数据的地址或者函数的地址填入到库相应的地方。然后这正如之前所讲的,违反了代码共享性。就如同我们所了解的,所有的问题都可以通过增加一个中间层来解决,在这种情形下,称之为全局偏移表或者got。

考虑下面的库:

$ cat test.c
extern int foo;

int function(void) {
    return foo;
}
$ gcc -shared -fPIC -o libtest.so test.c

这和之前的文件很像,但是foo是extern的。假设是由其他的库提供。让我们看一下在amd64上它是如何工作的:

$ objdump --disassemble libtest.so
[...]
00000000000005ac <function>:
 5ac:        55         push   %rbp
 5ad:        48 89 e5               mov    %rsp,%rbp
 5b0:        48 8b 05 71 02 20 00   mov    0x200271(%rip),%rax        # 200828 <_DYNAMIC+0x1a0>
 5b7:        8b 00                  mov    (%rax),%eax
 5b9:        5d                     pop    %rbp
 5ba:        c3                     retq

$ readelf --sections libtest.so
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
[...]
  [20] .got              PROGBITS         0000000000200818  00000818
       0000000000000020  0000000000000008  WA       0     0     8

$ readelf --relocs libtest.so
Relocation section '.rela.dyn' at offset 0x418 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
[...]
000000200828  000400000006 R_X86_64_GLOB_DAT 0000000000000000 foo + 0

反汇编的结果显示返回值位于当前指令偏移0x200271处:0x0200828。查看section header,这个地址位于.got区。接着我们查看重定位记录,可以发现有一个类型为R_X86_64_GLOB_DAT的重定位的意思是“找到foo的值,然后把它放在地址0x200828处”。

So, when this library is loaded, the dynamic loader will examine the relocation, go and find the value offoo and patch the.got entry as required. When it comes time for the code loads to
load that value, it will point to the right place and everything just works; without having to modify any code values and thus destroy code sharability.

所以,当共享库被加载,动态加载器会检查重定位项,找到foo的值,然后填入到.got对应的地方。当接下来代码要访问这个值,它会根据偏移量找到这个值,一切都会工作正常。

以上是数据的处理,那么函数调用呢?函数调用的中间层称之为procedure linkage table 或者PLT.代码不会直接调用外部的函数,而是通过一个plt stub。

$ cat test.c
int foo(void);

int function(void) {
    return foo();
}
$ gcc -shared -fPIC -o libtest.so test.c

$ objdump --disassemble libtest.so
[...]
00000000000005bc <function>:
 5bc:        55         push   %rbp
 5bd:        48 89 e5               mov    %rsp,%rbp
 5c0:        e8 0b ff ff ff         callq  4d0 <[email protected]>
 5c5:        5d                     pop    %rbp

$ objdump --disassemble-all libtest.so
00000000000004d0 <[email protected]>:
 4d0:   ff 25 82 03 20 00       jmpq   *0x200382(%rip)        # 200858 <_GLOBAL_OFFSET_TABLE_+0x18>
 4d6:   68 00 00 00 00          pushq  $0x0
 4db:   e9 e0 ff ff ff          jmpq   4c0 <_init+0x18>

$ readelf --relocs libtest.so
Relocation section '.rela.plt' at offset 0x478 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200858  000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0

现在,我们function跳转到0x4d0.反汇编,我们看到这是一个有趣的调用,我们跳转到当前rip指针偏移0x200382,也就是0x200858处。可以发现,这就是符号foo的重定位项的offset的值。

I让我们来看一下0x200858的初始值:

$ objdump --disassemble-all libtest.so

Disassembly of section .got.plt:

0000000000200840 <.got.plt>:
  200840:       98                      cwtl
  200841:       06                      (bad)
  200842:       20 00                   and    %al,(%rax)
        ...
  200858:       d6                      (bad)
  200859:       04 00                   add    $0x0,%al
  20085b:       00 00                   add    %al,(%rax)
  20085d:       00 00                   add    %al,(%rax)
  20085f:       00 e6                   add    %ah,%dh
  200861:       04 00                   add    $0x0,%al
  200863:       00 00                   add    %al,(%rax)
  200865:       00 00                   add    %al,(%rax)
        ...

0x200858的初始值是0x4d6,下一条指令的地址。把0要入栈中,然后跳转到0x4c0.通过查看代码我们可以发现,把GOT中那个的一个值压入栈中,然后跳到GOT中的第二个值。

00000000000004c0 <[email protected]>:
 4c0:   ff 35 82 03 20 00       pushq  0x200382(%rip)        # 200848 <_GLOBAL_OFFSET_TABLE_+0x8>
 4c6:   ff 25 84 03 20 00       jmpq   *0x200384(%rip)        # 200850 <_GLOBAL_OFFSET_TABLE_+0x10>
 4cc:   0f 1f 40 00             nopl   0x0(%rax)

What‘s going on here? What‘s actually happening is lazy binding — by convention when the dynamic linker loads a library, it will put an identifier and resolution function into known places in the GOT. Therefore, what happens is roughly this: on
the first call of a function, it falls through to call the default stub, which loads the identifier and calls into the dynamic linker, which at that point has enough information to figure out "hey, thislibtest.so is trying
to find the function foo". It will go ahead and find it, and then patch the address into the GOT such that thenext time the original PLT entry is called, it will load the actual address of the function, rather than
the lookup stub. Ingenious!

Out of this indirection falls another handy thing — the ability to modify the symbol binding order.LD_PRELOAD, for example, simply tells the dynamic loader it should insert a library as first to be looked-up for symbols;
therefore when the above binding happens if the preloaded library declares afoo, it will be chosen over any other one provided.

In summary — code should be read-only always, and to make it so that you can still access data from other libraries and call external functions these accesses are indirected through a GOT and PLT which live at compile-time known offsets.

原文地址:

https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html

理解got和plt

时间: 2024-10-10 17:24:43

理解got和plt的相关文章

《深入理解计算机系统(原书第三版)》pdf

下载地址:网盘下载 内容简介  · · · · · · 和第2版相比,本版内容上*大的变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础.主要更新如下: 基于x86-64,大量地重写代码,首次介绍对处理浮点数据的程序的机器级支持. 处理器体系结构修改为支持64位字和操作的设计. 引入更多的功能单元和更复杂的控制逻辑,使基于程序数据流表示的程序性能模型预测更加可靠. 扩充关于用GOT和PLT创建与位置无关代码的讨论,描述了更加强大的链接技术(比如库打桩). 增加了对信号处理程序

可执行文件(ELF)格式的理解

摘自http://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html 可执行文件(ELF)格式的理解 ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西.以及都以什么样的格式去放这些东西.它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用.可以说,ELF是构成众多xNIX

深入理解计算机系统笔记

我的博客上的比这个排版显示的更好一些,特别是图片 http://notelzg.github.io/2016/06/29/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/ 1. hello wordl 我们还是从hello world程序说起吧: #include <stdio.h> int main() { printf("hell

理解matplotlib绘图

matplotlib是基于Python语言的开源项目,旨在为Python提供一个数据绘图包.Matplotlib 可能是 Python 2D-绘图领域使用最广泛的套件.它能让使用者很轻松地将数据图形化,并且提供多样化的输出格式. matplotlib使用numpy进行数组运算,并调用一系列其他的Python库来实现硬件交互.matplotlib的核心是一套由对象构成的绘图API. 你需要安装Python, numpy和matplotlib.(可以到python.org下载Python编译器.相关

《机器学习系统设计》之数据理解和提炼

前言: 本系列是在作者学习<机器学习系统设计>([美] Willi Richert)过程中的思考与实践,全书通过Python从数据处理,到特征工程,再到模型选择,把机器学习解决问题的过程一一呈现.书中设计的源代码和数据集已上传到我的资源http://download.csdn.net/detail/solomon1558/8971649. 第1章通过一个简单的例子介绍机器学习的基本概念,揭示过拟合的风险,帮助我们增强理解和提炼数据的能力. 1. 背景介绍 假设互联网公司MLAAS为所有Web访

电子书 深入理解计算机系统.pdf

内容简介 和第2版相比,本版内容上*大的变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础.主要更新如下: 基于x86-64,大量地重写代码,首次介绍对处理浮点数据的程序的机器级支持. 处理器体系结构修改为支持64位字和操作的设计. 引入更多的功能单元和更复杂的控制逻辑,使基于程序数据流表示的程序性能模型预测更加可靠. 扩充关于用GOT和PLT创建与位置无关代码的讨论,描述了更加强大的链接技术(比如库打桩). 增加了对信号处理程序更细致的描述,包括异步信号安全的函数等. 采用

got &amp; plt

got plt类似与Windows PE文件中IAT(Import Address Table). 要使的代码地址无关,基本思想就是把与地址相关的部分放到数据段里面. ELF的做法是在数据段里面建立一个指向这些变量的指针数组,称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用. GOT本身是放在数据段,所以可以在模块加载时被修改.延迟绑定,基本思想就是当函数第一次被用到时才进行绑定 一段非常简单的代码: #includ

台湾大学林轩田教授机器学习基石课程理解及python实现----PLA

最近在班主任的带领下,开始观看台湾大学林轩田教授的机器学习基石课程,虽然吧,台湾人,汉语说得蛮6,但是还是听着怪怪的,不过内容非常值得刚刚入门的机器学习 小白学习,话不多说,直接进入正题. 1.基本介绍(貌似这里一般是应该背景介绍,但是,历史吗,自己去百度吧) (1)preceptron 翻译中文叫做感知器,如果你之前听说过神经网络的,它其实就是网络中的一个神经元,它自身的作用非常小,只能对于数据只能实现二分类,然而如果连成网络的 话,神经网络的每一层都可以作为一个线性函数或非线性函数,将函数复

&lt;深入理解计算机系统&gt; 通过程序的机器级表示来理解函数栈

C源码: void swap(int *a,int *b) { int c; c = *a; *a = *b; *b = c; } int main(void) { int a ; int b ; int ret; a =16; b = 64; ret = 0; swap(&a,&b); ret = a - b; return ret; } 编译: gcc -g func_stack.c -o a.out objdump -dS a.out > main.dump 可以得到反汇编的汇