selph
selph
发布于 2023-01-29 / 200 阅读
0
0

位置无关代码PIC

PIC简介

系统使用共享库的主要目的是允许多个进程共享内存中相同的库代码,从而节约内存空间。

位置无关代码(Position-Independent Code)技术可以加载无需重定位的代码,gcc使用-fpic选项可生成PIC代码,共享库则必须使用该项

本文主要内容:

  • PIC数据引用
  • 延迟绑定的流程

PIC数据引用

有一个有趣的事实:无论在内存中何处加载一个目标模块,其数据段和代码段的距离总是不变的,也就是说,​ 代码段中任意指令和数据段中任意变量之间的距离是一个运行时常量。

编译器在数据段开头的地方创建全局偏移量表GOT(Global Offset Table),在GOT中,每个被目标模块引用的全局数据目标都有8字节的条目,编译器还会为GOT每一个条目生成重定位记录,在加载时,动态链接器会重定位GOT中的每个条目,使之得到正确地址。

简单来说,就是对于共享库中的变量,是通过指令和变量之间距离常量来找的

PIC数据引用实验

实验环境:Ubuntu 22.04LTS +64位编译

实验代码,demolib:

// demolib.h
void add();
int g_num=0;
// demolib.c
// gcc -lazy -shared -fpic -o demolib.so demolib.c
#include<stdio.h>
#include "demolib.h"
void add(){
        g_num++;
        return;
}

demo:

// demo.c
// gcc -lazy -o demo demo.c ./demolib.so
#include <stdio.h>
#include "demolib.h"
int main(){
        printf("%d\n",g_num);
        add();
        printf("%d\n",g_num);
        add();
        printf("%d\n",g_num);
}

功能很简单,就是c去访问共享库的变量和方法

通过gdb打开该程序,查看访问变量g_num的指令:

image

可以看到,这里使用当前指令地址rip加上一个固定偏移0x2e9d来访问数据

PIC函数调用

对于函数,编译器没法预测函数的运行时地址,GNU编译系统使用**延迟绑定(Lazy Binding)**技术来解决这个问题:将过程地址的绑定推迟到第一次调用该过程时

有些so文件有成百上千的函数,一般只能用到其中很少一部分,动态链接器并不需要加载这一大堆用不上的重定位,所以通过延迟绑定来减少性能开销

延迟绑定通过两个数据结构的交互来实现:全局偏移量表GOT和过程链接表PLT(Procedure Linkage Table),如果目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT

PLT介绍

PLT是一个数组,每个条目16字节,每个被调用的库函数都有自己的PLT条目,每个条目负责调用一个具体的函数

  • PLT[0]是特殊条目,跳转到动态链接器中
  • PLT[1]调用系统启动函数__libc-start-main​,初始化执行环境,调用main函数并处理其返回值
  • PLT[2]开始的条目是用户代码调用的函数

GOT介绍

GOT也是一个数组,每个条目8字节,和PLT联合使用时:

  • GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息
  • GOT[2]是动态链接器在ld-linux.so模块的入口点
  • 其余条目对应被调用的函数,其地址在需要时被解析,每个条目对应一个PLT条目,初始时GOT条目指向PLT条目第二条指令

这里附一个来自参考资料[4]的示意图:

image

第一次调用的时候的跳转顺序是:fun@plt->PLT[0]->GOT[2]->fun

PIC函数调用实验

实验代码和环境同上,通过gdb打开程序

这里调用的printf函数和add函数都是pic函数,这里以printf函数为例:

image

可以看到进来这里后,调用到plt条目里去了:

image

这里有3条指令,endbr64,可以当作nop来看

第二条指令跳转进got表中的地址GOT[0]

image

接下来的指令:

image

printf的plt条目里,push一个参数,然后进行一次跳转,跳转到PLT[0]的地址上,

PLT[0]这里会再次push一个参数,也就是GOT[1],

然后跳转到动态链接器中,也就是GOT[2]保存的地址去进行处理,实际上是调用_dl_runtime_resolve_xsavec​函数去解析函数地址,然后最后跳转到函数里:

image

执行完之后返回

当第二次再次调用该函数时:

image

可以看到,这里的跳转地址被修改为函数地址了,而非PLT地址,这里已经完成了重定位操作,可以直接调用函数了就

参考资料


评论