计算机系统篇之链接(15):共享库拦截技术之运行时库打桩

Author: stormQ

Created: Saturday, 21. December 2019 11:58AM

Last Modified: Thursday, 05. November 2020 11:03PM



摘要

本文描述了 Linux 系统中运行时库打桩技术的实现原理,并展示了如何在加载程序时拦截标准库的 API(以malloc/free为例)的实现过程。

什么是运行时库打桩

库打桩(Library Interpositioning)是一种允许截获对共享库函数的调用,取而代之执行自己代码的技术。

运行时库打桩是库打桩技术中的一种,另外两种是编译时库打桩和链接时库打桩。它可以在程序加载时或程序运行过程中对共享库进行打桩。

运行时库打桩的强大之处在于只要能够访问可执行目标文件,就可以应用该技术。


运行时库打桩的实现原理

运行时库打桩的实现原理是基于动态链接器的LD_PRELOAD环境变量。如果LD_PRELOAD变量被设置为一个共享库路径名的列表(以空格或分号分隔),那么当你加载和运行一个程序,需要解析未定义的符号引用时,动态链接器会先搜索LD_PRELOAD中的共享库,然后才搜索任何其他的库。有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中的任何函数打桩,包括libc.so


如何在加载程序时拦截标准库的 API

以“在加载程序时,拦截标准库的 malloc() 和 free() 函数”为例,具体过程如下。

step 1: 编写自定义的malloc()函数

1)由于要拦截标准库的malloc()函数,相当于要神不知鬼不觉地将程序中所有对标准库函数malloc()的调用换成调用我们自定义的malloc()函数。所以,我们自定义的malloc()的函数原型必须与标准库中的保持一致。

自定义的malloc()函数:

extern "C" {

voidmalloc(size_t size)
{
}

}   // extern "C"

由于采用 C++ 编写,为了避免 C++ 默认对函数名重编(name mangling)所引起的副作用。我们将自定义的malloc()函数用extern "C"包裹起来。

2)从标准库中获取符号名称为malloc的符号定义的地址,即malloc()的函数地址

auto symbol_addr = dlsym(RTLD_NEXT, "malloc");

注:RTLD_NEXT表示下一个加载的共享库的句柄。这里,下一个加载的共享库默认就是libc.so。当然,这样做是有一定风险的。

注意: 为了让RTLD_NEXT可用,需要定义编译选项_GNU_SOURCE。我们可以在生成共享库时,将该选项传递给 g++,即-D_GNU_SOURCE

3)由于dlsym()函数的返回类型为void *,在使用前需要先进行类型转换

typedef void* (*__malloc)(size_t);
auto libc_malloc = reinterpret_cast<__malloc>(symbol_addr);
auto ptr = libc_malloc(size);

注:ptr指向所申请内存的首地址。

4)跟踪malloc()函数的调用情况

static thread_local bool called = false;
if (!called)
{
    called = true;
    LOG_INFO("malloc(%ld) = %p\n", size, ptr);
    called = false;
}

这里,需要注意两点: 1)引入静态局部变量called是为了防止死循环,因为printf()函数中会调用malloc()函数;2)修饰符thread_local表示变量called是线程局部对象,即每个线程都拥有一份私有的called变量,这样做是出于线程安全的考虑。

5)自定义malloc()函数的完整实现

extern "C" {

voidmalloc(size_t size)
{
    auto symbol_addr = dlsym(RTLD_NEXT, "malloc");
    if (!symbol_addr)
    {
        throw std::runtime_error(dlerror());
    }
    typedef void* (*__malloc)(size_t);
    auto libc_malloc = reinterpret_cast<__malloc>(symbol_addr);
    auto ptr = libc_malloc(size);

    static thread_local bool called = false;
    if (!called)
    {
        called = true;
        LOG_INFO("malloc(%ld) = %p\n", size, ptr);
        called = false;
    }

    return ptr;
}

}   // extern "C"

step 2: 编写自定义的free()函数

自定义free()函数的完整实现:

extern "C" {

void free(void *ptr)
{
    auto symbol_addr = dlsym(RTLD_NEXT, "free");
    if (!symbol_addr)
    {
        throw std::runtime_error(dlerror());
    }
    typedef void (*__free)(void *);
    auto libc_free = reinterpret_cast<__free>(symbol_addr);
    libc_free(ptr);
    LOG_INFO("free() = %p\n", ptr);
}

}   // extern "C"

step 3: 编写测试程序 main.cpp

main.cpp:

#include <malloc.h>

int main()
{
    int *p1 = static_cast<int *>(malloc(16));
    int *p2 = static_cast<int *>(malloc(32));
    free(p2);
    free(p1);
    return 0;
}

step 4: 生成共享库 mymalloc.so

$ g++ -o mymalloc.so mymalloc.cpp -shared -fpic -ldl -D_GNU_SOURCE -g

注:由于dlsym()函数定义在libdl.so中。所以,需要指定链接选项-ldl

step 5: 生成并运行测试程序

1)生成可执行目标文件 main

$ g++ -o main main.cpp -g

2)运行可执行目标文件 main

方式1:

$ LD_PRELOAD="./mymalloc.so" ./main

注意: 一定要用引号将./mymalloc.so括起来。否则,两者将都被认为是环境变量LD_PRELOAD的值。

方式2:

$ export LD_PRELOAD=./mymalloc.so
$ ./main

错误的运行方式:

$ LD_PRELOAD=./mymalloc.so
$ ./main

3)运行结果

$ LD_PRELOAD="./mymalloc.so" ./main
malloc(72704) = 0x21d4010
malloc(16) = 0x21e6030
malloc(32) = 0x21e6050
free() = 0x21e6050
free() = 0x21e6030

输出结果中的malloc(72704) = 0x21d4010,是在main()函数执行前打印的。

4)调试可执行目标文件 main

$ gdb -q ./main
Reading symbols from ./main...done.
(gdb) set environment LD_PRELOAD=./mymalloc.so
(gdb) start
Temporary breakpoint 1 at 0x40056e: file main.cpp, line 8.
Starting program: /home/test/li_cpp/main
malloc(72704) = 0x602010

Temporary breakpoint 1, main () at main.cpp:8
8        int *p1 = static_cast<int *>(malloc(16));
(gdb) n
malloc(16) 
0x614030
9        int *p2 = static_cast<int *>(malloc(32));
(gdb) 
malloc(32) = 0x614050
10        free(p2);
(gdb) 
free() = 0x614050
11        free(p1);
(gdb) c
Continuing.
free() = 0x614030
[Inferior 1 (process 28282) exited normally]
(gdb) 

注意: 设置可执行目标文件main的环境变量的操作必须在执行start前进行。

step 6: 完整程序

mymalloc.cpp:

// how to compile: g++ -o mymalloc.so mymalloc.cpp -shared -fpic -ldl -D_GNU_SOURCE -g

#include <dlfcn.h>
#include <cstdio>
#include <stdexcept>

#define LOG_INFO std::printf

extern "C" {

voidmalloc(size_t size)
{
    auto symbol_addr = dlsym(RTLD_NEXT, "malloc");
    if (!symbol_addr)
    {
        throw std::runtime_error(dlerror());
    }
    typedef void* (*__malloc)(size_t);
    auto libc_malloc = reinterpret_cast<__malloc>(symbol_addr);
    auto ptr = libc_malloc(size);

    static thread_local bool called = false;
    if (!called)
    {
        called = true;
        LOG_INFO("malloc(%ld) = %p\n", size, ptr);
        called = false;
    }

    return ptr;
}

void free(void *ptr)
{
    auto symbol_addr = dlsym(RTLD_NEXT, "free");
    if (!symbol_addr)
    {
        throw std::runtime_error(dlerror());
    }
    typedef void (*__free)(void *);
    auto libc_free = reinterpret_cast<__free>(symbol_addr);
    libc_free(ptr);
    LOG_INFO("free() = %p\n", ptr);
}

}   // extern "C"

main.cpp:

// how to compile: g++ -o main main.cpp -g
// how to run: LD_PRELOAD="./mymalloc.so" ./main

#include <malloc.h>

int main()
{
    int *p1 = static_cast<int *>(malloc(16));
    int *p2 = static_cast<int *>(malloc(32));
    free(p2);
    free(p1);
    return 0;
}

下一篇:计算机系统篇之链接(16):真正理解 RTLD_NEXT 的作用

上一篇:计算机系统篇之链接(14):.plt、.plt.got、.got 和 .got.plt sections 之间的区别

首页