计算机系统篇之链接(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
。
以“在加载程序时,拦截标准库的 malloc() 和 free() 函数”为例,具体过程如下。
step 1: 编写自定义的malloc()
函数
1)由于要拦截标准库的malloc()
函数,相当于要神不知鬼不觉地将程序中所有对标准库函数malloc()
的调用换成调用我们自定义的malloc()
函数。所以,我们自定义的malloc()
的函数原型必须与标准库中的保持一致。
自定义的malloc()
函数:
extern "C" {
void* malloc(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" {
void* malloc(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" {
void* malloc(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 之间的区别