LD_PRELOAD拦截库函数

系统调用是程序从用户空间请求内核服务的唯一方式(如 openreadwritefork)。它们通过软中断(如 int 0x80syscall指令)直接进入内核。用户程序通常不直接调用系统调用。

库函数是标准库(如 glibc)提供的封装函数。例如,fopenprintfmalloc等。它们内部可能会使用一个或多个系统调用来完成任务。

 

Q1: 如何通过劫持库函数来影响系统调用 ?

这是最常见的使用模式:通过劫持一个封装了系统调用的库函数,来监控、修改或阻止其行为。

示例:记录所有文件打开操作

编译与使用

运行后,ls命令在内部调用 fopen时,都会先执行我们自定义的版本,从而记录日志。

 

常见劫持目标

 

限制与规避

LD_PRELOAD并非万能,有许多安全机制会限制它:

 

Q2: 对象跟踪和对象流分析

OSDI'25 SOAR

将程序运行时离散的内存分配/释放事件,通过调用栈信息关联起来,聚合成有意义的“对象流”,从而分析其使用模式.

函数拦截原理

 

首先需要定义每次内存分配或释放的详细记录:

本代码中调用栈的设计目的是获取内存分配或释放的详细上下文信息。

 

malloc

注释:

【1】malloc 可能在调用栈跟踪或记录日志时再次触发(例如,backtracedlsym 函数可能内部调用了 malloc),因此需要防止进入无限递归。

 

如何跟踪对象的生命周期?

S1: 拦截内存分配和释放操作

libc_malloclibc_free是通过 LD_PRELOAD技术劫持系统内存分配函数时保存原始函数指针的变量。它们是实现函数拦截的关键机制。(通过 dlsym(RTLD_NEXT, ...) 获取)

 

S2: 记录时间戳

在分配和释放操作时分别记录准确的时间戳,帮助衡量生命周期。时间戳可以通过以下方法获取:

该方法获取的是高精度 CPU 时钟计数,相对较快,尤其适合分析短时间内的内存分配。

记录时间戳: 将时间戳存储在对象元数据信息中,用于后续分析:

 

S3: 关联对象与操作

当捕获到分配操作的内存地址(vaddr)时,将该地址用作所有日志记录的核心 "主键"。在释放操作中,通过该地址去匹配对应的分配操作。

方法实现:

分配时:

释放时:

查询与更新:

日志结构(如哈希表或平衡树等)提供快速查询方法:

 

S4: 分组对象生命周期(调用栈归类)

所有内存分配操作通过调用栈(backtrace())分组,分组机制是对象生命周期建模的关键:

通过调用栈归类,可以实现:

 

S5: 匹配生命周期并分析

一旦分配和释放日志被记录完成,可以通过以下方式计算和分析对象生命周期: